feat(pos): add shared slip-layout block catalog and resolver
This commit is contained in:
parent
102673d00b
commit
8e879d1447
1 changed files with 181 additions and 0 deletions
181
public/pos/slip-layout.js
Normal file
181
public/pos/slip-layout.js
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
// 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: 'header_title', enabled: true, text: null },
|
||||
{ id: 'logo', enabled: true },
|
||||
{ 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: '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,
|
||||
};
|
||||
})();
|
||||
Loading…
Reference in a new issue