Poprawki API i panelu admina

This commit is contained in:
2026-05-29 16:27:23 +02:00
parent 583021915a
commit 9b15131461
12 changed files with 390 additions and 58 deletions

View File

@@ -41,11 +41,6 @@
<h1 class="logo-text">Karczma Biesiada</h1>
<div id="tableLabel" class="table-badge">Wybierz stolik</div>
</header>
<div style="background: rgba(226, 176, 126, 0.1); border: 1px solid rgba(226, 176, 126, 0.3); border-radius: 12px; padding: 12px; margin-bottom: 24px; text-align: center; font-size: 13px; color: var(--text-muted); line-height: 1.5;">
<span style="color: var(--primary); font-weight: 600; display: block; margin-bottom: 4px;">Wdrażamy nową aplikację! 🚀</span>
Testujemy właśnie nowe funkcje. Niebawem z tego miejsca będziesz mógł wezwać obsługę lub poprosić o rachunek.
</div>
<!-- <div id="greetingBanner"
style="display:none; text-align:center; padding: 10px; font-weight:600; color:var(--primary); font-family:'Playfair Display', serif; font-size:18px;">
</div> -->
@@ -146,10 +141,10 @@
<span class="nav-icon">📖</span>
<span class="nav-label">Menu</span>
</div>
<!-- <div class="nav-item action-call" onclick="openWaiterDialog()">
<div class="nav-item action-call" onclick="openWaiterDialog()">
<span class="nav-icon">🛎️</span>
<span class="nav-label">Kelner</span>
</div> -->
</div>
<div class="nav-item action-bill" onclick="openBillDialog()">
<span class="nav-icon">💳</span>
<span class="nav-label">Rachunek</span>
@@ -245,7 +240,7 @@
<div style="display:flex; gap:12px;">
<button class="btn btn-secondary" style="flex:1;" onclick="goBackToBillList()"
id="btnBackToBills">Wróć</button>
<!-- <button class="btn btn-primary" style="flex:2;" onclick="goToStep('stepPayment')">Poproś rachunek</button> -->
<button class="btn btn-primary" style="flex:2;" onclick="proceedToBillPayment()">Poproś rachunek</button>
</div>
</div>

View File

@@ -871,6 +871,16 @@ header {
filter: grayscale(0) opacity(1);
}
.nav-item.nav-action-pending {
opacity: 0.45;
}
.nav-item.nav-action-pending .nav-label::after {
content: " · w toku";
font-size: 9px;
font-weight: 500;
}
.nav-item:active .nav-icon {
transform: scale(0.9);
}

View File

