feat(pos): add real thermal ticket printing via ESC/POS (#21)
- Add thermal_printer.py service generating raw ESC/POS bytes for 58mm/80mm printers - Add /pos/api/sales/<id>/print endpoint (escpos_raw or browser mode) - Add printer.js with WebUSB and Web Serial support for direct browser-to-printer - Add thermal print button in ticket modal with connect/print workflow Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -26,7 +26,11 @@ const POS = (() => {
|
||||
let searchTimeout = null;
|
||||
let customerSearchTimeout = null;
|
||||
|
||||
const fmt = (n) => '$' + parseFloat(n || 0).toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||
// Currency-aware formatter: reads pos_currency from localStorage
|
||||
const _posCurrency = localStorage.getItem('pos_currency') || 'MXN';
|
||||
const _currSymbols = { MXN: '$', USD: 'US$' };
|
||||
const _currLocale = _posCurrency === 'USD' ? 'en-US' : 'es-MX';
|
||||
const fmt = (n) => (_currSymbols[_posCurrency] || '$') + parseFloat(n || 0).toLocaleString(_currLocale, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||
|
||||
function headers() {
|
||||
return { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token };
|
||||
@@ -940,6 +944,45 @@ const POS = (() => {
|
||||
setTimeout(() => { if (area) area.style.display = 'none'; }, 500);
|
||||
}
|
||||
|
||||
// ─── Thermal Printing ─────────────────
|
||||
|
||||
async function connectThermal() {
|
||||
if (!window.NexusPrinter) { showToast('Printer module not loaded'); return; }
|
||||
const result = await NexusPrinter.connect();
|
||||
if (result.ok) {
|
||||
showToast('Impresora conectada: ' + (result.name || result.type));
|
||||
_updatePrinterButtons();
|
||||
} else {
|
||||
showToast(result.error || 'No se pudo conectar la impresora');
|
||||
}
|
||||
}
|
||||
|
||||
async function thermalPrint() {
|
||||
if (!window.NexusPrinter || !NexusPrinter.isConnected()) {
|
||||
showToast('Conecte una impresora termica primero');
|
||||
return;
|
||||
}
|
||||
if (!lastSaleId) { showToast('No hay venta para imprimir'); return; }
|
||||
const ok = await NexusPrinter.printSale(lastSaleId);
|
||||
if (ok) {
|
||||
showToast('Ticket enviado a impresora termica');
|
||||
} else {
|
||||
showToast('Error al imprimir. Reconecte la impresora.');
|
||||
}
|
||||
}
|
||||
|
||||
function _updatePrinterButtons() {
|
||||
const connectBtn = document.getElementById('btnConnectPrinter');
|
||||
const thermalBtn = document.getElementById('btnThermalPrint');
|
||||
if (window.NexusPrinter && NexusPrinter.isConnected()) {
|
||||
if (connectBtn) connectBtn.style.display = 'none';
|
||||
if (thermalBtn) thermalBtn.style.display = '';
|
||||
} else {
|
||||
if (connectBtn) connectBtn.style.display = '';
|
||||
if (thermalBtn) thermalBtn.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Last Sale ───────────────────────
|
||||
async function showLastSale() {
|
||||
if (!lastSaleId) { showToast('No hay venta reciente'); return; }
|
||||
@@ -1078,5 +1121,6 @@ const POS = (() => {
|
||||
creditSale, saveQuotation, createLayaway,
|
||||
showLastSale, openDrawer,
|
||||
showTicket, closeTicketModal, printTicket,
|
||||
connectThermal, thermalPrint,
|
||||
};
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user