|
|
|
|
@@ -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);
|
|
|
|
|
}
|
|
|
|
|
|