- Fix Enter key detection: handle multiple Enter key formats (\n, \r, \r\n) - Reduce flickering: lower render frequency from 60 FPS to 30 FPS - Fix menu bar visibility: re-render menu bar after content to prevent overwriting - Fix content positioning: explicit line positioning for categories and commands - Fix line shifting: clear lines before writing, control newlines manually - Limit visible items: prevent overflow with maxVisibleCategories/Commands - Improve CPU usage: increase sleep interval when no events processed This fixes: - Enter key not working for selection - Strong flickering of the application - Menu bar not visible or being overwritten - Top half of selection list not displayed - Lines being shifted/misaligned
384 lines
13 KiB
PHP
384 lines
13 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Application\Admin\Analytics;
|
|
|
|
use App\Application\Analytics\Service\AnalyticsDashboardService;
|
|
use App\Application\Analytics\Service\AnalyticsRealTimeService;
|
|
use App\Application\Analytics\Service\AnalyticsReportService;
|
|
use App\Framework\Analytics\AnalyticsCategory;
|
|
use App\Framework\Analytics\AnalyticsCollector;
|
|
use App\Framework\Analytics\Storage\AnalyticsStorage;
|
|
use App\Framework\Attributes\Route;
|
|
use App\Framework\Http\Method;
|
|
use App\Framework\Http\Status;
|
|
use App\Framework\Meta\MetaData;
|
|
use App\Framework\Router\AdminRoutes;
|
|
use App\Framework\Router\Result\JsonResult;
|
|
use App\Framework\Router\Result\ViewResult;
|
|
use App\Framework\Admin\Attributes\AdminPage;
|
|
use App\Framework\Admin\Attributes\AdminSection;
|
|
|
|
#[AdminSection(name: 'Analytics', icon: 'chart-bar', order: 4, description: 'Analytics and reporting')]
|
|
final readonly class AnalyticsController
|
|
{
|
|
public function __construct(
|
|
private AnalyticsDashboardService $dashboardService,
|
|
private AnalyticsStorage $storage,
|
|
private AnalyticsReportService $reportService,
|
|
private AnalyticsRealTimeService $realTimeService,
|
|
private AnalyticsCollector $analyticsCollector,
|
|
) {
|
|
}
|
|
|
|
#[AdminPage(title: 'Analytics Dashboard', icon: 'chart-bar', section: 'Analytics', order: 10)]
|
|
#[Route(path: '/admin/analytics', method: Method::GET, name: AdminRoutes::ANALYTICS_DASHBOARD)]
|
|
public function dashboard(): ViewResult
|
|
{
|
|
// Use mock data for now since analytics services may not be fully configured
|
|
$overview = [
|
|
'today_page_views' => 0,
|
|
'week_page_views' => 0,
|
|
'month_page_views' => 0,
|
|
'today_visitors' => 0,
|
|
'week_visitors' => 0,
|
|
'month_visitors' => 0,
|
|
'avg_load_time' => 'N/A',
|
|
'bounce_rate' => 'N/A',
|
|
'avg_session_duration' => 'N/A',
|
|
];
|
|
$topPages = [];
|
|
$trafficSources = [];
|
|
|
|
$data = [
|
|
'title' => 'Analytics Dashboard',
|
|
'today_page_views' => $overview['today_page_views'] ?? 0,
|
|
'week_page_views' => $overview['week_page_views'] ?? 0,
|
|
'month_page_views' => $overview['month_page_views'] ?? 0,
|
|
'today_visitors' => $overview['today_visitors'] ?? 0,
|
|
'week_visitors' => $overview['week_visitors'] ?? 0,
|
|
'month_visitors' => $overview['month_visitors'] ?? 0,
|
|
'avg_load_time' => $overview['avg_load_time'] ?? 'N/A',
|
|
'bounce_rate' => $overview['bounce_rate'] ?? 'N/A',
|
|
'avg_session_duration' => $overview['avg_session_duration'] ?? 'N/A',
|
|
'top_pages' => $topPages,
|
|
'traffic_sources' => $trafficSources,
|
|
'last_update' => date('Y-m-d H:i:s'),
|
|
];
|
|
|
|
return new ViewResult(
|
|
template: 'analytics-dashboard',
|
|
metaData: new MetaData('Analytics Dashboard', 'Website Analytics and User Behavior'),
|
|
data: $data
|
|
);
|
|
}
|
|
|
|
#[Route(path: '/admin/analytics/api/overview', method: Method::GET)]
|
|
public function getOverview(): JsonResult
|
|
{
|
|
try {
|
|
$endDate = $_GET['end_date'] ?? date('Y-m-d');
|
|
$startDate = $_GET['start_date'] ?? date('Y-m-d', strtotime('-30 days'));
|
|
|
|
// Calculate days between dates
|
|
$days = max(1, (strtotime($endDate) - strtotime($startDate)) / 86400 + 1);
|
|
|
|
$overview = $this->dashboardService->getOverview($startDate, $endDate);
|
|
|
|
return new JsonResult([
|
|
'success' => true,
|
|
'data' => $overview,
|
|
'period' => [
|
|
'start' => $startDate,
|
|
'end' => $endDate,
|
|
'days' => (int)$days,
|
|
],
|
|
]);
|
|
} catch (\Exception $e) {
|
|
return new JsonResult([
|
|
'success' => false,
|
|
'error' => $e->getMessage(),
|
|
], Status::from(500));
|
|
}
|
|
}
|
|
|
|
#[Route(path: '/admin/analytics/api/timeseries', method: Method::GET)]
|
|
public function getTimeSeries(): JsonResult
|
|
{
|
|
try {
|
|
$metric = $_GET['metric'] ?? 'page_views';
|
|
$period = $_GET['period'] ?? 'day';
|
|
$days = (int) ($_GET['days'] ?? 30);
|
|
|
|
$endDate = date('Y-m-d');
|
|
$startDate = date('Y-m-d', strtotime("-{$days} days"));
|
|
|
|
$timeSeries = $this->storage->getTimeSeries($metric, $startDate, $endDate, $period);
|
|
|
|
return new JsonResult([
|
|
'success' => true,
|
|
'data' => $timeSeries,
|
|
'metric' => $metric,
|
|
'period' => $period,
|
|
'range' => [
|
|
'start' => $startDate,
|
|
'end' => $endDate,
|
|
'days' => $days,
|
|
],
|
|
]);
|
|
} catch (\Exception $e) {
|
|
return new JsonResult([
|
|
'success' => false,
|
|
'error' => $e->getMessage(),
|
|
], Status::from(500));
|
|
}
|
|
}
|
|
|
|
#[Route(path: '/admin/analytics/api/top-pages', method: Method::GET)]
|
|
public function getTopPages(): JsonResult
|
|
{
|
|
try {
|
|
$limit = (int) ($_GET['limit'] ?? 10);
|
|
$days = (int) ($_GET['days'] ?? 30);
|
|
|
|
$endDate = date('Y-m-d');
|
|
$startDate = date('Y-m-d', strtotime("-{$days} days"));
|
|
|
|
$topPages = $this->dashboardService->getTopPages($startDate, $endDate, $limit);
|
|
|
|
return new JsonResult([
|
|
'success' => true,
|
|
'data' => $topPages,
|
|
'limit' => $limit,
|
|
'period' => [
|
|
'start' => $startDate,
|
|
'end' => $endDate,
|
|
'days' => $days,
|
|
],
|
|
]);
|
|
} catch (\Exception $e) {
|
|
return new JsonResult([
|
|
'success' => false,
|
|
'error' => $e->getMessage(),
|
|
], Status::from(500));
|
|
}
|
|
}
|
|
|
|
#[Route(path: '/admin/analytics/api/traffic-sources', method: Method::GET)]
|
|
public function getTrafficSources(): JsonResult
|
|
{
|
|
try {
|
|
$days = (int) ($_GET['days'] ?? 30);
|
|
|
|
$endDate = date('Y-m-d');
|
|
$startDate = date('Y-m-d', strtotime("-{$days} days"));
|
|
|
|
$sources = $this->dashboardService->getTrafficSources($startDate, $endDate);
|
|
|
|
return new JsonResult([
|
|
'success' => true,
|
|
'data' => $sources,
|
|
'period' => [
|
|
'start' => $startDate,
|
|
'end' => $endDate,
|
|
'days' => $days,
|
|
],
|
|
]);
|
|
} catch (\Exception $e) {
|
|
return new JsonResult([
|
|
'success' => false,
|
|
'error' => $e->getMessage(),
|
|
], Status::from(500));
|
|
}
|
|
}
|
|
|
|
#[Route(path: '/admin/analytics/api/user-behavior', method: Method::GET)]
|
|
public function getUserBehavior(): JsonResult
|
|
{
|
|
try {
|
|
$days = (int) ($_GET['days'] ?? 30);
|
|
|
|
$endDate = date('Y-m-d');
|
|
$startDate = date('Y-m-d', strtotime("-{$days} days"));
|
|
|
|
$behavior = $this->reportService->getUserBehavior($startDate, $endDate);
|
|
|
|
return new JsonResult([
|
|
'success' => true,
|
|
'data' => $behavior,
|
|
'period' => [
|
|
'start' => $startDate,
|
|
'end' => $endDate,
|
|
'days' => $days,
|
|
],
|
|
]);
|
|
} catch (\Exception $e) {
|
|
return new JsonResult([
|
|
'success' => false,
|
|
'error' => $e->getMessage(),
|
|
], Status::from(500));
|
|
}
|
|
}
|
|
|
|
#[Route(path: '/admin/analytics/api/business-metrics', method: Method::GET)]
|
|
public function getBusinessMetrics(): JsonResult
|
|
{
|
|
try {
|
|
$days = (int) ($_GET['days'] ?? 30);
|
|
|
|
$endDate = date('Y-m-d');
|
|
$startDate = date('Y-m-d', strtotime("-{$days} days"));
|
|
|
|
$metrics = $this->reportService->getBusinessMetrics($startDate, $endDate);
|
|
|
|
return new JsonResult([
|
|
'success' => true,
|
|
'data' => $metrics,
|
|
'period' => [
|
|
'start' => $startDate,
|
|
'end' => $endDate,
|
|
'days' => $days,
|
|
],
|
|
]);
|
|
} catch (\Exception $e) {
|
|
return new JsonResult([
|
|
'success' => false,
|
|
'error' => $e->getMessage(),
|
|
], Status::from(500));
|
|
}
|
|
}
|
|
|
|
#[Route(path: '/admin/analytics/api/real-time', method: Method::GET)]
|
|
public function getRealTimeData(): JsonResult
|
|
{
|
|
try {
|
|
$realTime = $this->realTimeService->getRealTimeData();
|
|
|
|
return new JsonResult([
|
|
'success' => true,
|
|
'data' => $realTime,
|
|
'timestamp' => time(),
|
|
'updated_at' => date('Y-m-d H:i:s'),
|
|
]);
|
|
} catch (\Exception $e) {
|
|
return new JsonResult([
|
|
'success' => false,
|
|
'error' => $e->getMessage(),
|
|
], Status::from(500));
|
|
}
|
|
}
|
|
|
|
#[Route(path: '/admin/analytics/api/events', method: Method::GET)]
|
|
public function getEvents(): JsonResult
|
|
{
|
|
try {
|
|
$category = $_GET['category'] ?? null;
|
|
$limit = (int) ($_GET['limit'] ?? 100);
|
|
$offset = (int) ($_GET['offset'] ?? 0);
|
|
$days = (int) ($_GET['days'] ?? 7);
|
|
|
|
$endDate = date('Y-m-d');
|
|
$startDate = date('Y-m-d', strtotime("-{$days} days"));
|
|
|
|
$categoryFilter = $category ? AnalyticsCategory::tryFrom($category) : null;
|
|
|
|
$events = $this->reportService->getEvents($startDate, $endDate, $categoryFilter, $limit, $offset);
|
|
$total = $this->reportService->getEventsCount($startDate, $endDate, $categoryFilter);
|
|
|
|
return new JsonResult([
|
|
'success' => true,
|
|
'data' => $events,
|
|
'pagination' => [
|
|
'limit' => $limit,
|
|
'offset' => $offset,
|
|
'total' => $total,
|
|
'has_more' => ($offset + $limit) < $total,
|
|
],
|
|
'filter' => [
|
|
'category' => $category,
|
|
'start' => $startDate,
|
|
'end' => $endDate,
|
|
],
|
|
]);
|
|
} catch (\Exception $e) {
|
|
return new JsonResult([
|
|
'success' => false,
|
|
'error' => $e->getMessage(),
|
|
], Status::from(500));
|
|
}
|
|
}
|
|
|
|
#[Route(path: '/admin/analytics/api/export', method: Method::GET)]
|
|
public function exportData(): JsonResult
|
|
{
|
|
try {
|
|
$format = $_GET['format'] ?? 'json';
|
|
$category = $_GET['category'] ?? null;
|
|
$days = (int) ($_GET['days'] ?? 30);
|
|
|
|
$endDate = date('Y-m-d');
|
|
$startDate = date('Y-m-d', strtotime("-{$days} days"));
|
|
|
|
$categoryFilter = $category ? AnalyticsCategory::tryFrom($category) : null;
|
|
|
|
$data = $this->reportService->exportData($startDate, $endDate, $categoryFilter, $format);
|
|
|
|
return new JsonResult([
|
|
'success' => true,
|
|
'data' => $data,
|
|
'format' => $format,
|
|
'exported_at' => date('Y-m-d H:i:s'),
|
|
'period' => [
|
|
'start' => $startDate,
|
|
'end' => $endDate,
|
|
'days' => $days,
|
|
],
|
|
]);
|
|
} catch (\Exception $e) {
|
|
return new JsonResult([
|
|
'success' => false,
|
|
'error' => $e->getMessage(),
|
|
], Status::from(500));
|
|
}
|
|
}
|
|
|
|
#[Route(path: '/admin/analytics/api/track', method: Method::POST)]
|
|
public function trackEvent(): JsonResult
|
|
{
|
|
try {
|
|
$action = $_POST['action'] ?? null;
|
|
$category = $_POST['category'] ?? 'user_behavior';
|
|
$properties = json_decode($_POST['properties'] ?? '{}', true);
|
|
|
|
if (! $action) {
|
|
return new JsonResult([
|
|
'success' => false,
|
|
'error' => 'Action is required',
|
|
], Status::from(400));
|
|
}
|
|
|
|
$categoryEnum = AnalyticsCategory::tryFrom($category);
|
|
if (! $categoryEnum) {
|
|
return new JsonResult([
|
|
'success' => false,
|
|
'error' => 'Invalid category',
|
|
], Status::from(400));
|
|
}
|
|
|
|
$this->analyticsCollector->trackAction($action, $categoryEnum, $properties);
|
|
|
|
return new JsonResult([
|
|
'success' => true,
|
|
'message' => 'Event tracked successfully',
|
|
'tracked_at' => date('Y-m-d H:i:s'),
|
|
]);
|
|
} catch (\Exception $e) {
|
|
return new JsonResult([
|
|
'success' => false,
|
|
'error' => $e->getMessage(),
|
|
], Status::from(500));
|
|
}
|
|
}
|
|
}
|