Baza danych i zapis akcji przywoływania kelnera. Czekam na endpointy lub coś z KDS
This commit is contained in:
54
public/staff/auth.php
Normal file
54
public/staff/auth.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
const ADMIN_USERNAME = 'admin';
|
||||
const ADMIN_PASSWORD = 'karczma2026!';
|
||||
|
||||
function startAdminSession(): void
|
||||
{
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
}
|
||||
|
||||
function isAdminLoggedIn(): bool
|
||||
{
|
||||
startAdminSession();
|
||||
return !empty($_SESSION['staff_logged_in']) && $_SESSION['staff_logged_in'] === true;
|
||||
}
|
||||
|
||||
function requireAdminAuth(bool $redirectToLogin = true): void
|
||||
{
|
||||
if (isAdminLoggedIn()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($redirectToLogin) {
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
function attemptAdminLogin(string $username, string $password): bool
|
||||
{
|
||||
startAdminSession();
|
||||
|
||||
if (hash_equals(ADMIN_USERNAME, $username) && hash_equals(ADMIN_PASSWORD, $password)) {
|
||||
$_SESSION['staff_logged_in'] = true;
|
||||
$_SESSION['staff_username'] = ADMIN_USERNAME;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function adminLogout(): void
|
||||
{
|
||||
startAdminSession();
|
||||
$_SESSION = [];
|
||||
if (ini_get('session.use_cookies')) {
|
||||
$params = session_get_cookie_params();
|
||||
setcookie(session_name(), '', time() - 42000, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
|
||||
}
|
||||
session_destroy();
|
||||
}
|
||||
|
||||
@@ -1,99 +1,111 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/auth.php';
|
||||
requireAdminAuth(true);
|
||||
require_once __DIR__ . '/../../config/database.php';
|
||||
|
||||
$tsql = "SELECT ID, Nazwa FROM dbo.NGastroStolik ORDER BY Nazwa";
|
||||
$stmt = sqlsrv_query($conn, $tsql);
|
||||
|
||||
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
|
||||
$baseUrl = "https://$host/app/public/app.html?h=";
|
||||
|
||||
echo "<!DOCTYPE html>
|
||||
<html lang='pl'>
|
||||
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
|
||||
$scriptDir = str_replace('\\', '/', dirname($_SERVER['SCRIPT_NAME'] ?? '/public/staff'));
|
||||
$publicDir = str_replace('\\', '/', dirname($scriptDir));
|
||||
$basePath = rtrim($publicDir, '/') . '/app.html?h=';
|
||||
$baseUrl = "{$scheme}://{$host}{$basePath}";
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="pl">
|
||||
<head>
|
||||
<meta charset='UTF-8'>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Generator Linków QR - Stoliki</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; padding: 20px; }
|
||||
table { border-collapse: collapse; width: 100%; max-width: 800px; }
|
||||
th, td { border: 1px solid #ccc; padding: 10px; text-align: left; }
|
||||
th { background: #eee; }
|
||||
a { color: #0066cc; text-decoration: none; }
|
||||
body { font-family: Inter, Arial, sans-serif; padding: 20px; background: #0f172a; color: #e2e8f0; }
|
||||
h1 { margin: 0 0 10px; }
|
||||
p { color: #94a3b8; }
|
||||
table { border-collapse: collapse; width: 100%; max-width: 1100px; background: #111827; border: 1px solid #334155; }
|
||||
th, td { border: 1px solid #334155; padding: 10px; text-align: left; }
|
||||
th { background: #0b1220; }
|
||||
a { color: #93c5fd; text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
.btn-qr { padding: 5px 10px; background: #3b82f6; color: #fff; border: none; border-radius: 4px; cursor: pointer; }
|
||||
.btn-qr { padding: 8px 10px; background: #3b82f6; color: #fff; border: none; border-radius: 8px; cursor: pointer; }
|
||||
.btn-qr:hover { background: #2563eb; }
|
||||
.modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); align-items: center; justify-content: center; }
|
||||
.modal-content { background: #fff; padding: 20px; border-radius: 8px; text-align: center; max-width: 400px; width: 90%; }
|
||||
.close { float: right; font-size: 24px; font-weight: bold; cursor: pointer; line-height: 20px; }
|
||||
.btn-nav { display:inline-block; margin-right:8px; margin-bottom:14px; color:#cbd5e1; text-decoration:none; border:1px solid #334155; padding:8px 12px; border-radius:10px; }
|
||||
.modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.7); align-items: center; justify-content: center; }
|
||||
.modal-content { background: #111827; border:1px solid #334155; padding: 20px; border-radius: 8px; text-align: center; max-width: 420px; width: 90%; }
|
||||
.close { float: right; font-size: 24px; font-weight: bold; cursor: pointer; line-height: 20px; color:#cbd5e1; }
|
||||
#qrcode { margin-top: 20px; display: flex; justify-content: center; }
|
||||
</style>
|
||||
<script src='https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js'></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Linki do aplikacji (Kody QR)</h1>
|
||||
<a class="btn-nav" href="index.php">Wróć do panelu admina</a>
|
||||
<a class="btn-nav" href="logout.php">Wyloguj</a>
|
||||
<h1>Linki do aplikacji (kody QR)</h1>
|
||||
<p>Skopiuj poniższe linki lub wygeneruj z nich kody QR do umieszczenia na stolikach.</p>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Nazwa stolika</th>
|
||||
<th>Hash (ID z bazy)</th>
|
||||
<th>Bezpieczny Link (KOD QR)</th>
|
||||
<th>Bezpieczny link</th>
|
||||
<th>Akcje</th>
|
||||
</tr>";
|
||||
</tr>
|
||||
<?php while ($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)): ?>
|
||||
<?php
|
||||
$id = strtoupper((string)$row['ID']);
|
||||
$nazwa = htmlspecialchars((string)$row['Nazwa'], ENT_QUOTES, 'UTF-8');
|
||||
$link = $baseUrl . $id;
|
||||
?>
|
||||
<tr>
|
||||
<td><strong><?= $nazwa ?></strong></td>
|
||||
<td style="font-size:.85em;color:#94a3b8;"><?= $id ?></td>
|
||||
<td><a href="<?= htmlspecialchars($link, ENT_QUOTES, 'UTF-8') ?>" target="_blank"><?= htmlspecialchars($link, ENT_QUOTES, 'UTF-8') ?></a></td>
|
||||
<td><button class="btn-qr" onclick="openQR('<?= htmlspecialchars($link, ENT_QUOTES, 'UTF-8') ?>', '<?= $nazwa ?>')">Pokaż QR</button></td>
|
||||
</tr>
|
||||
<?php endwhile; ?>
|
||||
</table>
|
||||
|
||||
while ($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) {
|
||||
$id = strtoupper($row['ID']);
|
||||
$nazwa = htmlspecialchars($row['Nazwa']);
|
||||
$link = $baseUrl . $id;
|
||||
echo "<tr>
|
||||
<td><strong>$nazwa</strong></td>
|
||||
<td style='font-size: 0.8em; color: #666;'>$id</td>
|
||||
<td><a href='$link' target='_blank'>$link</a></td>
|
||||
<td><button class='btn-qr' onclick='openQR(\"$link\", \"$nazwa\")'>Pokaż QR</button></td>
|
||||
</tr>";
|
||||
}
|
||||
|
||||
echo " </table>
|
||||
|
||||
<div id='qrModal' class='modal'>
|
||||
<div class='modal-content'>
|
||||
<span class='close' onclick='closeQR()'>×</span>
|
||||
<h2 id='qrTitle'>Kod QR</h2>
|
||||
<div id='qrcode'></div>
|
||||
<p style='margin-top:20px;'><a id='qrLink' href='' target='_blank' style='color: #3b82f6;'>Otwórz link w nowym oknie</a></p>
|
||||
<div id="qrModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="closeQR()">×</span>
|
||||
<h2 id="qrTitle">Kod QR</h2>
|
||||
<div id="qrcode"></div>
|
||||
<p style="margin-top:20px;"><a id="qrLink" href="" target="_blank">Otwórz link w nowym oknie</a></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function openQR(link, nazwa) {
|
||||
document.getElementById('qrModal').style.display = 'flex';
|
||||
document.getElementById('qrTitle').innerText = 'Stolik: ' + nazwa;
|
||||
document.getElementById('qrLink').href = link;
|
||||
|
||||
var qrContainer = document.getElementById('qrcode');
|
||||
qrContainer.innerHTML = ''; // Wyczyść poprzedni kod
|
||||
|
||||
document.getElementById("qrModal").style.display = "flex";
|
||||
document.getElementById("qrTitle").innerText = "Stolik: " + nazwa;
|
||||
document.getElementById("qrLink").href = link;
|
||||
|
||||
const qrContainer = document.getElementById("qrcode");
|
||||
qrContainer.innerHTML = "";
|
||||
|
||||
new QRCode(qrContainer, {
|
||||
text: link,
|
||||
width: 250,
|
||||
height: 250,
|
||||
colorDark : '#000000',
|
||||
colorLight : '#ffffff',
|
||||
correctLevel : QRCode.CorrectLevel.H
|
||||
colorDark: "#000000",
|
||||
colorLight: "#ffffff",
|
||||
correctLevel: QRCode.CorrectLevel.H
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function closeQR() {
|
||||
document.getElementById('qrModal').style.display = 'none';
|
||||
document.getElementById("qrModal").style.display = "none";
|
||||
}
|
||||
|
||||
window.onclick = function(event) {
|
||||
var modal = document.getElementById('qrModal');
|
||||
if (event.target == modal) {
|
||||
|
||||
window.onclick = function (event) {
|
||||
const modal = document.getElementById("qrModal");
|
||||
if (event.target === modal) {
|
||||
closeQR();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>";
|
||||
|
||||
</html>
|
||||
<?php
|
||||
sqlsrv_free_stmt($stmt);
|
||||
sqlsrv_close($conn);
|
||||
|
||||
255
public/staff/index.php
Normal file
255
public/staff/index.php
Normal file
@@ -0,0 +1,255 @@
|
||||
<?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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
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>
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/auth.php';
|
||||
requireAdminAuth(true);
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="pl">
|
||||
<head>
|
||||
@@ -233,6 +237,10 @@
|
||||
<span id="last-sync">Łączenie z bazą...</span>
|
||||
</div>
|
||||
</header>
|
||||
<div style="margin-bottom: 16px; display:flex; gap:8px; flex-wrap:wrap;">
|
||||
<a href="index.php" style="color:#93c5fd; text-decoration:none; border:1px solid #334155; padding:8px 12px; border-radius:10px;">Wróć do panelu admina</a>
|
||||
<a href="logout.php" style="color:#cbd5e1; text-decoration:none; border:1px solid #334155; padding:8px 12px; border-radius:10px;">Wyloguj</a>
|
||||
</div>
|
||||
|
||||
<div id="kds-grid" class="kds-grid">
|
||||
<div id="loading">
|
||||
|
||||
55
public/staff/login.php
Normal file
55
public/staff/login.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/auth.php';
|
||||
|
||||
startAdminSession();
|
||||
if (isAdminLoggedIn()) {
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$error = '';
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$username = trim((string)($_POST['username'] ?? ''));
|
||||
$password = (string)($_POST['password'] ?? '');
|
||||
if (attemptAdminLogin($username, $password)) {
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
$error = 'Nieprawidłowy login lub hasło.';
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="pl">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Panel Admina - Logowanie</title>
|
||||
<style>
|
||||
body { font-family: Inter, Arial, sans-serif; background: #0f172a; color: #e2e8f0; margin: 0; min-height: 100vh; display: grid; place-items: center; }
|
||||
.card { width: min(420px, 92vw); background: #111827; border: 1px solid #334155; border-radius: 14px; padding: 24px; }
|
||||
h1 { margin: 0 0 8px; font-size: 1.4rem; }
|
||||
p { margin: 0 0 20px; color: #94a3b8; }
|
||||
label { display: block; margin-bottom: 6px; font-size: .9rem; color: #cbd5e1; }
|
||||
input { width: 100%; box-sizing: border-box; border: 1px solid #475569; background: #0b1220; color: #e2e8f0; border-radius: 10px; padding: 11px 12px; margin-bottom: 14px; }
|
||||
button { width: 100%; border: 0; border-radius: 10px; padding: 11px 12px; background: #3b82f6; color: #fff; font-weight: 600; cursor: pointer; }
|
||||
.error { margin-bottom: 12px; padding: 10px; border-radius: 9px; background: #451a1a; color: #fecaca; border: 1px solid #7f1d1d; }
|
||||
.hint { margin-top: 14px; font-size: .82rem; color: #64748b; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<form class="card" method="post" action="">
|
||||
<h1>Panel Admina</h1>
|
||||
<p>Zaloguj się, aby przejść do KDS, generatora QR i analityki.</p>
|
||||
<?php if ($error !== ''): ?>
|
||||
<div class="error"><?= htmlspecialchars($error, ENT_QUOTES, 'UTF-8') ?></div>
|
||||
<?php endif; ?>
|
||||
<label for="username">Login</label>
|
||||
<input id="username" name="username" required autocomplete="username">
|
||||
<label for="password">Hasło</label>
|
||||
<input id="password" name="password" type="password" required autocomplete="current-password">
|
||||
<button type="submit">Zaloguj</button>
|
||||
<div class="hint">Dane logowania są ustawione w pliku `public/staff/auth.php`.</div>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
6
public/staff/logout.php
Normal file
6
public/staff/logout.php
Normal file
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/auth.php';
|
||||
adminLogout();
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
|
||||
Reference in New Issue
Block a user