181 lines
6.7 KiB
JavaScript
181 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: '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,
|
|
};
|
|
})();
|