diff --git a/backend/app/controllers/admin/analytics_controller.rb b/backend/app/controllers/admin/analytics_controller.rb
index 03e0ca3..df9e22c 100644
--- a/backend/app/controllers/admin/analytics_controller.rb
+++ b/backend/app/controllers/admin/analytics_controller.rb
@@ -36,6 +36,23 @@ module Admin
max_scroll: max_scroll_by_path[path].to_i
}
end.sort_by { |r| [-r[:pageviews], -r[:moves], -r[:clicks], r[:page_path]] }
+
+ pageviews_by_day = scope.group(:day).sum(:pageview_count)
+ clicks_by_day = cell_scope.group(:day).sum(:click_count)
+ moves_by_day = cell_scope.group(:day).sum(:move_count)
+ @trend = (@filters[:from]..@filters[:to]).map do |day|
+ {
+ day: day.iso8601,
+ pageviews: pageviews_by_day[day].to_i,
+ clicks: clicks_by_day[day].to_i,
+ moves: moves_by_day[day].to_i
+ }
+ end
+ @trend_totals = {
+ pageviews: @trend.sum { |r| r[:pageviews] },
+ clicks: @trend.sum { |r| r[:clicks] },
+ moves: @trend.sum { |r| r[:moves] }
+ }
end
def show
diff --git a/backend/app/views/admin/analytics/index.html.erb b/backend/app/views/admin/analytics/index.html.erb
index 80f2226..c415afd 100644
--- a/backend/app/views/admin/analytics/index.html.erb
+++ b/backend/app/views/admin/analytics/index.html.erb
@@ -58,6 +58,40 @@
<% end %>
+<% if @trend.any? && (@trend_totals[:pageviews].positive? || @trend_totals[:clicks].positive? || @trend_totals[:moves].positive?) %>
+
+ <%= t("admin.analytics.index.trend_title") %>
+ <%= t("admin.analytics.index.trend_lead") %>
+
+
+
<%= t("admin.analytics.index.table.pageviews") %>
+
<%= @trend_totals[:pageviews] %>
+
+
+
<%= t("admin.analytics.index.table.clicks") %>
+
<%= @trend_totals[:clicks] %>
+
+
+
<%= t("admin.analytics.index.table.moves") %>
+
<%= @trend_totals[:moves] %>
+
+
+
+
+
+
+
+
+
+<% end %>
+
<% if @pages.any? %>
diff --git a/backend/app/views/layouts/admin.html.erb b/backend/app/views/layouts/admin.html.erb
index 6b1b729..9b34054 100644
--- a/backend/app/views/layouts/admin.html.erb
+++ b/backend/app/views/layouts/admin.html.erb
@@ -5,7 +5,7 @@
<%= csrf_meta_tags %>
-
+
<%= yield :head %>
<% if content_for?(:replay_archive_styles) %>
diff --git a/backend/config/locales/admin.de.yml b/backend/config/locales/admin.de.yml
index b470b88..eae76cd 100644
--- a/backend/config/locales/admin.de.yml
+++ b/backend/config/locales/admin.de.yml
@@ -427,6 +427,8 @@ de:
lead: Aggregierte First-Party-Heatmaps (Bewegung/Klick) und Scrolltiefe, nur mit Statistik-Einwilligung. Keine personenbezogenen Daten.
none: Keine Daten im gewählten Zeitraum.
heatmap: Heatmap
+ trend_title: Verlauf über die Zeit
+ trend_lead: Tägliche Pageviews, Klicks und Bewegungen im gefilterten Zeitraum.
table:
path: Seite
pageviews: Pageviews
diff --git a/backend/config/locales/admin.en.yml b/backend/config/locales/admin.en.yml
index f29f9ef..7fc5f49 100644
--- a/backend/config/locales/admin.en.yml
+++ b/backend/config/locales/admin.en.yml
@@ -427,6 +427,8 @@ en:
lead: Aggregated first-party move/click heatmaps and scroll depth, only with analytics consent. No personal data.
none: No data in the selected period.
heatmap: Heatmap
+ trend_title: Trend over time
+ trend_lead: Daily pageviews, clicks and moves for the filtered period.
table:
path: Page
pageviews: Pageviews
diff --git a/backend/config/locales/admin.es.yml b/backend/config/locales/admin.es.yml
index 49c3ad7..4870269 100644
--- a/backend/config/locales/admin.es.yml
+++ b/backend/config/locales/admin.es.yml
@@ -427,6 +427,8 @@ es:
lead: Heatmaps de movimientos/clics y scroll agregados (first-party), solo con consentimiento estadístico. Sin datos personales.
none: No hay datos en el periodo seleccionado.
heatmap: Heatmap
+ trend_title: Evolución en el tiempo
+ trend_lead: Pageviews, clics y movimientos día a día en el periodo filtrado.
table:
path: Página
pageviews: Pageviews
diff --git a/backend/config/locales/admin.fr.yml b/backend/config/locales/admin.fr.yml
index 3ea30e8..5e844c5 100644
--- a/backend/config/locales/admin.fr.yml
+++ b/backend/config/locales/admin.fr.yml
@@ -427,6 +427,8 @@ fr:
lead: Heatmaps mouvements/clics et scroll agrégés (first-party), uniquement avec consentement statistiques. Aucune donnée personnelle.
none: Aucune donnée sur la période sélectionnée.
heatmap: Heatmap
+ trend_title: Évolution dans le temps
+ trend_lead: Pages vues, clics et mouvements par jour sur la période filtrée.
table:
path: Page
pageviews: Pages vues
diff --git a/backend/config/locales/admin.it.yml b/backend/config/locales/admin.it.yml
index 3a37e52..17f84c5 100644
--- a/backend/config/locales/admin.it.yml
+++ b/backend/config/locales/admin.it.yml
@@ -448,6 +448,8 @@ it:
lead: Heatmap movimenti/click e scroll aggregati (first-party), solo con consenso statistico. Nessun dato personale.
none: Nessun dato nel periodo selezionato.
heatmap: Heatmap
+ trend_title: Andamento nel tempo
+ trend_lead: Pageview, click e movimenti giorno per giorno nel periodo filtrato.
table:
path: Pagina
pageviews: Pageview
diff --git a/backend/public/admin-analytics.js b/backend/public/admin-analytics.js
new file mode 100644
index 0000000..299d139
--- /dev/null
+++ b/backend/public/admin-analytics.js
@@ -0,0 +1,81 @@
+(function () {
+ var trend = window.adminAnalyticsTrend || [];
+ var i18n = window.adminAnalyticsI18n || {};
+ var canvas = document.getElementById("chart-analytics-trend");
+ if (!canvas || !trend.length || typeof Chart === "undefined") return;
+
+ function dayLabel(dayStr) {
+ var parts = dayStr.split("-");
+ if (parts.length < 3) return dayStr;
+ var d = new Date(parseInt(parts[0], 10), parseInt(parts[1], 10) - 1, parseInt(parts[2], 10));
+ return d.toLocaleDateString([], { day: "2-digit", month: "short" });
+ }
+
+ var labels = trend.map(function (row) {
+ return dayLabel(row.day);
+ });
+
+ new Chart(canvas, {
+ type: "line",
+ data: {
+ labels: labels,
+ datasets: [
+ {
+ label: i18n.pageviews || "Pageviews",
+ data: trend.map(function (r) { return r.pageviews || 0; }),
+ borderColor: "rgba(229, 57, 53, 0.95)",
+ backgroundColor: "rgba(229, 57, 53, 0.18)",
+ fill: true,
+ tension: 0.25,
+ pointRadius: 3,
+ pointHoverRadius: 5
+ },
+ {
+ label: i18n.clicks || "Clicks",
+ data: trend.map(function (r) { return r.clicks || 0; }),
+ borderColor: "rgba(66, 165, 245, 0.95)",
+ backgroundColor: "transparent",
+ fill: false,
+ tension: 0.25,
+ pointRadius: 2,
+ pointHoverRadius: 4
+ },
+ {
+ label: i18n.moves || "Moves",
+ data: trend.map(function (r) { return r.moves || 0; }),
+ borderColor: "rgba(255, 193, 7, 0.85)",
+ backgroundColor: "transparent",
+ fill: false,
+ tension: 0.25,
+ pointRadius: 2,
+ pointHoverRadius: 4,
+ borderDash: [4, 3]
+ }
+ ]
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ interaction: { mode: "index", intersect: false },
+ animation: { duration: 280 },
+ scales: {
+ x: {
+ ticks: { maxTicksLimit: 14, color: "#9a9aad", font: { size: 10 } },
+ grid: { color: "rgba(255,255,255,0.06)" }
+ },
+ y: {
+ beginAtZero: true,
+ ticks: {
+ color: "#9a9aad",
+ font: { size: 10 },
+ precision: 0
+ },
+ grid: { color: "rgba(255,255,255,0.06)" }
+ }
+ },
+ plugins: {
+ legend: { labels: { color: "#ccc", boxWidth: 12 } }
+ }
+ }
+ });
+})();
diff --git a/backend/public/admin.css b/backend/public/admin.css
index f214752..8ddf83e 100644
--- a/backend/public/admin.css
+++ b/backend/public/admin.css
@@ -173,6 +173,15 @@ body.admin-body {
height: 200px;
}
+.chart-wrap--analytics {
+ height: 260px;
+ margin-top: 0.75rem;
+}
+
+.admin-analytics-trend__kpi {
+ margin: 0.75rem 0 0;
+}
+
.admin-panels {
display: grid;
grid-template-columns: 1.4fr 1fr;
diff --git a/backend/spec/requests/admin/analytics_spec.rb b/backend/spec/requests/admin/analytics_spec.rb
index 30aeb13..218614a 100644
--- a/backend/spec/requests/admin/analytics_spec.rb
+++ b/backend/spec/requests/admin/analytics_spec.rb
@@ -33,6 +33,25 @@ RSpec.describe "Admin analytics", type: :request do
expect(response.body).to include("/prezzi")
expect(response.body).to include("12")
expect(response.body).to include("40")
+ expect(response.body).to include("chart-analytics-trend")
+ expect(response.body).to include("adminAnalyticsTrend")
+ end
+
+ it "mostra il trend anche su più giorni" do
+ AnalyticsPageStat.create!(
+ day: 1.day.ago.to_date,
+ page_path: "/prezzi",
+ device: "desktop",
+ pageview_count: 5,
+ scroll_samples: 0,
+ scroll_sum_pct: 0,
+ max_scroll_pct: 0
+ )
+
+ get admin_analytics_path, params: { from: 2.days.ago.to_date, to: Time.zone.today }
+ expect(response).to have_http_status(:ok)
+ expect(response.body).to include("\"pageviews\":5")
+ expect(response.body).to include("\"pageviews\":12")
end
it "mostra la heatmap di una pagina con tab dispositivo" do