// Per-letterhead slip layout: which blocks print, in what order, // enabled or not, with what custom text. Single source of truth for // the editor (slip-layout.html), its live preview, and both print // transports (epos-print.js's XML builders, receipt.html's ESC/POS // builders) — all three must resolve the same way or the editor // would silently not apply to whichever transport a till is using. window.PosSlipLayout = (function () { 'use strict'; // Blocks that can be reordered but never disabled — a slip missing // these stops being a valid record of the transaction (e.g. a // receipt with no total). const REQUIRED = { sale: ['items', 'totals'], delivery: ['items', 'totals'], cashup: ['payment_breakdown', 'grand_total'], }; const LABELS = { header_title: 'Header title', logo: 'Logo', business_info: 'Business info', date_slip: 'Date & slip number', customer: 'Customer', items: 'Item list', totals: 'Totals & VAT', payment: 'Payment & change', footer: 'Footer message', date: 'Date', signature: 'Signature line', shift_date: 'Shift & date', payment_breakdown: 'Payment breakdown', grand_total: 'Grand total', cash_reconciliation: 'Cash reconciliation', denominations: 'Denominations counted', }; // Matches what each builder actually prints today (verified by // reading buildEposXml/buildEposDeliveryXml/buildEposCashupXml and // their Bluetooth ESC/POS equivalents) — this formalizes existing // content into blocks, it doesn't add anything new. Notably, // delivery notes don't print business info today (only a title), // and cash-up's title line becomes its own header_title block for // consistency with the other two slip types. const DEFAULTS = { sale: [ { id: 'logo', enabled: true }, { id: 'header_title', enabled: true, text: null }, { id: 'business_info', enabled: true }, { id: 'date_slip', enabled: true }, { id: 'customer', enabled: true }, { id: 'items', enabled: true }, { id: 'totals', enabled: true }, { id: 'payment', enabled: true }, { id: 'footer', enabled: true, text: null }, ], delivery: [ { id: 'logo', enabled: true }, { id: 'header_title', enabled: true, text: null }, { id: 'business_info', enabled: true }, { id: 'customer', enabled: true }, { id: 'date', enabled: true }, { id: 'items', enabled: true }, { id: 'totals', enabled: true }, { id: 'signature', enabled: true }, ], cashup: [ { id: 'logo', enabled: true }, { id: 'header_title', enabled: true, text: null }, { id: 'business_info', enabled: true }, { id: 'shift_date', enabled: true }, { id: 'payment_breakdown', enabled: true }, { id: 'grand_total', enabled: true }, { id: 'cash_reconciliation', enabled: true }, { id: 'denominations', enabled: true }, { id: 'footer', enabled: true, text: null }, ], }; const DEFAULT_SHOW_PRICING = true; const DEFAULT_HEADER_TITLE = 'WFSALES POS'; function isRequired(slipType, blockId) { return (REQUIRED[slipType] || []).includes(blockId); } function labelFor(blockId) { return LABELS[blockId] || blockId; } // Merge saved blocks with the default catalog: saved order/enabled/ // text wins. Any default block missing from `saved` (first time // this slip type has been customized, or a new block id shipped // after the letterhead's layout was last saved) is appended so // nothing silently disappears. Unknown/retired ids in `saved` are // dropped. Required blocks are forced enabled regardless of what // was saved, defensively. function resolveBlocks(slipType, saved) { const defaults = DEFAULTS[slipType] || []; const normalize = function (b) { return { id: b.id, enabled: isRequired(slipType, b.id) ? true : !!b.enabled, text: b.text != null ? b.text : null, }; }; if (!Array.isArray(saved) || !saved.length) { return defaults.map(function (b) { return normalize(b); }); } // Start from the saved order, dropping ids we no longer ship. const result = saved .filter(function (b) { return defaults.some(function (d) { return d.id === b.id; }); }) .map(function (b) { return Object.assign({}, b); }); const present = {}; result.forEach(function (b) { present[b.id] = true; }); // A block shipped after this letterhead last saved its layout has // to be slotted in at its intended spot — appending to the end // would drop e.g. a newly added logo/business_info block below the // signature line. Anchor each missing block just after the nearest // preceding default that IS present, so the operator's own // reordering is preserved. defaults.forEach(function (def, defIdx) { if (present[def.id]) return; let insertAt = 0; for (let i = defIdx - 1; i >= 0; i--) { const pos = result.findIndex(function (b) { return b.id === defaults[i].id; }); if (pos !== -1) { insertAt = pos + 1; break; } } result.splice(insertAt, 0, Object.assign({}, def)); present[def.id] = true; }); return result.map(function (b) { return normalize(b); }); } // -------- cache (till-local, mirrors letterhead-config.js) -------- function cacheKey(letterheadId) { return 'pos.slipLayout.' + letterheadId; } function getCached(letterheadId) { try { return JSON.parse(localStorage.getItem(cacheKey(letterheadId)) || 'null'); } catch (e) { return null; } } function setCached(letterheadId, layout) { try { localStorage.setItem(cacheKey(letterheadId), JSON.stringify(layout)); } catch (e) { /* quota or disabled */ } } // -------- network -------- async function fetchLayout(letterheadId) { const r = await window.posFetch('/slip-layout/' + encodeURIComponent(letterheadId)); if (!r.ok) throw new Error('slip-layout fetch returned ' + r.status); const j = await r.json(); if (j.layout) setCached(letterheadId, j.layout); return j.layout; // null when nothing saved yet } async function saveLayout(letterheadId, input) { const r = await window.posFetch('/slip-layout/' + encodeURIComponent(letterheadId), { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input), }); if (!r.ok) throw new Error('slip-layout save returned ' + r.status); const j = await r.json(); if (j.layout) setCached(letterheadId, j.layout); return j.layout; } // -------- print-time helpers -------- // Printing must never block on a settings-sync failure: use the // cached layout if we have one (refreshing it in the background for // next time), else the hardcoded defaults. function getBlocksForPrint(letterheadId, slipType) { const cached = letterheadId ? getCached(letterheadId) : null; if (letterheadId) { fetchLayout(letterheadId).catch(function () { /* keep cached/defaults on failure */ }); } const saved = cached ? cached[slipType + '_blocks'] : null; return resolveBlocks(slipType, saved); } function getShowPricingForPrint(letterheadId) { const cached = letterheadId ? getCached(letterheadId) : null; return cached && cached.delivery_show_pricing != null ? !!cached.delivery_show_pricing : DEFAULT_SHOW_PRICING; } return { DEFAULTS: DEFAULTS, DEFAULT_SHOW_PRICING: DEFAULT_SHOW_PRICING, DEFAULT_HEADER_TITLE: DEFAULT_HEADER_TITLE, isRequired: isRequired, labelFor: labelFor, resolveBlocks: resolveBlocks, getCached: getCached, setCached: setCached, fetchLayout: fetchLayout, saveLayout: saveLayout, getBlocksForPrint: getBlocksForPrint, getShowPricingForPrint: getShowPricingForPrint, }; })();