diff --git a/public/pos/cashup.html b/public/pos/cashup.html index 7437483..a3aa0f3 100644 --- a/public/pos/cashup.html +++ b/public/pos/cashup.html @@ -231,7 +231,7 @@ button:focus-visible,input:focus-visible,select:focus-visible{outline:0;box-shad
— products · cache pending
+— products · cache pending · letterhead —
+This till runs under the selected letterhead. Slips and receipts will use its name, address, VAT rate, and logo.
+ +diff --git a/public/pos/letterhead-config.js b/public/pos/letterhead-config.js new file mode 100644 index 0000000..bfebeec --- /dev/null +++ b/public/pos/letterhead-config.js @@ -0,0 +1,150 @@ +// Letterhead configuration for the till. +// +// The POS uses a `letterhead_settings` row from the main WFDASH +// PocketBase (shared with the PDF flow). This module: +// 1. Caches the *list* of available letterheads in localStorage +// so the settings page can show the picker offline. +// 2. Caches the *active* letterhead (the one the till is currently +// running under) in localStorage so the till can render receipts +// and compute VAT without re-fetching on every sale. +// 3. Exposes a tiny global API that other POS scripts (pos-app.js, +// receipt.html, etc.) can use without each one re-implementing +// the cache + fetch dance. +// +// Like pos-config.js, the cache key prefix is `pos.letterhead.*` +// so it's clear what's till data vs. shop data when debugging. +window.PosLetterhead = (function () { + 'use strict'; + + // Westcott Group is the canonical POS letterhead per the operator + // (per the 2026-09-04 audit). If the till has no active letterhead + // yet, we default-pick this id when the first letterhead list comes + // back. The id matches the PB row "5wezvypn6fy47p9" in the user's + // instance — see docs. + const DEFAULT_ID = '5wezvypn6fy47p9'; + const DEFAULT_DISPLAY_NAME = 'WESTSCOTT GROUP OF COMPANIES'; + + const KEY_ACTIVE_ID = 'pos.letterhead.activeId'; + const KEY_ACTIVE = 'pos.letterhead.active'; + const KEY_LIST = 'pos.letterhead.list'; + + function lsGet(key) { + try { return JSON.parse(localStorage.getItem(key) || 'null'); } catch (e) { return null; } + } + function lsSet(key, val) { + try { localStorage.setItem(key, JSON.stringify(val)); } catch (e) { /* quota or disabled */ } + } + function lsDel(key) { + try { localStorage.removeItem(key); } catch (e) { /* ignore */ } + } + + // -------- list (used by the settings picker) -------- + + function getCachedList() { + return lsGet(KEY_LIST) || []; + } + + function setCachedList(items) { + lsSet(KEY_LIST, items); + } + + // -------- active letterhead (used by sales + receipt) -------- + + function getActiveId() { + return lsGet(KEY_ACTIVE_ID) || null; + } + + function getActive() { + return lsGet(KEY_ACTIVE) || null; + } + + function setActive(item) { + if (!item || !item.id) return; + lsSet(KEY_ACTIVE_ID, item.id); + lsSet(KEY_ACTIVE, item); + } + + function clearActive() { + lsDel(KEY_ACTIVE_ID); + lsDel(KEY_ACTIVE); + } + + // -------- network calls -------- + + async function fetchList() { + const r = await window.posFetch('/letterhead'); + if (!r.ok) throw new Error('letterhead list returned ' + r.status); + const j = await r.json(); + const items = j.items || []; + setCachedList(items); + return items; + } + + async function fetchById(id) { + if (!id) throw new Error('letterhead id is required'); + const r = await window.posFetch('/letterhead/' + encodeURIComponent(id)); + if (!r.ok) { + if (r.status === 404) throw new Error('letterhead ' + id + ' not found'); + throw new Error('letterhead fetch returned ' + r.status); + } + const j = await r.json(); + const item = j.item; + if (!item) throw new Error('letterhead response missing item'); + setActive(item); + return item; + } + + /** + * One-shot boot helper. Called from pos-app.js and settings.html. + * - If we have a cached active letterhead, return it immediately. + * - Otherwise, list letterheads; if Westcott Group is in the list + * auto-pick it; else pick the first active one. + * - Fetch the full row in the background so the cache stays warm + * (returns the fresh row if it differs). + */ + async function ensureActive() { + const cached = getActive(); + if (cached && cached.id) { + // Refresh in the background — the till can keep using the cached + // copy while the network call is in flight. + fetchById(cached.id).catch(() => { /* keep cached on failure */ }); + return cached; + } + // No active yet — list and pick a default. + let list = getCachedList(); + if (!list.length) { + list = await fetchList(); + } + const westcott = list.find(x => + (x.display_name || '').toUpperCase().includes('WESTSCOTT') || + (x.company || '').toUpperCase().includes('WESTSCOTT') + ); + const pick = westcott || list[0]; + if (!pick) { + // No active letterheads in PB. Mark this so the UI can show + // a clear "you need to set up a letterhead" message instead of + // a generic error. + clearActive(); + return null; + } + return await fetchById(pick.id); + } + + return { + // getters + getActiveId, + getActive, + getCachedList, + // setters + setActive, + setCachedList, + clearActive, + // network + fetchList, + fetchById, + ensureActive, + // constants (for debugging / display in settings) + DEFAULT_ID, + DEFAULT_DISPLAY_NAME, + }; +})(); diff --git a/public/pos/pos-app.js b/public/pos/pos-app.js index 2a4fab2..02b6a3e 100644 --- a/public/pos/pos-app.js +++ b/public/pos/pos-app.js @@ -122,8 +122,23 @@ async function loadProducts(){ } async function loadLetterhead(){ - // Letterhead comes from the main app in a later task if needed; - // the receipt already falls back to "WFSALES POS" when absent. + // Fetch + cache the active letterhead via letterhead-config.js. + // On a fresh till, this auto-picks Westcott Group (the only + // letterhead the POS uses per the 2026-09-04 operator decision). + // The fetched row drives: + // - state.letterhead.* → shown on the on-screen receipt + // - state.letterhead.vat_percent → cart VAT math (replaces the + // hardcoded 15% so a 0% letterhead shows no VAT line) + // - sale.letterhead.* → attached to the saved sale, consumed by + // receipt.html, epos-print.js (buildEposXml), and the + // history-reprint redirect. + try{ + const item = await window.PosLetterhead.ensureActive(); + state.letterhead = item; + }catch(e){ + console.warn('Letterhead load failed; receipt will fall back to "WFSALES POS"', e); + state.letterhead = null; + } } function buildCategories(){ @@ -325,11 +340,22 @@ function cartTotals(){ discountAmount = Math.min((gross * d.input) / 100, gross); } // VAT-inclusive model: selling_price is the shelf price (incl. VAT). - // Subtotal excluding VAT = total / 1.15, VAT = total - sub + // The VAT rate is driven by the active letterhead (default 15% if + // no letterhead is loaded yet — same as the previous hardcoded + // behaviour, so a fresh till on a flaky network still works). + const vatPct = Number(state.letterhead && state.letterhead.vat_percent) || 15; const total = Math.max(0, gross - discountAmount); - const sub = total / 1.15; - const vat = total - sub; - return {count, gross, discountAmount, sub, vat, total}; + let sub, vat; + if (vatPct > 0) { + const factor = 1 + vatPct / 100; + sub = total / factor; + vat = total - sub; + } else { + // 0% VAT (e.g. CNL / ISHERWOOD) — no VAT line, sub == total. + sub = total; + vat = 0; + } + return {count, gross, discountAmount, sub, vat, total, vatPct}; } /** @@ -769,6 +795,14 @@ async function confirmSale(){ gross: totals.gross, discount: { kind: d.kind, input: d.input, amount: totals.discountAmount }, sub, vat, total, + // Attach the active letterhead so the on-screen receipt AND + // the printed slip can show the right shop name, address, + // phone, VAT number, footer, and logo. receipt.html's + // renderReceipt + epos-print.js's buildEposXml both read + // sale.letterhead.* (and fall back to "WFSALES POS" / blank + // if it's null, so a missing letterhead is a non-fatal + // degradation, not a crash). + letterhead: state.letterhead || null, items: Array.from(state.cart.values()).map(({p,qty,unitPriceOverride})=>({ sku: p.sku, description: p.description, unit: p.unit, cost_price: p.cost_price, diff --git a/public/pos/pos.html b/public/pos/pos.html index 2d1b0a7..77ce83e 100644 --- a/public/pos/pos.html +++ b/public/pos/pos.html @@ -330,7 +330,7 @@ input{font:inherit;color:inherit}
diff --git a/public/pos/settings.html b/public/pos/settings.html new file mode 100644 index 0000000..388aa83 --- /dev/null +++ b/public/pos/settings.html @@ -0,0 +1,334 @@ + + +
+ + + +
+ + + + +
+