diff --git a/src/Controller/Administration/VisitorAnalyticsController.php b/src/Controller/Administration/VisitorAnalyticsController.php index 45ec58d..34f5808 100644 --- a/src/Controller/Administration/VisitorAnalyticsController.php +++ b/src/Controller/Administration/VisitorAnalyticsController.php @@ -33,6 +33,14 @@ class VisitorAnalyticsController extends AbstractController // Session-based visit breakdown $sessionStats = $visitRepository->getVisitsBySession($last7d); + // New metrics for improved dashboard + $totalVisits = $visitRepository->getTotalVisits(); + $avgVisitsPerSession = $visitRepository->getAverageVisitsPerSession(); + $bounceRate = $visitRepository->getBounceRate(); + $visitsPerDay = $visitRepository->getVisitsPerDay(30); + $mostPopularRoutes = $visitRepository->getMostPopularRoutes(5); + $recentVisits = $visitRepository->getRecentVisits(10); + return $this->render('admin/analytics.html.twig', [ 'visitStats' => $visitStats, 'last24hCount' => $last24hCount, @@ -41,6 +49,13 @@ class VisitorAnalyticsController extends AbstractController 'uniqueVisitors7d' => $uniqueVisitors7d, 'totalUniqueVisitors' => $totalUniqueVisitors, 'sessionStats' => $sessionStats, + // New variables for dashboard + 'totalVisits' => $totalVisits, + 'avgVisitsPerSession' => $avgVisitsPerSession, + 'bounceRate' => $bounceRate, + 'visitsPerDay' => $visitsPerDay, + 'mostPopularRoutes' => $mostPopularRoutes, + 'recentVisits' => $recentVisits, ]); } } diff --git a/src/Repository/VisitRepository.php b/src/Repository/VisitRepository.php index 613dd68..a04da67 100644 --- a/src/Repository/VisitRepository.php +++ b/src/Repository/VisitRepository.php @@ -98,4 +98,110 @@ class VisitRepository extends ServiceEntityRepository return (int) $qb->getQuery()->getSingleScalarResult(); } + + /** + * Returns total number of visits. + */ + public function getTotalVisits(): int + { + return (int) $this->createQueryBuilder('v') + ->select('COUNT(v.id)') + ->getQuery() + ->getSingleScalarResult(); + } + + /** + * Returns number of unique visitors (distinct sessionId). + */ + public function getUniqueVisitors(): int + { + return (int) $this->createQueryBuilder('v') + ->select('COUNT(DISTINCT v.sessionId)') + ->where('v.sessionId IS NOT NULL') + ->getQuery() + ->getSingleScalarResult(); + } + + /** + * Returns visits grouped by day (YYYY-MM-DD => count) using native SQL for PostgreSQL compatibility. + */ + public function getVisitsPerDay(int $days = 30): array + { + $from = (new \DateTimeImmutable())->modify("-{$days} days"); + $conn = $this->getEntityManager()->getConnection(); + $sql = 'SELECT DATE(visited_at) as day, COUNT(id) as count + FROM visit + WHERE visited_at >= :from + GROUP BY day + ORDER BY day ASC'; + $result = $conn->executeQuery( + $sql, + ['from' => $from->format('Y-m-d H:i:s')] + ); + return $result->fetchAllAssociative(); + } + + /** + * Returns the most popular routes (top N). + */ + public function getMostPopularRoutes(int $limit = 5): array + { + return $this->createQueryBuilder('v') + ->select('v.route, COUNT(v.id) as count') + ->groupBy('v.route') + ->orderBy('count', 'DESC') + ->setMaxResults($limit) + ->getQuery() + ->getResult(); + } + + /** + * Returns the most recent visits (with route, sessionId, visitedAt). + */ + public function getRecentVisits(int $limit = 10): array + { + return $this->createQueryBuilder('v') + ->select('v.route, v.sessionId, v.visitedAt') + ->orderBy('v.visitedAt', 'DESC') + ->setMaxResults($limit) + ->getQuery() + ->getResult(); + } + + /** + * Returns the average number of visits per session. + */ + public function getAverageVisitsPerSession(): float + { + $totalVisits = $this->getTotalVisits(); + $uniqueSessions = $this->getUniqueVisitors(); + if ($uniqueSessions === 0) { + return 0.0; + } + return round($totalVisits / $uniqueSessions, 2); + } + + /** + * Returns the bounce rate (percentage of sessions with only one visit). + */ + public function getBounceRate(): float + { + $qb = $this->createQueryBuilder('v') + ->select('v.sessionId, COUNT(v.id) as visitCount') + ->where('v.sessionId IS NOT NULL') + ->groupBy('v.sessionId'); + $sessions = $qb->getQuery()->getResult(); + $singleVisitSessions = 0; + $totalSessions = 0; + foreach ($sessions as $session) { + $totalSessions++; + if ($session['visitCount'] == 1) { + $singleVisitSessions++; + } + } + if ($totalSessions === 0) { + return 0.0; + } + return round(($singleVisitSessions / $totalSessions) * 100, 2); + } } diff --git a/templates/admin/analytics.html.twig b/templates/admin/analytics.html.twig index c0b8ecb..b4f09b4 100644 --- a/templates/admin/analytics.html.twig +++ b/templates/admin/analytics.html.twig @@ -6,28 +6,113 @@
Tracked by session ID (includes both anonymous and logged-in visitors)
+| Date | +Visits | +
|---|---|
| {{ stat.day|date('Y-m-d') }} | +{{ stat.count }} | +
No visit data for the last 30 days.
+ {% endif %}Tracked by session ID (includes both anonymous and logged-in visitors)
+| Route | +Visits | +
|---|---|
| {{ stat.route }} | +{{ stat.count }} | +
No route data available.
+ {% endif %}| Route | +Session ID | +Visited At | +
|---|---|---|
| {{ visit.route }} | +{{ visit.sessionId|slice(0, 12) }}... |
+ {{ visit.visitedAt|date('Y-m-d H:i') }} | +
No recent visits recorded.
+ {% endif %} +| {{ stat.route }} | {{ stat.count }} | @@ -45,7 +130,7 @@
No visit data recorded in the last 7 days.
+No routes with 5 or more visits recorded in the last 7 days.
{% endif %}{{ stat.sessionId|slice(0, 12) }}...