Files
karczma-aplikacja-stoliki/legacy/stolik.html
2026-05-27 08:47:59 +02:00

351 lines
10 KiB
HTML
Raw 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 status zamówienia</title>
<style>
:root {
--bg1: #0c1222;
--bg2: #141b31;
--card: #1f2a44;
--card-soft: #243252;
--muted: #aab4c9;
--ok: #10b981;
--warn: #f59e0b;
--accent: #7c3aed;
--accent2: #22d3ee;
--text: #f8fafc;
}
body {
margin: 0;
font-family: "Segoe UI", Arial, sans-serif;
background: radial-gradient(circle at top, #1d2a4a 0%, var(--bg1) 45%, var(--bg2) 100%);
color: var(--text);
min-height: 100vh;
display: flex;
justify-content: center;
padding: 14px;
box-sizing: border-box;
}
.wrap { width: 100%; max-width: 700px; }
.card {
background: linear-gradient(180deg, var(--card), var(--card-soft));
border: 1px solid rgba(255,255,255,.09);
border-radius: 18px;
padding: 16px;
margin-bottom: 14px;
box-shadow: 0 12px 28px rgba(0,0,0,.28);
backdrop-filter: blur(4px);
}
h1 { margin: 0 0 6px; font-size: 26px; }
.muted { color: var(--muted); font-size: 14px; line-height: 1.35; }
.pill {
display: inline-block;
padding: 6px 10px;
border-radius: 999px;
background: rgba(124,58,237,.22);
border: 1px solid rgba(124,58,237,.45);
font-size: 12px;
margin-bottom: 8px;
}
.status {
font-size: 21px;
font-weight: bold;
margin-top: 8px;
}
.status.prep { color: var(--warn); }
.status.done { color: var(--ok); }
.progress-wrap {
margin-top: 10px;
width: 100%;
background: rgba(255,255,255,.12);
border-radius: 999px;
height: 10px;
overflow: hidden;
}
.progress {
height: 100%;
width: 0%;
background: linear-gradient(90deg, var(--accent2), var(--accent));
transition: width .45s ease;
}
.loader-row {
display: flex;
gap: 12px;
align-items: center;
margin-top: 8px;
padding: 10px;
border-radius: 12px;
background: rgba(255,255,255,.06);
}
.spinner {
width: 34px;
height: 34px;
border-radius: 50%;
border: 3px solid rgba(255,255,255,.2);
border-top-color: #fff;
animation: spin .8s linear infinite;
flex-shrink: 0;
}
.search-icon {
font-size: 18px;
animation: bob 1s ease-in-out infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
@keyframes bob {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-3px); }
}
ul { list-style: none; padding: 0; margin: 12px 0 0; }
li {
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
padding: 11px 10px;
border-bottom: 1px solid rgba(255,255,255,.1);
}
li:last-child { border-bottom: none; }
.item-name {
font-weight: 600;
line-height: 1.3;
}
.qty {
min-width: 54px;
text-align: right;
color: #dbe5ff;
font-weight: bold;
background: rgba(34,211,238,.18);
border: 1px solid rgba(34,211,238,.35);
border-radius: 10px;
padding: 5px 8px;
}
.hidden { display: none !important; }
</style>
</head>
<body>
<div class="wrap">
<div class="card">
<div class="pill">🍽️ Twoje zamówienie</div>
<h1>Karczma Biesiada</h1>
<div class="muted" id="tableLabel">Status zamówienia stolika</div>
<div class="status prep" id="prepStatus">Łączenie z serwerem…</div>
<div class="muted" id="meta">-</div>
<div class="progress-wrap"><div class="progress" id="progress"></div></div>
<div id="loadingBox" class="loader-row">
<div class="spinner"></div>
<div>
<div style="font-weight:600;display:flex;align-items:center;gap:6px;">
<span class="search-icon">🔎</span>
Szukamy Twojego zamówienia…
</div>
<div class="muted">Jesteśmy w trakcie wyszukiwania. Daj nam chwilkę ✨</div>
</div>
</div>
</div>
<div class="card">
<h3 style="margin:0 0 6px;">Co już jest na Twoim rachunku</h3>
<div class="muted" id="empty">Czekam na dane…</div>
<ul id="items" style="display:none;"></ul>
</div>
</div>
<script>
const params = new URLSearchParams(location.search);
const tableParam = (params.get("table") || "").trim();
const tableLabelEl = document.getElementById("tableLabel");
const prepStatusEl = document.getElementById("prepStatus");
const metaEl = document.getElementById("meta");
const emptyEl = document.getElementById("empty");
const itemsEl = document.getElementById("items");
const loadingBoxEl = document.getElementById("loadingBox");
const progressEl = document.getElementById("progress");
tableLabelEl.textContent = tableParam
? `Status zamówienia stolik ${tableParam}`
: "Brak numeru stolika w adresie";
function parseNumber(value) {
const n = parseFloat(String(value || "0").replace(",", "."));
return Number.isFinite(n) ? n : 0;
}
function detectTableNumber(bill) {
const text = `${bill?.Remark || ""} ${bill?.Description || ""}`;
const m = text.match(/STOLIK\s*([0-9A-Z]+)/i);
return m ? m[1] : "";
}
function byLatestDate(a, b) {
return new Date(b?.Date || 0).getTime() - new Date(a?.Date || 0).getTime();
}
function minutesAgoText(dateIso) {
const ts = new Date(dateIso || Date.now()).getTime();
if (!ts) return "chwilę temu";
const diffMin = Math.max(0, Math.floor((Date.now() - ts) / 60000));
if (diffMin < 1) return "przed chwilą";
if (diffMin === 1) return "1 minutę temu";
if (diffMin < 5) return `${diffMin} minuty temu`;
return `${diffMin} minut temu`;
}
function setProgress(percent) {
const p = Math.max(0, Math.min(100, percent || 0));
progressEl.style.width = `${p}%`;
}
function renderBills(bills) {
loadingBoxEl.classList.add("hidden");
const allArticles = bills.flatMap((b) => Array.isArray(b?.Articles) ? b.Articles : []);
if (!allArticles.length) {
emptyEl.style.display = "block";
itemsEl.style.display = "none";
emptyEl.textContent = "Brak pozycji do wyświetlenia dla tego stolika.";
prepStatusEl.textContent = "W trakcie przygotowywania 🍳";
prepStatusEl.className = "status prep";
setProgress(0);
return;
}
const grouped = new Map();
let sumTodo = 0;
let sumDone = 0;
for (const a of allArticles) {
const name = (a.Name || "Pozycja").trim();
const qty = parseNumber(a.QuantityToDo || a.QuantitySet || "1");
const done = parseNumber(a.QuantityDone || "0");
sumTodo += qty;
sumDone += done;
grouped.set(name, (grouped.get(name) || 0) + qty);
}
const entries = [...grouped.entries()];
itemsEl.innerHTML = "";
for (const [name, qty] of entries) {
const li = document.createElement("li");
li.innerHTML = `<span class="item-name">${name}</span><span class="qty">x ${qty % 1 === 0 ? qty.toFixed(0) : qty.toFixed(2)}</span>`;
itemsEl.appendChild(li);
}
emptyEl.style.display = "none";
itemsEl.style.display = "block";
const progress = sumTodo > 0 ? (sumDone / sumTodo) * 100 : 0;
setProgress(progress);
if (sumTodo > 0 && sumDone >= sumTodo) {
prepStatusEl.textContent = "Gotowe do odbioru ✅";
prepStatusEl.className = "status done";
} else if (sumDone > 0) {
prepStatusEl.textContent = "Część zamówienia już gotowa ⏳";
prepStatusEl.className = "status prep";
} else {
prepStatusEl.textContent = "W trakcie przygotowywania 🍳";
prepStatusEl.className = "status prep";
}
const byDateAsc = [...bills].sort((a, b) => new Date(a?.Date || 0) - new Date(b?.Date || 0));
const firstOrder = byDateAsc[0];
const billNumbers = bills.map((b) => b?.Description).filter(Boolean).join(", ");
const billsLabel = bills.length === 1 ? "1 rachunek" : `${bills.length} rachunki`;
metaEl.textContent = `${billsLabel} • Nr: ${billNumbers || "-"} • Pierwsze zamówienie: ${minutesAgoText(firstOrder?.Date)} • Godzina: ${new Date(firstOrder?.Date || Date.now()).toLocaleTimeString()}`;
}
function handleBillsMessage(parsed) {
if (!parsed || parsed.Type !== "bills" || !Array.isArray(parsed.Bills)) return;
if (!tableParam) {
loadingBoxEl.classList.add("hidden");
prepStatusEl.textContent = "Brak numeru stolika w URL";
prepStatusEl.className = "status prep";
emptyEl.style.display = "block";
emptyEl.textContent = "Użyj adresu np. /stolik.html?table=9";
itemsEl.style.display = "none";
setProgress(0);
return;
}
const norm = tableParam.toLowerCase();
const matches = parsed.Bills.filter((bill) => {
const t = detectTableNumber(bill).toLowerCase();
return t === norm;
}).sort(byLatestDate);
if (!matches.length) {
loadingBoxEl.classList.remove("hidden");
prepStatusEl.textContent = "Oczekujemy na zamówienie";
prepStatusEl.className = "status prep";
metaEl.textContent = `Stolik ${tableParam}`;
emptyEl.style.display = "block";
emptyEl.textContent = `Jeszcze nie widzimy aktywnego zamówienia dla stolika ${tableParam}.`;
itemsEl.style.display = "none";
setProgress(0);
return;
}
renderBills(matches);
}
const proto = location.protocol === "https:" ? "wss" : "ws";
const ws = new WebSocket(`${proto}://${location.host}/ws`);
ws.onmessage = (evt) => {
let data;
try {
data = JSON.parse(evt.data);
} catch {
return;
}
if (data.event === "snapshot") {
handleBillsMessage(data.parsed);
return;
}
if (data.event === "message") {
handleBillsMessage(data.parsed);
}
};
ws.onerror = () => {
loadingBoxEl.classList.add("hidden");
prepStatusEl.textContent = "Błąd połączenia";
prepStatusEl.className = "status prep";
setProgress(0);
};
</script>
</body>
</html>