From e697f66e9830621891ea223cb71a950329a2e244 Mon Sep 17 00:00:00 2001 From: Dave Date: Fri, 6 Nov 2026 02:18:34 +0200 Subject: [PATCH] feat(pos): show the stock quantity on the till tile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- public/pos/pos-app.js | 9 +++++++-- public/pos/pos.html | 12 ++++++++++++ public/pos/sw.js | 8 +++++++- src/lib/pos-cart-quantity.test.ts | 22 ++++++++++++++++++++++ src/lib/server/pos-service.test.ts | 27 +++++++++++++++++++++++++++ src/lib/server/pos-service.ts | 2 ++ src/lib/types.ts | 2 ++ 7 files changed, 79 insertions(+), 3 deletions(-) diff --git a/public/pos/pos-app.js b/public/pos/pos-app.js index bf9fd20..d091476 100644 --- a/public/pos/pos-app.js +++ b/public/pos/pos-app.js @@ -277,11 +277,16 @@ function render(){ const icon = p.category_icon ? `` : initials(p.category||p.brand||'?'); - return ``; diff --git a/public/pos/pos.html b/public/pos/pos.html index c9f10dc..6bafb40 100644 --- a/public/pos/pos.html +++ b/public/pos/pos.html @@ -96,6 +96,11 @@ input{font:inherit;color:inherit} .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)} @@ -307,6 +312,13 @@ input{font:inherit;color:inherit} :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)} diff --git a/public/pos/sw.js b/public/pos/sw.js index 1927b2b..f41851c 100644 --- a/public/pos/sw.js +++ b/public/pos/sw.js @@ -60,7 +60,13 @@ // plus blanking the figure when the row is hidden. Also carries the dark // palette lift. // v37: pos-app.js accepts a comma as the decimal point (custom item price, cash given). -const CACHE = "pos-v37"; +// v38: the catalogue tile shows the stock quantity. current_qty was already +// coming down the wire — the till just never drew it. The number is shown +// as it is (negatives included) and the tile is tinted when the quantity is +// not positive. Nothing is blocked: the item still sells. +// Bumped because pos-app.js and pos.html are both served cache-first out of +// the shell; without a new CACHE name the tills keep the old ones. +const CACHE = "pos-v38"; const APP_SHELL = [ './', './index.html', diff --git a/src/lib/pos-cart-quantity.test.ts b/src/lib/pos-cart-quantity.test.ts index 7ab06ac..88f5a9f 100644 --- a/src/lib/pos-cart-quantity.test.ts +++ b/src/lib/pos-cart-quantity.test.ts @@ -264,3 +264,25 @@ describe("POS [hidden] attribute actually hides", () => { ); }); }); + +describe("the POS tile shows the stock quantity", () => { + it("renders the quantity on the tile", () => { + expect(POS_APP).toContain("current_qty"); + expect(POS_APP).toContain("in stock"); + }); + + it("marks the tile out of stock when the quantity is not positive", () => { + expect(POS_APP).toMatch(/oos/); + }); + + it("styles the out-of-stock tile in both themes", () => { + // The cue only works if the eye finds it. The dark theme restates + // `.prod .sub` at a higher specificity than `.prod.oos .sub`, so + // without a dark rule of its own the out-of-stock colour is + // overridden and the tile looks like every other one. + expect(POS_HTML).toMatch(/\.prod\.oos \.sub\{[^}]*color:var\(--danger\)/); + expect(POS_HTML).toMatch( + /:root\[data-theme="dark"\] \.prod\.oos \.sub\{[^}]*color:/, + ); + }); +}); diff --git a/src/lib/server/pos-service.test.ts b/src/lib/server/pos-service.test.ts index 193f010..10626f7 100644 --- a/src/lib/server/pos-service.test.ts +++ b/src/lib/server/pos-service.test.ts @@ -711,3 +711,30 @@ describe("POS never sells at R0", () => { expect(pb._salesCalls).toHaveLength(0); }); }); + +describe("listActiveStockCatalog", () => { + it("returns min_qty alongside current_qty", async () => { + const pb = { + collection: () => ({ + getFullList: vi.fn().mockResolvedValue([ + { + id: "si1", + sku: "SKU-1", + name: "Widget", + description: "A widget", + unit: "ea", + category: "", + current_qty: 5, + min_qty: 2, + selling_price: 10, + cost_price: 5, + is_active: true, + }, + ]), + }), + }; + const out = await listActiveStockCatalog(pb as never); + expect(out[0].min_qty).toBe(2); + expect(out[0].current_qty).toBe(5); + }); +}); diff --git a/src/lib/server/pos-service.ts b/src/lib/server/pos-service.ts index ef6e3e0..1ca8c36 100644 --- a/src/lib/server/pos-service.ts +++ b/src/lib/server/pos-service.ts @@ -157,6 +157,7 @@ export async function listActiveStockCatalog(pb: PosPb): Promise