WFDASH-WEBAPP/public/pos/slip-layout.js
Dave d47394723b refactor(pos): make buildEposCashupXml block-driven, fix broken logo image tag
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.
2026-09-04 13:31:13 +02:00

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,
};
})();