Porządki v1

This commit is contained in:
2026-05-27 08:47:59 +02:00
parent 46cbf4d05b
commit 22989d9f9b
16 changed files with 1142 additions and 3 deletions

731
legacy/stolik3.html Normal file
View File

@@ -0,0 +1,731 @@
<!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>