diff --git a/public/pos/pos-app.js b/public/pos/pos-app.js index d091476..47395ff 100644 --- a/public/pos/pos-app.js +++ b/public/pos/pos-app.js @@ -277,18 +277,17 @@ function render(){ const icon = p.category_icon ? `` : initials(p.category||p.brand||'?'); - // The catalogue already carries current_qty — the till just never drew - // it. Shown as it is: a negative quantity reads "-2581 in stock", - // because the real number is the signal the operator asked for. - const stockQty = Number(p.current_qty ?? 0); + // The catalogue row already carries the figure; draw it as it is. + const rawQty = Number(p.current_qty); + const stockQty = Number.isFinite(rawQty) ? rawQty : 0; const oos = stockQty <= 0; return ``; }).join('') + (more > 0 diff --git a/public/pos/pos.html b/public/pos/pos.html index 6bafb40..1f760e4 100644 --- a/public/pos/pos.html +++ b/public/pos/pos.html @@ -94,12 +94,16 @@ input{font:inherit;color:inherit} .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{font-size:14px;font-weight:600;letter-spacing:-.005em;color:var(--fg);font-variant-numeric:tabular-nums;white-space:nowrap;text-align:right} .prod .price small{font-size:11px;color:var(--muted-fg);font-weight:500;margin-left:2px} +/* The quantity sits under the price, not in the .sub line: .sub is + nowrap + ellipsis, so a long SKU truncated it and the number — and the + in-cart count beside it — fell off the right edge. */ +.prod .stock{display:block;font-size:11px;font-weight:500;letter-spacing:.01em;color:var(--muted-fg);margin-top:2px;white-space:nowrap} /* 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)} + tints the number. The number itself is never hidden or clamped. */ +.prod.oos .stock{color:var(--danger)} .prod.oos .name{opacity:.72} /* ---- Cart bottom sheet ---- */ @@ -312,13 +316,14 @@ 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 +:root[data-theme="dark"] .prod .stock{color:var(--icon-fg)} +/* The dark theme restates `.prod .stock` at a higher specificity than + `.prod.oos .stock`, 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"] .prod.oos .stock{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 f41851c..863b852 100644 --- a/public/pos/sw.js +++ b/public/pos/sw.js @@ -66,7 +66,12 @@ // 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"; +// v39: the quantity moved out of the .sub line and under the price — +// the sub line truncates on a long SKU, which pushed the number and the +// in-cart count off the right edge. The sub line is back to what it was. +// 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-v39"; const APP_SHELL = [ './', './index.html', diff --git a/src/lib/pos-cart-quantity.test.ts b/src/lib/pos-cart-quantity.test.ts index 88f5a9f..deebd5e 100644 --- a/src/lib/pos-cart-quantity.test.ts +++ b/src/lib/pos-cart-quantity.test.ts @@ -266,23 +266,65 @@ describe("POS [hidden] attribute actually hides", () => { }); describe("the POS tile shows the stock quantity", () => { + // The first version of these assertions matched the file's PROSE: the + // comment above the tile code contained "current_qty" and "-2581 in + // stock", and the local variable was called `oos`. Five mutations of + // the implementation — a min-based cue, a lost zero boundary, a clamp, + // a round, and the whole segment deleted — all sailed past them. So + // the assertions below pin the EXPRESSIONS, not the words. it("renders the quantity on the tile", () => { - expect(POS_APP).toContain("current_qty"); - expect(POS_APP).toContain("in stock"); + expect(POS_APP).toMatch(/const rawQty = Number\(p\.current_qty\);/); + expect(POS_APP).toMatch( + /const stockQty = Number\.isFinite\(rawQty\) \? rawQty : 0;/, + ); + expect(POS_APP).toMatch(/\$\{stockQty\} in stock/); }); it("marks the tile out of stock when the quantity is not positive", () => { - expect(POS_APP).toMatch(/oos/); + // current_qty <= 0 is the cue. min_qty is set on 48 of 4,083 + // sellable items, so a min-based rule would badge every tile and + // mean nothing — the till must never read it. + expect(POS_APP).not.toMatch(/min_qty/); + expect(POS_APP).toMatch(/const oos = stockQty <= 0;/); + expect(POS_APP).toMatch(/class="prod\$\{oos \? ' 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\)/); + it("shows the number as it is, negatives included", () => { + // No clamping, no rounding: a negative quantity must still read + // "-2581 in stock", because the real number is the signal. + expect(POS_APP).not.toMatch( + /Math\.(max|round|trunc|floor|ceil)\([^)]*stockQty/, + ); + expect(POS_APP).not.toMatch(/toFixed\([^)]*stockQty/); + }); + + it("keeps the quantity out of the truncating sub line", () => { + // .sub is nowrap + ellipsis, so a long SKU truncated it: the number + // fell off the right edge and pushed the in-cart count out with it. + const sub = POS_APP.match(/