feat(config): agrega personalización de ticket con logo y opciones
- Nuevos endpoints /pos/api/config/receipt (GET/PUT) - Sección en config.html para subir logo, datos del negocio y toggles visuales - pos.js carga la config y renderiza el ticket usando logo, RFC, dirección, teléfono, mensajes y opciones de desglose/empleado/pago configurables - Logo se redimensiona a 300px de ancho y se guarda como base64 JPEG en tenant_config Tests: 35 passed
This commit is contained in:
@@ -30,6 +30,7 @@ const POS = (() => {
|
||||
let canEditPrice = false;
|
||||
let canCreateWorkshopOrder = false;
|
||||
let canCreateLayaway = false;
|
||||
let receiptConfig = {};
|
||||
|
||||
// Currency-aware formatter: reads pos_currency from localStorage
|
||||
const _posCurrency = localStorage.getItem('pos_currency') || 'MXN';
|
||||
@@ -134,8 +135,9 @@ const POS = (() => {
|
||||
showToast(`Modo conversion: Cotizacion #${convertQuoteId}. El pago convertira la cotizacion en venta.`);
|
||||
}
|
||||
|
||||
// Load current register
|
||||
// Load current register and receipt config
|
||||
await loadRegister();
|
||||
await loadReceiptConfig();
|
||||
|
||||
// Setup event listeners
|
||||
setupKeyboard();
|
||||
@@ -163,6 +165,20 @@ const POS = (() => {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadReceiptConfig() {
|
||||
try {
|
||||
receiptConfig = await api('/pos/api/config/receipt');
|
||||
} catch (e) {
|
||||
console.warn('Could not load receipt config:', e);
|
||||
receiptConfig = {};
|
||||
}
|
||||
}
|
||||
|
||||
function rc(key, fallback) {
|
||||
const v = receiptConfig[key];
|
||||
return v !== undefined && v !== null && v !== '' ? v : fallback;
|
||||
}
|
||||
|
||||
function showOpenRegisterModal() {
|
||||
document.getElementById('openRegisterModal').classList.add('open');
|
||||
document.getElementById('registerOpenResult').innerHTML = '';
|
||||
@@ -1283,12 +1299,37 @@ const POS = (() => {
|
||||
</div>`;
|
||||
});
|
||||
|
||||
const cfg = receiptConfig || {};
|
||||
const showLogo = cfg.show_logo && cfg.logo;
|
||||
const showRfc = cfg.show_rfc !== false;
|
||||
const showAddress = cfg.show_address;
|
||||
const showPhone = cfg.show_phone;
|
||||
const showIva = cfg.show_iva_breakdown !== false;
|
||||
const showPayment = cfg.show_payment_details !== false;
|
||||
const showEmployee = cfg.show_employee;
|
||||
const storeName = rc('store_name', 'NEXUS AUTOPARTS');
|
||||
const storeTagline = rc('tagline', 'Tu conexion con las refacciones');
|
||||
const storeRfc = rc('rfc', 'NAU210315XX1');
|
||||
const storeAddress = rc('address', '');
|
||||
const storePhone = rc('phone', '');
|
||||
const thanksMsg = rc('thanks_message', 'Gracias por su compra!');
|
||||
const footerMsg = rc('footer', 'Conserve su ticket como comprobante.');
|
||||
|
||||
let storeInfoLines = [];
|
||||
if (currentRegister && currentRegister.branch_name) storeInfoLines.push(`Sucursal: ${currentRegister.branch_name}`);
|
||||
if (showEmployee && sale.employee_name) storeInfoLines.push(`Atendió: ${sale.employee_name}`);
|
||||
if (showRfc && storeRfc) storeInfoLines.push(`RFC: ${storeRfc}`);
|
||||
if (showAddress && storeAddress) storeInfoLines.push(storeAddress);
|
||||
if (showPhone && storePhone) storeInfoLines.push(`Tel: ${storePhone}`);
|
||||
|
||||
const logoHtml = showLogo ? `<div class="ticket-logo-wrap"><img src="${cfg.logo}" alt="Logo" class="ticket-logo" style="max-width:140px;max-height:70px;object-fit:contain;"></div>` : '';
|
||||
|
||||
const ticketHtml = `
|
||||
<div class="store-name">NEXUS AUTOPARTS</div>
|
||||
<div class="store-tagline">Tu conexion con las refacciones</div>
|
||||
${logoHtml}
|
||||
<div class="store-name">${storeName}</div>
|
||||
${storeTagline ? `<div class="store-tagline">${storeTagline}</div>` : ''}
|
||||
<div class="store-info">
|
||||
Sucursal: ${currentRegister ? currentRegister.branch_name || '' : ''}<br>
|
||||
RFC: NAU210315XX1
|
||||
${storeInfoLines.join('<br>')}
|
||||
</div>
|
||||
<hr class="divider-double">
|
||||
<div class="folio-line">
|
||||
@@ -1314,13 +1355,12 @@ const POS = (() => {
|
||||
<span>Subtotal:</span><span>${fmt(sale.subtotal)}</span>
|
||||
</div>
|
||||
${sale.discount_total > 0 ? `<div class="total-line"><span>Descuento:</span><span>-${fmt(sale.discount_total)}</span></div>` : ''}
|
||||
<div class="total-line">
|
||||
<span>IVA 16%:</span><span>${fmt(sale.tax_total)}</span>
|
||||
</div>
|
||||
${showIva ? `<div class="total-line"><span>IVA:</span><span>${fmt(sale.tax_total)}</span></div>` : ''}
|
||||
<div class="total-line grand">
|
||||
<span>TOTAL:</span><span>${fmt(sale.total)}</span>
|
||||
</div>
|
||||
</div>
|
||||
${showPayment ? `
|
||||
<hr class="divider">
|
||||
<div class="payment-section">
|
||||
<div class="ticket-row">
|
||||
@@ -1333,11 +1373,11 @@ const POS = (() => {
|
||||
<div class="ticket-row" style="font-weight: bold;">
|
||||
<span>Cambio:</span><span>${fmt(sale.change_given || 0)}</span>
|
||||
</div>` : ''}
|
||||
</div>
|
||||
</div>` : ''}
|
||||
<hr class="divider">
|
||||
<div class="footer-section">
|
||||
<div class="thanks">Gracias por su compra!</div>
|
||||
<div>Conserve su ticket como comprobante.</div>
|
||||
<div class="thanks">${thanksMsg}</div>
|
||||
${footerMsg ? `<div>${footerMsg}</div>` : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user