Also fixes a logo-ordering bug this surfaced: slip-layout.js's sale and cashup defaults put header_title before logo, which (once a real logo renders instead of the always-empty placeholder used in earlier regression tests) reversed the original image-then-title print order. Reordered both to logo-first and added the missing logo block to cashup's default catalog, since cashup's original code already attempted (brokenly) to print one.
182 lines
6.7 KiB
JavaScript
182 lines
6.7 KiB
JavaScript
// 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: 'header_title', enabled: true, text: null },
|
|
{ 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 base = Array.isArray(saved) && saved.length ? saved.slice() : defaults.map(function (b) { return Object.assign({}, b); });
|
|
const seen = {};
|
|
base.forEach(function (b) { seen[b.id] = true; });
|
|
defaults.forEach(function (def) {
|
|
if (!seen[def.id]) base.push(Object.assign({}, def));
|
|
});
|
|
return base
|
|
.filter(function (b) { return defaults.some(function (d) { return d.id === b.id; }); })
|
|
.map(function (b) {
|
|
return {
|
|
id: b.id,
|
|
enabled: isRequired(slipType, b.id) ? true : !!b.enabled,
|
|
text: b.text != null ? b.text : null,
|
|
};
|
|
});
|
|
}
|
|
|
|
// -------- 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,
|
|
};
|
|
})();
|