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 = " $sqlRecentOpens = "
SELECT SELECT
created_at, created_at,
session_id,
table_id, table_id,
zone, zone,
device_type, device_type,
@@ -147,6 +148,45 @@ try {
$stmtRecentOpens->execute($baseParams); $stmtRecentOpens->execute($baseParams);
$recentOpens = $stmtRecentOpens ? $stmtRecentOpens->fetchAll() : []; $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 = " $sqlQueueSummary = "
SELECT SELECT
COUNT(*) AS total_actions, COUNT(*) AS total_actions,
@@ -171,6 +211,8 @@ try {
table_id, table_id,
message_type, message_type,
message_text, message_text,
otwierajacy_imie,
otwierajacy_nazwisko,
api_sent, api_sent,
status_kds, status_kds,
created_at created_at

View File

@@ -18,9 +18,14 @@ $tsqlBills = "
r.ID, r.ID,
r.Numer, r.Numer,
r.Opis, 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 FROM dbo.NGastroDTRachunek r
LEFT JOIN dbo.NGastroStolik s ON s.ID = r.StolikID 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) WHERE CAST(r.DataOtwarcia as DATE) = CAST(GETDATE() as DATE)
AND r.Status = 0 AND r.Status = 0
"; ";
@@ -63,12 +68,22 @@ while ($row = sqlsrv_fetch_array($stmtBills, SQLSRV_FETCH_ASSOC)) {
if ($isMatched) { if ($isMatched) {
$billId = $row['ID']; $billId = $row['ID'];
$matchedBillIds[] = $billId; $matchedBillIds[] = $billId;
$imie = trim((string) ($row['OtwierajacyImie'] ?? ''));
$nazwisko = trim((string) ($row['OtwierajacyNazwisko'] ?? ''));
$nick = trim((string) ($row['OtwierajacyNick'] ?? ''));
$pelneImie = trim($imie . ' ' . $nazwisko);
$bills[$billId] = [ $bills[$billId] = [
'id' => $billId, 'id' => $billId,
'numer' => $row['Numer'], 'numer' => $row['Numer'],
'opis' => $row['Opis'], 'opis' => $row['Opis'],
'suma' => 0, '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__ . '/../config/database.php';
require_once __DIR__ . '/get_table_name.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') { if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405); http_response_code(405);
echo json_encode([ echo json_encode([
'status' => 'error', 'status' => 'error',
'message' => 'Method not allowed' 'message' => 'Method not allowed',
], JSON_UNESCAPED_UNICODE); ], JSON_UNESCAPED_UNICODE);
exit; exit;
} }
@@ -19,7 +69,7 @@ if (!is_array($data)) {
http_response_code(400); http_response_code(400);
echo json_encode([ echo json_encode([
'status' => 'error', 'status' => 'error',
'message' => 'Invalid JSON payload' 'message' => 'Invalid JSON payload',
], JSON_UNESCAPED_UNICODE); ], JSON_UNESCAPED_UNICODE);
exit; exit;
} }
@@ -28,13 +78,15 @@ $tableId = isset($data['tableId']) ? trim((string)$data['tableId']) : '';
$messageType = isset($data['messageType']) ? trim((string) $data['messageType']) : ''; $messageType = isset($data['messageType']) ? trim((string) $data['messageType']) : '';
$messageText = isset($data['messageText']) ? trim((string) $data['messageText']) : ''; $messageText = isset($data['messageText']) ? trim((string) $data['messageText']) : '';
$qrHash = isset($data['qrHash']) ? trim((string) $data['qrHash']) : ''; $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']; $allowedTypes = ['waiter_call', 'bill_request'];
if ($messageType === '' || !in_array($messageType, $allowedTypes, true)) { if ($messageType === '' || !in_array($messageType, $allowedTypes, true)) {
http_response_code(422); http_response_code(422);
echo json_encode([ echo json_encode([
'status' => 'error', 'status' => 'error',
'message' => 'Invalid messageType' 'message' => 'Invalid messageType',
], JSON_UNESCAPED_UNICODE); ], JSON_UNESCAPED_UNICODE);
exit; exit;
} }
@@ -50,7 +102,7 @@ if ($tableId === '') {
http_response_code(422); http_response_code(422);
echo json_encode([ echo json_encode([
'status' => 'error', 'status' => 'error',
'message' => 'tableId is required' 'message' => 'tableId is required',
], JSON_UNESCAPED_UNICODE); ], JSON_UNESCAPED_UNICODE);
exit; exit;
} }
@@ -59,14 +111,33 @@ if ($messageText === '') {
http_response_code(422); http_response_code(422);
echo json_encode([ echo json_encode([
'status' => 'error', 'status' => 'error',
'message' => 'messageText is required' 'message' => 'messageText is required',
], JSON_UNESCAPED_UNICODE); ], JSON_UNESCAPED_UNICODE);
exit; 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) { if (strlen($tableId) > 32) {
$tableId = substr($tableId, 0, 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 { try {
$pdo = getAnalyticsPdo(); $pdo = getAnalyticsPdo();
@@ -75,6 +146,8 @@ try {
table_id, table_id,
message_type, message_type,
message_text, message_text,
otwierajacy_imie,
otwierajacy_nazwisko,
api_sent, api_sent,
status_kds, status_kds,
created_at created_at
@@ -82,6 +155,8 @@ try {
:table_id, :table_id,
:message_type, :message_type,
:message_text, :message_text,
:otwierajacy_imie,
:otwierajacy_nazwisko,
0, 0,
0, 0,
NOW(3) NOW(3)
@@ -92,17 +167,18 @@ try {
':table_id' => $tableId, ':table_id' => $tableId,
':message_type' => $messageType, ':message_type' => $messageType,
':message_text' => $messageText, ':message_text' => $messageText,
':otwierajacy_imie' => $otwierajacyImie !== '' ? $otwierajacyImie : null,
':otwierajacy_nazwisko' => $otwierajacyNazwisko !== '' ? $otwierajacyNazwisko : null,
]); ]);
echo json_encode([ echo json_encode([
'status' => 'success', 'status' => 'success',
'id' => (int)$pdo->lastInsertId() 'id' => (int) $pdo->lastInsertId(),
], JSON_UNESCAPED_UNICODE); ], JSON_UNESCAPED_UNICODE);
} catch (Throwable $e) { } catch (Throwable $e) {
http_response_code(500); http_response_code(500);
echo json_encode([ echo json_encode([
'status' => 'error', 'status' => 'error',
'message' => 'Queue insert failed' 'message' => 'Queue insert failed',
], JSON_UNESCAPED_UNICODE); ], JSON_UNESCAPED_UNICODE);
} }

View File

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

@@ -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 = {}) { function queueGuestAction(messageType, messageText, extra = {}) {
const operator = getQueueOperatorFields();
const body = { const body = {
tableId: tableParam || null, tableId: tableParam || null,
qrHash: hashParam || null, qrHash: hashParam || null,
messageType, messageType,
messageText, messageText,
extra otwierajacyImie: operator.otwierajacyImie,
otwierajacyNazwisko: operator.otwierajacyNazwisko,
extra,
}; };
fetch(guestActionQueueEndpoint, { fetch(guestActionQueueEndpoint, {
@@ -677,6 +718,7 @@ window.openBillDialog = async function () {
if (result.status === 'success' && result.data.length > 0) { if (result.status === 'success' && result.data.length > 0) {
const bills = result.data; const bills = result.data;
cacheOpenBills(bills);
if (bills.length === 1) { if (bills.length === 1) {
showBillReview(bills[0]); showBillReview(bills[0]);
@@ -1094,6 +1136,7 @@ function startApp() {
initUserProfile(); initUserProfile();
fetchOrders(); fetchOrders();
prefetchOpenBills();
if (!window.ordersInterval) { if (!window.ordersInterval) {
window.ordersInterval = setInterval(fetchOrders, 10000); window.ordersInterval = setInterval(fetchOrders, 10000);
} }

View File

@@ -31,6 +31,8 @@ requireAdminAuth(true);
.chip.active { background: var(--accent); border-color: var(--accent); color: #fff; } .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 { margin: 0; padding-left: 18px; line-height: 1.6; color: #cbd5e1; }
.legend-list li { margin-bottom: 6px; } .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; } } @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> </style>
</head> </head>
@@ -98,8 +100,8 @@ requireAdminAuth(true);
<div class="card col-12"> <div class="card col-12">
<h3>Ostatnie otwarcia stron stolików</h3> <h3>Ostatnie otwarcia stron stolików</h3>
<table> <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> <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="6" class="muted">Ładowanie...</td></tr></tbody> <tbody id="recentOpensBody"><tr><td colspan="7" class="muted">Ładowanie...</td></tr></tbody>
</table> </table>
</div> </div>
@@ -113,8 +115,8 @@ requireAdminAuth(true);
<tbody id="guestQueueSummaryBody"><tr><td colspan="4" class="muted">Ładowanie...</td></tr></tbody> <tbody id="guestQueueSummaryBody"><tr><td colspan="4" class="muted">Ładowanie...</td></tr></tbody>
</table> </table>
<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> <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="7" class="muted">Ładowanie...</td></tr></tbody> <tbody id="guestQueueBody"><tr><td colspan="8" class="muted">Ładowanie...</td></tr></tbody>
</table> </table>
</div> </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>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>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>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> </ul>
</div> </div>
</div> </div>
@@ -197,9 +200,17 @@ requireAdminAuth(true);
const when = dt && !Number.isNaN(dt.getTime()) ? dt.toLocaleString("pl-PL") : (r.created_at || "-"); const when = dt && !Number.isNaN(dt.getTime()) ? dt.toLocaleString("pl-PL") : (r.created_at || "-");
const device = r.device_type || "-"; const device = r.device_type || "-";
const browser = r.browser || "-"; 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("") }).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 || {}; const queueSummary = data.guestQueueSummary || {};
document.getElementById("guestQueueSummaryBody").innerHTML = ` document.getElementById("guestQueueSummaryBody").innerHTML = `
@@ -224,15 +235,19 @@ requireAdminAuth(true);
.replace(/&/g, "&amp;") .replace(/&/g, "&amp;")
.replace(/</g, "&lt;") .replace(/</g, "&lt;")
.replace(/>/g, "&gt;"); .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("") }).join("")
: `<tr><td colspan="7" class="muted">Brak danych</td></tr>`; : `<tr><td colspan="8" class="muted">Brak danych</td></tr>`;
} catch (e) { } catch (e) {
document.getElementById("topTablesBody").innerHTML = `<tr><td colspan="5" class="muted">Nie udało się załadować analityki.</td></tr>`; 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("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("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 { } finally {
isLoadingAnalytics = false; isLoadingAnalytics = false;
} }

View File

@@ -135,11 +135,22 @@ requireAdminAuth(true);
color: var(--accent); color: var(--accent);
font-weight: 600; font-weight: 600;
font-size: 1.1rem; font-size: 1.1rem;
margin-bottom: 15px; margin-bottom: 8px;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.5px; 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 { .order-items {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -226,6 +237,68 @@ requireAdminAuth(true);
0% { transform: rotate(0deg); } 0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); } 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> </style>
</head> </head>
<body> <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> <a href="logout.php" style="color:#cbd5e1; text-decoration:none; border:1px solid #334155; padding:8px 12px; border-radius:10px;">Wyloguj</a>
</div> </div>
<div id="guest-alerts" class="guest-alerts"></div>
<div id="kds-grid" class="kds-grid"> <div id="kds-grid" class="kds-grid">
<div id="loading"> <div id="loading">
<div class="loader-spinner"></div> <div class="loader-spinner"></div>
@@ -254,6 +329,74 @@ requireAdminAuth(true);
const lastSyncEl = document.getElementById('last-sync'); const lastSyncEl = document.getElementById('last-sync');
const dotEl = document.getElementById('connection-dot'); const dotEl = document.getElementById('connection-dot');
let previousDataString = ""; 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() { async function fetchOrders() {
try { try {
@@ -303,6 +446,7 @@ requireAdminAuth(true);
stolik: item.NazwaStolika || item.StolikID, stolik: item.NazwaStolika || item.StolikID,
stolikId: item.StolikID, stolikId: item.StolikID,
time: item.DataDodania, time: item.DataDodania,
otwierajacy: formatOtwierajacy(item),
groups: {} groups: {}
}; };
} }
@@ -338,6 +482,9 @@ requireAdminAuth(true);
const timeStr = new Date(order.time).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}); 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 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 stolikText = order.stolik ? stolikLink : 'BRAK STOLIKA';
const operatorHtml = order.otwierajacy
? `<div class="order-operator">Kelner: <strong>${order.otwierajacy}</strong></div>`
: '';
let itemsHtml = ''; let itemsHtml = '';
@@ -375,6 +522,7 @@ requireAdminAuth(true);
<div class="order-time">${timeStr}</div> <div class="order-time">${timeStr}</div>
</div> </div>
<div class="order-table">${stolikText}</div> <div class="order-table">${stolikText}</div>
${operatorHtml}
<div class="order-items"> <div class="order-items">
${itemsHtml} ${itemsHtml}
</div> </div>
@@ -384,11 +532,11 @@ requireAdminAuth(true);
}); });
} }
// Pierwsze pobranie
fetchOrders(); fetchOrders();
fetchGuestAlerts();
// Pętla odświeżania co 3 sekundy
setInterval(fetchOrders, 3000); setInterval(fetchOrders, 3000);
setInterval(fetchGuestAlerts, 3000);
</script> </script>
</body> </body>
</html> </html>

View File

@@ -3,6 +3,8 @@ CREATE TABLE IF NOT EXISTS guest_action_queue (
table_id VARCHAR(32) NOT NULL, table_id VARCHAR(32) NOT NULL,
message_type ENUM('waiter_call', 'bill_request') NOT NULL, message_type ENUM('waiter_call', 'bill_request') NOT NULL,
message_text TEXT 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, api_sent TINYINT(1) NOT NULL DEFAULT 0,
status_kds TINYINT(1) NOT NULL DEFAULT 0, status_kds TINYINT(1) NOT NULL DEFAULT 0,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), 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";