@@ -103,6 +103,12 @@ function trackEvent(eventName, payload = {}) {
}
let cachedOpenBills = [];
let guestPendingActions = {
waiter_call: false,
bill_request: false,
};
let guestPendingPollTimer = null;
const GUEST_PENDING_POLL_MS = 15000;
function cacheOpenBills(bills) {
cachedOpenBills = Array.isArray(bills) ? bills : [];
@@ -140,7 +146,114 @@ async function prefetchOpenBills() {
}
}
function queueGuestAction(messageType, messageText, extra = {}) {
/**
* Komunikat do kolejki KDS — zwykły tekst, wiersze oddzielone \n (w JSON jako entery).
* @param {string} title
* @param {{ label?: string, value?: string }[]} lines
*/
function formatGuestQueueMessage(title, lines = []) {
const rows = (Array.isArray(lines) ? lines : [])
.map((line) => {
const label = String(line?.label ?? "").trim();
const value = String(line?.value ?? "").trim();
if (!label && !value) return "";
if (label && value) return `${label} ${value}`;
return label || value;
})
.filter(Boolean);
if (!rows.length) {
return title;
}
return `${title}\n${rows.join("\n")}`;
}
function buildWaiterCallQueueMessage() {
return "Przywołanie kelnera";
}
function buildBillRequestQueueMessage(docType) {
const lines = [
{ label: "Forma płatności:", value: billState.payment || "nieznana" },
{ label: "Dokument:", value: docType === "faktura" ? "faktura" : "paragon" },
];
if (docType === "faktura") {
lines.push({ label: "NIP:", value: billState.nip || "—" });
lines.push({ label: "Firma:", value: billState.company?.name || "—" });
const addressParts = [
billState.company?.street,
[billState.company?.zip, billState.company?.city].filter(Boolean).join(" "),
].filter(Boolean);
if (addressParts.length) {
lines.push({ label: "Adres:", value: addressParts.join(", ") });
}
}
return formatGuestQueueMessage("Prośba o rachunek", lines);
}
function guestActionBlockedMessage(messageType) {
if (messageType === "waiter_call") {
return "Kelner został już wezwany. Poczekaj, aż obsługa potwierdzi zgłoszenie na panelu.";
}
return "Prośba o rachunek została już wysłana. Poczekaj, aż obsługa ją obsłuży.";
}
function updateGuestActionNavState() {
const waiterNav = document.querySelector(".bottom-nav .action-call");
const billNav = document.querySelector(".bottom-nav .action-bill");
if (waiterNav) {
waiterNav.classList.toggle("nav-action-pending", guestPendingActions.waiter_call);
}
if (billNav) {
billNav.classList.toggle("nav-action-pending", guestPendingActions.bill_request);
}
}
async function refreshGuestPendingActions() {
if (!hashParam && !tableParam) {
return guestPendingActions;
}
const params = new URLSearchParams();
if (hashParam) params.set("h", hashParam);
if (tableParam) params.set("tableId", tableParam);
try {
const res = await fetch(`${guestActionQueueEndpoint}?${params.toString()}`);
const result = await res.json();
if (result.status === "success" && result.pending) {
guestPendingActions.waiter_call = !!result.pending.waiter_call;
guestPendingActions.bill_request = !!result.pending.bill_request;
updateGuestActionNavState();
}
} catch {
// best effort
}
return guestPendingActions;
}
function startGuestPendingPoll() {
if (guestPendingPollTimer) return;
guestPendingPollTimer = setInterval(() => {
if (!hashParam && !tableParam) return;
refreshGuestPendingActions();
}, GUEST_PENDING_POLL_MS);
}
async function ensureGuestActionAllowed(messageType) {
await refreshGuestPendingActions();
if (!guestPendingActions[messageType]) {
return true;
}
showToast(guestActionBlockedMessage(messageType));
return false;
}
async function queueGuestAction(messageType, messageText, extra = {}) {
const operator = getQueueOperatorFields();
const body = {
tableId: tableParam || null,
@@ -152,14 +265,31 @@ function queueGuestAction(messageType, messageText, extra = {}) {
extra,
};
fetch(guestActionQueueEndpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
keepalive: true
}).catch(() => {
// best effort - ignore queue errors in UI
});
try {
const res = await fetch(guestActionQueueEndpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
keepalive: true,
});
const result = await res.json().catch(() => ({}));
if (res.status === 409 || result.code === "pending_on_kds") {
guestPendingActions[messageType] = true;
updateGuestActionNavState();
return { ok: false, reason: "pending" };
}
if (!res.ok || result.status !== "success") {
return { ok: false, reason: "error" };
}
guestPendingActions[messageType] = true;
updateGuestActionNavState();
return { ok: true };
} catch {
return { ok: false, reason: "error" };
}
}
if (hashParam) {
@@ -606,6 +736,8 @@ async function fetchOrders() {
if (result.tableName && result.tableName !== '') {
tableLabel.textContent = result.tableName.toUpperCase().startsWith("STOLIK") ? result.tableName : `Stolik ${result.tableName}`;
tableParam = result.tableName; // Aktualizacja do właściwej nazwy na poczet innych zapytań
refreshGuestPendingActions();
startGuestPendingPoll();
}
// API teraz samo filtruje i zwraca tylko to co nas interesuje (za pomocą mocnego wyrażenia regularnego)
@@ -678,16 +810,35 @@ function sendApiSimulated(actionName, details) {
// }
}
window.callWaiter = function (type) {
if (type === 'order') {
trackEvent("waiter_call_requested", { waiterType: "order" });
queueGuestAction("waiter_call", "Przywołanie kelnera", { waiterType: "order" });
sendApiSimulated("CallWaiter_Order", { table: tableParam });
showToast("Kelner wkrótce do Ciebie podejdzie!");
window.callWaiter = async function (type) {
if (type !== "order") return;
if (!(await ensureGuestActionAllowed("waiter_call"))) {
return;
}
const queued = await queueGuestAction("waiter_call", buildWaiterCallQueueMessage(), {
waiterType: "order",
});
if (!queued.ok) {
if (queued.reason === "pending") {
showToast(guestActionBlockedMessage("waiter_call"));
} else {
showToast("Nie udało się wysłać wezwania. Spróbuj ponownie za chwilę.");
}
return;
}
trackEvent("waiter_call_requested", { waiterType: "order" });
sendApiSimulated("CallWaiter_Order", { table: tableParam });
showToast("Kelner wkrótce do Ciebie podejdzie!");
};
window.openWaiterDialog = function () {
window.openWaiterDialog = async function () {
if (!(await ensureGuestActionAllowed("waiter_call"))) {
return;
}
document.getElementById("waiterModal").classList.add("active");
document.body.style.overflow = 'hidden';
};
@@ -697,12 +848,20 @@ window.closeWaiterDialog = function () {
document.body.style.overflow = '';
};
window.confirmCallWaiter = function () {
window.confirmCallWaiter = async function () {
closeWaiterDialog();
callWaiter('order');
await callWaiter("order");
};
window.proceedToBillPayment = async function () {
if (!(await ensureGuestActionAllowed("bill_request"))) {
return;
}
goToStep("stepPayment");
};
window.openBillDialog = async function () {
await refreshGuestPendingActions();
trackEvent("bill_dialog_opened");
billState = { payment: '', doc: '', nip: '', company: null, selectedBillId: null };
document.getElementById("billModal").classList.add("active");
@@ -900,15 +1059,25 @@ window.selectPayment = function (method) {
goToStep("stepDocument");
};
window.selectDocument = function (docType) {
window.selectDocument = async function (docType) {
billState.doc = docType;
if (docType === 'paragon') {
trackEvent("bill_request_sent", { docType: "paragon" });
const queueMessage = `Prośba o rachunek | forma płatności: ${billState.payment || "nieznana"} | dokument: paragon`;
queueGuestAction("bill_request", queueMessage, {
if (!(await ensureGuestActionAllowed("bill_request"))) {
return;
}
const queued = await queueGuestAction("bill_request", buildBillRequestQueueMessage("paragon"), {
payment: billState.payment || null,
docType: "paragon"
docType: "paragon",
});
if (!queued.ok) {
if (queued.reason === "pending") {
showToast(guestActionBlockedMessage("bill_request"));
} else {
showToast("Nie udało się wysłać prośby o rachunek. Spróbuj ponownie za chwilę.");
}
return;
}
trackEvent("bill_request_sent", { docType: "paragon" });
closeBillDialog();
sendApiSimulated("CallWaiter_Bill", { table: tableParam, billId: billState.selectedBillId, payment: billState.payment, doc: 'paragon' });
showToast("Kelner przyniesie paragon do opłacenia!");
@@ -1084,21 +1253,34 @@ window.editCompanyData = function () {
}
};
window.confirmInvoice = function () {
window.confirmInvoice = async function () {
billState.company.name = document.getElementById("cmpName").value;
billState.company.street = document.getElementById("cmpStreet").value;
billState.company.zip = document.getElementById("cmpZip").value;
billState.company.city = document.getElementById("cmpCity").value;
closeBillDialog();
trackEvent("bill_request_sent", { docType: "faktura" });
const queueMessage = `Prośba o rachunek | forma płatności: ${billState.payment || "nieznana"} | dokument: faktura | NIP: ${billState.nip || "-"} | firma: ${billState.company?.name || "-"}`;
queueGuestAction("bill_request", queueMessage, {
if (!(await ensureGuestActionAllowed("bill_request"))) {
return;
}
const queued = await queueGuestAction("bill_request", buildBillRequestQueueMessage("faktura"), {
payment: billState.payment || null,
docType: "faktura",
nip: billState.nip || null,
company: billState.company || null
company: billState.company || null,
});
if (!queued.ok) {
if (queued.reason === "pending") {
showToast(guestActionBlockedMessage("bill_request"));
} else {
showToast("Nie udało się wysłać prośby o rachunek. Spróbuj ponownie za chwilę.");
}
return;
}
closeBillDialog();
trackEvent("bill_request_sent", { docType: "faktura" });
sendApiSimulated("CallWaiter_Bill", {
table: tableParam,
billId: billState.selectedBillId,
@@ -1137,6 +1319,8 @@ function startApp() {
initUserProfile();
fetchOrders();
prefetchOpenBills();
refreshGuestPendingActions();
startGuestPendingPoll();
if (!window.ordersInterval) {
window.ordersInterval = setInterval(fetchOrders, 10000);
}

View File

@@ -235,7 +235,7 @@ requireAdminAuth(true);
.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>`;
return `<tr><td>${row.id}</td><td>${when}</td><td>${row.table_id || "-"}</td><td>${kelner}</td><td>${typeLabel}</td><td style="white-space:pre-line;max-width:360px">${messageText}</td><td>${apiSent}</td><td>${kdsDone}</td></tr>`;
}).join("")
: `<tr><td colspan="8" class="muted">Brak danych</td></tr>`;
} catch (e) {

View File

@@ -292,6 +292,7 @@ requireAdminAuth(true);
font-size: 0.9rem;
color: #cbd5e1;
line-height: 1.4;
white-space: pre-line;
}
.guest-alert-time {