Admin analytics: heatmap per device, anteprima staff e analisi costi.

Separa heatmap mobile/desktop, opt-out analytics per operatori, dashboard costi con KPI e trend mensili.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-28 19:25:38 +02:00
co-authored by Cursor
parent e0e07e5316
commit 29c8afb94d
43 changed files with 1572 additions and 58 deletions
+132
View File
@@ -0,0 +1,132 @@
(function () {
var trend = window.adminCostTrend || [];
var i18n = window.adminCostI18n || {};
if (!trend.length || typeof Chart === "undefined") return;
function monthLabel(monthStr) {
var parts = monthStr.split("-");
if (parts.length < 2) return monthStr;
var d = new Date(parseInt(parts[0], 10), parseInt(parts[1], 10) - 1, 1);
return d.toLocaleDateString([], { month: "short", year: "2-digit" });
}
var labels = trend.map(function (row) {
return monthLabel(row.month);
});
var chartOptions = {
responsive: true,
maintainAspectRatio: false,
animation: { duration: 300 },
scales: {
x: {
ticks: { maxTicksLimit: 12, color: "#9a9aad", font: { size: 10 } },
grid: { color: "rgba(255,255,255,0.06)" }
},
y: {
beginAtZero: true,
ticks: { color: "#9a9aad", font: { size: 10 } },
grid: { color: "rgba(255,255,255,0.06)" }
}
},
plugins: { legend: { labels: { color: "#ccc", boxWidth: 12 } } }
};
var euroTicks = {
ticks: {
color: "#9a9aad",
font: { size: 10 },
callback: function (v) {
return "€" + v;
}
}
};
var costRevenueCanvas = document.getElementById("chart-cost-revenue");
if (costRevenueCanvas) {
new Chart(costRevenueCanvas, {
type: "bar",
data: {
labels: labels,
datasets: [
{
label: i18n.cost || "Cost",
data: trend.map(function (r) { return (r.cost_cents || 0) / 100; }),
backgroundColor: "rgba(229, 57, 53, 0.65)"
},
{
label: i18n.revenue || "Revenue",
data: trend.map(function (r) { return (r.revenue_cents || 0) / 100; }),
backgroundColor: "rgba(67, 160, 71, 0.65)"
},
{
label: i18n.margin || "Margin",
data: trend.map(function (r) { return (r.margin_cents || 0) / 100; }),
backgroundColor: "rgba(255, 193, 7, 0.55)"
}
]
},
options: {
...chartOptions,
scales: {
x: chartOptions.scales.x,
y: { ...chartOptions.scales.y, ...euroTicks }
}
}
});
}
var costHourCanvas = document.getElementById("chart-cost-hour");
if (costHourCanvas) {
new Chart(costHourCanvas, {
type: "line",
data: {
labels: labels,
datasets: [{
label: i18n.costPerHour || "Cost/hour",
data: trend.map(function (r) {
return r.cost_per_hour_cents ? r.cost_per_hour_cents / 100 : 0;
}),
borderColor: "#e53935",
backgroundColor: "rgba(229, 57, 53, 0.12)",
fill: true,
tension: 0.35,
pointRadius: 2,
borderWidth: 2
}]
},
options: {
...chartOptions,
scales: {
x: chartOptions.scales.x,
y: { ...chartOptions.scales.y, ...euroTicks }
},
plugins: { legend: { display: false } }
}
});
}
var hoursCanvas = document.getElementById("chart-hours");
if (hoursCanvas) {
new Chart(hoursCanvas, {
type: "line",
data: {
labels: labels,
datasets: [{
label: i18n.hours || "Hours",
data: trend.map(function (r) { return r.hours || 0; }),
borderColor: "#43a047",
backgroundColor: "rgba(67, 160, 71, 0.12)",
fill: true,
tension: 0.35,
pointRadius: 2,
borderWidth: 2
}]
},
options: {
...chartOptions,
plugins: { legend: { display: false } }
}
});
}
})();
+13
View File
@@ -693,6 +693,13 @@ body.admin-body {
background: #3a1515;
}
.admin-device-tab-count {
margin-left: 0.35rem;
padding: 0 0.35rem;
font-size: 0.75rem;
color: var(--muted);
}
.admin-locale-panel {
display: grid;
gap: 0.75rem;
@@ -793,6 +800,12 @@ body.admin-body {
gap: 0.6rem;
}
.admin-analytics-preview__status {
margin: 0 0 0.75rem;
color: #a5f0b8;
font-size: 0.9rem;
}
.admin-filter-count {
font-size: 0.85rem;
}
+10
View File
@@ -37,6 +37,11 @@
return document.body && document.body.getAttribute("data-ga-id");
}
function isAnalyticsSuppressed() {
var meta = document.querySelector("meta[name='mltv-analytics-suppress']");
return meta && meta.getAttribute("content") === "1";
}
function loadGoogleAnalytics() {
var id = measurementId();
if (!id || window.__mltvGaLoaded) return;
@@ -68,6 +73,7 @@
}
function applyConsent(consent) {
if (isAnalyticsSuppressed()) return;
if (consent && consent.analytics) {
loadGoogleAnalytics();
if (typeof window.mltvSiteAnalyticsStart === "function") {
@@ -94,6 +100,10 @@
if (acceptAll) {
acceptAll.addEventListener("click", function () {
if (isAnalyticsSuppressed()) {
hideBanner(banner);
return;
}
var c = writeConsent({ analytics: true });
applyConsent(c);
hideBanner(banner);
+28
View File
@@ -3549,3 +3549,31 @@ a.replay-archive__thumb:hover {
grid-template-columns: 1fr;
}
}
.mltv-preview-badge {
position: fixed;
bottom: 16px;
left: 16px;
z-index: 1200;
display: flex;
align-items: center;
gap: 10px;
padding: 8px 12px;
border-radius: 8px;
background: rgba(20, 20, 28, 0.94);
border: 1px solid #444;
color: #ddd;
font-size: 0.82rem;
line-height: 1.3;
box-shadow: 0 4px 18px rgba(0, 0, 0, 0.35);
}
.mltv-preview-badge__link {
color: #ff8a80;
text-decoration: none;
font-weight: 600;
}
.mltv-preview-badge__link:hover {
color: #ff5252;
}
+8 -2
View File
@@ -29,6 +29,11 @@
return false;
}
function isAnalyticsSuppressed() {
var meta = document.querySelector("meta[name='mltv-analytics-suppress']");
return meta && meta.getAttribute("content") === "1";
}
function hasAnalyticsConsent() {
try {
var raw = localStorage.getItem(STORAGE_KEY);
@@ -104,7 +109,7 @@
function flush(useBeacon) {
flushMovesIntoQueue();
if (!queue.length || !hasAnalyticsConsent()) {
if (!queue.length || !hasAnalyticsConsent() || isAnalyticsSuppressed()) {
queue = [];
return;
}
@@ -236,7 +241,7 @@
}
function captureSnapshot() {
if (!hasAnalyticsConsent() || alreadyCapturedToday()) return;
if (!hasAnalyticsConsent() || isAnalyticsSuppressed() || alreadyCapturedToday()) return;
loadHtml2Canvas(function (html2canvas) {
var size = pageSize();
html2canvas(document.documentElement, {
@@ -281,6 +286,7 @@
function start() {
if (started) return;
if (excludedPath(location.pathname)) return;
if (isAnalyticsSuppressed()) return;
if (!hasAnalyticsConsent()) return;
started = true;
maxScroll = scrollPct();