Wykrywanie imienia i nazwiska kelnera oraz info czy sesja dostala sie do aplikacji

This commit is contained in:
2026-05-28 20:46:35 +02:00
parent a72b5afcc7
commit d374723fd6
12 changed files with 467 additions and 30 deletions

View File

@@ -133,6 +133,7 @@ try {
$sqlRecentOpens = "
SELECT
created_at,
session_id,
table_id,
zone,
device_type,
@@ -147,6 +148,45 @@ try {
$stmtRecentOpens->execute($baseParams);
$recentOpens = $stmtRecentOpens ? $stmtRecentOpens->fetchAll() : [];
$sessionOutcomes = [];
$sessionIds = [];
foreach ($recentOpens as $openRow) {
$sid = trim((string) ($openRow['session_id'] ?? ''));
if ($sid !== '') {
$sessionIds[$sid] = true;
}
}
$sessionIds = array_keys($sessionIds);
if (!empty($sessionIds)) {
$placeholders = implode(',', array_fill(0, count($sessionIds), '?'));
$sqlSessionFlags = "
SELECT
session_id,
MAX(CASE WHEN event_name = 'session_start' THEN 1 ELSE 0 END) AS reached_app,
MAX(CASE WHEN event_name = 'view_menu' THEN 1 ELSE 0 END) AS entered_menu
FROM analytics_events
WHERE session_id IN ({$placeholders})
GROUP BY session_id
";
$stmtSessionFlags = $pdo->prepare($sqlSessionFlags);
$stmtSessionFlags->execute($sessionIds);
while ($flagRow = $stmtSessionFlags->fetch()) {
$sessionOutcomes[$flagRow['session_id']] = [
'reached_app' => (int) $flagRow['reached_app'],
'entered_menu' => (int) $flagRow['entered_menu'],
];
}
}
foreach ($recentOpens as &$openRow) {
$sid = trim((string) ($openRow['session_id'] ?? ''));
$flags = $sessionOutcomes[$sid] ?? ['reached_app' => 0, 'entered_menu' => 0];
$openRow['reached_app'] = $flags['reached_app'];
$openRow['entered_menu'] = $flags['entered_menu'];
}
unset($openRow);
$sqlQueueSummary = "
SELECT
COUNT(*) AS total_actions,
@@ -171,6 +211,8 @@ try {
table_id,
message_type,
message_text,
otwierajacy_imie,
otwierajacy_nazwisko,
api_sent,
status_kds,
created_at

View File

@@ -18,9 +18,14 @@ $tsqlBills = "
r.ID,
r.Numer,
r.Opis,
s.Nazwa as NazwaStolika
s.Nazwa as NazwaStolika,
o.Imie AS OtwierajacyImie,
o.Nazwisko AS OtwierajacyNazwisko,
o.Nick AS OtwierajacyNick
FROM dbo.NGastroDTRachunek r
LEFT JOIN dbo.NGastroStolik s ON s.ID = r.StolikID
LEFT JOIN dbo.NGastroUzytkownik u ON u.ID = r.UzytkownikOtwierajacyID
LEFT JOIN dbo.NSysOperator o ON o.ID = u.OperatorID
WHERE CAST(r.DataOtwarcia as DATE) = CAST(GETDATE() as DATE)
AND r.Status = 0
";
@@ -63,12 +68,22 @@ while ($row = sqlsrv_fetch_array($stmtBills, SQLSRV_FETCH_ASSOC)) {
if ($isMatched) {
$billId = $row['ID'];
$matchedBillIds[] = $billId;
$imie = trim((string) ($row['OtwierajacyImie'] ?? ''));
$nazwisko = trim((string) ($row['OtwierajacyNazwisko'] ?? ''));
$nick = trim((string) ($row['OtwierajacyNick'] ?? ''));
$pelneImie = trim($imie . ' ' . $nazwisko);
$bills[$billId] = [
'id' => $billId,
'numer' => $row['Numer'],
'opis' => $row['Opis'],
'suma' => 0,
'pozycje' => []
'pozycje' => [],
'otwierajacy' => [
'imie' => $imie,
'nazwisko' => $nazwisko,
'nick' => $nick,
'nazwa' => $pelneImie !== '' ? $pelneImie : $nick,
],
];
}
}

View File

@@ -3,12 +3,62 @@ header('Content-Type: application/json; charset=utf-8');
require_once __DIR__ . '/../config/database.php';
require_once __DIR__ . '/get_table_name.php';
require_once __DIR__ . '/resolve_table_operator.php';
$kdsSecret = 'karczma_kuchnia';
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$secret = isset($_GET['kds_secret']) ? trim((string) $_GET['kds_secret']) : '';
if ($secret !== $kdsSecret) {
http_response_code(403);
echo json_encode([
'status' => 'error',
'message' => 'Forbidden',
], JSON_UNESCAPED_UNICODE);
exit;
}
try {
$pdo = getAnalyticsPdo();
$stmt = $pdo->query("
SELECT
id,
table_id,
message_type,
message_text,
otwierajacy_imie,
otwierajacy_nazwisko,
api_sent,
status_kds,
created_at
FROM guest_action_queue
WHERE status_kds = 0
AND created_at >= DATE_SUB(NOW(), INTERVAL 12 HOUR)
ORDER BY created_at ASC
LIMIT 50
");
$rows = $stmt->fetchAll();
echo json_encode([
'status' => 'success',
'count' => count($rows),
'data' => $rows,
], JSON_UNESCAPED_UNICODE);
} catch (Throwable $e) {
http_response_code(500);
echo json_encode([
'status' => 'error',
'message' => 'Queue fetch failed',
], JSON_UNESCAPED_UNICODE);
}
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode([
'status' => 'error',
'message' => 'Method not allowed'
'message' => 'Method not allowed',
], JSON_UNESCAPED_UNICODE);
exit;
}
@@ -19,22 +69,24 @@ if (!is_array($data)) {
http_response_code(400);
echo json_encode([
'status' => 'error',
'message' => 'Invalid JSON payload'
'message' => 'Invalid JSON payload',
], JSON_UNESCAPED_UNICODE);
exit;
}
$tableId = isset($data['tableId']) ? trim((string)$data['tableId']) : '';
$messageType = isset($data['messageType']) ? trim((string)$data['messageType']) : '';
$messageText = isset($data['messageText']) ? trim((string)$data['messageText']) : '';
$qrHash = isset($data['qrHash']) ? trim((string)$data['qrHash']) : '';
$tableId = isset($data['tableId']) ? trim((string) $data['tableId']) : '';
$messageType = isset($data['messageType']) ? trim((string) $data['messageType']) : '';
$messageText = isset($data['messageText']) ? trim((string) $data['messageText']) : '';
$qrHash = isset($data['qrHash']) ? trim((string) $data['qrHash']) : '';
$otwierajacyImie = isset($data['otwierajacyImie']) ? trim((string) $data['otwierajacyImie']) : '';
$otwierajacyNazwisko = isset($data['otwierajacyNazwisko']) ? trim((string) $data['otwierajacyNazwisko']) : '';
$allowedTypes = ['waiter_call', 'bill_request'];
if ($messageType === '' || !in_array($messageType, $allowedTypes, true)) {
http_response_code(422);
echo json_encode([
'status' => 'error',
'message' => 'Invalid messageType'
'message' => 'Invalid messageType',
], JSON_UNESCAPED_UNICODE);
exit;
}
@@ -50,7 +102,7 @@ if ($tableId === '') {
http_response_code(422);
echo json_encode([
'status' => 'error',
'message' => 'tableId is required'
'message' => 'tableId is required',
], JSON_UNESCAPED_UNICODE);
exit;
}
@@ -59,14 +111,33 @@ if ($messageText === '') {
http_response_code(422);
echo json_encode([
'status' => 'error',
'message' => 'messageText is required'
'message' => 'messageText is required',
], JSON_UNESCAPED_UNICODE);
exit;
}
if ($otwierajacyImie === '' && $otwierajacyNazwisko === '' && isset($conn)) {
$operator = resolveOperatorForTable($conn, strtolower($tableId));
if ($otwierajacyImie === '' && $operator['imie'] !== '') {
$otwierajacyImie = $operator['imie'];
}
if ($otwierajacyNazwisko === '' && $operator['nazwisko'] !== '') {
$otwierajacyNazwisko = $operator['nazwisko'];
}
if ($otwierajacyImie === '' && $otwierajacyNazwisko === '' && $operator['nick'] !== '') {
$otwierajacyImie = $operator['nick'];
}
}
if (strlen($tableId) > 32) {
$tableId = substr($tableId, 0, 32);
}
if (strlen($otwierajacyImie) > 100) {
$otwierajacyImie = substr($otwierajacyImie, 0, 100);
}
if (strlen($otwierajacyNazwisko) > 100) {
$otwierajacyNazwisko = substr($otwierajacyNazwisko, 0, 100);
}
try {
$pdo = getAnalyticsPdo();
@@ -75,6 +146,8 @@ try {
table_id,
message_type,
message_text,
otwierajacy_imie,
otwierajacy_nazwisko,
api_sent,
status_kds,
created_at
@@ -82,6 +155,8 @@ try {
:table_id,
:message_type,
:message_text,
:otwierajacy_imie,
:otwierajacy_nazwisko,
0,
0,
NOW(3)
@@ -92,17 +167,18 @@ try {
':table_id' => $tableId,
':message_type' => $messageType,
':message_text' => $messageText,
':otwierajacy_imie' => $otwierajacyImie !== '' ? $otwierajacyImie : null,
':otwierajacy_nazwisko' => $otwierajacyNazwisko !== '' ? $otwierajacyNazwisko : null,
]);
echo json_encode([
'status' => 'success',
'id' => (int)$pdo->lastInsertId()
'id' => (int) $pdo->lastInsertId(),
], JSON_UNESCAPED_UNICODE);
} catch (Throwable $e) {
http_response_code(500);
echo json_encode([
'status' => 'error',
'message' => 'Queue insert failed'
'message' => 'Queue insert failed',
], JSON_UNESCAPED_UNICODE);
}

View File

@@ -34,6 +34,9 @@ $tsql = "
r.Numer AS NumerRachunku,
r.Opis,
r.NumerReczny AS NumerRecznyRachunku,
o.Imie AS OtwierajacyImie,
o.Nazwisko AS OtwierajacyNazwisko,
o.Nick AS OtwierajacyNick,
rp.ID AS PozycjaID,
rp.StatusRealizacji,
rp.Ilosc,
@@ -51,6 +54,8 @@ $tsql = "
LEFT JOIN dbo.NGastroTowar t ON t.ID = rp.TowarID
LEFT JOIN dbo.NGastroTowar tz ON tz.ID = rp.ZestawID
LEFT JOIN dbo.NGastroStolik s ON s.ID = r.StolikID
LEFT JOIN dbo.NGastroUzytkownik u ON u.ID = r.UzytkownikOtwierajacyID
LEFT JOIN dbo.NSysOperator o ON o.ID = u.OperatorID
LEFT JOIN dbo.NGastroKonfiguracjaDrukowaniaZamowien kdz ON kdz.ID = ISNULL(rp.KonfiguracjaDrukowaniaZamowienID, t.KonfiguracjaDrukowaniaZamowienID)
WHERE r.Status = 0
AND rp.StatusRealizacji > 0

View File

@@ -0,0 +1,74 @@
<?php
function tableNameMatchesBill(string $tableParam, string $stolikNazwa, string $opis): bool
{
if ($tableParam === '') {
return true;
}
$normalizedTableParam = str_replace('o', '0', strtolower($tableParam));
$normalizedStolikNazwa = str_replace('o', '0', strtolower($stolikNazwa));
$normalizedOpis = str_replace('o', '0', strtolower($opis));
if (
$normalizedStolikNazwa === $normalizedTableParam ||
$normalizedOpis === $normalizedTableParam
) {
return true;
}
$pattern = '/\b' . preg_quote($normalizedTableParam, '/') . '\b/i';
return (bool) (preg_match($pattern, $normalizedStolikNazwa) || preg_match($pattern, $normalizedOpis));
}
/**
* @return array{imie: string, nazwisko: string, nick: string}
*/
function resolveOperatorForTable($conn, string $tableParam): array
{
$empty = ['imie' => '', 'nazwisko' => '', 'nick' => ''];
if (!$conn || $tableParam === '') {
return $empty;
}
$tsql = "
SELECT
s.Nazwa AS NazwaStolika,
r.Opis,
o.Imie,
o.Nazwisko,
o.Nick
FROM dbo.NGastroDTRachunek r
LEFT JOIN dbo.NGastroStolik s ON s.ID = r.StolikID
LEFT JOIN dbo.NGastroUzytkownik u ON u.ID = r.UzytkownikOtwierajacyID
LEFT JOIN dbo.NSysOperator o ON o.ID = u.OperatorID
WHERE CAST(r.DataOtwarcia AS DATE) = CAST(GETDATE() AS DATE)
AND r.Status = 0
";
$stmt = sqlsrv_query($conn, $tsql);
if ($stmt === false) {
return $empty;
}
while ($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) {
$stolikNazwa = (string) ($row['NazwaStolika'] ?? '');
$opis = (string) ($row['Opis'] ?? '');
if (!tableNameMatchesBill($tableParam, $stolikNazwa, $opis)) {
continue;
}
sqlsrv_free_stmt($stmt);
return [
'imie' => trim((string) ($row['Imie'] ?? '')),
'nazwisko' => trim((string) ($row['Nazwisko'] ?? '')),
'nick' => trim((string) ($row['Nick'] ?? '')),
];
}
sqlsrv_free_stmt($stmt);
return $empty;
}

View File

@@ -11,7 +11,7 @@
href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;600;700&family=Playfair+Display:wght@700&display=swap"
rel="stylesheet">
<link rel="stylesheet" href="assets/css/app.css">
</head>
</head>
<body>

View File

@@ -102,13 +102,54 @@ function trackEvent(eventName, payload = {}) {
});
}
let cachedOpenBills = [];
function cacheOpenBills(bills) {
cachedOpenBills = Array.isArray(bills) ? bills : [];
}
function getQueueOperatorFields() {
const bills = cachedOpenBills;
if (!bills.length) {
return { otwierajacyImie: "", otwierajacyNazwisko: "" };
}
let bill = bills[0];
if (billState.selectedBillId) {
const selected = bills.find((b) => b.id === billState.selectedBillId);
if (selected) bill = selected;
}
const o = bill.otwierajacy || {};
return {
otwierajacyImie: String(o.imie || "").trim(),
otwierajacyNazwisko: String(o.nazwisko || "").trim(),
};
}
async function prefetchOpenBills() {
if (!hashParam) return;
try {
const res = await fetch(`../api/bills.php?h=${encodeURIComponent(hashParam)}`);
const result = await res.json();
if (result.status === "success") {
cacheOpenBills(result.data);
}
} catch {
// best effort
}
}
function queueGuestAction(messageType, messageText, extra = {}) {
const operator = getQueueOperatorFields();
const body = {
tableId: tableParam || null,
qrHash: hashParam || null,
messageType,
messageText,
extra
otwierajacyImie: operator.otwierajacyImie,
otwierajacyNazwisko: operator.otwierajacyNazwisko,
extra,
};
fetch(guestActionQueueEndpoint, {
@@ -677,6 +718,7 @@ window.openBillDialog = async function () {
if (result.status === 'success' && result.data.length > 0) {
const bills = result.data;
cacheOpenBills(bills);
if (bills.length === 1) {
showBillReview(bills[0]);
@@ -1094,6 +1136,7 @@ function startApp() {
initUserProfile();
fetchOrders();
prefetchOpenBills();
if (!window.ordersInterval) {
window.ordersInterval = setInterval(fetchOrders, 10000);
}

View File

@@ -31,6 +31,8 @@ requireAdminAuth(true);
.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; }
.status-ok { color: #22c55e; font-weight: 700; }
.status-fail { color: #ef4444; font-weight: 700; }
@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>
@@ -98,8 +100,8 @@ requireAdminAuth(true);
<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>
<thead><tr><th>Kiedy</th><th>Stolik</th><th>Strefa</th><th>Menu</th><th>Urządzenie</th><th>Przeglądarka</th><th>IP</th></tr></thead>
<tbody id="recentOpensBody"><tr><td colspan="7" class="muted">Ładowanie...</td></tr></tbody>
</table>
</div>
@@ -113,8 +115,8 @@ requireAdminAuth(true);
<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>
<thead><tr><th>ID</th><th>Kiedy</th><th>Stolik</th><th>Kelner</th><th>Typ</th><th>Treść</th><th>Wysłane API</th><th>KDS gotowe</th></tr></thead>
<tbody id="guestQueueBody"><tr><td colspan="8" class="muted">Ładowanie...</td></tr></tbody>
</table>
</div>
@@ -127,6 +129,7 @@ requireAdminAuth(true);
<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>
<li><strong>Kolumna Menu (✓/✗)</strong> - ✓ oznacza wejście do menu w tej sesji; ✗ oznacza brak wejścia do menu (w tym zatrzymanie na geo).</li>
</ul>
</div>
</div>
@@ -197,9 +200,17 @@ requireAdminAuth(true);
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>`;
const enteredMenu = Number(r.entered_menu) === 1;
const reachedApp = Number(r.reached_app) === 1;
let menuCell = `<span class="status-fail" title="Zatrzymano na ekranie lokalizacji">✗</span>`;
if (enteredMenu) {
menuCell = `<span class="status-ok" title="Wszedł do menu">✓</span>`;
} else if (reachedApp) {
menuCell = `<span class="status-fail" title="Wszedł do aplikacji, ale nie otworzył menu">✗</span>`;
}
return `<tr><td>${when}</td><td>${r.table_id || "-"}</td><td>${r.zone || "-"}</td><td>${menuCell}</td><td>${device}</td><td>${browser}</td><td>${r.ip_address || "-"}</td></tr>`;
}).join("")
: `<tr><td colspan="6" class="muted">Brak danych</td></tr>`;
: `<tr><td colspan="7" class="muted">Brak danych</td></tr>`;
const queueSummary = data.guestQueueSummary || {};
document.getElementById("guestQueueSummaryBody").innerHTML = `
@@ -224,15 +235,19 @@ requireAdminAuth(true);
.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>`;
const kelner = [row.otwierajacy_imie, row.otwierajacy_nazwisko]
.map(v => String(v || "").trim())
.filter(Boolean)
.join(" ") || "-";
return `<tr><td>${row.id}</td><td>${when}</td><td>${row.table_id || "-"}</td><td>${kelner}</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>`;
: `<tr><td colspan="8" 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("recentOpensBody").innerHTML = `<tr><td colspan="7" 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>`;
document.getElementById("guestQueueBody").innerHTML = `<tr><td colspan="8" class="muted">Nie udało się pobrać kolejki.</td></tr>`;
} finally {
isLoadingAnalytics = false;
}

View File

@@ -135,11 +135,22 @@ requireAdminAuth(true);
color: var(--accent);
font-weight: 600;
font-size: 1.1rem;
margin-bottom: 15px;
margin-bottom: 8px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.order-operator {
color: var(--text-muted);
font-size: 0.9rem;
margin-bottom: 15px;
}
.order-operator strong {
color: #cbd5e1;
font-weight: 600;
}
.order-items {
display: flex;
flex-direction: column;
@@ -226,6 +237,68 @@ requireAdminAuth(true);
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.guest-alerts {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 24px;
}
.guest-alert {
border-radius: 14px;
padding: 16px 18px;
border: 1px solid #334155;
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
animation: slideIn 0.3s ease-out;
}
.guest-alert-waiter {
border-left: 5px solid var(--danger);
box-shadow: 0 0 24px rgba(239, 68, 68, 0.15);
}
.guest-alert-bill {
border-left: 5px solid var(--warning);
}
.guest-alert-title {
font-size: 1.05rem;
font-weight: 800;
margin-bottom: 6px;
text-transform: uppercase;
letter-spacing: 0.4px;
}
.guest-alert-waiter .guest-alert-title {
color: #fca5a5;
}
.guest-alert-bill .guest-alert-title {
color: #fcd34d;
}
.guest-alert-operator {
font-size: 0.95rem;
color: var(--text-muted);
margin-bottom: 8px;
}
.guest-alert-operator strong {
color: #e2e8f0;
}
.guest-alert-msg {
font-size: 0.9rem;
color: #cbd5e1;
line-height: 1.4;
}
.guest-alert-time {
margin-top: 8px;
font-size: 0.8rem;
color: var(--text-muted);
}
</style>
</head>
<body>
@@ -242,6 +315,8 @@ requireAdminAuth(true);
<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="guest-alerts" class="guest-alerts"></div>
<div id="kds-grid" class="kds-grid">
<div id="loading">
<div class="loader-spinner"></div>
@@ -254,6 +329,74 @@ requireAdminAuth(true);
const lastSyncEl = document.getElementById('last-sync');
const dotEl = document.getElementById('connection-dot');
let previousDataString = "";
let previousAlertsString = "";
function escapeHtml(value) {
return String(value ?? "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
}
function formatOtwierajacy(item) {
const imie = (item.OtwierajacyImie || "").trim();
const nazwisko = (item.OtwierajacyNazwisko || "").trim();
const pelne = `${imie} ${nazwisko}`.trim();
if (pelne) return pelne;
return (item.OtwierajacyNick || "").trim();
}
function formatQueueOperator(row) {
const imie = (row.otwierajacy_imie || "").trim();
const nazwisko = (row.otwierajacy_nazwisko || "").trim();
return `${imie} ${nazwisko}`.trim();
}
async function fetchGuestAlerts() {
try {
const response = await fetch("../../api/guest_action_queue.php?kds_secret=karczma_kuchnia");
const result = await response.json();
if (result.status !== "success") return;
const alertsString = JSON.stringify(result.data || []);
if (alertsString !== previousAlertsString) {
renderGuestAlerts(result.data || []);
previousAlertsString = alertsString;
}
} catch {
// best effort
}
}
function renderGuestAlerts(items) {
const container = document.getElementById("guest-alerts");
if (!items.length) {
container.innerHTML = "";
return;
}
container.innerHTML = items.map(row => {
const isWaiter = row.message_type === "waiter_call";
const typeLabel = isWaiter ? "Wezwanie kelnera" : "Prośba o rachunek";
const operator = formatQueueOperator(row);
const operatorHtml = operator
? `<div class="guest-alert-operator">Kelner stolika: <strong>${escapeHtml(operator)}</strong></div>`
: "";
const dt = row.created_at ? new Date(String(row.created_at).replace(" ", "T")) : null;
const when = dt && !Number.isNaN(dt.getTime())
? dt.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })
: "";
return `
<div class="guest-alert ${isWaiter ? "guest-alert-waiter" : "guest-alert-bill"}">
<div class="guest-alert-title">${typeLabel} · STOLIK ${escapeHtml(row.table_id || "?")}</div>
${operatorHtml}
<div class="guest-alert-msg">${escapeHtml(row.message_text || "")}</div>
<div class="guest-alert-time">${when}</div>
</div>
`;
}).join("");
}
async function fetchOrders() {
try {
@@ -303,6 +446,7 @@ requireAdminAuth(true);
stolik: item.NazwaStolika || item.StolikID,
stolikId: item.StolikID,
time: item.DataDodania,
otwierajacy: formatOtwierajacy(item),
groups: {}
};
}
@@ -338,6 +482,9 @@ requireAdminAuth(true);
const timeStr = new Date(order.time).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
const stolikLink = order.stolikId ? `<a href="../app.html?h=${order.stolikId}" target="_blank" style="color: var(--accent); text-decoration: underline;">STOLIK: ${order.stolik}</a>` : `STOLIK: ${order.stolik}`;
const stolikText = order.stolik ? stolikLink : 'BRAK STOLIKA';
const operatorHtml = order.otwierajacy
? `<div class="order-operator">Kelner: <strong>${order.otwierajacy}</strong></div>`
: '';
let itemsHtml = '';
@@ -375,6 +522,7 @@ requireAdminAuth(true);
<div class="order-time">${timeStr}</div>
</div>
<div class="order-table">${stolikText}</div>
${operatorHtml}
<div class="order-items">
${itemsHtml}
</div>
@@ -384,11 +532,11 @@ requireAdminAuth(true);
});
}
// Pierwsze pobranie
fetchOrders();
// Pętla odświeżania co 3 sekundy
fetchGuestAlerts();
setInterval(fetchOrders, 3000);
setInterval(fetchGuestAlerts, 3000);
</script>
</body>
</html>

View File

@@ -3,6 +3,8 @@ CREATE TABLE IF NOT EXISTS guest_action_queue (
table_id VARCHAR(32) NOT NULL,
message_type ENUM('waiter_call', 'bill_request') NOT NULL,
message_text TEXT NOT NULL,
otwierajacy_imie VARCHAR(100) NULL,
otwierajacy_nazwisko VARCHAR(100) NULL,
api_sent TINYINT(1) NOT NULL DEFAULT 0,
status_kds TINYINT(1) NOT NULL DEFAULT 0,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),

View File

@@ -0,0 +1,3 @@
ALTER TABLE guest_action_queue
ADD COLUMN otwierajacy_imie VARCHAR(100) NULL AFTER message_text,
ADD COLUMN otwierajacy_nazwisko VARCHAR(100) NULL AFTER otwierajacy_imie;

View File

@@ -0,0 +1,14 @@
<?php
require_once __DIR__ . '/../config/database.php';
$pdo = getAnalyticsPdo();
$columns = $pdo->query("SHOW COLUMNS FROM guest_action_queue LIKE 'otwierajacy_imie'")->fetch();
if ($columns) {
echo "Kolumny otwierajacy_* już istnieją.\n";
exit(0);
}
$sql = file_get_contents(__DIR__ . '/guest_action_queue_add_operator.sql');
$pdo->exec($sql);
echo "Dodano kolumny otwierajacy_imie i otwierajacy_nazwisko.\n";