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

732 lines
20 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, viewport-fit=cover" />
<title>Karczma Biesiada</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=Playfair+Display:wght@400;500&family=DM+Sans:wght@300;400;500&display=swap" rel="stylesheet">
<style>
:root {
--ink: #1a1410;
--ink-2: #3d342b;
--ink-3: #7a6d63;
--cream: #faf7f2;
--cream-2: #f2ede4;
--cream-3: #e8e0d4;
--gold: #c9a84c;
--gold-lt: #f0d89a;
--gold-dk: #8c6b22;
--green: #2d5a3d;
--green-lt: #e8f2eb;
--amber: #b85c1a;
--amber-lt: #faeee5;
--r: 16px;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html {
font-family: 'DM Sans', sans-serif;
font-size: 16px;
color: var(--ink);
background: var(--cream);
}
body {
min-height: 100dvh;
display: flex;
flex-direction: column;
background:
radial-gradient(ellipse 80% 40% at 50% 0%, rgba(201,168,76,.10) 0%, transparent 70%),
var(--cream);
}
/* ── HEADER ── */
header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px 20px 0;
animation: fadeDown .5s ease both;
}
.brand {
display: flex;
flex-direction: column;
gap: 1px;
}
.brand-name {
font-family: 'Playfair Display', serif;
font-size: 20px;
font-weight: 400;
color: var(--ink);
line-height: 1;
}
.brand-tagline {
font-size: 11px;
font-weight: 300;
color: var(--ink-3);
letter-spacing: .06em;
text-transform: uppercase;
}
.table-pill {
display: flex;
align-items: center;
gap: 6px;
background: var(--ink);
color: var(--cream);
font-size: 13px;
font-weight: 500;
padding: 6px 13px;
border-radius: 999px;
}
.table-pill svg { opacity: .65; }
/* ── HERO ── */
.hero {
padding: 36px 20px 28px;
text-align: center;
animation: fadeUp .55s .1s ease both;
}
.hero-icon {
width: 60px;
height: 60px;
border-radius: 50%;
background: var(--cream-2);
border: 1px solid var(--cream-3);
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 18px;
transition: background .4s, border-color .4s;
}
.hero-icon svg {
width: 26px; height: 26px;
transition: opacity .3s;
}
.hero-title {
font-family: 'Playfair Display', serif;
font-size: 26px;
font-weight: 400;
color: var(--ink);
margin-bottom: 8px;
line-height: 1.2;
}
.hero-sub {
font-size: 14px;
font-weight: 300;
color: var(--ink-3);
line-height: 1.6;
max-width: 280px;
margin: 0 auto;
}
/* ── STATUS BADGE ── */
.status-wrap {
display: flex;
justify-content: center;
margin-top: 20px;
}
.status-badge {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 7px 15px;
border-radius: 999px;
font-size: 13px;
font-weight: 500;
border: 1px solid transparent;
transition: all .35s ease;
}
.status-badge.s-idle {
background: var(--cream-2);
border-color: var(--cream-3);
color: var(--ink-3);
}
.status-badge.s-cooking {
background: var(--amber-lt);
border-color: rgba(184,92,26,.25);
color: var(--amber);
}
.status-badge.s-partial {
background: #e8f0fa;
border-color: rgba(59,100,180,.22);
color: #2a5cb8;
}
.status-badge.s-done {
background: var(--green-lt);
border-color: rgba(45,90,61,.22);
color: var(--green);
}
.status-dot {
width: 7px; height: 7px;
border-radius: 50%;
flex-shrink: 0;
background: currentColor;
}
.status-dot.pulse { animation: dotpulse 1.6s ease-in-out infinite; }
@keyframes dotpulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: .4; transform: scale(.65); }
}
/* ── PROGRESS ── */
.progress-section {
padding: 0 20px;
margin-bottom: 8px;
animation: fadeUp .5s .15s ease both;
}
.progress-meta {
display: flex;
justify-content: space-between;
font-size: 12px;
font-weight: 400;
color: var(--ink-3);
margin-bottom: 7px;
}
.progress-track {
height: 4px;
background: var(--cream-3);
border-radius: 999px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: var(--green);
border-radius: 999px;
transition: width .6s cubic-bezier(.4, 0, .2, 1);
}
/* ── LOADER ── */
.loader-section {
margin: 8px 20px 0;
padding: 28px 20px;
border-radius: var(--r);
border: 1px solid var(--cream-3);
background: var(--cream-2);
text-align: center;
animation: fadeUp .5s .2s ease both;
}
.loader-ring {
width: 36px; height: 36px;
margin: 0 auto 14px;
border-radius: 50%;
border: 2px solid var(--cream-3);
border-top-color: var(--gold);
animation: spin .85s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.loader-title {
font-size: 14px;
font-weight: 500;
color: var(--ink-2);
margin-bottom: 4px;
}
.loader-sub {
font-size: 13px;
font-weight: 300;
color: var(--ink-3);
line-height: 1.5;
}
/* ── ITEMS CARD ── */
.items-card {
margin: 14px 20px 0;
border-radius: var(--r);
border: 1px solid var(--cream-3);
background: #fff;
overflow: hidden;
animation: fadeUp .5s .2s ease both;
}
.items-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px 12px;
border-bottom: 1px solid var(--cream-3);
}
.items-label {
font-size: 11px;
font-weight: 500;
color: var(--ink-3);
text-transform: uppercase;
letter-spacing: .07em;
}
.items-count {
font-size: 12px;
color: var(--ink-3);
background: var(--cream-2);
padding: 2px 8px;
border-radius: 999px;
border: 1px solid var(--cream-3);
}
.order-row {
display: flex;
align-items: center;
gap: 12px;
padding: 13px 16px;
border-bottom: 1px solid var(--cream-3);
transition: background .15s;
}
.order-row:last-child { border-bottom: none; }
.order-row:active { background: var(--cream-2); }
.row-dot {
width: 6px; height: 6px;
border-radius: 50%;
background: var(--gold);
flex-shrink: 0;
}
.row-name {
flex: 1;
font-size: 15px;
font-weight: 400;
color: var(--ink);
line-height: 1.35;
}
.row-qty {
font-size: 13px;
font-weight: 500;
color: var(--ink-3);
background: var(--cream-2);
border: 1px solid var(--cream-3);
border-radius: 6px;
padding: 3px 9px;
white-space: nowrap;
}
/* ── META TAGS ── */
.meta-row {
display: flex;
gap: 6px;
flex-wrap: wrap;
padding: 14px 20px 0;
animation: fadeUp .5s .3s ease both;
}
.meta-tag {
font-size: 12px;
font-weight: 300;
color: var(--ink-3);
background: var(--cream-2);
border: 1px solid var(--cream-3);
border-radius: 6px;
padding: 3px 9px;
}
/* ── FOOTER ── */
footer {
margin-top: auto;
padding: 28px 20px 24px;
text-align: center;
animation: fadeUp .5s .35s ease both;
}
.footer-line {
font-size: 12px;
font-weight: 300;
color: var(--cream-3);
}
.footer-divider {
width: 32px;
height: 1px;
background: var(--cream-3);
margin: 14px auto;
}
.ws-status {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 11px;
color: var(--ink-3);
}
.ws-dot {
width: 6px; height: 6px;
border-radius: 50%;
background: var(--ink-3);
}
.ws-dot.connected { background: var(--green); }
.ws-dot.error { background: var(--amber); }
/* ── HELPERS ── */
.hidden { display: none !important; }
@keyframes fadeUp {
from { opacity: 0; transform: translateY(14px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes fadeDown {
from { opacity: 0; transform: translateY(-8px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes itemIn {
from { opacity: 0; transform: translateX(-10px); }
to { opacity: 1; transform: translateX(0); }
}
.order-row { animation: itemIn .35s ease both; }
</style>
</head>
<body>
<header>
<div class="brand">
<span class="brand-name">Karczma Biesiada</span>
<span class="brand-tagline">Status zamówienia</span>
</div>
<div class="table-pill" id="tablePill">
<svg width="12" height="12" viewBox="0 0 16 16" fill="none">
<rect x="1" y="7" width="14" height="2" rx="1" fill="currentColor"/>
<rect x="3" y="9" width="2" height="5" rx="1" fill="currentColor"/>
<rect x="11" y="9" width="2" height="5" rx="1" fill="currentColor"/>
</svg>
<span id="tableLabel">Stolik&nbsp;</span>
</div>
</header>
<!-- HERO -->
<div class="hero">
<div class="hero-icon" id="heroIcon">
<!-- icon swapped by JS -->
<svg id="iconIdle" viewBox="0 0 24 24" fill="none" stroke="var(--ink-3)" stroke-width="1.5">
<circle cx="12" cy="12" r="9"/>
<path d="M12 7v5l3 2" stroke-linecap="round"/>
</svg>
<svg id="iconCooking" class="hidden" viewBox="0 0 24 24" fill="none" stroke="var(--amber)" stroke-width="1.5">
<path d="M3 12c0-5 2-8 9-8s9 3 9 8" stroke-linecap="round"/>
<path d="M3 12h18M12 12v6" stroke-linecap="round"/>
<path d="M9 18h6" stroke-linecap="round"/>
<path d="M8 5.5c0-1 .5-2 1-2.5M12 5c0-1 .5-2 1-2.5M16 5.5c0-1-.5-2-1-2.5" stroke-linecap="round"/>
</svg>
<svg id="iconDone" class="hidden" viewBox="0 0 24 24" fill="none" stroke="var(--green)" stroke-width="1.5">
<circle cx="12" cy="12" r="9"/>
<path d="M8 12l3 3 5-5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<h1 class="hero-title" id="heroTitle">Sprawdzamy…</h1>
<p class="hero-sub" id="heroSub">Łączymy się z kuchnią. Chwileczkę.</p>
<div class="status-wrap">
<span class="status-badge s-idle" id="statusBadge">
<span class="status-dot pulse" id="statusDot"></span>
<span id="statusText">Łączenie</span>
</span>
</div>
</div>
<!-- PROGRESS -->
<div class="progress-section hidden" id="progressSection">
<div class="progress-meta">
<span>Postęp przygotowania</span>
<span id="progressPct">0%</span>
</div>
<div class="progress-track">
<div class="progress-fill" id="progressFill" style="width:0%"></div>
</div>
</div>
<!-- LOADER -->
<div class="loader-section" id="loaderSection">
<div class="loader-ring"></div>
<div class="loader-title">Szukamy Twojego zamówienia</div>
<div class="loader-sub">Łączymy się z kuchnią.<br>Może chwilkę potrwać.</div>
</div>
<!-- ITEMS -->
<div class="items-card hidden" id="itemsCard">
<div class="items-header">
<span class="items-label">Na rachunku</span>
<span class="items-count" id="itemsCount">0 pozycji</span>
</div>
<div id="itemsList"></div>
</div>
<!-- META -->
<div class="meta-row hidden" id="metaRow"></div>
<footer>
<div class="footer-divider"></div>
<div class="ws-status">
<span class="ws-dot" id="wsDot"></span>
<span id="wsLabel">Rozłączono</span>
</div>
</footer>
<script>
/* ── HELPERS ── */
const $ = id => document.getElementById(id);
const params = new URLSearchParams(location.search);
const tableParam = (params.get("table") || "").trim();
// Set table label
$("tableLabel").textContent = tableParam ? `Stolik\u00a0${tableParam}` : "Brak stolika";
function parseNum(v) {
const n = parseFloat(String(v || "0").replace(",", "."));
return Number.isFinite(n) ? n : 0;
}
function detectTable(bill) {
const t = `${bill?.Remark || ""} ${bill?.Description || ""}`;
const m = t.match(/STOLIK\s*([0-9A-Z]+)/i);
return m ? m[1] : "";
}
function minutesAgo(iso) {
const diff = Math.max(0, Math.floor((Date.now() - new Date(iso || Date.now())) / 60000));
if (diff < 1) return "przed chwilą";
if (diff === 1) return "1 min temu";
if (diff < 5) return `${diff} min temu`;
return `${diff} min temu`;
}
/* ── RENDER STATES ── */
function setIcon(name) {
["Idle","Cooking","Done"].forEach(n => $("icon"+n).classList.add("hidden"));
$("icon"+name).classList.remove("hidden");
}
function setHeroIcon(state) {
const icon = $("heroIcon");
icon.style.background = state === "done" ? "var(--green-lt)"
: state === "cooking" || state === "partial" ? "var(--amber-lt)"
: "var(--cream-2)";
icon.style.borderColor = state === "done" ? "rgba(45,90,61,.2)"
: state === "cooking" || state === "partial" ? "rgba(184,92,26,.2)"
: "var(--cream-3)";
if (state === "done") setIcon("Done");
else if (state === "cooking" || state === "partial") setIcon("Cooking");
else setIcon("Idle");
}
function setStatus(state, text) {
const badge = $("statusBadge");
const dot = $("statusDot");
badge.className = "status-badge";
dot.className = "status-dot";
if (state === "cooking") { badge.classList.add("s-cooking"); dot.classList.add("pulse"); }
else if (state === "partial") { badge.classList.add("s-partial"); dot.classList.add("pulse"); }
else if (state === "done") badge.classList.add("s-done");
else badge.classList.add("s-idle");
$("statusText").textContent = text;
}
function setProgress(pct) {
const p = Math.max(0, Math.min(100, pct));
$("progressFill").style.width = p + "%";
$("progressPct").textContent = Math.round(p) + "%";
}
function showEl(id, show) {
$(id).classList.toggle("hidden", !show);
}
/* ── RENDER BILLS ── */
function renderBills(bills) {
// Hide loader
showEl("loaderSection", false);
if (!bills.length) {
setHeroIcon("idle");
$("heroTitle").textContent = "Cisza w kuchni";
$("heroSub").textContent = "Aktualnie nie mamy aktywnego zamówienia dla tego stolika.";
setStatus("idle", "Brak zamówienia");
showEl("progressSection", false);
showEl("itemsCard", false);
showEl("metaRow", false);
return;
}
const allArticles = bills.flatMap(b => Array.isArray(b?.Articles) ? b.Articles : []);
if (!allArticles.length) {
setHeroIcon("cooking");
$("heroTitle").textContent = "Gotujemy dla Ciebie";
$("heroSub").textContent = "Zamówienie przyjęte — kuchnia właśnie zaczyna.";
setStatus("cooking", "W trakcie przygotowywania");
showEl("progressSection", true);
setProgress(0);
showEl("itemsCard", false);
showEl("metaRow", false);
return;
}
// Group items
const grouped = new Map();
let sumTodo = 0, sumDone = 0;
for (const a of allArticles) {
const name = (a.Name || "Pozycja").trim();
const qty = parseNum(a.QuantityToDo || a.QuantitySet || "1");
const done = parseNum(a.QuantityDone || "0");
sumTodo += qty;
sumDone += done;
grouped.set(name, (grouped.get(name) || 0) + qty);
}
const progress = sumTodo > 0 ? (sumDone / sumTodo) * 100 : 0;
// Render list
const list = $("itemsList");
list.innerHTML = "";
[...grouped.entries()].forEach(([name, qty], i) => {
const row = document.createElement("div");
row.className = "order-row";
row.style.animationDelay = (i * 0.05) + "s";
row.innerHTML = `
<span class="row-dot"></span>
<span class="row-name">${name}</span>
<span class="row-qty">× ${qty % 1 === 0 ? qty.toFixed(0) : qty.toFixed(2)}</span>
`;
list.appendChild(row);
});
const count = grouped.size;
$("itemsCount").textContent = count + (count === 1 ? " pozycja" : count < 5 ? " pozycje" : " pozycji");
// Status
if (sumDone >= sumTodo && sumTodo > 0) {
setHeroIcon("done");
$("heroTitle").textContent = "Smacznego!";
$("heroSub").textContent = "Wszystko gotowe. Kelner zaraz do Was dotrze.";
setStatus("done", "Gotowe do podania");
} else if (sumDone > 0) {
setHeroIcon("partial");
$("heroTitle").textContent = "Część już jedzie!";
$("heroSub").textContent = "Kilka pozycji gotowych — reszta zaraz będzie.";
setStatus("partial", "Część gotowa do podania");
} else {
setHeroIcon("cooking");
$("heroTitle").textContent = "Gotujemy dla Ciebie";
$("heroSub").textContent = "Kuchnia pracuje nad Twoim zamówieniem.";
setStatus("cooking", "W trakcie przygotowywania");
}
showEl("progressSection", true);
setProgress(progress);
showEl("itemsCard", true);
// Meta
const sorted = [...bills].sort((a, b) => new Date(a?.Date || 0) - new Date(b?.Date || 0));
const first = sorted[0];
const nums = bills.map(b => b?.Description).filter(Boolean).join(", ");
const metaRow = $("metaRow");
metaRow.innerHTML = [
`Stolik ${tableParam}`,
nums ? `Nr: ${nums}` : null,
first?.Date ? `Zamówiono ${minutesAgo(first.Date)}` : null,
first?.Date ? `Godz. ${new Date(first.Date).toLocaleTimeString("pl-PL", {hour:"2-digit", minute:"2-digit"})}` : null,
].filter(Boolean).map(t => `<span class="meta-tag">${t}</span>`).join("");
showEl("metaRow", true);
}
/* ── WEBSOCKET ── */
function setWS(state) {
const dot = $("wsDot");
const lbl = $("wsLabel");
dot.className = "ws-dot";
if (state === "connected") { dot.classList.add("connected"); lbl.textContent = "Połączono z kuchnią"; }
else if (state === "error") { dot.classList.add("error"); lbl.textContent = "Błąd połączenia"; }
else { lbl.textContent = "Rozłączono"; }
}
function handleMessage(parsed) {
if (!parsed || parsed.Type !== "bills" || !Array.isArray(parsed.Bills)) return;
if (!tableParam) {
showEl("loaderSection", false);
$("heroTitle").textContent = "Brak numeru stolika";
$("heroSub").textContent = "Użyj adresu z parametrem ?table=9";
setStatus("idle", "Brak stolika");
return;
}
const norm = tableParam.toLowerCase();
const matches = parsed.Bills.filter(b => detectTable(b).toLowerCase() === norm)
.sort((a, b) => new Date(b?.Date || 0) - new Date(a?.Date || 0));
renderBills(matches);
}
if (tableParam) {
const proto = location.protocol === "https:" ? "wss" : "ws";
const ws = new WebSocket(`${proto}://${location.host}/ws`);
ws.onopen = () => setWS("connected");
ws.onmessage = evt => {
let data;
try { data = JSON.parse(evt.data); } catch { return; }
if (data.event === "snapshot" || data.event === "message") handleMessage(data.parsed);
};
ws.onerror = () => {
setWS("error");
showEl("loaderSection", false);
$("heroTitle").textContent = "Błąd połączenia";
$("heroSub").textContent = "Nie udało się połączyć z kuchnią. Odśwież stronę.";
setStatus("idle", "Błąd");
};
ws.onclose = () => setWS("disconnected");
} else {
// No table param
showEl("loaderSection", false);
$("heroTitle").textContent = "Brak numeru stolika";
$("heroSub").textContent = "Zeskanuj kod QR przy swoim stoliku, aby zobaczyć zamówienie.";
setStatus("idle", "Brak stolika");
$("tableLabel").textContent = "Brak stolika";
}
</script>
</body>
</html>