Files
karczma-aplikacja-stoliki/public/staff/index.php

256 lines
15 KiB
PHP

<?php
require_once __DIR__ . '/auth.php';
requireAdminAuth(true);
?>
<!doctype html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Panel Admina</title>
<style>
:root { --bg:#0f172a; --surface:#111827; --line:#334155; --text:#e2e8f0; --muted:#94a3b8; --accent:#3b82f6; }
* { box-sizing: border-box; }
body { margin: 0; font-family: Inter, Arial, sans-serif; background: var(--bg); color: var(--text); }
.wrap { width: min(1200px, 94vw); margin: 24px auto; }
.top { display: flex; justify-content: space-between; align-items: center; gap: 10px; margin-bottom: 16px; }
.title { font-size: 1.5rem; font-weight: 800; }
.btn { display: inline-block; border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px; color: var(--text); text-decoration: none; background: #0b1220; }
.btn.primary { background: var(--accent); border-color: var(--accent); color: white; }
.grid { display: grid; gap: 14px; grid-template-columns: repeat(12,1fr); }
.card { background: var(--surface); border: 1px solid var(--line); border-radius: 12px; padding: 14px; }
.card h3 { margin: 0 0 8px; font-size: 1rem; color: #cbd5e1; }
.kpi { font-size: 1.8rem; font-weight: 800; margin-top: 4px; }
.muted { color: var(--muted); font-size: .88rem; }
.col-3 { grid-column: span 3; } .col-4 { grid-column: span 4; } .col-6 { grid-column: span 6; } .col-8 { grid-column: span 8; } .col-12 { grid-column: span 12; }
table { width: 100%; border-collapse: collapse; }
th, td { border-bottom: 1px solid #263446; padding: 8px 6px; text-align: left; font-size: .9rem; }
th { color: #cbd5e1; }
.filters { display: flex; gap: 8px; flex-wrap: wrap; }
.chip { border: 1px solid var(--line); border-radius: 999px; padding: 7px 11px; background: #0b1220; color: var(--text); cursor: pointer; }
.chip.active { background: var(--accent); border-color: var(--accent); color: #fff; }
.legend-list { margin: 0; padding-left: 18px; line-height: 1.6; color: #cbd5e1; }
.legend-list li { margin-bottom: 6px; }
@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>
<body>
<div class="wrap">
<div class="top">
<div>
<div class="title">Panel Admina</div>
<div class="muted">KDS, generator QR i analityka operacyjna.</div>
</div>
<div style="display:flex; gap:8px; flex-wrap:wrap;">
<a class="btn" href="kds.php">Otwórz KDS</a>
<a class="btn" href="generator.php">Generator QR</a>
<a class="btn" href="logout.php">Wyloguj</a>
</div>
</div>
<div class="card col-12" style="margin-bottom:14px;">
<div class="filters">
<button class="chip active" data-days="7">Ostatnie 7 dni</button>
<button class="chip" data-days="30">Ostatnie 30 dni</button>
<button class="chip" data-days="90">Ostatnie 90 dni</button>
<button class="chip" data-days="this_year">Ten rok</button>
<button class="chip" data-days="last_year">Poprzedni rok</button>
<button class="chip" data-days="all">Cały dostępny</button>
</div>
</div>
<div class="grid">
<div class="card col-3"><h3>Skanowania QR</h3><div class="kpi" id="kpiScans">-</div><div class="muted">Otwarcia stron stolików</div></div>
<div class="card col-3"><h3>Sesje</h3><div class="kpi" id="kpiSessions">-</div><div class="muted">Użytkownicy, którzy weszli do aplikacji</div></div>
<div class="card col-3"><h3>Lokalizacja OK</h3><div class="kpi" id="kpiGeoPass">-</div><div class="muted">Pozytywna weryfikacja geolokalizacji</div></div>
<div class="card col-3"><h3>Przywołania kelnera</h3><div class="kpi" id="kpiWaiterCall">-</div><div class="muted">Wysłane prośby o podejście obsługi</div></div>
<div class="card col-8">
<h3>Top stoliki</h3>
<table>
<thead><tr><th>Stolik</th><th>Skany</th><th>Sesje</th><th>Geo pass</th><th>Geo fail</th></tr></thead>
<tbody id="topTablesBody"><tr><td colspan="5" class="muted">Ładowanie...</td></tr></tbody>
</table>
</div>
<div class="card col-4">
<h3>Strefy (Karczma/Taras)</h3>
<table>
<thead><tr><th>Strefa</th><th>Skany</th><th>Sesje</th></tr></thead>
<tbody id="zonesBody"><tr><td colspan="3" class="muted">Ładowanie...</td></tr></tbody>
</table>
</div>
<div class="card col-6">
<h3>Lejek użycia (droga użytkownika)</h3>
<table>
<tbody id="funnelBody"></tbody>
</table>
</div>
<div class="card col-6">
<h3>Geolokalizacja</h3>
<table>
<tbody id="geoBody"></tbody>
</table>
</div>
<div class="card col-12">
<h3>Ostatnie otwarcia stron stolików</h3>
<table>
<thead><tr><th>Kiedy</th><th>Stolik</th><th>Strefa</th><th>Urządzenie</th><th>Przeglądarka</th><th>IP</th></tr></thead>
<tbody id="recentOpensBody"><tr><td colspan="6" class="muted">Ładowanie...</td></tr></tbody>
</table>
</div>
<div class="card col-12">
<h3>Kolejka akcji gościa (historia + statusy)</h3>
<div class="muted" style="margin-bottom:10px;">
Historia jest zachowywana. Rekordów nie usuwamy, pracujemy na statusach `api_sent` i `status_kds`.
</div>
<table style="margin-bottom:10px;">
<thead><tr><th>Wszystkie akcje</th><th>Do wysyłki API</th><th>Oczekuje na KDS</th><th>Oznaczone jako gotowe</th></tr></thead>
<tbody id="guestQueueSummaryBody"><tr><td colspan="4" class="muted">Ładowanie...</td></tr></tbody>
</table>
<table>
<thead><tr><th>ID</th><th>Kiedy</th><th>Stolik</th><th>Typ</th><th>Treść</th><th>Wysłane API</th><th>KDS gotowe</th></tr></thead>
<tbody id="guestQueueBody"><tr><td colspan="7" class="muted">Ładowanie...</td></tr></tbody>
</table>
</div>
<div class="card col-12">
<h3>Jak czytać te dane?</h3>
<ul class="legend-list">
<li><strong>Skanowania QR</strong> - ile razy otwarto stronę stolika z kodu QR.</li>
<li><strong>Sesje</strong> - ile razy użytkownik faktycznie wszedł do aplikacji po starcie.</li>
<li><strong>Lokalizacja OK</strong> - ile razy weryfikacja geolokalizacji zakończyła się powodzeniem.</li>
<li><strong>Przywołania kelnera</strong> - ile razy gość poprosił o podejście obsługi.</li>
<li><strong>Top stoliki</strong> - które stoliki są najczęściej otwierane i używane.</li>
<li><strong>Lejek użycia</strong> - droga użytkownika: wejście -> start aplikacji -> menu -> rachunek.</li>
</ul>
</div>
</div>
</div>
<script>
const chips = Array.from(document.querySelectorAll(".chip"));
let selectedDays = "7";
let isLoadingAnalytics = false;
function setChip(days) {
chips.forEach(c => c.classList.toggle("active", c.dataset.days === String(days)));
}
function n(value) {
return Number(value || 0).toLocaleString("pl-PL");
}
async function loadAnalytics(days) {
if (isLoadingAnalytics) return;
isLoadingAnalytics = true;
try {
const res = await fetch(`../../api/analytics_reports.php?days=${days}`, { credentials: "same-origin" });
const data = await res.json();
if (data.status !== "success") throw new Error("API error");
const totalScans = (data.topTables || []).reduce((a, r) => a + Number(r.qr_scans || 0), 0);
const totalSessions = (data.topTables || []).reduce((a, r) => a + Number(r.sessions || 0), 0);
document.getElementById("kpiScans").textContent = n(totalScans);
document.getElementById("kpiSessions").textContent = n(totalSessions);
document.getElementById("kpiGeoPass").textContent = n(data.geolocation?.passed);
document.getElementById("kpiWaiterCall").textContent = n(data.funnel?.waiter_call_requested);
const topBody = document.getElementById("topTablesBody");
const top = data.topTables || [];
topBody.innerHTML = top.length
? top.map(r => `<tr><td>${r.table_id ?? "-"}</td><td>${n(r.qr_scans)}</td><td>${n(r.sessions)}</td><td>${n(r.geo_pass)}</td><td>${n(r.geo_fail)}</td></tr>`).join("")
: `<tr><td colspan="5" class="muted">Brak danych</td></tr>`;
const zonesBody = document.getElementById("zonesBody");
const zones = data.zoneStats || [];
zonesBody.innerHTML = zones.length
? zones.map(r => `<tr><td>${r.zone ?? "unknown"}</td><td>${n(r.qr_scans)}</td><td>${n(r.sessions)}</td></tr>`).join("")
: `<tr><td colspan="3" class="muted">Brak danych</td></tr>`;
const f = data.funnel || {};
document.getElementById("funnelBody").innerHTML = `
<tr><td>Otwarcie strony stolika (QR)</td><td>${n(f.qr_scan)}</td></tr>
<tr><td>Start aplikacji</td><td>${n(f.session_start)}</td></tr>
<tr><td>Wejście do menu</td><td>${n(f.view_menu)}</td></tr>
<tr><td>Przywołanie kelnera</td><td>${n(f.waiter_call_requested)}</td></tr>
<tr><td>Otwarcie modułu rachunku</td><td>${n(f.bill_dialog_opened)}</td></tr>
<tr><td>Wysłanie prośby o rachunek</td><td>${n(f.bill_request_sent)}</td></tr>
`;
const g = data.geolocation || {};
document.getElementById("geoBody").innerHTML = `
<tr><td>Lokalizacja potwierdzona</td><td>${n(g.passed)}</td></tr>
<tr><td>Lokalizacja odrzucona / błąd</td><td>${n(g.failed)}</td></tr>
<tr><td>Wejście z hosta bypass</td><td>${n(g.bypass)}</td></tr>
`;
const recent = data.recentOpens || [];
const recentBody = document.getElementById("recentOpensBody");
recentBody.innerHTML = recent.length
? recent.map(r => {
const dt = r.created_at ? new Date(String(r.created_at).replace(" ", "T")) : null;
const when = dt && !Number.isNaN(dt.getTime()) ? dt.toLocaleString("pl-PL") : (r.created_at || "-");
const device = r.device_type || "-";
const browser = r.browser || "-";
return `<tr><td>${when}</td><td>${r.table_id || "-"}</td><td>${r.zone || "-"}</td><td>${device}</td><td>${browser}</td><td>${r.ip_address || "-"}</td></tr>`;
}).join("")
: `<tr><td colspan="6" class="muted">Brak danych</td></tr>`;
const queueSummary = data.guestQueueSummary || {};
document.getElementById("guestQueueSummaryBody").innerHTML = `
<tr>
<td>${n(queueSummary.total)}</td>
<td>${n(queueSummary.pendingApi)}</td>
<td>${n(queueSummary.pendingKds)}</td>
<td>${n(queueSummary.doneKds)}</td>
</tr>
`;
const queueRows = data.guestQueue || [];
const queueBody = document.getElementById("guestQueueBody");
queueBody.innerHTML = queueRows.length
? queueRows.map(row => {
const dt = row.created_at ? new Date(String(row.created_at).replace(" ", "T")) : null;
const when = dt && !Number.isNaN(dt.getTime()) ? dt.toLocaleString("pl-PL") : (row.created_at || "-");
const typeLabel = row.message_type === "waiter_call" ? "Przywołanie kelnera" : "Prośba o rachunek";
const apiSent = Number(row.api_sent) === 1 ? "Tak" : "Nie";
const kdsDone = Number(row.status_kds) === 1 ? "Tak" : "Nie";
const messageText = String(row.message_text || "-")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
return `<tr><td>${row.id}</td><td>${when}</td><td>${row.table_id || "-"}</td><td>${typeLabel}</td><td>${messageText}</td><td>${apiSent}</td><td>${kdsDone}</td></tr>`;
}).join("")
: `<tr><td colspan="7" class="muted">Brak danych</td></tr>`;
} catch (e) {
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="6" class="muted">Nie udało się pobrać ostatnich otwarć.</td></tr>`;
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="7" class="muted">Nie udało się pobrać kolejki.</td></tr>`;
} finally {
isLoadingAnalytics = false;
}
}
chips.forEach(chip => {
chip.addEventListener("click", () => {
selectedDays = chip.dataset.days;
setChip(selectedDays);
loadAnalytics(selectedDays);
});
});
setChip(selectedDays);
loadAnalytics(selectedDays);
setInterval(() => loadAnalytics(selectedDays), 60000);
</script>
</body>
</html>