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 @@

Page Visit Analytics

+
+
+

Total Visits

+
    +
  • Last 24 hours: {{ last24hCount }}
  • +
  • Last 7 days: {{ last7dCount }}
  • +
  • All time: {{ totalVisits }}
  • +
+
+ +
+

Unique Visitors

+
    +
  • Last 24 hours: {{ uniqueVisitors24h }}
  • +
  • Last 7 days: {{ uniqueVisitors7d }}
  • +
  • All time: {{ totalUniqueVisitors }}
  • +
+

Tracked by session ID (includes both anonymous and logged-in visitors)

+
+ +
+

Engagement

+
    +
  • Avg. Visits/Session: {{ avgVisitsPerSession }}
  • +
  • Bounce Rate: {{ bounceRate }}%
  • +
+
+
+
-

Total Visits

- +

Visits Per Day (Last 30 Days)

+ {% if visitsPerDay|length > 0 %} + + + + + + + + + {% for stat in visitsPerDay %} + + + + + {% endfor %} + +
DateVisits
{{ stat.day|date('Y-m-d') }}{{ stat.count }}
+ {% else %} +

No visit data for the last 30 days.

+ {% endif %}
-

Unique Visitors

- -

Tracked by session ID (includes both anonymous and logged-in visitors)

+

Most Popular Routes (All Time)

+ {% if mostPopularRoutes|length > 0 %} + + + + + + + + + {% for stat in mostPopularRoutes %} + + + + + {% endfor %} + +
RouteVisits
{{ stat.route }}{{ stat.count }}
+ {% else %} +

No route data available.

+ {% endif %}
-

Visit Count by Route (Last 7 Days)

+

Recent Visits

+ {% if recentVisits|length > 0 %} + + + + + + + + + + {% for visit in recentVisits %} + + + + + + {% endfor %} + +
RouteSession IDVisited At
{{ visit.route }}{{ visit.sessionId|slice(0, 12) }}...{{ visit.visitedAt|date('Y-m-d H:i') }}
+ {% else %} +

No recent visits recorded.

+ {% endif %} +
- {% if visitStats|length > 0 %} +
+

Visit Count by Route (Last 7 Days)

+ {% set filteredVisitStats = visitStats|filter(stat => stat.count >= 5) %} + {% if filteredVisitStats|length > 0 %} @@ -36,7 +121,7 @@ - {% for stat in visitStats %} + {% for stat in filteredVisitStats %} @@ -45,7 +130,7 @@
{{ stat.route }} {{ stat.count }}
{% else %} -

No visit data recorded in the last 7 days.

+

No routes with 5 or more visits recorded in the last 7 days.

{% endif %}
@@ -63,12 +148,14 @@ {% for stat in sessionStats %} + {% if stat.visitCount > 1 %} {{ stat.sessionId|slice(0, 12) }}... {{ stat.visitCount }} {{ stat.firstVisit|date('M d, H:i') }} {{ stat.lastVisit|date('M d, H:i') }} + {% endif %} {% endfor %}