Files
2026-05-27 08:47:59 +02:00

1155 lines
34 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="pl">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Karczma Biesiada Twoje Zamówienie</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;600;700&family=Playfair+Display:wght@700&display=swap" rel="stylesheet">
<style>
:root {
--primary: #e2b07e; /* Ciepłe złoto/miedź */
--bg: #0f0f11;
--surface: #1c1c1f;
--surface-light: #2c2c2e;
--text-main: #ffffff;
--text-muted: #9a9a9e;
--success: #4ade80;
--accent: #f59e0b;
--radius: 20px;
}
* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
body {
margin: 0;
font-family: 'Plus Jakarta Sans', sans-serif;
background-color: var(--bg);
color: var(--text-main);
line-height: 1.6;
overflow-x: hidden;
}
/* --- LOADER SCREEN --- */
#loadingScreen {
position: fixed;
inset: 0;
z-index: 100;
background: var(--bg);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px;
text-align: center;
transition: opacity 0.5s ease, visibility 0.5s;
}
.loader-icon {
width: 80px;
height: 80px;
border: 3px solid var(--surface-light);
border-top: 3px solid var(--primary);
border-radius: 50%;
animation: spin 1s cubic-bezier(0.4, 0, 0.2, 1) infinite;
margin-bottom: 24px;
}
.loader-text h2 {
font-family: 'Playfair Display', serif;
margin: 0 0 8px;
color: var(--primary);
}
.loader-msg { color: var(--text-muted); font-size: 14px; min-height: 20px; }
/* --- MAIN LAYOUT --- */
.container {
max-width: 500px;
margin: 0 auto;
padding: 24px 16px 100px;
}
header {
text-align: center;
margin-bottom: 32px;
padding-top: 20px;
}
.logo-text {
font-family: 'Playfair Display', serif;
font-size: 28px;
margin: 0;
background: linear-gradient(to right, #fff, var(--primary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.table-badge {
display: inline-block;
background: var(--surface-light);
padding: 6px 16px;
border-radius: 99px;
font-size: 13px;
font-weight: 600;
margin-top: 8px;
color: var(--primary);
border: 1px solid rgba(226, 176, 126, 0.2);
}
/* --- STATUS CARD --- */
.status-card {
background: var(--surface);
border-radius: var(--radius);
padding: 24px;
margin-bottom: 24px;
position: relative;
overflow: hidden;
box-shadow: 0 20px 40px rgba(0,0,0,0.4);
border: 1px solid rgba(255,255,255,0.05);
}
.status-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 20px;
}
.status-title {
font-size: 14px;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--text-muted);
display: block;
margin-bottom: 4px;
}
.status-value {
font-size: 22px;
font-weight: 700;
color: var(--text-main);
}
.progress-container {
height: 8px;
background: var(--surface-light);
border-radius: 10px;
margin: 15px 0;
overflow: hidden;
}
.progress-bar {
height: 100%;
width: 0%;
background: linear-gradient(90deg, var(--primary), var(--accent));
box-shadow: 0 0 15px rgba(226, 176, 126, 0.4);
transition: width 1s cubic-bezier(0.4, 0, 0.2, 1);
}
/* --- ITEMS LIST --- */
.items-container h3 {
font-size: 18px;
margin: 0 0 16px 8px;
}
.item-card {
background: var(--surface);
margin-bottom: 12px;
padding: 16px;
border-radius: 16px;
display: flex;
justify-content: space-between;
align-items: center;
transition: transform 0.2s;
border-left: 4px solid var(--surface-light);
}
.item-card.ready { border-left-color: var(--success); }
.item-card.archived { opacity: 0.82; }
.history-section { margin-top: 26px; }
.history-note {
font-size: 12px;
color: var(--text-muted);
margin: 0 0 12px 8px;
line-height: 1.45;
}
.item-info { display: flex; flex-direction: column; }
.item-name { font-weight: 600; font-size: 16px; }
.item-meta { font-size: 12px; color: var(--text-muted); }
.item-qty {
background: var(--surface-light);
padding: 6px 12px;
border-radius: 10px;
font-weight: 700;
color: var(--primary);
}
/* --- EMPTY STATE --- */
.empty-state {
text-align: center;
padding: 40px 20px;
}
.empty-icon { font-size: 48px; margin-bottom: 16px; opacity: 0.5; }
/* --- ANIMATIONS --- */
@keyframes spin { to { transform: rotate(360deg); } }
.hidden { opacity: 0; visibility: hidden; pointer-events: none; display: none; }
.meta-footer {
font-size: 11px;
color: var(--text-muted);
margin-top: 20px;
text-align: center;
}
/* --- ACTION CARD --- */
.action-card {
background: var(--surface);
border-radius: var(--radius);
padding: 16px;
margin-bottom: 20px;
text-align: center;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
border: 1px solid rgba(255,255,255,0.05);
position: relative;
overflow: hidden;
}
.action-card h3 {
font-size: 15px;
margin: 0 0 12px;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--text-muted);
}
.action-buttons {
display: flex;
flex-direction: row;
gap: 10px;
}
.btn {
font-family: inherit;
font-size: 15px;
font-weight: 600;
padding: 14px 20px;
border-radius: 12px;
border: none;
cursor: pointer;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
width: 100%;
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
}
.action-btn {
flex-direction: column;
font-size: 13px;
line-height: 1.3;
padding: 14px 8px;
gap: 6px;
flex: 1;
}
.action-btn span {
font-size: 24px;
}
.btn-primary {
background: linear-gradient(135deg, var(--primary), var(--accent));
color: var(--bg);
box-shadow: 0 4px 15px rgba(226, 176, 126, 0.3);
}
.btn-primary:active {
transform: scale(0.98);
}
.btn-secondary {
background: var(--surface-light);
color: var(--primary);
border: 1px solid rgba(226, 176, 126, 0.2);
}
.btn-secondary:active {
transform: scale(0.98);
}
/* --- MODAL DIALOG --- */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(15, 15, 17, 0.85);
backdrop-filter: blur(8px);
z-index: 200;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease, visibility 0.3s;
}
.modal-overlay.active {
opacity: 1;
visibility: visible;
}
.modal-content {
background: var(--surface);
width: 100%;
max-width: 400px;
border-radius: 24px;
padding: 24px;
box-shadow: 0 20px 50px rgba(0,0,0,0.5);
border: 1px solid rgba(255,255,255,0.1);
transform: translateY(20px) scale(0.95);
transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.modal-overlay.active .modal-content {
transform: translateY(0) scale(1);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.modal-header h3 {
margin: 0;
font-size: 20px;
color: var(--primary);
font-family: 'Playfair Display', serif;
}
.close-btn {
background: transparent;
color: var(--text-muted);
border: none;
font-size: 28px;
cursor: pointer;
padding: 0;
line-height: 1;
transition: color 0.2s;
}
.close-btn:hover {
color: var(--text-main);
}
.step {
display: none;
animation: fadeInStep 0.4s ease forwards;
}
.step.active {
display: block;
}
@keyframes fadeInStep {
from { opacity: 0; transform: translateX(10px); }
to { opacity: 1; transform: translateX(0); }
}
.option-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin-bottom: 24px;
}
.option-card {
background: var(--surface-light);
border: 1px solid rgba(226, 176, 126, 0.1);
border-radius: 16px;
padding: 20px;
text-align: center;
cursor: pointer;
transition: all 0.2s ease;
}
.option-card:hover {
background: rgba(226, 176, 126, 0.15);
transform: translateY(-2px);
}
.option-icon {
font-size: 32px;
margin-bottom: 8px;
display: block;
}
.option-label {
font-weight: 600;
font-size: 14px;
color: var(--text-main);
}
.input-group {
margin-bottom: 20px;
}
.input-label {
display: block;
font-size: 13px;
color: var(--text-muted);
margin-bottom: 8px;
}
.input-field {
width: 100%;
background: var(--bg);
border: 1px solid var(--surface-light);
color: var(--text-main);
padding: 14px;
border-radius: 12px;
font-family: inherit;
font-size: 16px;
transition: border-color 0.2s;
}
.input-field:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 2px rgba(226, 176, 126, 0.2);
}
.company-details {
background: var(--bg);
padding: 16px;
border-radius: 12px;
margin-bottom: 24px;
border: 1px solid var(--surface-light);
}
.company-input {
width: 100%;
background: transparent;
border: 1px solid transparent;
color: var(--text-main);
font-family: inherit;
font-size: 15px;
padding: 8px;
border-radius: 8px;
transition: all 0.2s;
}
.company-input:not([readonly]) {
border-color: var(--surface-light);
background: var(--surface-light);
}
.company-input:not([readonly]):focus {
outline: none;
border-color: var(--primary);
}
.company-input.muted {
color: var(--text-muted);
font-size: 13px;
}
/* --- TOAST --- */
.toast {
position: fixed;
bottom: 30px;
left: 50%;
transform: translateX(-50%) translateY(100px);
background: linear-gradient(135deg, var(--success), #22c55e);
color: var(--bg);
padding: 14px 28px;
border-radius: 30px;
font-weight: 700;
font-size: 15px;
box-shadow: 0 10px 30px rgba(74, 222, 128, 0.3);
opacity: 0;
transition: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55);
z-index: 300;
display: flex;
align-items: center;
gap: 10px;
}
.toast.active {
transform: translateX(-50%) translateY(0);
opacity: 1;
}
</style>
</head>
<body>
<div id="loadingScreen">
<div class="loader-icon"></div>
<div class="loader-text">
<h2>Karczma Biesiada</h2>
<div class="loader-msg" id="loaderMsg">Łączenie z kuchnią...</div>
</div>
</div>
<div class="container">
<header>
<h1 class="logo-text">Karczma Biesiada</h1>
<div id="tableLabel" class="table-badge">Wybierz stolik</div>
</header>
<main id="mainContent">
<section class="action-card">
<h3>Przywołaj kelnera</h3>
<div class="action-buttons">
<button class="btn btn-primary action-btn" onclick="callWaiter('order')">
<span>🛎️</span> Złóż zamówienie
</button>
<button class="btn btn-secondary action-btn" onclick="openBillDialog()">
<span>💳</span> Poproś o rachunek
</button>
</div>
</section>
<section class="status-card">
<div class="status-header">
<div>
<span class="status-title">Aktualny status</span>
<div id="prepStatus" class="status-value">Oczekiwanie...</div>
</div>
<div id="statusIcon" style="font-size: 28px;"></div>
</div>
<div class="progress-container">
<div id="progressBar" class="progress-bar"></div>
</div>
<div id="statusMeta" style="font-size: 12px; color: var(--text-muted);">
Sprawdzamy co pysznego się przygotowuje...
</div>
</section>
<section class="items-container">
<h3>Twoje zamówione dania</h3>
<div id="emptyState" class="empty-state hidden">
<div class="empty-icon">🍽️</div>
<p style="color: var(--text-muted)">Aktualnie nie przygotowujemy niczego dla tego stolika.</p>
<p style="font-size: 14px">Jeśli właśnie złożyłeś zamówienie, daj nam chwilkę na jego przetworzenie.</p>
</div>
<div id="itemsList"></div>
</section>
<section id="historySection" class="items-container history-section hidden">
<h3>Twoje poprzednie zamówienia</h3>
<p class="history-note">To są pozycje z innych wizyt, które były widoczne na tym telefonie po zeskanowaniu kodów QR. Jeśli kiedyś byłeś w restauracji bez skanowania kodu, tych pozycji tu nie będzie 🙂</p>
<div id="historyList"></div>
</section>
<div id="metaFooter" class="meta-footer"></div>
</main>
</div>
<!-- BILL DIALOG -->
<div class="modal-overlay" id="billModal">
<div class="modal-content">
<div class="modal-header">
<h3 id="modalTitle">Rozliczenie</h3>
<button class="close-btn" onclick="closeBillDialog()">&times;</button>
</div>
<!-- Step 1: Payment Method -->
<div class="step active" id="stepPayment">
<p style="margin-top:0; color:var(--text-muted); font-size:14px; margin-bottom: 20px;">Wybierz preferowaną formę płatności:</p>
<div class="option-grid">
<div class="option-card" onclick="selectPayment('karta')">
<span class="option-icon">💳</span>
<span class="option-label">Karta</span>
</div>
<div class="option-card" onclick="selectPayment('gotówka')">
<span class="option-icon">💵</span>
<span class="option-label">Gotówka</span>
</div>
</div>
</div>
<!-- Step 2: Document Type -->
<div class="step" id="stepDocument">
<p style="margin-top:0; color:var(--text-muted); font-size:14px; margin-bottom: 20px;">Jakiego dokumentu potrzebujesz?</p>
<div class="option-grid">
<div class="option-card" onclick="selectDocument('paragon')">
<span class="option-icon">🧾</span>
<span class="option-label">Paragon</span>
</div>
<div class="option-card" onclick="selectDocument('faktura')">
<span class="option-icon">📄</span>
<span class="option-label">Faktura</span>
</div>
</div>
<button class="btn btn-secondary" onclick="goToStep('stepPayment')" style="margin-top: 8px;">Wróć</button>
</div>
<!-- Step 3: NIP Input -->
<div class="step" id="stepNIP">
<p style="margin-top:0; color:var(--text-muted); font-size:14px; margin-bottom: 20px;">Wprowadź NIP firmy, abyśmy mogli automatycznie pobrać dane.</p>
<div class="input-group">
<label class="input-label">Numer NIP</label>
<input type="text" id="nipInput" class="input-field" placeholder="np. 1234567890" autocomplete="off" />
</div>
<div style="display:flex; gap:12px; margin-top: 24px;">
<button class="btn btn-secondary" style="flex:1;" onclick="goToStep('stepDocument')">Wróć</button>
<button class="btn btn-primary" style="flex:2;" onclick="fetchGUS()" id="btnGUS">Pobierz z GUS</button>
</div>
</div>
<!-- Step 4: Verify Data -->
<div class="step" id="stepVerify">
<p style="margin-top:0; color:var(--text-muted); font-size:14px; margin-bottom: 20px;">Czy poniższe dane do faktury są prawidłowe?</p>
<div class="company-details">
<input type="text" id="cmpName" class="company-input" style="font-weight:700; margin-bottom:4px;" readonly />
<input type="text" id="cmpAddress" class="company-input" style="margin-bottom:4px;" readonly />
<input type="text" id="cmpNip" class="company-input muted" readonly />
</div>
<div style="display:flex; gap:12px; flex-direction:column;">
<button class="btn btn-primary" onclick="confirmInvoice()">Tak, poproszę fakturę!</button>
<div style="display:flex; gap:12px;">
<button class="btn btn-secondary" onclick="goToStep('stepNIP')">Zmień NIP</button>
<button class="btn btn-secondary" onclick="editCompanyData()" id="btnEditCompany">Popraw ręcznie</button>
</div>
</div>
</div>
</div>
</div>
<!-- Toast -->
<div class="toast" id="toastMsg">
<span style="font-size:20px;"></span> <span id="toastText">Wysłano!</span>
</div>
<script>
const params = new URLSearchParams(location.search);
let tableParam = (params.get("table") || "").trim();
// Jeśli brak numeru stolika w URL zapytaj użytkownika
if (!tableParam) {
const input = prompt("Podaj numer stolika:");
const trimmed = (input || "").trim();
if (trimmed) {
const newUrl = new URL(location.href);
newUrl.searchParams.set("table", trimmed);
location.replace(newUrl.toString());
}
}
// UI Elements
const loadingScreen = document.getElementById("loadingScreen");
const loaderMsg = document.getElementById("loaderMsg");
const tableLabel = document.getElementById("tableLabel");
const prepStatus = document.getElementById("prepStatus");
const progressBar = document.getElementById("progressBar");
const statusMeta = document.getElementById("statusMeta");
const itemsList = document.getElementById("itemsList");
const emptyState = document.getElementById("emptyState");
const metaFooter = document.getElementById("metaFooter");
const statusIcon = document.getElementById("statusIcon");
const storageKey = `stolik2_state_${(tableParam || "unknown").toLowerCase()}`;
const historyKey = "stolik2_global_history";
const historySection = document.getElementById("historySection");
const historyList = document.getElementById("historyList");
const SIX_MONTHS_MS = 180 * 24 * 60 * 60 * 1000;
const HOT_WINDOW_MS = 5 * 60 * 60 * 1000;
// Dynamic Loader Messages
const LOADER_MIN_MS = 10_000;
const loadStartTime = Date.now();
const msgs = ["Rozgrzewamy piece...", "Szef kuchni sprawdza składniki...", "Łączenie z sercem restauracji...", "Prawie gotowe..."];
let msgIdx = 0;
const msgInterval = setInterval(() => {
msgIdx = (msgIdx + 1) % msgs.length;
loaderMsg.textContent = msgs[msgIdx];
}, 4000);
// Initial State
if (tableParam) {
tableLabel.textContent = `Stolik ${tableParam}`;
}
function hideLoader() {
const elapsed = Date.now() - loadStartTime;
const remaining = Math.max(0, LOADER_MIN_MS - elapsed);
setTimeout(() => {
loadingScreen.classList.add("hidden");
clearInterval(msgInterval);
}, remaining);
}
function updateUI(bills) {
// Hide loader after minimum display time
hideLoader();
const allArticles = bills.flatMap(b => Array.isArray(b?.Articles) ? b.Articles : []);
const items = mergeWithPersistedItems(allArticles);
renderGlobalHistory();
if (items.length === 0) {
showEmptyState();
return;
}
renderItems(items);
updateStatus(bills, items);
}
function detectTableCandidates(bill) {
const remark = String(bill?.Remark || "");
const description = String(bill?.Description || "").trim();
// 1) klasyczny format: "STOLIK 9", "STOLIK 11A"
const stolikMatch = remark.match(/STOLIK\s*([0-9A-Z]+)/i);
const fromRemark = stolikMatch ? stolikMatch[1].toLowerCase() : "";
// 2) czasem numer bywa w samym Description
const fromDescription = description.toLowerCase();
return { fromRemark, fromDescription, remark: remark.toLowerCase() };
}
function showEmptyState() {
emptyState.classList.remove("hidden");
itemsList.innerHTML = "";
prepStatus.textContent = "Brak aktywnych zamówień";
statusIcon.textContent = "🍃";
progressBar.style.width = "0%";
statusMeta.textContent = "Zapraszamy do złożenia zamówienia u kelnera.";
// Historia może istnieć nawet gdy brak bieżących pozycji
renderGlobalHistory();
}
function normalizeArticleName(rawName) {
const name = String(rawName || "Pozycja");
// Usuwa gramatury typu: "300G", "250 G", "500/200/150G".
const withoutWeight = name.replace(
/\b\d+(?:[.,]\d+)?(?:\s*\/\s*\d+(?:[.,]\d+)?)*\s*[gG]\b/g,
""
);
return withoutWeight
.replace(/\s{2,}/g, " ")
.replace(/\s+([,.;:!?])/g, "$1")
.trim() || "Pozycja";
}
function loadPersistedItems() {
try {
const raw = localStorage.getItem(storageKey);
if (!raw) return [];
const parsed = JSON.parse(raw);
return Array.isArray(parsed) ? parsed : [];
} catch {
return [];
}
}
function savePersistedItems(items) {
try {
localStorage.setItem(storageKey, JSON.stringify(items));
} catch {
// brak miejsca/tryb prywatny
}
}
function loadGlobalHistory() {
try {
const raw = localStorage.getItem(historyKey);
if (!raw) return [];
const parsed = JSON.parse(raw);
return Array.isArray(parsed) ? parsed : [];
} catch {
return [];
}
}
function saveGlobalHistory(entries) {
const now = Date.now();
const cleaned = entries
.filter((e) => e && e.name && Number.isFinite(e.archivedAt))
.filter((e) => (now - e.archivedAt) <= SIX_MONTHS_MS);
try {
localStorage.setItem(historyKey, JSON.stringify(cleaned));
} catch {
// ignore storage errors
}
}
function addItemsToGlobalHistory(items, sourceTable) {
if (!items.length) return;
const now = Date.now();
const existing = loadGlobalHistory();
// zapobiegamy dokładnym duplikatom (ta sama nazwa + qty + stół blisko czasu)
const dedupWindowMs = 2 * 60 * 1000;
const toAdd = items.filter((item) => {
return !existing.some((h) =>
h.name === item.name &&
Number(h.qty) === Number(item.qty) &&
String(h.sourceTable || "") === String(sourceTable || "") &&
Math.abs((h.archivedAt || 0) - now) <= dedupWindowMs
);
}).map((item) => ({
name: item.name,
qty: item.qty,
sourceTable: sourceTable || "?",
archivedAt: now
}));
if (!toAdd.length) return;
saveGlobalHistory([...existing, ...toAdd]);
}
function renderGlobalHistory() {
const now = Date.now();
const history = loadGlobalHistory()
.filter((e) => (now - (e.archivedAt || 0)) <= SIX_MONTHS_MS)
.sort((a, b) => (b.archivedAt || 0) - (a.archivedAt || 0));
saveGlobalHistory(history);
if (!history.length) {
historySection.classList.add("hidden");
historyList.innerHTML = "";
return;
}
historySection.classList.remove("hidden");
historyList.innerHTML = "";
history.forEach((entry) => {
const dt = new Date(entry.archivedAt || Date.now());
const div = document.createElement("div");
div.className = "item-card archived ready";
div.innerHTML = `
<div class="item-info">
<span class="item-name">${entry.name}</span>
<span class="item-meta">Stolik ${entry.sourceTable || "?"}${dt.toLocaleDateString("pl-PL")} ${dt.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'})}</span>
</div>
<div class="item-qty">x${entry.qty}</div>
`;
historyList.appendChild(div);
});
}
function mergeWithPersistedItems(articles) {
const current = new Map();
articles.forEach(a => {
const name = normalizeArticleName(a.Name);
const todo = parseFloat(String(a.QuantityToDo || a.QuantitySet || "0").replace(",","."));
const done = parseFloat(String(a.QuantityDone || "0").replace(",","."));
if (!current.has(name)) {
current.set(name, { name, qty: 0, done: 0, present: true, completedByDisappear: false });
}
const curr = current.get(name);
curr.qty += Number.isFinite(todo) ? todo : 0;
curr.done += Number.isFinite(done) ? done : 0;
});
const persisted = loadPersistedItems();
const persistedMap = new Map(persisted.map(i => [i.name, i]));
const merged = [];
// Aktualnie obecne pozycje z WS
current.forEach((item) => {
merged.push({
...item,
present: true,
completedByDisappear: false,
updatedAt: Date.now()
});
});
// Pozycje, które były wcześniej, ale zniknęły z WS -> zostają jako gotowe
persistedMap.forEach((oldItem, name) => {
if (current.has(name)) return;
const qty = Number.isFinite(oldItem?.qty) ? oldItem.qty : 0;
const archivedAt = Number.isFinite(oldItem?.archivedAt) ? oldItem.archivedAt : Date.now();
const shouldMoveToGlobal = (Date.now() - archivedAt) > HOT_WINDOW_MS;
if (shouldMoveToGlobal) {
addItemsToGlobalHistory([{ name, qty }], tableParam);
return;
}
merged.push({
name,
qty,
done: qty,
present: false,
completedByDisappear: true,
archivedAt,
updatedAt: Date.now()
});
});
// Aktywne na górze, gotowe (zniknięte) na dole
merged.sort((a, b) => {
if (a.present !== b.present) return a.present ? -1 : 1;
return a.name.localeCompare(b.name, "pl");
});
savePersistedItems(merged);
return merged;
}
function renderItems(items) {
emptyState.classList.add("hidden");
itemsList.innerHTML = "";
items.forEach((item) => {
const isReady = item.done >= item.qty && item.qty > 0;
const div = document.createElement("div");
div.className = `item-card ${isReady ? 'ready' : ''} ${item.present ? '' : 'archived'}`;
let meta = "🔥 W przygotowaniu";
if (isReady && item.completedByDisappear) {
meta = "✅ Gotowe (zrealizowane)";
} else if (isReady) {
meta = "✅ Gotowe";
}
div.innerHTML = `
<div class="item-info">
<span class="item-name">${item.name}</span>
<span class="item-meta">${meta}</span>
</div>
<div class="item-qty">x${item.qty}</div>
`;
itemsList.appendChild(div);
});
renderGlobalHistory();
}
function updateStatus(bills, items) {
let total = 0;
let done = 0;
items.forEach(i => {
total += Number.isFinite(i.qty) ? i.qty : 0;
done += Number.isFinite(i.done) ? i.done : 0;
});
const pct = total > 0 ? (done / total) * 100 : 0;
progressBar.style.width = `${pct}%`;
if (pct >= 100) {
prepStatus.textContent = "Gotowe do podania!";
statusIcon.textContent = "😋";
statusMeta.textContent = "Wszystkie Twoje dania opuściły już kuchnię.";
} else if (pct > 0) {
prepStatus.textContent = "Częściowo gotowe";
statusIcon.textContent = "🍳";
statusMeta.textContent = "Pierwsze pyszności już na Ciebie czekają!";
} else {
prepStatus.textContent = "W przygotowaniu";
statusIcon.textContent = "👨‍🍳";
statusMeta.textContent = "Twoje zamówienie jest właśnie tworzone przez naszych kucharzy.";
}
// Footer meta
const newest = [...bills].sort((a,b) => new Date(b?.Date || 0) - new Date(a?.Date || 0))[0];
const time = newest?.Date
? new Date(newest.Date).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})
: "--:--";
metaFooter.textContent = `Zamówienie złożone o godzinie ${time} • Stolik ${tableParam}`;
}
// WebSocket Logic
function connect() {
const proto = location.protocol === "https:" ? "wss" : "ws";
const ws = new WebSocket(`${proto}://${location.host}/ws`);
ws.onmessage = (evt) => {
try {
const data = JSON.parse(evt.data);
const parsed = data?.parsed;
if (!parsed || !Array.isArray(parsed.Bills)) return;
if (!tableParam) {
updateUI([]);
return;
}
const myTable = tableParam.toLowerCase();
const matches = parsed.Bills.filter(b => {
const c = detectTableCandidates(b);
// Używamy \b by uniknąć sytuacji gdzie myTable="1" pasuje do "stolik 16"
const regex = new RegExp(`stolik\\s*${myTable}\\b`, "i");
return (
c.fromRemark === myTable ||
c.fromDescription === myTable ||
regex.test(c.remark)
);
});
updateUI(matches);
} catch (e) { console.error("Data error", e); }
};
ws.onclose = () => setTimeout(connect, 3000); // Auto-reconnect
ws.onerror = () => {
loaderMsg.textContent = "Problem z połączeniem. Próbujemy ponownie...";
};
}
connect();
// --- CALL WAITER LOGIC ---
let billState = { payment: '', doc: '', nip: '', company: null };
function showToast(msg) {
const t = document.getElementById("toastMsg");
document.getElementById("toastText").textContent = msg;
t.classList.remove("active");
void t.offsetWidth; // trigger reflow
t.classList.add("active");
setTimeout(() => t.classList.remove("active"), 3500);
}
function sendApiSimulated(actionName, details) {
console.log(`[SYMULACJA API] Akcja: ${actionName}`, details);
// Przykładowe wysłanie docelowo:
// if (window.socket && window.socket.readyState === WebSocket.OPEN) {
// window.socket.send(JSON.stringify({ action: "sendUpstream", payload: { type: actionName, table: tableParam, ...details } }));
// }
}
window.callWaiter = function(type) {
if (type === 'order') {
sendApiSimulated("CallWaiter_Order", { table: tableParam });
showToast("Kelner wkrótce do Ciebie podejdzie!");
}
};
window.openBillDialog = function() {
billState = { payment: '', doc: '', nip: '', company: null };
document.getElementById("billModal").classList.add("active");
goToStep("stepPayment");
};
window.closeBillDialog = function() {
document.getElementById("billModal").classList.remove("active");
};
window.goToStep = function(stepId) {
document.querySelectorAll(".step").forEach(s => s.classList.remove("active"));
document.getElementById(stepId).classList.add("active");
};
window.selectPayment = function(method) {
billState.payment = method;
goToStep("stepDocument");
};
window.selectDocument = function(docType) {
billState.doc = docType;
if (docType === 'paragon') {
closeBillDialog();
sendApiSimulated("CallWaiter_Bill", { table: tableParam, payment: billState.payment, doc: 'paragon' });
showToast("Kelner przyniesie paragon do opłacenia!");
} else {
goToStep("stepNIP");
document.getElementById("nipInput").value = '';
setTimeout(() => document.getElementById("nipInput").focus(), 100);
}
};
window.fetchGUS = function() {
const nip = document.getElementById("nipInput").value.replace(/[\s-]/g, '');
if (nip.length < 10) {
alert("Wprowadź poprawny numer NIP.");
return;
}
const btn = document.getElementById("btnGUS");
btn.textContent = "Szukam...";
btn.disabled = true;
// Symulacja pobrania z GUS
setTimeout(() => {
btn.textContent = "Pobierz z GUS";
btn.disabled = false;
billState.nip = nip;
billState.company = {
name: "Przykładowa Firma Sp. z o.o.",
address: "ul. Gastronomiczna 12/4, 00-120 Warszawa",
nip: nip
};
document.getElementById("cmpName").value = billState.company.name;
document.getElementById("cmpAddress").value = billState.company.address;
document.getElementById("cmpNip").value = "NIP: " + billState.company.nip;
// reset do readonly
document.getElementById("cmpName").readOnly = true;
document.getElementById("cmpAddress").readOnly = true;
document.getElementById("btnEditCompany").textContent = "Popraw ręcznie";
goToStep("stepVerify");
}, 1200);
};
window.editCompanyData = function() {
const n = document.getElementById("cmpName");
const a = document.getElementById("cmpAddress");
const btn = document.getElementById("btnEditCompany");
if (n.readOnly) {
n.readOnly = false;
a.readOnly = false;
n.focus();
btn.textContent = "Zakończ edycję";
} else {
n.readOnly = true;
a.readOnly = true;
btn.textContent = "Popraw ręcznie";
}
};
window.confirmInvoice = function() {
billState.company.name = document.getElementById("cmpName").value;
billState.company.address = document.getElementById("cmpAddress").value;
closeBillDialog();
sendApiSimulated("CallWaiter_Bill", {
table: tableParam,
payment: billState.payment,
doc: 'faktura',
nip: billState.nip,
company: billState.company
});
showToast("Dziękujemy! Prośba o fakturę została wysłana.");
};
// Fallback: If no data after 25s, show empty state anyway
setTimeout(() => {
if (!loadingScreen.classList.contains("hidden")) {
updateUI([]);
}
}, 25000);
</script>
</body>
</html>