The catalog endpoint already sends current_qty and the till already receives it — pos-app.js just never drew it. The number is shown as it is, negatives included, and the tile is styled as out of stock when the quantity is not positive. The cue is current_qty <= 0, not current_qty <= min_qty: only 48 of the 4,083 sellable items have a reorder point set, so a min-based rule would badge every tile and mean nothing. Nothing is blocked. The cashier can still sell the item and the quantity still goes into minus.
609 lines
43 KiB
HTML
609 lines
43 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, user-scalable=no">
|
||
<meta name="theme-color" content="#ff385c">
|
||
<title>New sale</title>
|
||
<link rel="manifest" href="manifest.json">
|
||
<link rel="stylesheet" href="fa/all.min.css">
|
||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='7' fill='%23ff385c'/><text x='16' y='22' font-family='-apple-system,sans-serif' font-size='17' font-weight='700' text-anchor='middle' fill='white'>W</text></svg>">
|
||
<style>
|
||
:root{
|
||
--bg:#ffffff;--surface:#fff7f8;--fg:#222222;--muted:#dddddd;--border:#ffe3e8;--accent:#ff385c;
|
||
--ok:#0f8a3f;--warn:#b14a00;--danger:#c92a2a;
|
||
/* Text greys were hardcoded per-component, which is why the dark theme
|
||
had to restate 43 of them and why the greys drifted out of spec. One
|
||
token each; the dark block below rebinds them to lighter values so
|
||
they clear WCAG AA against a dark canvas. */
|
||
--muted-fg:#717171;--icon-fg:#888;--placeholder-fg:#666;
|
||
--shadow-up:0 -1px 0 var(--muted), 0 -8px 24px rgba(34,34,34,.06);
|
||
--font:'Inter',system-ui,-apple-system,Segoe UI,Helvetica Neue,Arial,sans-serif;
|
||
}
|
||
*{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent}
|
||
/* Any class rule that sets `display` beats the UA stylesheet's
|
||
`[hidden]{display:none}`, so a hidden element stays visible and every
|
||
JS `.hidden = true/false` silently does nothing. `.cat-menu` and
|
||
`.cat-backdrop` already carry per-element overrides for this; this
|
||
one rule covers every element (and every future one) instead of
|
||
relying on someone remembering to add a twin. */
|
||
[hidden]{display:none !important}
|
||
html,body{height:100%;background:var(--bg);color:var(--fg);font:15px/1.45 var(--font);-webkit-font-smoothing:antialiased;overscroll-behavior:none;overflow-x:hidden}
|
||
body{display:flex;flex-direction:column;min-height:100dvh;max-width:480px;margin:0 auto;background:var(--bg);padding-bottom:env(safe-area-inset-bottom)}
|
||
button{font:inherit;color:inherit;background:none;border:0;cursor:pointer}
|
||
button:focus-visible,input:focus-visible,select:focus-visible{outline:0;box-shadow:0 0 0 3px rgba(255,56,92,.30);border-radius:10px}
|
||
.topbar .back:focus-visible,.topbar .theme-btn:focus-visible{box-shadow:0 0 0 3px rgba(255,56,92,.30);border-radius:50%}
|
||
input{font:inherit;color:inherit}
|
||
|
||
/* ---- Topbar ---- */
|
||
.topbar{display:flex;align-items:center;gap:10px;padding:14px 16px 10px;background:var(--bg);position:sticky;top:0;z-index:10;border-bottom:1px solid transparent;transition:border-color .15s}
|
||
.topbar.scrolled{border-bottom-color:var(--muted)}
|
||
.topbar .back{width:36px;height:36px;border-radius:50%;display:grid;place-items:center;color:var(--fg)}
|
||
.topbar .back:active{background:var(--surface)}
|
||
.topbar h1{font-size:17px;font-weight:600;letter-spacing:-.01em;flex:1}
|
||
.topbar .netpill{font-size:11px;letter-spacing:.06em;text-transform:uppercase;font-weight:600;color:var(--muted-fg);padding:4px 8px;border:1px solid var(--muted);border-radius:999px}
|
||
.topbar .netpill.off{color:var(--danger);border-color:var(--danger);background:#fff5f5}
|
||
|
||
/* ---- Search ---- */
|
||
.search{padding:6px 16px 10px;background:var(--bg);position:sticky;top:60px;z-index:9}
|
||
.search-box{display:flex;align-items:center;gap:10px;padding:11px 14px;border:1px solid var(--muted);border-radius:12px;background:#fff;box-shadow:0 1px 2px rgba(34,34,34,.02)}
|
||
.search-box:focus-within{border-color:var(--accent);box-shadow:0 0 0 3px rgba(255,56,92,.12)}
|
||
.search-box svg{width:18px;height:18px;color:var(--muted-fg);flex:0 0 auto;stroke-width:1.7}
|
||
.search-box input{flex:1;border:0;outline:0;background:transparent;font-size:15px;min-width:0}
|
||
.search-box input::placeholder{color:#9a9a9a}
|
||
|
||
/* ---- Category dropdown ---- */
|
||
.cat-row{display:flex;align-items:center;gap:10px;padding:4px 16px 12px;background:var(--bg);position:sticky;top:118px;z-index:8}
|
||
.cat-trigger{display:flex;align-items:center;gap:8px;padding:10px 14px;border:1px solid var(--muted);border-radius:10px;background:#fff;font-size:14px;font-weight:500;color:var(--fg);min-height:42px;max-width:65%;letter-spacing:-.005em}
|
||
.cat-trigger:active{background:var(--surface)}
|
||
.cat-trigger .cat-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||
.cat-trigger .cat-chev{width:16px;height:16px;flex:0 0 auto;color:var(--muted-fg);transition:transform .18s}
|
||
.cat-trigger[aria-expanded="true"] .cat-chev{transform:rotate(180deg)}
|
||
.cat-count{font-size:12px;color:var(--muted-fg);letter-spacing:.01em;font-variant-numeric:tabular-nums}
|
||
|
||
.cat-menu{position:fixed;left:16px;right:16px;top:auto;background:#fff;border:1px solid var(--muted);border-radius:14px;box-shadow:0 12px 40px rgba(34,34,34,.18);z-index:60;max-width:448px;margin:0 auto;overflow:hidden;transform-origin:top center;animation:catIn .15s ease}
|
||
@keyframes catIn{from{opacity:0;transform:translateY(-4px) scale(.98)}to{opacity:1;transform:translateY(0) scale(1)}}
|
||
.cat-menu[hidden]{display:none}
|
||
.cat-menu-head{max-height:60dvh;overflow-y:auto;-webkit-overflow-scrolling:touch;padding:4px 0}
|
||
.cat-item{display:flex;align-items:center;gap:12px;padding:12px 16px;width:100%;text-align:left;font-size:14px;color:var(--fg);font-weight:500;letter-spacing:-.005em;border-bottom:1px solid var(--muted);background:none;min-height:44px}
|
||
.cat-item:last-child{border-bottom:0}
|
||
.cat-item:active{background:var(--surface)}
|
||
.cat-item .nm{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||
.cat-item .ct{font-size:12px;color:var(--muted-fg);font-variant-numeric:tabular-nums;letter-spacing:.02em;flex:0 0 auto}
|
||
.cat-item.active{background:var(--surface);color:var(--accent)}
|
||
.cat-item.active .ct{color:var(--accent)}
|
||
.cat-item.active::before{content:'';width:6px;height:6px;border-radius:50%;background:var(--accent);flex:0 0 auto}
|
||
|
||
.cat-backdrop{position:fixed;inset:0;background:transparent;z-index:55}
|
||
.cat-backdrop[hidden]{display:none}
|
||
|
||
/* ---- Catalog list ---- */
|
||
.catalog{flex:1;overflow-y:auto;-webkit-overflow-scrolling:touch;padding:0 16px 220px}
|
||
.catalog .meta{padding:12px 0 6px;font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--muted-fg);display:flex;justify-content:space-between}
|
||
.catalog .empty{padding:60px 20px;text-align:center;color:#9a9a9a}
|
||
.catalog .more{display:flex;flex-direction:column;align-items:center;gap:2px;width:100%;margin:14px 0 8px;padding:14px;border:1px solid var(--muted);border-radius:12px;background:var(--surface-card);color:var(--fg);font-size:14px;font-weight:600;letter-spacing:-.005em}
|
||
.catalog .more small{font-size:11px;font-weight:500;color:var(--muted-fg);letter-spacing:.02em}
|
||
.catalog .more:active{background:var(--surface)}
|
||
.catalog .empty h3{font-size:16px;font-weight:600;color:var(--fg);margin-bottom:6px;letter-spacing:-.01em}
|
||
.catalog .empty p{font-size:13px;line-height:1.5}
|
||
|
||
.prod{display:flex;align-items:center;gap:12px;padding:12px 0;border-bottom:1px solid var(--muted);min-height:60px;background:none;border-left:0;border-right:0;border-top:0;width:100%;text-align:left}
|
||
.prod:active{background:var(--surface)}
|
||
.prod .ic{width:40px;height:40px;border-radius:10px;background:var(--surface);border:1px solid var(--border);display:grid;place-items:center;color:var(--accent);font-size:12px;font-weight:700;letter-spacing:.04em;flex:0 0 auto}
|
||
.prod .ic .ic-fa{font-size:18px;line-height:1;color:var(--accent)}
|
||
.prod .body{flex:1;min-width:0;padding-right:8px}
|
||
.prod .name{font-size:14px;font-weight:500;line-height:1.3;letter-spacing:-.005em;color:var(--fg);overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}
|
||
.prod .sub{font-size:11px;color:var(--muted-fg);margin-top:2px;letter-spacing:.01em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||
.prod .price{font-size:14px;font-weight:600;letter-spacing:-.005em;color:var(--fg);font-variant-numeric:tabular-nums;white-space:nowrap}
|
||
.prod .price small{font-size:11px;color:var(--muted-fg);font-weight:500;margin-left:2px}
|
||
/* Out of stock: the quantity is not positive. The cue is informational —
|
||
the item still sells (the till oversells on purpose) — so it only
|
||
tints the line. The number itself is never hidden or clamped. */
|
||
.prod.oos .sub{color:var(--danger)}
|
||
.prod.oos .name{opacity:.72}
|
||
|
||
/* ---- Cart bottom sheet ---- */
|
||
.cart-bar{position:fixed;left:0;right:0;bottom:0;background:#fff;border-top:1px solid var(--muted);padding:12px 16px calc(12px + env(safe-area-inset-bottom));z-index:20;display:flex;align-items:center;gap:12px;box-shadow:var(--shadow-up);max-width:480px;margin:0 auto;transform:translateY(100%);transition:transform .22s cubic-bezier(.2,.8,.2,1)}
|
||
.cart-bar.show{transform:translateY(0)}
|
||
.cart-bar .count{display:flex;flex-direction:column;line-height:1.15}
|
||
.cart-bar .count b{font-size:20px;font-weight:700;letter-spacing:-.02em;font-variant-numeric:tabular-nums}
|
||
.cart-bar .count span{font-size:11px;color:var(--muted-fg);letter-spacing:.04em;text-transform:uppercase}
|
||
.cart-bar .total{margin-left:auto;font-size:20px;font-weight:700;letter-spacing:-.02em;color:var(--fg);font-variant-numeric:tabular-nums}
|
||
.cart-bar .checkout{padding:14px 22px;background:var(--accent);color:#fff;border-radius:12px;font-weight:600;font-size:15px;letter-spacing:-.005em;min-height:48px;display:flex;align-items:center;gap:6px}
|
||
.cart-bar .checkout:active{background:#e5324f}
|
||
.cart-bar .checkout svg{width:18px;height:18px;stroke-width:2}
|
||
|
||
/* ---- Cart drawer ---- */
|
||
.drawer{position:fixed;inset:0;background:rgba(34,34,34,.5);z-index:50;opacity:0;pointer-events:none;transition:opacity .2s}
|
||
.drawer.show{opacity:1;pointer-events:auto}
|
||
.sheet{position:absolute;left:0;right:0;bottom:0;background:#fff;border-radius:18px 18px 0 0;max-height:88dvh;display:flex;flex-direction:column;transform:translateY(100%);transition:transform .25s cubic-bezier(.2,.8,.2,1);max-width:480px;margin:0 auto;padding-bottom:env(safe-area-inset-bottom)}
|
||
.drawer.show .sheet{transform:translateY(0)}
|
||
.sheet-head{display:flex;align-items:center;padding:18px 20px 8px;border-bottom:1px solid var(--muted)}
|
||
.sheet-head h2{font-size:18px;font-weight:700;letter-spacing:-.01em;flex:1}
|
||
.sheet-head .x{width:32px;height:32px;border-radius:50%;background:var(--surface);display:grid;place-items:center;color:var(--fg)}
|
||
.sheet-body{flex:1;overflow-y:auto;-webkit-overflow-scrolling:touch;padding:4px 0}
|
||
.line{display:flex;align-items:center;gap:12px;padding:14px 20px;border-bottom:1px solid var(--muted)}
|
||
.line .nm{flex:1;min-width:0;font-size:14px;font-weight:500;line-height:1.3;letter-spacing:-.005em}
|
||
.line .nm small{display:block;font-size:11px;color:var(--muted-fg);margin-top:2px;letter-spacing:.01em}
|
||
.qty{display:flex;align-items:center;border:1px solid var(--muted);border-radius:10px;overflow:hidden}
|
||
.qty button{width:34px;height:34px;display:grid;place-items:center;color:var(--fg);font-size:16px;font-weight:600}
|
||
.qty button:active{background:var(--surface)}
|
||
.qty .v{min-width:44px;padding:6px 2px;text-align:center;font-size:14px;font-weight:600;font-variant-numeric:tabular-nums;color:var(--fg);border-radius:6px}
|
||
.qty .v:active{background:var(--surface)}
|
||
.line .pr{width:78px;text-align:right;font-size:14px;font-weight:600;font-variant-numeric:tabular-nums;color:var(--fg)}
|
||
.line .rm{width:32px;height:32px;border-radius:50%;color:#9a9a9a;display:grid;place-items:center}
|
||
.line .rm:active{background:#fff0f0;color:var(--danger)}
|
||
.sheet-foot{padding:14px 20px 18px;border-top:1px solid var(--muted);background:#fff}
|
||
.totals{display:flex;justify-content:space-between;padding:6px 0;font-size:14px;color:var(--muted-fg);letter-spacing:.01em}
|
||
.totals b{color:var(--fg);font-variant-numeric:tabular-nums}
|
||
.totals.grand{padding-top:12px;margin-top:8px;border-top:1px solid var(--muted);font-size:17px;color:var(--fg);font-weight:700;letter-spacing:-.01em}
|
||
|
||
/* ---- Ad-hoc (custom) line ---- */
|
||
/* A dashed outline rather than a filled button: this is a secondary
|
||
action next to the real catalogue, and it should read as "something
|
||
extra" rather than compete with the checkout CTA below it. */
|
||
.custom-item-btn{width:100%;margin-bottom:12px;padding:10px 12px;border:1px dashed #c9c9c9;border-radius:9px;background:none;color:var(--muted-fg);font-size:13px;font-weight:600;letter-spacing:.01em;text-align:center;cursor:pointer}
|
||
.custom-item-btn:active{background:#f4f4f4}
|
||
/* Ties a custom line back to the "not stock" meaning at a glance, and
|
||
stops anyone mistaking it for a catalogue item on a stocktake. */
|
||
.custom-line .nm{color:#8a6d3b}
|
||
.custom-line .nm small{color:#a08a5e}
|
||
|
||
/* ---- Discount block ---- */
|
||
/* The trigger rides INSIDE the Subtotal row (see the markup below), so it
|
||
adds no vertical height at all — the footer keeps every pixel it had
|
||
before. It stays visible rather than collapsing away, because a discount
|
||
control that appears from nowhere is one nobody finds under pressure. */
|
||
.discount-trigger{display:inline-flex;align-items:center;gap:3px;margin-left:8px;padding:3px 6px;border:0;border-radius:6px;background:none;color:var(--muted-fg);font-size:12px;font-weight:600;letter-spacing:.01em;vertical-align:middle}
|
||
.discount-trigger:active{background:var(--surface);color:var(--fg)}
|
||
.discount-trigger svg{width:13px;height:13px;color:inherit;transition:transform .18s}
|
||
.discount-trigger[aria-expanded="true"] svg{transform:rotate(180deg)}
|
||
.discount-trigger.has-value{color:var(--accent)}
|
||
.discount-trigger.has-value svg{color:var(--accent)}
|
||
.discount-body{padding:12px 0 4px;display:flex;flex-direction:column;gap:10px}
|
||
.discount-pills{display:flex;gap:6px;padding:3px;background:var(--surface);border:1px solid var(--muted);border-radius:999px;width:fit-content}
|
||
.discount-pill{padding:7px 16px;border-radius:999px;font-size:13px;font-weight:600;letter-spacing:-.005em;color:var(--muted-fg);min-height:34px}
|
||
.discount-pill.active{background:#fff;color:var(--accent);box-shadow:0 1px 2px rgba(34,34,34,.06)}
|
||
.discount-input-row{display:flex;align-items:center;gap:8px}
|
||
.discount-input-row input{flex:1 1 0;min-width:0;padding:11px 12px;border:1px solid var(--muted);border-radius:10px;font-size:15px;font-weight:600;text-align:right;font-variant-numeric:tabular-nums;outline:0;background:#fff}
|
||
.discount-input-row input:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(255,56,92,.12)}
|
||
.discount-suffix{font-size:13px;font-weight:600;color:var(--muted-fg);width:18px;text-align:left;letter-spacing:-.005em}
|
||
.discount-apply{padding:11px 16px;background:var(--accent);color:#fff;border-radius:10px;font-size:13px;font-weight:600;letter-spacing:-.005em;min-height:42px;min-width:78px}
|
||
.discount-apply:active{background:#e5324f}
|
||
.discount-hint{font-size:11px;color:var(--muted-fg);margin:0;letter-spacing:.01em}
|
||
.totals.discount-line{color:var(--danger)}
|
||
.totals.discount-line b{color:var(--danger)}
|
||
|
||
/* ---- Edit line modal (long-press) ---- */
|
||
.edit-backdrop{position:fixed;inset:0;background:rgba(34,34,34,.5);z-index:70;opacity:0;pointer-events:none;transition:opacity .15s}
|
||
.edit-backdrop.show{opacity:1;pointer-events:auto}
|
||
/* The sheet is a SIBLING of the backdrop, not a child, so it cannot be
|
||
styled via `.edit-backdrop.show .edit-sheet` — that selector never
|
||
matched and the sheet stayed at opacity:0 forever, which showed the
|
||
operator a grey screen with no dialog on it. It needs its own stacking
|
||
above the backdrop (z-index 70) and its own .show rule. Keep them
|
||
siblings: nesting the sheet inside the backdrop would make every tap on
|
||
the sheet bubble to the backdrop's onclick and close it. */
|
||
.edit-sheet{position:fixed;left:50%;bottom:24px;z-index:71;transform:translate(-50%, 24px);background:#fff;border-radius:18px;width:calc(100% - 32px);max-width:448px;padding:18px 18px 14px;opacity:0;transition:opacity .15s, transform .22s cubic-bezier(.2,.8,.2,1);box-shadow:0 20px 60px rgba(34,34,34,.30);pointer-events:none;display:flex;flex-direction:column;gap:12px;max-height:80dvh;overflow-y:auto;-webkit-overflow-scrolling:touch;padding-bottom:calc(14px + env(safe-area-inset-bottom))}
|
||
.edit-sheet.show{opacity:1;transform:translate(-50%, 0);pointer-events:auto}
|
||
.edit-sheet h3{font-size:15px;font-weight:600;letter-spacing:-.01em;margin:0;display:flex;align-items:center;gap:8px}
|
||
.edit-sheet h3 small{font-size:11px;font-weight:500;color:var(--muted-fg);letter-spacing:.02em;margin-left:auto}
|
||
.edit-row{display:flex;flex-direction:column;gap:6px}
|
||
.edit-row label{font-size:11px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--muted-fg)}
|
||
.edit-row input{width:100%;box-sizing:border-box;padding:12px 12px;border:1px solid var(--muted);border-radius:10px;font-size:16px;font-weight:600;text-align:right;font-variant-numeric:tabular-nums;outline:0;background:#fff}
|
||
.edit-row input:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(255,56,92,.12)}
|
||
.edit-actions{display:flex;gap:8px;margin-top:4px}
|
||
.edit-actions button{flex:1;padding:13px;border-radius:12px;font-size:14px;font-weight:600;letter-spacing:-.005em;min-height:46px}
|
||
.edit-actions .primary{background:var(--accent);color:#fff}
|
||
.edit-actions .primary:active{background:#e5324f}
|
||
.edit-actions .secondary{background:#fff;border:1px solid var(--muted);color:var(--fg)}
|
||
.edit-actions .secondary:active{background:var(--surface)}
|
||
.edit-actions .danger{background:#fff;border:1px solid #f0c0c0;color:var(--danger)}
|
||
.edit-actions .danger:active{background:#fff0f0}
|
||
|
||
/* Long-press hint chip — shown briefly when a line is held long
|
||
enough to count down to the modal opening, so the operator
|
||
knows the long-press did something. */
|
||
.line-holding{background:var(--surface);transition:background .15s}
|
||
|
||
/* ---- Payment sheet ---- */
|
||
.pay-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;padding:8px 20px 16px}
|
||
.pay-tile{padding:18px 14px;border:1.5px solid var(--muted);border-radius:14px;background:#fff;text-align:left;min-height:92px;display:flex;flex-direction:column;justify-content:space-between}
|
||
.pay-tile:active{background:var(--surface)}
|
||
.pay-tile .lab{font-size:14px;font-weight:600;letter-spacing:-.01em}
|
||
.pay-tile .sub{font-size:11px;color:var(--muted-fg);margin-top:2px;letter-spacing:.01em}
|
||
.pay-tile .ic{width:32px;height:32px;border-radius:8px;background:var(--surface);color:var(--accent);display:grid;place-items:center;margin-bottom:8px}
|
||
.pay-tile .ic svg{width:18px;height:18px;stroke-width:1.7}
|
||
.pay-tile.selected{border-color:var(--accent);background:#fff8fa}
|
||
.pay-tile.selected .ic{background:var(--accent);color:#fff}
|
||
|
||
.cash-block{padding:0 16px 20px;display:none}
|
||
.cash-block.show{display:block}
|
||
.cash-row{display:flex;gap:8px;margin-bottom:10px;min-width:0}
|
||
.cash-row .field{flex:1 1 0;min-width:0;display:flex;flex-direction:column}
|
||
.cash-row label{font-size:11px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--muted-fg);margin-bottom:4px}
|
||
.cash-row input{width:100%;box-sizing:border-box;padding:12px 10px;border:1px solid var(--muted);border-radius:10px;font-size:16px;font-weight:600;text-align:right;font-variant-numeric:tabular-nums;outline:0;background:#fff;min-width:0}
|
||
.cash-row input:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(255,56,92,.12)}
|
||
.quick{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:14px}
|
||
.quick button{padding:9px 13px;border:1px solid var(--muted);border-radius:999px;font-size:13px;font-weight:500;background:#fff;color:var(--fg);min-height:36px}
|
||
.quick button:active{background:var(--surface);border-color:#bbb}
|
||
.change-out{padding:14px;border-radius:12px;background:var(--surface);border:1px solid var(--border);text-align:center;min-height:64px;display:flex;flex-direction:column;justify-content:center;opacity:0;max-height:0;padding-top:0;padding-bottom:0;overflow:hidden;transition:opacity .15s, max-height .2s, padding .2s, border-width .2s;border-width:0}
|
||
.change-out.show{opacity:1;max-height:120px;padding:14px;border-width:1px}
|
||
.change-out .lbl{font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--muted-fg)}
|
||
.change-out .amt{font-size:32px;font-weight:700;letter-spacing:-.02em;color:var(--ok);font-variant-numeric:tabular-nums;margin-top:4px}
|
||
.change-out.short .amt{color:var(--danger)}
|
||
|
||
.cust-block{padding:0 16px 16px;display:none}
|
||
.cust-block.show{display:block}
|
||
.cust-block .search-row{display:flex;gap:8px;margin-bottom:10px;min-width:0}
|
||
.cust-block input{flex:1 1 0;width:100%;box-sizing:border-box;min-width:0;padding:12px 10px;border:1px solid var(--muted);border-radius:10px;font-size:14px;outline:0;background:#fff}
|
||
.cust-block input:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(255,56,92,.12)}
|
||
.cust-results{border:1px solid var(--muted);border-radius:10px;max-height:160px;overflow-y:auto;background:#fff}
|
||
.cust-results:empty{display:none}
|
||
.cust-results div{padding:10px 14px;border-bottom:1px solid var(--muted);font-size:14px;display:flex;align-items:center;gap:8px;cursor:pointer}
|
||
.cust-results div:last-child{border-bottom:0}
|
||
.cust-results div:active{background:var(--surface)}
|
||
.cust-results small{color:var(--muted-fg);font-size:11px;margin-left:auto;letter-spacing:.02em}
|
||
/* Count header + empty state. Both are divs inside .cust-results, so they
|
||
need the row cursor/padding neutralised explicitly. */
|
||
.cust-results .cust-count{padding:7px 14px;background:var(--surface);border-bottom:1px solid var(--muted);font-size:11px;font-weight:600;letter-spacing:.02em;color:var(--muted-fg);cursor:default}
|
||
.cust-results .cust-none{padding:11px 14px;font-size:13px;color:var(--muted-fg);cursor:default}
|
||
.cust-results .cust-count:active,.cust-results .cust-none:active{background:none}
|
||
.cust-picked{padding:10px 12px;background:var(--surface);border:1px solid var(--border);border-radius:10px;font-size:13px;display:none;align-items:center;gap:8px;margin-bottom:8px}
|
||
.cust-picked.show{display:flex}
|
||
.cust-picked b{flex:1;font-weight:600}
|
||
.cust-picked button{color:var(--danger);font-size:12px;font-weight:600;letter-spacing:.04em;text-transform:uppercase}
|
||
|
||
.final-cta{width:calc(100% - 40px);margin:0 20px 18px;padding:16px;background:var(--accent);color:#fff;border-radius:14px;font-size:16px;font-weight:600;letter-spacing:-.01em;min-height:54px;display:flex;align-items:center;justify-content:center;gap:8px}
|
||
.final-cta:active{background:#e5324f}
|
||
.final-cta:disabled{background:#e0e0e0;color:#999}
|
||
.final-cta svg{width:20px;height:20px;stroke-width:2}
|
||
|
||
/* ---- theme toggle ---- */
|
||
.theme-btn{width:36px;height:36px;border-radius:50%;display:grid;place-items:center;color:var(--fg);flex:0 0 auto;background:transparent;border:0;cursor:pointer;position:relative}
|
||
.theme-btn:active{background:var(--surface)}
|
||
.theme-btn svg{width:18px;height:18px}
|
||
.theme-btn .i-moon{display:none}
|
||
:root[data-theme="dark"] .theme-btn .i-sun{display:none}
|
||
:root[data-theme="dark"] .theme-btn .i-moon{display:block}
|
||
:root[data-theme="auto"] .theme-btn .i-moon{display:none}
|
||
:root[data-theme="auto"] .theme-btn .i-sun{display:block}
|
||
|
||
/* ---- dark theme (auto follows prefers-color-scheme) ---- */
|
||
/* Dark ramp. The previous canvas was #0f0f0f (L* 4.3 — effectively black)
|
||
with surfaces only 3-4 L* above it, and mixed a warm #1c1416 against a
|
||
neutral #181818 in the same UI. That near-black canvas plus two
|
||
competing surface tones is what read as "muddy". This is a single
|
||
cool-neutral ramp with four real steps, and the text greys are lifted
|
||
so the most-used one clears WCAG AA (was 3.93:1, now 6.49:1). */
|
||
:root[data-theme="dark"]{
|
||
--bg:#161619; /* L* 7.4 — lifted off black */
|
||
--surface:#1e1e23; /* L* 11.5 — cards, drawers, fields */
|
||
--raised:#26262d; /* L* 15.4 — menus and modals sit above surfaces */
|
||
--fg:#f2f2f5;
|
||
--muted:#303039; /* L* 20.2 — separators */
|
||
--border:#3b3b46; /* L* 25.3 — outlines */
|
||
--muted-fg:#9a9aa6; /* secondary text 6.49:1 on --bg (AA) */
|
||
--icon-fg:#8e8e9a; /* icons / labels 5.58:1 on --bg (AA) */
|
||
--placeholder-fg:#7c7c88; /* placeholder 4.38:1 */
|
||
--shadow-up:0 -1px 0 var(--muted), 0 -8px 24px rgba(0,0,0,.4);
|
||
}
|
||
:root[data-theme="dark"] body{background:var(--bg);color:var(--fg)}
|
||
:root[data-theme="dark"] .topbar{background:var(--bg);border-bottom-color:var(--muted)}
|
||
:root[data-theme="dark"] .search-box{background:var(--surface);border-color:var(--muted)}
|
||
:root[data-theme="dark"] .search-box:focus-within{border-color:var(--accent);box-shadow:0 0 0 3px rgba(255,56,92,.2)}
|
||
:root[data-theme="dark"] .search-box svg{color:var(--icon-fg)}
|
||
:root[data-theme="dark"] .search-box input{color:var(--fg)}
|
||
:root[data-theme="dark"] .search-box input::placeholder{color:var(--placeholder-fg)}
|
||
:root[data-theme="dark"] .chip{background:var(--surface);border-color:var(--muted);color:var(--fg)}
|
||
:root[data-theme="dark"] .chip.active{background:var(--fg);color:var(--bg);border-color:var(--fg)}
|
||
:root[data-theme="dark"] .cat-trigger{background:var(--surface);border-color:var(--muted);color:var(--fg)}
|
||
:root[data-theme="dark"] .cat-trigger .cat-chev{color:var(--icon-fg)}
|
||
:root[data-theme="dark"] .cat-count{color:var(--icon-fg)}
|
||
:root[data-theme="dark"] .cat-menu{background:var(--raised);border-color:var(--muted);box-shadow:0 12px 40px rgba(0,0,0,.5)}
|
||
:root[data-theme="dark"] .cat-item{color:var(--fg);border-bottom-color:var(--muted)}
|
||
:root[data-theme="dark"] .cat-item:active{background:var(--surface)}
|
||
:root[data-theme="dark"] .cat-item .ct{color:var(--icon-fg)}
|
||
:root[data-theme="dark"] .cat-item.active{background:var(--surface);color:var(--accent)}
|
||
:root[data-theme="dark"] .prod{border-bottom-color:var(--muted)}
|
||
:root[data-theme="dark"] .prod:active{background:var(--surface)}
|
||
:root[data-theme="dark"] .prod .ic{background:var(--surface);border-color:var(--border);color:var(--accent)}
|
||
:root[data-theme="dark"] .prod .name{color:var(--fg)}
|
||
:root[data-theme="dark"] .prod .sub{color:var(--icon-fg)}
|
||
:root[data-theme="dark"] .prod .price{color:var(--fg)}
|
||
:root[data-theme="dark"] .prod .price small{color:var(--icon-fg)}
|
||
/* The dark theme restates `.prod .sub` at a higher specificity than
|
||
`.prod.oos .sub`, so the out-of-stock colour needs a dark rule of
|
||
its own or it never shows. `#ff6680` is the dark danger colour
|
||
this file already uses (.totals.discount-line, .edit-actions .danger);
|
||
--danger itself is not rebound for dark and would not clear AA on the
|
||
dark canvas. */
|
||
:root[data-theme="dark"] .prod.oos .sub{color:#ff6680}
|
||
:root[data-theme="dark"] .cart-bar{background:var(--surface);border-top-color:var(--muted);box-shadow:var(--shadow-up)}
|
||
:root[data-theme="dark"] .cart-bar .count span{color:var(--icon-fg)}
|
||
:root[data-theme="dark"] .cart-bar .total{color:var(--fg)}
|
||
:root[data-theme="dark"] .drawer{background:rgba(0,0,0,.7)}
|
||
:root[data-theme="dark"] .sheet{background:var(--surface)}
|
||
:root[data-theme="dark"] .sheet-head{border-bottom-color:var(--muted)}
|
||
:root[data-theme="dark"] .sheet-head .x{background:var(--surface);color:var(--fg)}
|
||
:root[data-theme="dark"] .line{border-bottom-color:var(--muted)}
|
||
:root[data-theme="dark"] .line .nm{color:var(--fg)}
|
||
:root[data-theme="dark"] .line .nm small{color:var(--icon-fg)}
|
||
:root[data-theme="dark"] .qty{border-color:var(--muted)}
|
||
:root[data-theme="dark"] .qty button{color:var(--fg)}
|
||
:root[data-theme="dark"] .qty button:active{background:var(--surface)}
|
||
:root[data-theme="dark"] .qty .v{color:var(--fg)}
|
||
:root[data-theme="dark"] .line .pr{color:var(--fg)}
|
||
:root[data-theme="dark"] .line .rm{color:var(--placeholder-fg)}
|
||
:root[data-theme="dark"] .line .rm:active{background:#3a2429;color:var(--accent)}
|
||
:root[data-theme="dark"] .sheet-foot{background:var(--surface);border-top-color:var(--muted)}
|
||
:root[data-theme="dark"] .totals{color:var(--icon-fg)}
|
||
:root[data-theme="dark"] .totals b{color:var(--fg)}
|
||
:root[data-theme="dark"] .totals.grand{border-top-color:var(--muted);color:var(--fg)}
|
||
:root[data-theme="dark"] .discount-trigger{color:var(--icon-fg)}
|
||
:root[data-theme="dark"] .custom-item-btn{border-color:#3a3a3a;color:#999}
|
||
:root[data-theme="dark"] .custom-item-btn:active{background:#222}
|
||
:root[data-theme="dark"] .custom-line .nm{color:#d2b47c}
|
||
:root[data-theme="dark"] .custom-line .nm small{color:#a08a5e}
|
||
:root[data-theme="dark"] .discount-trigger:active{color:var(--fg)}
|
||
:root[data-theme="dark"] .discount-trigger.has-value{color:#ff6680}
|
||
:root[data-theme="dark"] .discount-pills{background:#222;border-color:var(--muted)}
|
||
:root[data-theme="dark"] .discount-pill{color:var(--icon-fg)}
|
||
:root[data-theme="dark"] .discount-pill.active{background:var(--surface);color:#ff6680;box-shadow:0 1px 2px rgba(0,0,0,.4)}
|
||
:root[data-theme="dark"] .discount-input-row input{background:var(--surface);border-color:var(--muted);color:var(--fg)}
|
||
:root[data-theme="dark"] .discount-input-row input:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(255,56,92,.2)}
|
||
:root[data-theme="dark"] .discount-suffix{color:var(--icon-fg)}
|
||
:root[data-theme="dark"] .discount-hint{color:var(--icon-fg)}
|
||
:root[data-theme="dark"] .totals.discount-line{color:#ff6680}
|
||
:root[data-theme="dark"] .totals.discount-line b{color:#ff6680}
|
||
:root[data-theme="dark"] .empty{color:var(--placeholder-fg)}
|
||
:root[data-theme="dark"] .empty h3{color:var(--fg)}
|
||
:root[data-theme="dark"] .empty p{color:var(--icon-fg)}
|
||
:root[data-theme="dark"] .netpill{background:transparent;color:var(--icon-fg);border-color:var(--muted)}
|
||
:root[data-theme="dark"] .netpill.off{color:#ff6680;border-color:#ff6680;background:#2a1a1a}
|
||
:root[data-theme="dark"] .back:active{background:var(--surface)}
|
||
:root[data-theme="dark"] .pay-tile{background:var(--surface);border-color:var(--muted);color:var(--fg)}
|
||
:root[data-theme="dark"] .pay-tile:active{background:var(--surface)}
|
||
:root[data-theme="dark"] .pay-tile .ic{background:var(--surface);color:var(--accent)}
|
||
:root[data-theme="dark"] .pay-tile.selected{border-color:var(--accent);background:#2a1a1a}
|
||
:root[data-theme="dark"] .pay-tile.selected .ic{background:var(--accent);color:#fff}
|
||
:root[data-theme="dark"] .cash-row input{background:var(--surface);border-color:var(--muted);color:var(--fg)}
|
||
:root[data-theme="dark"] .cash-row input:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(255,56,92,.2)}
|
||
:root[data-theme="dark"] .cash-row input[readonly]{background:#222;color:#aaa}
|
||
:root[data-theme="dark"] .cash-row label{color:var(--icon-fg)}
|
||
:root[data-theme="dark"] .quick button{background:var(--surface);border-color:var(--muted);color:var(--fg)}
|
||
:root[data-theme="dark"] .quick button:active{background:var(--surface);border-color:#444}
|
||
:root[data-theme="dark"] .change-out{background:var(--surface);border-color:var(--border);color:var(--fg)}
|
||
:root[data-theme="dark"] .change-out .lbl{color:var(--icon-fg)}
|
||
:root[data-theme="dark"] .cust-block input{background:var(--surface);border-color:var(--muted);color:var(--fg)}
|
||
:root[data-theme="dark"] .cust-block input:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(255,56,92,.2)}
|
||
:root[data-theme="dark"] .cust-results{background:var(--surface);border-color:var(--muted)}
|
||
:root[data-theme="dark"] .cust-results div{border-bottom-color:var(--muted);color:var(--fg)}
|
||
:root[data-theme="dark"] .cust-results div:active{background:var(--surface)}
|
||
:root[data-theme="dark"] .cust-results small{color:var(--icon-fg)}
|
||
:root[data-theme="dark"] .cust-results .cust-count{background:#222;border-bottom-color:var(--muted);color:var(--icon-fg)}
|
||
:root[data-theme="dark"] .cust-results .cust-none{color:var(--icon-fg)}
|
||
:root[data-theme="dark"] .cust-picked{background:var(--surface);border-color:var(--border);color:var(--fg)}
|
||
:root[data-theme="dark"] .edit-sheet{background:var(--raised);box-shadow:0 20px 60px rgba(0,0,0,.6)}
|
||
:root[data-theme="dark"] .edit-sheet h3 small{color:var(--icon-fg)}
|
||
:root[data-theme="dark"] .edit-row label{color:var(--icon-fg)}
|
||
:root[data-theme="dark"] .edit-row input{background:var(--surface);border-color:var(--muted);color:var(--fg)}
|
||
:root[data-theme="dark"] .edit-row input:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(255,56,92,.2)}
|
||
:root[data-theme="dark"] .edit-actions .secondary{background:var(--surface);border-color:var(--muted);color:var(--fg)}
|
||
:root[data-theme="dark"] .edit-actions .secondary:active{background:var(--surface)}
|
||
:root[data-theme="dark"] .edit-actions .danger{background:var(--surface);border-color:#5a2429;color:#ff6680}
|
||
:root[data-theme="dark"] .edit-actions .danger:active{background:#2a1a1a}
|
||
:root[data-theme="dark"] .line-holding{background:var(--surface)}
|
||
:root[data-theme="dark"] .final-cta:disabled{background:#2a2a2a;color:var(--placeholder-fg)}
|
||
@media (prefers-reduced-motion: reduce){*{transition-duration:0.01ms !important;animation-duration:0.01ms !important}}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<header class="topbar" id="topbar">
|
||
<button class="back" data-od-id="back-btn" onclick="location.href='index.html'" aria-label="Back">
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 6l-6 6 6 6"/></svg>
|
||
</button>
|
||
<h1>New sale</h1>
|
||
<span class="netpill" id="netpill">…</span>
|
||
<button class="theme-btn" data-theme-toggle data-od-id="theme-toggle" aria-label="Toggle theme" title="Toggle theme" onclick="(window.WFTheme&&window.WFTheme.cycle())||(window.__wfCycle&&window.__wfCycle())">
|
||
<svg class="i-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/></svg>
|
||
<svg class="i-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
|
||
</button>
|
||
</header>
|
||
|
||
<div class="search">
|
||
<div class="search-box">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
|
||
<input id="q" type="search" placeholder="Search SKU or product" autocomplete="off" inputmode="search" enterkeyhint="search">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="cat-row">
|
||
<button class="cat-trigger" id="catTrigger" data-od-id="cat-trigger" aria-haspopup="listbox" aria-expanded="false">
|
||
<span class="cat-label" id="catLabel">All categories</span>
|
||
<svg class="cat-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M6 9l6 6 6-6"/></svg>
|
||
</button>
|
||
<span class="cat-count" id="catCount">— products</span>
|
||
</div>
|
||
|
||
<div class="cat-menu" id="catMenu" role="listbox" hidden>
|
||
<div class="cat-menu-head" id="catMenuItems"></div>
|
||
</div>
|
||
|
||
<div class="cat-backdrop" id="catBackdrop" hidden></div>
|
||
|
||
<main class="catalog" id="catalog">
|
||
<div class="empty" id="empty">
|
||
<h3>Loading catalog…</h3>
|
||
<p>Fetching products from the register.</p>
|
||
</div>
|
||
</main>
|
||
|
||
<div class="cart-bar" id="cartBar" data-od-id="cart-bar">
|
||
<button class="back" style="width:44px;height:44px;border-radius:50%;background:var(--surface);border:1px solid var(--border)" onclick="openDrawer()" aria-label="View cart">
|
||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"><path d="M3 6h2l2 12h12l2-9H6"/><circle cx="9" cy="20" r="1.4"/><circle cx="17" cy="20" r="1.4"/></svg>
|
||
</button>
|
||
<div class="count"><b id="cartCount">0</b><span>items</span></div>
|
||
<div class="total" id="cartTotal">R 0.00</div>
|
||
<button class="checkout" data-od-id="checkout-btn" onclick="openPayment()">
|
||
Charge
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M5 12h14M13 5l7 7-7 7"/></svg>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Cart drawer -->
|
||
<div class="drawer" id="cartDrawer">
|
||
<div class="sheet">
|
||
<div class="sheet-head">
|
||
<h2>Cart</h2>
|
||
<button class="x" onclick="closeDrawer()" aria-label="Close">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 6l12 12M18 6L6 18"/></svg>
|
||
</button>
|
||
</div>
|
||
<div class="sheet-body" id="cartLines"></div>
|
||
<div class="sheet-foot">
|
||
<!-- Ad-hoc line: anything the customer asks for that isn't a
|
||
catalogue item (a cut bolt, a measurement) gets typed in
|
||
here at whatever price was agreed. It rings up and prints
|
||
like any other line but never touches stock. -->
|
||
<button class="custom-item-btn" data-od-id="add-custom-item" onclick="openCustomItem()">
|
||
+ Add a custom item
|
||
</button>
|
||
<!-- The discount trigger rides inside the Subtotal row so the footer
|
||
costs no extra height. Tapping it expands the R / % pills, number
|
||
input and Apply button below; the discounted amount is then shown
|
||
in red on its own totals line, and the server re-derives the
|
||
resolved rand value at commit time. -->
|
||
<div class="totals">
|
||
<span>Subtotal (ex VAT)
|
||
<button class="discount-trigger" id="discountTrigger" data-od-id="discount-trigger" onclick="toggleDiscount()">
|
||
<span id="discountTriggerLabel">Add discount</span>
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M6 9l6 6 6-6"/></svg>
|
||
</button>
|
||
</span>
|
||
<b id="subTotal">R 0.00</b>
|
||
</div>
|
||
<div class="totals"><span id="vatLabel">VAT 15%</span><b id="vatTotal">R 0.00</b></div>
|
||
|
||
<div class="discount-body" id="discountBody" hidden>
|
||
<div class="discount-pills" role="group" aria-label="Discount tier">
|
||
<button class="discount-pill" id="discTier20" data-od-id="discount-20" onclick="setDiscountTier(20)">20% off</button>
|
||
<button class="discount-pill" id="discTier40" data-od-id="discount-40" onclick="setDiscountTier(40)">40% off</button>
|
||
</div>
|
||
<p class="discount-hint" id="discHint">Tap a tier to apply it. Tap it again to clear.</p>
|
||
</div>
|
||
|
||
<div class="totals discount-line" id="discountLine" hidden>
|
||
<span>Discount</span><b id="discountLineAmount">− R 0.00</b>
|
||
</div>
|
||
|
||
<div class="totals grand"><span>Total (incl. VAT)</span><b id="grandTotal">R 0.00</b></div>
|
||
<button class="final-cta" data-od-id="cart-checkout" onclick="closeDrawer();openPayment()">
|
||
Take payment
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M5 12h14M13 5l7 7-7 7"/></svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Payment drawer -->
|
||
<div class="drawer" id="payDrawer">
|
||
<div class="sheet">
|
||
<div class="sheet-head">
|
||
<h2>Take payment</h2>
|
||
<button class="x" onclick="closePayment()" aria-label="Close">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 6l12 12M18 6L6 18"/></svg>
|
||
</button>
|
||
</div>
|
||
<div class="sheet-body">
|
||
<div class="totals" style="padding:10px 20px 0"><span>Subtotal</span><b id="paySub">R 0.00</b></div>
|
||
<div class="totals" style="padding:0 20px"><span>VAT 15%</span><b id="payVat">R 0.00</b></div>
|
||
<div class="totals grand" style="padding:14px 20px"><span>Total due</span><b id="payTotal">R 0.00</b></div>
|
||
<div class="pay-grid" id="payGrid">
|
||
<button class="pay-tile" data-od-id="pay-cash" data-meth="cash" onclick="pickMethod('cash')">
|
||
<div><div class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="3" y="6" width="18" height="12" rx="2"/><circle cx="12" cy="12" r="2.5"/></svg></div><div class="lab">Cash</div><div class="sub">Calculate change</div></div>
|
||
</button>
|
||
<button class="pay-tile" data-od-id="pay-card" data-meth="card" onclick="pickMethod('card')">
|
||
<div><div class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="3" y="6" width="18" height="12" rx="2"/><path d="M3 10h18"/></svg></div><div class="lab">Card</div><div class="sub">No change</div></div>
|
||
</button>
|
||
<button class="pay-tile" data-od-id="pay-eft" data-meth="eft" onclick="pickMethod('eft')">
|
||
<div><div class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M4 7h16M4 12h16M4 17h10"/></svg></div><div class="lab">EFT</div><div class="sub">Instant transfer</div></div>
|
||
</button>
|
||
<button class="pay-tile" data-od-id="pay-account" data-meth="account" onclick="pickMethod('account')">
|
||
<div><div class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M19 8v6M22 11h-6"/></svg></div><div class="lab">Account</div><div class="sub">Print delivery note</div></div>
|
||
</button>
|
||
</div>
|
||
|
||
<div class="cash-block" id="cashBlock">
|
||
<div class="cash-row">
|
||
<div class="field"><label>Total due</label><input id="dueIn" type="text" readonly tabindex="-1"></div>
|
||
<div class="field"><label>Cash given</label><input id="givenIn" type="text" inputmode="decimal" placeholder="0.00"></div>
|
||
</div>
|
||
<div class="quick" id="quick"></div>
|
||
<div class="change-out" id="changeOut">
|
||
<div class="lbl">Change to give</div>
|
||
<div class="amt" id="changeAmt">R 0.00</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="cust-block" id="custBlock">
|
||
<div class="cust-picked" id="custPicked"><b></b><button onclick="clearCust()">Remove</button></div>
|
||
<div class="search-row">
|
||
<input id="custQ" placeholder="Search customer" autocomplete="off">
|
||
</div>
|
||
<div class="cust-results" id="custResults"></div>
|
||
</div>
|
||
</div>
|
||
<div style="padding:0 20px 18px">
|
||
<button class="final-cta" id="confirmBtn" data-od-id="confirm-pay" disabled onclick="confirmSale()">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M5 12l5 5 9-11"/></svg>
|
||
Complete sale
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Edit line modal (long-press a cart row to open) -->
|
||
<div class="edit-backdrop" id="editBackdrop" onclick="closeEditLine()"></div>
|
||
<div class="edit-sheet" id="editSheet" role="dialog" aria-labelledby="editTitle" aria-modal="true">
|
||
<h3 id="editTitle">Edit line <small id="editSku"></small></h3>
|
||
<div class="edit-row">
|
||
<label for="editQty">Quantity</label>
|
||
<input id="editQty" type="text" inputmode="decimal" autocomplete="off" data-od-id="edit-qty">
|
||
</div>
|
||
<p class="discount-hint" id="editHint">Type a quantity like 800, then tap Save.</p>
|
||
<div class="edit-actions">
|
||
<button class="danger" data-od-id="edit-remove" onclick="editRemove()">Remove</button>
|
||
<button class="secondary" data-od-id="edit-cancel" onclick="closeEditLine()">Cancel</button>
|
||
<button class="primary" data-od-id="edit-save" onclick="editSave()">Save</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Custom (ad-hoc) item composer -->
|
||
<div class="edit-backdrop" id="customBackdrop" onclick="closeCustomItem()"></div>
|
||
<div class="edit-sheet" id="customSheet" role="dialog" aria-labelledby="customTitle" aria-modal="true">
|
||
<h3 id="customTitle">Custom item</h3>
|
||
<p class="discount-hint">Anything that isn't stock — e.g. "Bolt cutting". It rings up at the price you type, and doesn't come out of stock.</p>
|
||
<div class="edit-row">
|
||
<label for="customDesc">Description</label>
|
||
<input id="customDesc" type="text" autocomplete="off" placeholder="Bolt cutting" data-od-id="custom-desc">
|
||
</div>
|
||
<div class="edit-row">
|
||
<label for="customPrice">Price (ex VAT)</label>
|
||
<input id="customPrice" type="text" inputmode="decimal" autocomplete="off" placeholder="0.00" data-od-id="custom-price">
|
||
</div>
|
||
<div class="edit-actions">
|
||
<button class="secondary" data-od-id="custom-cancel" onclick="closeCustomItem()">Cancel</button>
|
||
<button class="primary" data-od-id="custom-save" onclick="saveCustomItem()">Add to cart</button>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="pos-config.js"></script>
|
||
<script src="theme.js"></script>
|
||
<script src="letterhead-config.js"></script>
|
||
<script src="pos-search.js"></script>
|
||
<script src="pos-app.js"></script>
|
||
<script>if("serviceWorker" in navigator){navigator.serviceWorker.register("sw.js").catch(function(){});}</script>
|
||
</body>
|
||
</html>
|