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:
@@ -752,3 +752,99 @@ def set_onboarding_status():
|
|||||||
cur.close()
|
cur.close()
|
||||||
conn.close()
|
conn.close()
|
||||||
return jsonify({'completed': completed == 'true'})
|
return jsonify({'completed': completed == 'true'})
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Receipt / Ticket Customization ────────────────────────────────────────
|
||||||
|
|
||||||
|
RECEIPT_CONFIG_KEYS = [
|
||||||
|
'receipt_logo',
|
||||||
|
'receipt_store_name',
|
||||||
|
'receipt_tagline',
|
||||||
|
'receipt_rfc',
|
||||||
|
'receipt_address',
|
||||||
|
'receipt_phone',
|
||||||
|
'receipt_footer',
|
||||||
|
'receipt_thanks_message',
|
||||||
|
'receipt_show_logo',
|
||||||
|
'receipt_show_rfc',
|
||||||
|
'receipt_show_address',
|
||||||
|
'receipt_show_phone',
|
||||||
|
'receipt_show_iva_breakdown',
|
||||||
|
'receipt_show_payment_details',
|
||||||
|
'receipt_show_employee',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@config_bp.route('/receipt', methods=['GET'])
|
||||||
|
@require_auth('pos.view')
|
||||||
|
def get_receipt_config():
|
||||||
|
"""Get receipt customization settings."""
|
||||||
|
conn = get_tenant_conn(g.tenant_id)
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute(
|
||||||
|
"SELECT key, value FROM tenant_config WHERE key = ANY(%s)",
|
||||||
|
(RECEIPT_CONFIG_KEYS,)
|
||||||
|
)
|
||||||
|
rows = {row[0]: row[1] for row in cur.fetchall()}
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def _bool(key, default=False):
|
||||||
|
v = rows.get(key, 'true' if default else 'false')
|
||||||
|
return str(v).lower() == 'true'
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'logo': rows.get('receipt_logo', ''),
|
||||||
|
'store_name': rows.get('receipt_store_name', ''),
|
||||||
|
'tagline': rows.get('receipt_tagline', ''),
|
||||||
|
'rfc': rows.get('receipt_rfc', ''),
|
||||||
|
'address': rows.get('receipt_address', ''),
|
||||||
|
'phone': rows.get('receipt_phone', ''),
|
||||||
|
'footer': rows.get('receipt_footer', ''),
|
||||||
|
'thanks_message': rows.get('receipt_thanks_message', 'Gracias por su compra!'),
|
||||||
|
'show_logo': _bool('receipt_show_logo', True),
|
||||||
|
'show_rfc': _bool('receipt_show_rfc', True),
|
||||||
|
'show_address': _bool('receipt_show_address', False),
|
||||||
|
'show_phone': _bool('receipt_show_phone', False),
|
||||||
|
'show_iva_breakdown': _bool('receipt_show_iva_breakdown', True),
|
||||||
|
'show_payment_details': _bool('receipt_show_payment_details', True),
|
||||||
|
'show_employee': _bool('receipt_show_employee', False),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@config_bp.route('/receipt', methods=['PUT'])
|
||||||
|
@require_auth('config.edit')
|
||||||
|
def update_receipt_config():
|
||||||
|
"""Update receipt customization settings."""
|
||||||
|
data = request.get_json() or {}
|
||||||
|
conn = get_tenant_conn(g.tenant_id)
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
settings = {
|
||||||
|
'receipt_logo': data.get('logo', ''),
|
||||||
|
'receipt_store_name': data.get('store_name', ''),
|
||||||
|
'receipt_tagline': data.get('tagline', ''),
|
||||||
|
'receipt_rfc': data.get('rfc', ''),
|
||||||
|
'receipt_address': data.get('address', ''),
|
||||||
|
'receipt_phone': data.get('phone', ''),
|
||||||
|
'receipt_footer': data.get('footer', ''),
|
||||||
|
'receipt_thanks_message': data.get('thanks_message', 'Gracias por su compra!'),
|
||||||
|
'receipt_show_logo': 'true' if data.get('show_logo') else 'false',
|
||||||
|
'receipt_show_rfc': 'true' if data.get('show_rfc') else 'false',
|
||||||
|
'receipt_show_address': 'true' if data.get('show_address') else 'false',
|
||||||
|
'receipt_show_phone': 'true' if data.get('show_phone') else 'false',
|
||||||
|
'receipt_show_iva_breakdown': 'true' if data.get('show_iva_breakdown') else 'false',
|
||||||
|
'receipt_show_payment_details': 'true' if data.get('show_payment_details') else 'false',
|
||||||
|
'receipt_show_employee': 'true' if data.get('show_employee') else 'false',
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value in settings.items():
|
||||||
|
cur.execute("""
|
||||||
|
INSERT INTO tenant_config (key, value) VALUES (%s, %s)
|
||||||
|
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value
|
||||||
|
""", (key, value))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
return jsonify({'message': 'Receipt configuration updated'})
|
||||||
|
|||||||
@@ -443,6 +443,133 @@ const Config = (() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Receipt / ticket customization
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
let _receiptLogo = '';
|
||||||
|
|
||||||
|
async function loadReceiptConfig() {
|
||||||
|
try {
|
||||||
|
var res = await fetch(API + '/receipt', { headers: headers() });
|
||||||
|
if (!res.ok) return;
|
||||||
|
var d = await res.json();
|
||||||
|
_receiptLogo = d.logo || '';
|
||||||
|
setVal('receipt-store-name', d.store_name);
|
||||||
|
setVal('receipt-tagline', d.tagline);
|
||||||
|
setVal('receipt-rfc', d.rfc);
|
||||||
|
setVal('receipt-address', d.address);
|
||||||
|
setVal('receipt-phone', d.phone);
|
||||||
|
setVal('receipt-thanks', d.thanks_message);
|
||||||
|
setVal('receipt-footer', d.footer);
|
||||||
|
setChecked('receipt-show-logo', d.show_logo);
|
||||||
|
setChecked('receipt-show-rfc', d.show_rfc);
|
||||||
|
setChecked('receipt-show-address', d.show_address);
|
||||||
|
setChecked('receipt-show-phone', d.show_phone);
|
||||||
|
setChecked('receipt-show-iva', d.show_iva_breakdown);
|
||||||
|
setChecked('receipt-show-payment', d.show_payment_details);
|
||||||
|
setChecked('receipt-show-employee', d.show_employee);
|
||||||
|
renderReceiptLogoThumb();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Config.loadReceiptConfig:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setChecked(id, v) {
|
||||||
|
var el = document.getElementById(id);
|
||||||
|
if (el) el.checked = !!v;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getChecked(id) {
|
||||||
|
var el = document.getElementById(id);
|
||||||
|
return el ? el.checked : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderReceiptLogoThumb() {
|
||||||
|
var thumb = document.getElementById('receipt-logo-thumb');
|
||||||
|
var removeBtn = document.getElementById('receipt-logo-remove');
|
||||||
|
if (!thumb) return;
|
||||||
|
if (_receiptLogo) {
|
||||||
|
thumb.innerHTML = '<img src="' + escapeHtml(_receiptLogo) + '" style="max-width:100%;max-height:100%;object-fit:contain;" alt="Logo ticket">';
|
||||||
|
if (removeBtn) removeBtn.style.display = '';
|
||||||
|
} else {
|
||||||
|
thumb.innerHTML = '<span style="color:var(--color-text-muted);font-size:var(--text-caption);text-align:center;padding:var(--space-2);">Sin logo</span>';
|
||||||
|
if (removeBtn) removeBtn.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleReceiptLogo(input) {
|
||||||
|
var file = input && input.files ? input.files[0] : null;
|
||||||
|
if (!file) return;
|
||||||
|
if (!file.type.match(/^image\/(png|jpeg|jpg|webp)$/)) {
|
||||||
|
toast('Solo se permiten imágenes PNG, JPG o WebP', 'error');
|
||||||
|
input.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var reader = new FileReader();
|
||||||
|
reader.onload = function(e) {
|
||||||
|
var img = new Image();
|
||||||
|
img.onload = function() {
|
||||||
|
var maxWidth = 300;
|
||||||
|
var scale = Math.min(1, maxWidth / img.width);
|
||||||
|
var w = Math.round(img.width * scale);
|
||||||
|
var h = Math.round(img.height * scale);
|
||||||
|
var canvas = document.createElement('canvas');
|
||||||
|
canvas.width = w;
|
||||||
|
canvas.height = h;
|
||||||
|
var ctx = canvas.getContext('2d');
|
||||||
|
ctx.fillStyle = '#ffffff';
|
||||||
|
ctx.fillRect(0, 0, w, h);
|
||||||
|
ctx.drawImage(img, 0, 0, w, h);
|
||||||
|
_receiptLogo = canvas.toDataURL('image/jpeg', 0.85);
|
||||||
|
renderReceiptLogoThumb();
|
||||||
|
toast('Logo cargado. Guarda los cambios para aplicarlo.', 'ok');
|
||||||
|
};
|
||||||
|
img.src = e.target.result;
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
input.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeReceiptLogo() {
|
||||||
|
_receiptLogo = '';
|
||||||
|
renderReceiptLogoThumb();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveReceiptConfig() {
|
||||||
|
if (!checkAuth()) return;
|
||||||
|
var data = {
|
||||||
|
logo: _receiptLogo,
|
||||||
|
store_name: getVal('receipt-store-name'),
|
||||||
|
tagline: getVal('receipt-tagline'),
|
||||||
|
rfc: getVal('receipt-rfc'),
|
||||||
|
address: getVal('receipt-address'),
|
||||||
|
phone: getVal('receipt-phone'),
|
||||||
|
thanks_message: getVal('receipt-thanks'),
|
||||||
|
footer: getVal('receipt-footer'),
|
||||||
|
show_logo: getChecked('receipt-show-logo'),
|
||||||
|
show_rfc: getChecked('receipt-show-rfc'),
|
||||||
|
show_address: getChecked('receipt-show-address'),
|
||||||
|
show_phone: getChecked('receipt-show-phone'),
|
||||||
|
show_iva_breakdown: getChecked('receipt-show-iva'),
|
||||||
|
show_payment_details: getChecked('receipt-show-payment'),
|
||||||
|
show_employee: getChecked('receipt-show-employee'),
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
var res = await fetch(API + '/receipt', {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: headers(),
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
var err = await res.json().catch(function() { return { error: res.statusText }; });
|
||||||
|
throw new Error(err.error || 'Error al guardar');
|
||||||
|
}
|
||||||
|
toast('Configuración de ticket guardada', 'ok');
|
||||||
|
} catch (e) {
|
||||||
|
toast(e.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function saveAll() {
|
async function saveAll() {
|
||||||
if (!checkAuth()) return;
|
if (!checkAuth()) return;
|
||||||
var btn = document.getElementById('btn-save-all');
|
var btn = document.getElementById('btn-save-all');
|
||||||
@@ -454,6 +581,7 @@ const Config = (() => {
|
|||||||
await saveVehicleCompatSource();
|
await saveVehicleCompatSource();
|
||||||
await saveAllowedBrands();
|
await saveAllowedBrands();
|
||||||
await saveModules();
|
await saveModules();
|
||||||
|
await saveReceiptConfig();
|
||||||
toast('Configuración guardada', 'ok');
|
toast('Configuración guardada', 'ok');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast(e.message, 'error');
|
toast(e.message, 'error');
|
||||||
@@ -881,6 +1009,7 @@ const Config = (() => {
|
|||||||
loadVehicleCompatSource();
|
loadVehicleCompatSource();
|
||||||
loadAllowedBrands();
|
loadAllowedBrands();
|
||||||
loadModules();
|
loadModules();
|
||||||
|
loadReceiptConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', init);
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
@@ -900,6 +1029,7 @@ const Config = (() => {
|
|||||||
loadCurrency, saveCurrency,
|
loadCurrency, saveCurrency,
|
||||||
loadVehicleCompatSource, saveVehicleCompatSource,
|
loadVehicleCompatSource, saveVehicleCompatSource,
|
||||||
loadModules, saveModules,
|
loadModules, saveModules,
|
||||||
|
loadReceiptConfig, saveReceiptConfig, handleReceiptLogo, removeReceiptLogo,
|
||||||
openModal, closeModal, openBranchModal, editBranch
|
openModal, closeModal, openBranchModal, editBranch
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ const POS = (() => {
|
|||||||
let canEditPrice = false;
|
let canEditPrice = false;
|
||||||
let canCreateWorkshopOrder = false;
|
let canCreateWorkshopOrder = false;
|
||||||
let canCreateLayaway = false;
|
let canCreateLayaway = false;
|
||||||
|
let receiptConfig = {};
|
||||||
|
|
||||||
// Currency-aware formatter: reads pos_currency from localStorage
|
// Currency-aware formatter: reads pos_currency from localStorage
|
||||||
const _posCurrency = localStorage.getItem('pos_currency') || 'MXN';
|
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.`);
|
showToast(`Modo conversion: Cotizacion #${convertQuoteId}. El pago convertira la cotizacion en venta.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load current register
|
// Load current register and receipt config
|
||||||
await loadRegister();
|
await loadRegister();
|
||||||
|
await loadReceiptConfig();
|
||||||
|
|
||||||
// Setup event listeners
|
// Setup event listeners
|
||||||
setupKeyboard();
|
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() {
|
function showOpenRegisterModal() {
|
||||||
document.getElementById('openRegisterModal').classList.add('open');
|
document.getElementById('openRegisterModal').classList.add('open');
|
||||||
document.getElementById('registerOpenResult').innerHTML = '';
|
document.getElementById('registerOpenResult').innerHTML = '';
|
||||||
@@ -1283,12 +1299,37 @@ const POS = (() => {
|
|||||||
</div>`;
|
</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 = `
|
const ticketHtml = `
|
||||||
<div class="store-name">NEXUS AUTOPARTS</div>
|
${logoHtml}
|
||||||
<div class="store-tagline">Tu conexion con las refacciones</div>
|
<div class="store-name">${storeName}</div>
|
||||||
|
${storeTagline ? `<div class="store-tagline">${storeTagline}</div>` : ''}
|
||||||
<div class="store-info">
|
<div class="store-info">
|
||||||
Sucursal: ${currentRegister ? currentRegister.branch_name || '' : ''}<br>
|
${storeInfoLines.join('<br>')}
|
||||||
RFC: NAU210315XX1
|
|
||||||
</div>
|
</div>
|
||||||
<hr class="divider-double">
|
<hr class="divider-double">
|
||||||
<div class="folio-line">
|
<div class="folio-line">
|
||||||
@@ -1314,13 +1355,12 @@ const POS = (() => {
|
|||||||
<span>Subtotal:</span><span>${fmt(sale.subtotal)}</span>
|
<span>Subtotal:</span><span>${fmt(sale.subtotal)}</span>
|
||||||
</div>
|
</div>
|
||||||
${sale.discount_total > 0 ? `<div class="total-line"><span>Descuento:</span><span>-${fmt(sale.discount_total)}</span></div>` : ''}
|
${sale.discount_total > 0 ? `<div class="total-line"><span>Descuento:</span><span>-${fmt(sale.discount_total)}</span></div>` : ''}
|
||||||
<div class="total-line">
|
${showIva ? `<div class="total-line"><span>IVA:</span><span>${fmt(sale.tax_total)}</span></div>` : ''}
|
||||||
<span>IVA 16%:</span><span>${fmt(sale.tax_total)}</span>
|
|
||||||
</div>
|
|
||||||
<div class="total-line grand">
|
<div class="total-line grand">
|
||||||
<span>TOTAL:</span><span>${fmt(sale.total)}</span>
|
<span>TOTAL:</span><span>${fmt(sale.total)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
${showPayment ? `
|
||||||
<hr class="divider">
|
<hr class="divider">
|
||||||
<div class="payment-section">
|
<div class="payment-section">
|
||||||
<div class="ticket-row">
|
<div class="ticket-row">
|
||||||
@@ -1333,11 +1373,11 @@ const POS = (() => {
|
|||||||
<div class="ticket-row" style="font-weight: bold;">
|
<div class="ticket-row" style="font-weight: bold;">
|
||||||
<span>Cambio:</span><span>${fmt(sale.change_given || 0)}</span>
|
<span>Cambio:</span><span>${fmt(sale.change_given || 0)}</span>
|
||||||
</div>` : ''}
|
</div>` : ''}
|
||||||
</div>
|
</div>` : ''}
|
||||||
<hr class="divider">
|
<hr class="divider">
|
||||||
<div class="footer-section">
|
<div class="footer-section">
|
||||||
<div class="thanks">Gracias por su compra!</div>
|
<div class="thanks">${thanksMsg}</div>
|
||||||
<div>Conserve su ticket como comprobante.</div>
|
${footerMsg ? `<div>${footerMsg}</div>` : ''}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -255,7 +255,79 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ===============================================================
|
<!-- ===============================================================
|
||||||
SECTION 3: MÓDULOS E INTEGRACIONES
|
SECTION 3: PERSONALIZACIÓN DE TICKET
|
||||||
|
=============================================================== -->
|
||||||
|
<div class="settings-section">
|
||||||
|
<div class="settings-section__header">
|
||||||
|
<div class="settings-section__icon">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="settings-section__title">Personalización de Ticket</div>
|
||||||
|
<div class="settings-section__desc">Logo, datos y opciones que aparecen en el ticket de venta</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-card">
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-group form-group--full">
|
||||||
|
<label class="form-label">Logo del negocio</label>
|
||||||
|
<input type="file" id="receipt-logo" accept="image/png,image/jpeg,image/jpg,image/webp" onchange="Config.handleReceiptLogo(this)" style="display:none;" />
|
||||||
|
<div id="receipt-logo-preview" style="display:flex;align-items:center;gap:var(--space-3);flex-wrap:wrap;">
|
||||||
|
<div id="receipt-logo-thumb" style="width:80px;height:80px;border:1px dashed var(--color-border);border-radius:var(--radius-md);display:flex;align-items:center;justify-content:center;overflow:hidden;background:var(--color-surface-2);">
|
||||||
|
<span style="color:var(--color-text-muted);font-size:var(--text-caption);text-align:center;padding:var(--space-2);">Sin logo</span>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn--secondary btn--sm" onclick="document.getElementById('receipt-logo').click()">Subir imagen</button>
|
||||||
|
<button class="btn btn--ghost btn--sm" id="receipt-logo-remove" onclick="Config.removeReceiptLogo()" style="display:none;">Quitar</button>
|
||||||
|
</div>
|
||||||
|
<div class="form-hint">Recomendado: PNG/JPG con fondo blanco o transparente, máx. 300x150 px.</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Nombre en ticket</label>
|
||||||
|
<input class="form-input" id="receipt-store-name" type="text" placeholder="Ej: Refacciones El Toro" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Slogan / Línea debajo del nombre</label>
|
||||||
|
<input class="form-input" id="receipt-tagline" type="text" placeholder="Ej: Tu conexion con las refacciones" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">RFC en ticket</label>
|
||||||
|
<input class="form-input" id="receipt-rfc" type="text" placeholder="Ej: RET260101ABC" maxlength="13" style="text-transform:uppercase;" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group form-group--full">
|
||||||
|
<label class="form-label">Dirección en ticket</label>
|
||||||
|
<input class="form-input" id="receipt-address" type="text" placeholder="Calle, Número, Colonia, CP, Ciudad" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Teléfono en ticket</label>
|
||||||
|
<input class="form-input" id="receipt-phone" type="tel" placeholder="Ej: 664-123-4567" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group form-group--full">
|
||||||
|
<label class="form-label">Mensaje de agradecimiento</label>
|
||||||
|
<input class="form-input" id="receipt-thanks" type="text" placeholder="Gracias por su compra!" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group form-group--full">
|
||||||
|
<label class="form-label">Pie de página adicional</label>
|
||||||
|
<input class="form-input" id="receipt-footer" type="text" placeholder="Ej: Conserve su ticket como comprobante." />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-grid" style="margin-top:var(--space-4);">
|
||||||
|
<label style="display:flex;align-items:center;gap:var(--space-2);cursor:pointer;"><input type="checkbox" id="receipt-show-logo" style="width:auto;" checked /> Mostrar logo</label>
|
||||||
|
<label style="display:flex;align-items:center;gap:var(--space-2);cursor:pointer;"><input type="checkbox" id="receipt-show-rfc" style="width:auto;" checked /> Mostrar RFC</label>
|
||||||
|
<label style="display:flex;align-items:center;gap:var(--space-2);cursor:pointer;"><input type="checkbox" id="receipt-show-address" style="width:auto;" /> Mostrar dirección</label>
|
||||||
|
<label style="display:flex;align-items:center;gap:var(--space-2);cursor:pointer;"><input type="checkbox" id="receipt-show-phone" style="width:auto;" /> Mostrar teléfono</label>
|
||||||
|
<label style="display:flex;align-items:center;gap:var(--space-2);cursor:pointer;"><input type="checkbox" id="receipt-show-iva" style="width:auto;" checked /> Mostrar desglose de IVA</label>
|
||||||
|
<label style="display:flex;align-items:center;gap:var(--space-2);cursor:pointer;"><input type="checkbox" id="receipt-show-payment" style="width:auto;" checked /> Mostrar detalle de pago</label>
|
||||||
|
<label style="display:flex;align-items:center;gap:var(--space-2);cursor:pointer;"><input type="checkbox" id="receipt-show-employee" style="width:auto;" /> Mostrar nombre del empleado</label>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top:var(--space-4);text-align:right;">
|
||||||
|
<button class="btn btn--primary" onclick="Config.saveReceiptConfig()">Guardar ticket</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ===============================================================
|
||||||
|
SECTION 4: MÓDULOS E INTEGRACIONES
|
||||||
=============================================================== -->
|
=============================================================== -->
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<div class="settings-section__header">
|
<div class="settings-section__header">
|
||||||
|
|||||||
Reference in New Issue
Block a user