Add device statistics to analytics reports and implement pie chart visualization in admin panel

This commit is contained in:
2026-05-29 16:52:10 +02:00
parent 9b15131461
commit 8de221ba79
2 changed files with 112 additions and 0 deletions

View File

@@ -131,6 +131,27 @@ try {
$stmtGeo->execute($baseParams);
$geo = $stmtGeo->fetch() ?: ['geo_passed' => 0, 'geo_failed' => 0, 'geo_bypass' => 0];
$sqlDeviceStats = "
SELECT
COALESCE(NULLIF(device_type, ''), 'other') AS device_type,
COUNT(*) AS total
FROM analytics_events
WHERE event_name = 'qr_scan' {$whereWindow}
GROUP BY COALESCE(NULLIF(device_type, ''), 'other')
";
$stmtDeviceStats = $pdo->prepare($sqlDeviceStats);
$stmtDeviceStats->execute($baseParams);
$deviceRows = $stmtDeviceStats->fetchAll();
$deviceStats = ['ios' => 0, 'android' => 0, 'other' => 0];
foreach ($deviceRows as $row) {
$key = (string) ($row['device_type'] ?? 'other');
if (!isset($deviceStats[$key])) {
$deviceStats['other'] += (int) $row['total'];
} else {
$deviceStats[$key] = (int) $row['total'];
}
}
$sqlRecentOpens = "
SELECT
created_at,
@@ -248,6 +269,7 @@ try {
'failed' => (int)$geo['geo_failed'],
'bypass' => (int)$geo['geo_bypass'],
],
'deviceStats' => $deviceStats,
'recentOpens' => $recentOpens,
'guestQueueSummary' => [
'total' => (int)$queueSummary['total_actions'],

View File

@@ -8,6 +8,7 @@ requireAdminAuth(true);
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Panel Admina</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<style>
:root { --bg:#0f172a; --surface:#111827; --line:#334155; --text:#e2e8f0; --muted:#94a3b8; --accent:#3b82f6; }
* { box-sizing: border-box; }
@@ -33,6 +34,10 @@ requireAdminAuth(true);
.legend-list li { margin-bottom: 6px; }
.status-ok { color: #22c55e; font-weight: 700; }
.status-fail { color: #ef4444; font-weight: 700; }
.chart-wrap { max-width: 360px; margin: 0 auto; }
.chart-legend { display: flex; flex-wrap: wrap; gap: 12px 20px; justify-content: center; margin-top: 12px; font-size: .88rem; color: #cbd5e1; }
.chart-legend-item { display: flex; align-items: center; gap: 6px; }
.chart-legend-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
@media (max-width: 900px) { .col-3,.col-4,.col-6,.col-8 { grid-column: span 12; } .top { flex-direction: column; align-items: flex-start; } }
</style>
</head>
@@ -105,6 +110,16 @@ requireAdminAuth(true);
</table>
</div>
<div class="card col-12">
<h3>Urządzenia (skanowania QR)</h3>
<div class="muted" style="margin-bottom:12px;">Udział iPhone (iOS), Android i pozostałych w wybranym okresie.</div>
<div class="chart-wrap">
<canvas id="devicePieChart" height="260"></canvas>
</div>
<div id="deviceChartLegend" class="chart-legend"></div>
<div id="deviceChartEmpty" class="muted" style="text-align:center; display:none; margin-top:8px;">Brak skanowań w tym okresie.</div>
</div>
<div class="card col-12">
<h3>Kolejka akcji gościa (historia + statusy)</h3>
<div class="muted" style="margin-bottom:10px;">
@@ -139,6 +154,78 @@ requireAdminAuth(true);
const chips = Array.from(document.querySelectorAll(".chip"));
let selectedDays = "7";
let isLoadingAnalytics = false;
let devicePieChart = null;
const deviceChartLabels = {
ios: "iPhone (iOS)",
android: "Android",
other: "Inne",
};
const deviceChartColors = {
ios: "#60a5fa",
android: "#34d399",
other: "#94a3b8",
};
function renderDevicePieChart(stats) {
const canvas = document.getElementById("devicePieChart");
const legendEl = document.getElementById("deviceChartLegend");
const emptyEl = document.getElementById("deviceChartEmpty");
if (!canvas || typeof Chart === "undefined") return;
const order = ["ios", "android", "other"];
const values = order.map(k => Number(stats?.[k] || 0));
const total = values.reduce((a, b) => a + b, 0);
if (devicePieChart) {
devicePieChart.destroy();
devicePieChart = null;
}
if (total === 0) {
canvas.style.display = "none";
legendEl.innerHTML = "";
emptyEl.style.display = "block";
return;
}
canvas.style.display = "block";
emptyEl.style.display = "none";
devicePieChart = new Chart(canvas, {
type: "pie",
data: {
labels: order.map(k => deviceChartLabels[k]),
datasets: [{
data: values,
backgroundColor: order.map(k => deviceChartColors[k]),
borderColor: "#111827",
borderWidth: 2,
}],
},
options: {
responsive: true,
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
label(ctx) {
const v = ctx.parsed || 0;
const pct = total ? ((v / total) * 100).toFixed(1) : "0";
return ` ${ctx.label}: ${v.toLocaleString("pl-PL")} (${pct}%)`;
},
},
},
},
},
});
legendEl.innerHTML = order.map((k, i) => {
const v = values[i];
const pct = total ? ((v / total) * 100).toFixed(1) : "0";
return `<span class="chart-legend-item"><span class="chart-legend-dot" style="background:${deviceChartColors[k]}"></span>${deviceChartLabels[k]}: ${n(v)} (${pct}%)</span>`;
}).join("");
}
function setChip(days) {
chips.forEach(c => c.classList.toggle("active", c.dataset.days === String(days)));
@@ -208,6 +295,8 @@ requireAdminAuth(true);
}).join("")
: `<tr><td colspan="7" class="muted">Brak danych</td></tr>`;
renderDevicePieChart(data.deviceStats || {});
const queueSummary = data.guestQueueSummary || {};
document.getElementById("guestQueueSummaryBody").innerHTML = `
<tr>
@@ -242,6 +331,7 @@ requireAdminAuth(true);
document.getElementById("topTablesBody").innerHTML = `<tr><td colspan="5" class="muted">Nie udało się załadować analityki.</td></tr>`;
document.getElementById("zonesBody").innerHTML = `<tr><td colspan="3" class="muted">Sprawdź połączenie/API.</td></tr>`;
document.getElementById("recentOpensBody").innerHTML = `<tr><td colspan="7" class="muted">Nie udało się pobrać ostatnich otwarć.</td></tr>`;
renderDevicePieChart({});
document.getElementById("guestQueueSummaryBody").innerHTML = `<tr><td colspan="4" class="muted">Nie udało się pobrać podsumowania kolejki.</td></tr>`;
document.getElementById("guestQueueBody").innerHTML = `<tr><td colspan="8" class="muted">Nie udało się pobrać kolejki.</td></tr>`;
} finally {