From 54729b1a51537d40ece0611368c61ac9bb04795a Mon Sep 17 00:00:00 2001 From: Suherdy Yacob Date: Sun, 7 Dec 2025 22:40:50 +0700 Subject: [PATCH] fix the width scaling error, fix the font scaling, removing debug logging --- static/src/js/bluetooth_printer_manager.js | 156 ++++---- static/src/js/escpos_graphics.js | 135 +++---- static/src/js/html_to_image.js | 393 ++++++++++++--------- static/src/js/pos_receipt_printer.js | 346 ++++++++---------- static/src/js/pos_session_integration.js | 62 ++-- 5 files changed, 557 insertions(+), 535 deletions(-) diff --git a/static/src/js/bluetooth_printer_manager.js b/static/src/js/bluetooth_printer_manager.js index fb99a87..8795cd9 100644 --- a/static/src/js/bluetooth_printer_manager.js +++ b/static/src/js/bluetooth_printer_manager.js @@ -70,22 +70,22 @@ export class TimeoutError extends Error { export class BluetoothPrinterManager { constructor(errorNotificationService = null) { // Debug logging - this.debugMode = true; // Set to false to disable verbose logging - + this.debugMode = false; // Set to false to disable verbose logging + // Connection state this.device = null; this.server = null; this.service = null; this.characteristic = null; this.connectionStatus = 'disconnected'; - + // Reconnection state this.reconnectAttempts = 0; this.maxReconnectAttempts = 3; this.reconnectDelays = [1000, 2000, 4000]; // Exponential backoff in milliseconds this.isReconnecting = false; this.autoReconnectEnabled = true; - + // Event listeners this.eventListeners = { 'connection-status-changed': [], @@ -95,21 +95,21 @@ export class BluetoothPrinterManager { 'reconnection-success': [], 'reconnection-failure': [] }; - + // Printer state this.lastError = null; this.isPrinting = false; - + // Error notification service this.errorNotificationService = errorNotificationService; - + // Bluetooth service UUID for serial port profile (commonly used by thermal printers) // Using the standard Serial Port Profile UUID (SPP) // Most thermal printers use SPP for communication this.serviceUUID = '00001101-0000-1000-8000-00805f9b34fb'; // Serial Port Profile this.characteristicUUID = '00002af1-0000-1000-8000-00805f9b34fb'; } - + /** * Set error notification service * @param {Object} errorNotificationService - Error notification service instance @@ -139,8 +139,8 @@ export class BluetoothPrinterManager { * @returns {boolean} True if available */ isBluetoothAvailable() { - const available = typeof navigator !== 'undefined' && - navigator.bluetooth !== undefined; + const available = typeof navigator !== 'undefined' && + navigator.bluetooth !== undefined; this._log(`Bluetooth API available: ${available}`); return available; } @@ -162,7 +162,7 @@ export class BluetoothPrinterManager { try { this._log('Starting device scan...'); - + // Request bluetooth device with filters for thermal printers // Many thermal printers advertise with specific name patterns const device = await navigator.bluetooth.requestDevice({ @@ -190,7 +190,7 @@ export class BluetoothPrinterManager { return [device]; } catch (error) { this._log('Filtered scan failed, trying acceptAllDevices fallback', error.name); - + // If filtered search fails, try acceptAllDevices as fallback if (error.name === 'NotFoundError') { try { @@ -219,7 +219,7 @@ export class BluetoothPrinterManager { } throw cancelError; } - + // Log unexpected errors if (this.errorNotificationService) { this.errorNotificationService.logError(error, { operation: 'scanDevices' }); @@ -247,7 +247,7 @@ export class BluetoothPrinterManager { try { this._log('Attempting to connect to printer...', deviceId); this._setConnectionStatus('connecting'); - + // If deviceId is actually a device object from scanDevices, use it directly let device; if (typeof deviceId === 'object' && deviceId.gatt) { @@ -259,15 +259,15 @@ export class BluetoothPrinterManager { const devices = await navigator.bluetooth.getDevices(); this._log(`Found ${devices.length} previously paired devices`); device = devices.find(d => d.id === deviceId || d.name === deviceId); - + if (!device) { this._log('Device not found in paired devices'); const error = new DeviceNotFoundError(`Device ${deviceId} not found`); if (this.errorNotificationService) { - this.errorNotificationService.handleError(error, { + this.errorNotificationService.handleError(error, { operation: 'connectToPrinter', - deviceId: deviceId + deviceId: deviceId }); } throw error; @@ -276,7 +276,7 @@ export class BluetoothPrinterManager { this.device = device; this._log('Device selected', { name: this.device.name, id: this.device.id }); - + // Set up disconnect handler this.device.addEventListener('gattserverdisconnected', () => { this._log('GATT server disconnected event fired'); @@ -287,7 +287,7 @@ export class BluetoothPrinterManager { this._log('Connecting to GATT server...'); this.server = await this.device.gatt.connect(); this._log('GATT server connected successfully'); - + // Try to get the printer service - try multiple common UUIDs const serviceUUIDs = [ this.serviceUUID, // Serial Port Profile @@ -296,7 +296,7 @@ export class BluetoothPrinterManager { '0000ffe0-0000-1000-8000-00805f9b34fb', // Common serial service '6e400001-b5a3-f393-e0a9-e50e24dcca9e', // Nordic UART Service ]; - + const characteristicUUIDs = [ this.characteristicUUID, // Default characteristic '00002af1-0000-1000-8000-00805f9b34fb', // Write characteristic @@ -304,19 +304,19 @@ export class BluetoothPrinterManager { '0000ffe1-0000-1000-8000-00805f9b34fb', // Common write characteristic '6e400002-b5a3-f393-e0a9-e50e24dcca9e', // Nordic UART TX ]; - + let serviceFound = false; - + for (const serviceUUID of serviceUUIDs) { try { - console.log(`Trying service UUID: ${serviceUUID}`); + this._log(`Trying service UUID: ${serviceUUID}`); this.service = await this.server.getPrimaryService(serviceUUID); - + // Try to find a writable characteristic for (const charUUID of characteristicUUIDs) { try { this.characteristic = await this.service.getCharacteristic(charUUID); - console.log(`Found characteristic: ${charUUID}`); + this._log(`Found characteristic: ${charUUID}`); serviceFound = true; break; } catch (charError) { @@ -324,7 +324,7 @@ export class BluetoothPrinterManager { continue; } } - + if (serviceFound) { break; } @@ -333,10 +333,10 @@ export class BluetoothPrinterManager { continue; } } - + if (!serviceFound) { console.warn('Standard printer services not found, attempting to use any available writable characteristic'); - + // Last resort: try to find any writable characteristic try { const services = await this.server.getPrimaryServices(); @@ -346,7 +346,7 @@ export class BluetoothPrinterManager { if (char.properties.write || char.properties.writeWithoutResponse) { this.service = service; this.characteristic = char; - console.log(`Using fallback characteristic: ${char.uuid}`); + this._log(`Using fallback characteristic: ${char.uuid}`); serviceFound = true; break; } @@ -356,9 +356,9 @@ export class BluetoothPrinterManager { } catch (fallbackError) { console.error('Failed to find any writable characteristic:', fallbackError); if (this.errorNotificationService) { - this.errorNotificationService.logError(fallbackError, { + this.errorNotificationService.logError(fallbackError, { operation: 'findWritableCharacteristic', - deviceName: this.device.name + deviceName: this.device.name }); } } @@ -376,16 +376,16 @@ export class BluetoothPrinterManager { } catch (error) { this._setConnectionStatus('error'); this.lastError = error.message; - + if (error instanceof DeviceNotFoundError) { throw error; } - + const connectionError = new ConnectionFailedError(`Failed to connect: ${error.message}`); if (this.errorNotificationService) { - this.errorNotificationService.handleError(connectionError, { + this.errorNotificationService.handleError(connectionError, { operation: 'connectToPrinter', - originalError: error.message + originalError: error.message }); } throw connectionError; @@ -398,7 +398,7 @@ export class BluetoothPrinterManager { */ async disconnect() { this.autoReconnectEnabled = false; - + if (this.server && this.server.connected) { try { await this.server.disconnect(); @@ -411,7 +411,7 @@ export class BluetoothPrinterManager { this.server = null; this.service = null; this.characteristic = null; - + this._setConnectionStatus('disconnected'); this.lastError = null; } @@ -444,9 +444,9 @@ export class BluetoothPrinterManager { if (!this.characteristic) { const error = new TransmissionError('Printer characteristic not available'); if (this.errorNotificationService) { - this.errorNotificationService.handleError(error, { + this.errorNotificationService.handleError(error, { operation: 'sendData', - reason: 'characteristic_unavailable' + reason: 'characteristic_unavailable' }); } throw error; @@ -455,36 +455,36 @@ export class BluetoothPrinterManager { try { this.isPrinting = true; const startTime = performance.now(); - + // OPTIMIZED: Use larger chunks for graphics data (faster transmission) // Graphics data can handle larger chunks than text commands const isLargeData = escposData.length > 1000; const chunkSize = isLargeData ? 512 : 20; // Much larger chunks for graphics - + const chunks = []; for (let i = 0; i < escposData.length; i += chunkSize) { chunks.push(escposData.slice(i, i + chunkSize)); } - console.log(`[Bluetooth] Sending ${chunks.length} chunks (${escposData.length} bytes, ${chunkSize} bytes/chunk)`); + this._log(`Sending ${chunks.length} chunks (${escposData.length} bytes, ${chunkSize} bytes/chunk)`); // Determine write method based on characteristic properties const useWriteWithoutResponse = this.characteristic.properties.writeWithoutResponse; const useWrite = this.characteristic.properties.write; - + if (!useWrite && !useWriteWithoutResponse) { throw new Error('Characteristic does not support write operations'); } // OPTIMIZED: Reduce delays for faster transmission - const delay = isLargeData ? + const delay = isLargeData ? (useWriteWithoutResponse ? 10 : 5) : // Much shorter delays for graphics (useWriteWithoutResponse ? 50 : 25); // Normal delays for text // Send each chunk for (let i = 0; i < chunks.length; i++) { const chunk = chunks[i]; - + try { if (useWriteWithoutResponse) { // Faster but no acknowledgment @@ -493,16 +493,16 @@ export class BluetoothPrinterManager { // Slower but with acknowledgment await this.characteristic.writeValue(chunk); } - + // OPTIMIZED: Minimal delay between chunks if (delay > 0) { await this._sleep(delay); } - + // Progress logging every 20% if (i % Math.ceil(chunks.length / 5) === 0) { const progress = Math.round((i / chunks.length) * 100); - console.log(`[Bluetooth] Progress: ${progress}%`); + this._log(`Progress: ${progress}%`); } } catch (chunkError) { console.error(`Failed to send chunk ${i + 1}/${chunks.length}:`, chunkError); @@ -513,22 +513,22 @@ export class BluetoothPrinterManager { const endTime = performance.now(); const duration = ((endTime - startTime) / 1000).toFixed(2); const speed = (escposData.length / 1024 / (duration || 1)).toFixed(2); - - console.log(`[Bluetooth] Transmission complete in ${duration}s (${speed} KB/s)`); + + this._log(`Transmission complete in ${duration}s (${speed} KB/s)`); this.isPrinting = false; this._emit('print-completed', { success: true }); - + return true; } catch (error) { this.isPrinting = false; this._emit('print-failed', { error: error.message }); - + const transmissionError = new TransmissionError(`Failed to send data: ${error.message}`); if (this.errorNotificationService) { - this.errorNotificationService.handleError(transmissionError, { + this.errorNotificationService.handleError(transmissionError, { operation: 'sendData', dataSize: escposData.length, - originalError: error.message + originalError: error.message }); } throw transmissionError; @@ -578,13 +578,13 @@ export class BluetoothPrinterManager { for (let attempt = 0; attempt < this.maxReconnectAttempts; attempt++) { this.reconnectAttempts = attempt + 1; - + // Emit reconnection attempt event this._emit('reconnection-attempt', { attempt: this.reconnectAttempts, maxAttempts: this.maxReconnectAttempts }); - + // Notify via error service if (this.errorNotificationService) { this.errorNotificationService.handleReconnectionAttempt( @@ -592,32 +592,32 @@ export class BluetoothPrinterManager { this.maxReconnectAttempts ); } - + try { - console.log(`Reconnection attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts}`); - + this._log(`Reconnection attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts}`); + // Try to reconnect await this.connectToPrinter(this.device); - + this.isReconnecting = false; this.reconnectAttempts = 0; - - console.log('Reconnection successful'); - + + this._log('Reconnection successful'); + // Emit reconnection success event this._emit('reconnection-success', { deviceName: this.device.name }); - + // Notify via error service if (this.errorNotificationService) { this.errorNotificationService.handleReconnectionSuccess(this.device.name); } - + return true; } catch (error) { console.error(`Reconnection attempt ${this.reconnectAttempts} failed:`, error); - + // Log the error if (this.errorNotificationService) { this.errorNotificationService.logError(error, { @@ -626,11 +626,11 @@ export class BluetoothPrinterManager { maxAttempts: this.maxReconnectAttempts }); } - + // Wait before next attempt (exponential backoff) if (attempt < this.maxReconnectAttempts - 1) { const delay = this.reconnectDelays[attempt]; - console.log(`Waiting ${delay}ms before next attempt`); + this._log(`Waiting ${delay}ms before next attempt`); await this._sleep(delay); } } @@ -640,19 +640,19 @@ export class BluetoothPrinterManager { this.isReconnecting = false; this._setConnectionStatus('error'); this.lastError = 'Reconnection failed after maximum attempts'; - + console.error('All reconnection attempts failed'); - + // Emit reconnection failure event this._emit('reconnection-failure', { attempts: this.maxReconnectAttempts }); - + // Notify via error service if (this.errorNotificationService) { this.errorNotificationService.handleReconnectionFailure(); } - + return false; } @@ -694,12 +694,12 @@ export class BluetoothPrinterManager { * @private */ _onDisconnected() { - console.log('Bluetooth device disconnected'); + this._log('Bluetooth device disconnected'); this._setConnectionStatus('disconnected'); - + // Attempt auto-reconnection if enabled if (this.autoReconnectEnabled) { - console.log('Starting auto-reconnection...'); + this._log('Starting auto-reconnection...'); this.autoReconnect().catch(error => { console.error('Auto-reconnection failed:', error); }); @@ -714,7 +714,7 @@ export class BluetoothPrinterManager { _setConnectionStatus(status) { const oldStatus = this.connectionStatus; this.connectionStatus = status; - + if (oldStatus !== status) { const statusData = { oldStatus, @@ -722,9 +722,9 @@ export class BluetoothPrinterManager { deviceName: this.device ? this.device.name : null, timestamp: Date.now() }; - + this._emit('connection-status-changed', statusData); - + // Notify error service about status change if (this.errorNotificationService) { this.errorNotificationService.handleStatusChange(statusData); diff --git a/static/src/js/escpos_graphics.js b/static/src/js/escpos_graphics.js index 49e3a7f..fe24457 100644 --- a/static/src/js/escpos_graphics.js +++ b/static/src/js/escpos_graphics.js @@ -35,34 +35,39 @@ export class EscPosGraphics { * @returns {Uint8Array} Complete ESC/POS command sequence */ generateBitmapCommands(bitmap) { - console.log('[EscPosGraphics] Generating bitmap commands (optimized)...'); - console.log('[EscPosGraphics] Original dimensions:', bitmap.width, 'x', bitmap.height); - + + const startTime = performance.now(); - + // OPTIMIZATION: Remove blank lines from top and bottom const optimizedBitmap = this._removeBlankLines(bitmap); - console.log('[EscPosGraphics] Optimized dimensions:', optimizedBitmap.width, 'x', optimizedBitmap.height); - console.log('[EscPosGraphics] Saved', bitmap.height - optimizedBitmap.height, 'blank lines'); - - const commands = []; - + + // Initialize printer - commands.push(...this.initialize()); - + const initCmd = this.initialize(); + // Print bitmap using raster graphics mode const rasterCommands = this._generateRasterGraphics(optimizedBitmap); - commands.push(...rasterCommands); - + // Feed paper and cut - commands.push(...this._feedAndCut(4)); - - const result = new Uint8Array(commands); - + const feedCmd = this._feedAndCut(4); + + // Combine all commands safely without spreading large arrays + const totalLength = initCmd.length + rasterCommands.length + feedCmd.length; + const result = new Uint8Array(totalLength); + + let offset = 0; + result.set(initCmd, offset); + offset += initCmd.length; + + result.set(rasterCommands, offset); + offset += rasterCommands.length; + + result.set(new Uint8Array(feedCmd), offset); + const endTime = performance.now(); - console.log('[EscPosGraphics] Command generation took:', (endTime - startTime).toFixed(2), 'ms'); - console.log('[EscPosGraphics] Generated', result.length, 'bytes of commands'); - + + return result; } @@ -75,14 +80,14 @@ export class EscPosGraphics { */ _removeBlankLines(bitmap) { const { data, width, height, bytesPerLine } = bitmap; - + // Find first non-blank line from top let firstLine = 0; for (let y = 0; y < height; y++) { const lineStart = y * bytesPerLine; const lineEnd = lineStart + bytesPerLine; const lineData = data.slice(lineStart, lineEnd); - + // Check if line has any black pixels const hasContent = lineData.some(byte => byte !== 0); if (hasContent) { @@ -90,14 +95,14 @@ export class EscPosGraphics { break; } } - + // Find last non-blank line from bottom let lastLine = height - 1; for (let y = height - 1; y >= firstLine; y--) { const lineStart = y * bytesPerLine; const lineEnd = lineStart + bytesPerLine; const lineData = data.slice(lineStart, lineEnd); - + // Check if line has any black pixels const hasContent = lineData.some(byte => byte !== 0); if (hasContent) { @@ -105,18 +110,18 @@ export class EscPosGraphics { break; } } - + // Extract only the content lines const newHeight = lastLine - firstLine + 1; const newData = new Uint8Array(bytesPerLine * newHeight); - + for (let y = 0; y < newHeight; y++) { const srcStart = (firstLine + y) * bytesPerLine; const srcEnd = srcStart + bytesPerLine; const dstStart = y * bytesPerLine; newData.set(data.slice(srcStart, srcEnd), dstStart); } - + return { data: newData, width: width, @@ -134,37 +139,39 @@ export class EscPosGraphics { * @returns {Array} Command bytes */ _generateRasterGraphics(bitmap) { - const commands = []; const { data, width, height, bytesPerLine } = bitmap; - + // Calculate dimensions const widthBytes = bytesPerLine; const widthLow = widthBytes & 0xFF; const widthHigh = (widthBytes >> 8) & 0xFF; const heightLow = height & 0xFF; const heightHigh = (height >> 8) & 0xFF; - - console.log('[EscPosGraphics] Bitmap width:', width, 'pixels'); - console.log('[EscPosGraphics] Bitmap height:', height, 'lines'); - console.log('[EscPosGraphics] Bytes per line:', bytesPerLine); - console.log('[EscPosGraphics] Width bytes (xL xH):', widthLow, widthHigh, '=', widthBytes); - console.log('[EscPosGraphics] Height (yL yH):', heightLow, heightHigh, '=', height); - console.log('[EscPosGraphics] Total data size:', data.length, 'bytes'); - console.log('[EscPosGraphics] Expected data size:', bytesPerLine * height, 'bytes'); - + + + // GS v 0 - Print raster bitmap // Format: GS v 0 m xL xH yL yH d1...dk - // m = mode (0 = normal, 1 = double width, 2 = double height, 3 = quadruple) - commands.push(GS, 0x76, 0x30, 0x00); // GS v 0 m (m=0 for normal) - commands.push(widthLow, widthHigh); // xL xH (width in bytes) - commands.push(heightLow, heightHigh); // yL yH (height in dots) - - // Add bitmap data - commands.push(...data); - - // Add line feed after image - commands.push(LF); - + // Header length: 8 bytes + const header = [ + GS, 0x76, 0x30, 0x00, // GS v 0 m (m=0 for normal) + widthLow, widthHigh, // xL xH (width in bytes) + heightLow, heightHigh // yL yH (height in dots) + ]; + + // Create Uint8Array for the complete command + const totalLength = header.length + data.length + 1; // +1 for LF + const commands = new Uint8Array(totalLength); + + // Set header + commands.set(header, 0); + + // Set bitmap data + commands.set(data, header.length); + + // Set LF + commands[totalLength - 1] = LF; + return commands; } @@ -179,7 +186,7 @@ export class EscPosGraphics { _generateBitImageCommands(bitmap) { const commands = []; const { data, width, height, bytesPerLine } = bitmap; - + // Print line by line using ESC * command for (let y = 0; y < height; y++) { // ESC * m nL nH d1...dk @@ -187,18 +194,18 @@ export class EscPosGraphics { const mode = 33; const nL = width & 0xFF; const nH = (width >> 8) & 0xFF; - + commands.push(ESC, 0x2A, mode, nL, nH); - + // Add line data const lineStart = y * bytesPerLine; const lineEnd = lineStart + bytesPerLine; commands.push(...data.slice(lineStart, lineEnd)); - + // Line feed commands.push(LF); } - + return commands; } @@ -211,16 +218,16 @@ export class EscPosGraphics { */ _feedAndCut(lines = 3) { const commands = []; - + // Feed lines for (let i = 0; i < lines; i++) { commands.push(LF); } - + // Cut paper (GS V m) // m = 0 (full cut), 1 (partial cut) commands.push(GS, 0x56, 0x00); - + return commands; } @@ -234,13 +241,13 @@ export class EscPosGraphics { */ splitIntoChunks(commands, chunkSize = 1024) { const chunks = []; - + for (let i = 0; i < commands.length; i += chunkSize) { const chunk = commands.slice(i, i + chunkSize); chunks.push(chunk); } - - console.log('[EscPosGraphics] Split into', chunks.length, 'chunks'); + + return chunks; } @@ -254,12 +261,12 @@ export class EscPosGraphics { const height = 200; const bytesPerLine = Math.ceil(width / 8); const data = new Uint8Array(bytesPerLine * height); - + // Create a test pattern (checkerboard) for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const isBlack = ((Math.floor(x / 8) + Math.floor(y / 8)) % 2) === 0; - + if (isBlack) { const byteIndex = y * bytesPerLine + Math.floor(x / 8); const bitIndex = 7 - (x % 8); @@ -267,14 +274,14 @@ export class EscPosGraphics { } } } - + const bitmap = { data: data, width: width, height: height, bytesPerLine: bytesPerLine }; - + return this.generateBitmapCommands(bitmap); } } diff --git a/static/src/js/html_to_image.js b/static/src/js/html_to_image.js index 833484f..73def7d 100644 --- a/static/src/js/html_to_image.js +++ b/static/src/js/html_to_image.js @@ -31,12 +31,11 @@ export class HtmlToImageConverter { * @returns {Promise} Canvas with rendered HTML */ async htmlToCanvas(element) { - console.log('[HtmlToImage] Converting HTML to canvas...'); - console.log('[HtmlToImage] Paper width:', this.paperWidth, 'pixels'); - + + // Clone the element to avoid modifying the original const clone = element.cloneNode(true); - + // Apply receipt styling to clone for proper rendering clone.style.width = `${this.paperWidth}px`; clone.style.maxWidth = `${this.paperWidth}px`; @@ -47,47 +46,12 @@ export class HtmlToImageConverter { clone.style.backgroundColor = 'white'; clone.style.color = 'black'; clone.style.overflow = 'visible'; - - // Scale all fonts by 100% (2x) to match test print readability + + // Scale all fonts by 150% (3x) to match test print readability // Test print uses ESC/POS text mode which has larger fonts - const fontScale = 2.0; - console.log('[HtmlToImage] Scaling fonts by', fontScale, 'x to match test print readability'); - - const allElements = clone.querySelectorAll('*'); - allElements.forEach(element => { - const style = window.getComputedStyle(element); - const currentFontSize = parseFloat(style.fontSize); - - if (currentFontSize > 0) { - const newFontSize = currentFontSize * fontScale; - element.style.fontSize = `${newFontSize}px`; - } - - // Also reduce padding to prevent overflow and cropping - const paddingLeft = parseFloat(style.paddingLeft) || 0; - const paddingRight = parseFloat(style.paddingRight) || 0; - - // If padding is excessive, reduce it - if (paddingLeft > 8) { - element.style.paddingLeft = '4px'; - } - if (paddingRight > 8) { - element.style.paddingRight = '4px'; - } - }); - - // Also scale the root element font size - const rootFontSize = parseFloat(window.getComputedStyle(clone).fontSize); - if (rootFontSize > 0) { - clone.style.fontSize = `${rootFontSize * fontScale}px`; - } - - // Reduce root padding to prevent cropping - clone.style.paddingLeft = '4px'; - clone.style.paddingRight = '4px'; - - console.log('[HtmlToImage] Fonts scaled 2x, padding reduced, rendering at paper width:', this.paperWidth, 'px'); - + const fontScale = 1.2; + + // Create a temporary container to measure height // Container must be large enough to hold the scaled content const container = document.createElement('div'); @@ -101,14 +65,66 @@ export class HtmlToImageConverter { container.style.overflow = 'visible'; container.style.zIndex = '-1000'; container.style.boxSizing = 'border-box'; - + container.appendChild(clone); document.body.appendChild(container); - + + const allElements = clone.querySelectorAll('*'); + allElements.forEach(element => { + const style = window.getComputedStyle(element); + const currentFontSize = parseFloat(style.fontSize); + + if (currentFontSize > 0) { + const newFontSize = currentFontSize * fontScale; + element.style.fontSize = `${newFontSize}px`; + } + + // Also reduce padding to prevent overflow and cropping + const paddingLeft = parseFloat(style.paddingLeft) || 0; + const paddingRight = parseFloat(style.paddingRight) || 0; + + // If padding is excessive, reduce it + if (paddingLeft > 8) { + element.style.paddingLeft = '4px'; + } + if (paddingRight > 8) { + element.style.paddingRight = '4px'; + } + }); + + // Also scale the root element font size + const rootFontSize = parseFloat(window.getComputedStyle(clone).fontSize); + if (rootFontSize > 0) { + clone.style.fontSize = `${rootFontSize * fontScale}px`; + } + + // Reduce root padding to prevent cropping + clone.style.paddingLeft = '4px'; + clone.style.paddingRight = '4px'; + + + + // Create a temporary container to measure height + // Container must be large enough to hold the scaled content + // Duplicate container declaration removed + container.style.position = 'fixed'; + container.style.left = '-9999px'; // Move off-screen instead of using opacity + container.style.top = '0'; + container.style.width = `${this.paperWidth}px`; + container.style.maxWidth = `${this.paperWidth}px`; + container.style.minWidth = `${this.paperWidth}px`; + container.style.backgroundColor = 'white'; + container.style.overflow = 'visible'; + container.style.zIndex = '-1000'; + container.style.boxSizing = 'border-box'; + + container.appendChild(clone); + document.body.appendChild(container); + try { // Wait for layout to settle, fonts to load, and images to load await new Promise(resolve => setTimeout(resolve, 200)); - + // Load all images in the clone const images = clone.querySelectorAll('img'); await Promise.all(Array.from(images).map(img => { @@ -123,36 +139,32 @@ export class HtmlToImageConverter { } }); })); - + // Get the actual rendered dimensions const rect = clone.getBoundingClientRect(); const width = this.paperWidth; const height = Math.ceil(rect.height); - - console.log('[HtmlToImage] Rendered dimensions:', rect.width, 'x', rect.height); - console.log('[HtmlToImage] Final canvas size:', width, 'x', height); - console.log('[HtmlToImage] Container rect:', rect); - console.log('[HtmlToImage] Receipt HTML preview (first 1000 chars):', clone.outerHTML.substring(0, 1000)); - console.log('[HtmlToImage] Receipt text content:', clone.textContent.substring(0, 500)); - + + + // Create canvas with exact dimensions const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d', { alpha: false, willReadFrequently: false }); - + // Fill with white background ctx.fillStyle = 'white'; ctx.fillRect(0, 0, width, height); - + // Render the DOM to canvas using improved method // Content is already at the correct width, no scaling needed - console.log('[HtmlToImage] Rendering DOM to canvas...'); + await this._renderDomToCanvasImproved(clone, ctx, 0, 0, width, height); - - console.log('[HtmlToImage] Canvas rendering complete'); + + return canvas; - + } finally { // Clean up document.body.removeChild(container); @@ -173,17 +185,17 @@ export class HtmlToImageConverter { * @returns {Promise} Height used */ async _renderDomToCanvasImproved(element, ctx, offsetX, offsetY, canvasWidth, canvasHeight) { - console.log('[HtmlToImage] Rendering DOM to canvas (improved method)...'); - + + // Background is already filled, no need to fill again - + // Get the bounding rect of the root element to calculate offsets const rootRect = element.getBoundingClientRect(); - + // Render element recursively with proper offset calculation await this._renderElement(element, ctx, -rootRect.left, -rootRect.top, rootRect); - - console.log('[HtmlToImage] Rendering complete'); + + return canvasHeight; } @@ -201,50 +213,114 @@ export class HtmlToImageConverter { if (!element || element.nodeType !== Node.ELEMENT_NODE) { return; } - + const style = window.getComputedStyle(element); - + // Skip hidden elements if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') { return; } - + const rect = element.getBoundingClientRect(); const x = rect.left + offsetX; const y = rect.top + offsetY; - + // Render background const bgColor = style.backgroundColor; if (bgColor && bgColor !== 'rgba(0, 0, 0, 0)' && bgColor !== 'transparent') { ctx.fillStyle = bgColor; ctx.fillRect(x, y, rect.width, rect.height); } - + // Render borders this._renderBorders(ctx, x, y, rect.width, rect.height, style); - + // Render images if (element.tagName === 'IMG') { await this._renderImage(element, ctx, x, y, rect.width, rect.height); } - - // Render text content + + // Render children (text and elements) if (element.childNodes.length > 0) { + // Check if this is a specialized text-only element (optimization + wrapping handling) + // We keep this optimization for simple blocks, but improve the check + // Actually, to fix mixed content, we should just iterate childNodes. + // But we need to handle wrapping for long text nodes. + // For now, let's assume standard recursion is safer for mixed content. + const hasOnlyText = Array.from(element.childNodes).every( node => node.nodeType === Node.TEXT_NODE || (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'BR') ); - + + // Use the block text renderer mainly for simple text blocks which might need wrapping logic + // that _renderText provides (it handles word wrapping based on width). + // However, relying ONLY on this means mixed content fails. + if (hasOnlyText) { this._renderText(element, ctx, x, y, rect.width, rect.height, style); } else { - // Render children recursively - for (const child of element.children) { - await this._renderElement(child, ctx, offsetX, offsetY, rootRect); + // Mixed content or complex structure + for (const node of element.childNodes) { + if (node.nodeType === Node.ELEMENT_NODE) { + await this._renderElement(node, ctx, offsetX, offsetY, rootRect); + } else if (node.nodeType === Node.TEXT_NODE) { + const text = node.textContent.trim(); + if (text) { + // Text node in mixed content + // We need its exact position + const range = document.createRange(); + range.selectNode(node); + const textRect = range.getBoundingClientRect(); + + // Calculate relative position + const textX = textRect.left + offsetX; + const textY = textRect.top + offsetY; + + // Draw the text node using parent's style + // Note: Text nodes don't have padding/margin themselves effectively (handled by layout) + this._drawTextNode(ctx, text, textX, textY, textRect.width, style); + } + } } } } } + /** + * Draw a single text node + * + * @private + * @param {CanvasRenderingContext2D} ctx - Canvas context + * @param {string} text - Text to draw + * @param {number} x - X position + * @param {number} y - Y position + * @param {number} width - Node width + * @param {CSSStyleDeclaration} style - Computed style of parent + */ + _drawTextNode(ctx, text, x, y, width, style) { + // Set font + const fontSize = parseFloat(style.fontSize) || 12; + const fontWeight = style.fontWeight; + const fontFamily = style.fontFamily.split(',')[0].replace(/['"]/g, '') || 'monospace'; + const isBold = fontWeight === 'bold' || parseInt(fontWeight) >= 600; + + ctx.font = `${isBold ? 'bold' : 'normal'} ${fontSize}px ${fontFamily}`; + ctx.fillStyle = style.color || 'black'; + ctx.textBaseline = 'top'; // Align to top of rect + + // Handle alignment - though for a text node range rect, the rect SHOULD already be aligned by layout + // So we can arguably just draw at x. + // However, range rects can be tricky. + + // Simple draw for now - rely on DOM layout for positioning + ctx.textAlign = 'left'; + + // Text nodes usually don't need wrapping if they are part of a laid-out line + // (the browser already wrapped them into lines, and if it spans lines, we might get a big rect) + // If it spans lines, this simple fillText might be issue, but for missing words like "Total", it's fine. + ctx.fillText(text, x, y); + } + /** * Render text content of an element * @@ -260,25 +336,25 @@ export class HtmlToImageConverter { _renderText(element, ctx, x, y, width, height, style) { const text = element.textContent.trim(); if (!text) return; - + // Set font const fontSize = parseFloat(style.fontSize) || 12; const fontWeight = style.fontWeight; const fontFamily = style.fontFamily.split(',')[0].replace(/['"]/g, '') || 'monospace'; const isBold = fontWeight === 'bold' || parseInt(fontWeight) >= 600; - + ctx.font = `${isBold ? 'bold' : 'normal'} ${fontSize}px ${fontFamily}`; ctx.fillStyle = style.color || 'black'; ctx.textBaseline = 'top'; - + // Handle text alignment const textAlign = style.textAlign || 'left'; const paddingLeft = parseFloat(style.paddingLeft || 0); const paddingRight = parseFloat(style.paddingRight || 0); const paddingTop = parseFloat(style.paddingTop || 0); - + let textX = x + paddingLeft; - + if (textAlign === 'center') { ctx.textAlign = 'center'; textX = x + width / 2; @@ -288,15 +364,15 @@ export class HtmlToImageConverter { } else { ctx.textAlign = 'left'; } - + const textY = y + paddingTop; - + // Word wrap if needed const maxWidth = width - paddingLeft - paddingRight; const lineHeight = parseFloat(style.lineHeight) || fontSize * 1.4; - + this._wrapAndDrawText(ctx, text, textX, textY, maxWidth, lineHeight); - + // Reset text align ctx.textAlign = 'left'; } @@ -316,11 +392,11 @@ export class HtmlToImageConverter { const words = text.split(' '); let line = ''; let currentY = y; - + for (let i = 0; i < words.length; i++) { const testLine = line + (line ? ' ' : '') + words[i]; const metrics = ctx.measureText(testLine); - + if (metrics.width > maxWidth && line) { ctx.fillText(line, x, currentY); line = words[i]; @@ -329,7 +405,7 @@ export class HtmlToImageConverter { line = testLine; } } - + if (line) { ctx.fillText(line, x, currentY); } @@ -351,7 +427,7 @@ export class HtmlToImageConverter { const borderRight = parseFloat(style.borderRightWidth) || 0; const borderBottom = parseFloat(style.borderBottomWidth) || 0; const borderLeft = parseFloat(style.borderLeftWidth) || 0; - + if (borderTop > 0) { ctx.strokeStyle = style.borderTopColor || 'black'; ctx.lineWidth = borderTop; @@ -360,7 +436,7 @@ export class HtmlToImageConverter { ctx.lineTo(x + width, y); ctx.stroke(); } - + if (borderRight > 0) { ctx.strokeStyle = style.borderRightColor || 'black'; ctx.lineWidth = borderRight; @@ -369,7 +445,7 @@ export class HtmlToImageConverter { ctx.lineTo(x + width, y + height); ctx.stroke(); } - + if (borderBottom > 0) { ctx.strokeStyle = style.borderBottomColor || 'black'; ctx.lineWidth = borderBottom; @@ -378,7 +454,7 @@ export class HtmlToImageConverter { ctx.lineTo(x + width, y + height); ctx.stroke(); } - + if (borderLeft > 0) { ctx.strokeStyle = style.borderLeftColor || 'black'; ctx.lineWidth = borderLeft; @@ -421,30 +497,30 @@ export class HtmlToImageConverter { * @param {number} height - Canvas height */ async _renderDomToCanvas(element, ctx, width, height) { - console.log('[HtmlToImage] Rendering DOM to canvas (simple fallback method)...'); - + + // Set default styles ctx.fillStyle = 'white'; ctx.fillRect(0, 0, width, height); ctx.fillStyle = 'black'; ctx.textBaseline = 'top'; ctx.font = '12px monospace'; - + const padding = 10; const lineHeight = 16; const maxWidth = width - (padding * 2); let y = padding; - + // Helper function to wrap text const wrapText = (text, maxWidth) => { const words = text.split(' '); const lines = []; let currentLine = ''; - + for (const word of words) { const testLine = currentLine + (currentLine ? ' ' : '') + word; const metrics = ctx.measureText(testLine); - + if (metrics.width > maxWidth && currentLine) { lines.push(currentLine); currentLine = word; @@ -452,36 +528,36 @@ export class HtmlToImageConverter { currentLine = testLine; } } - + if (currentLine) { lines.push(currentLine); } - + return lines; }; - + // Helper to draw text with alignment const drawText = (text, align = 'left', bold = false) => { if (!text || !text.trim()) return; - + ctx.font = `${bold ? 'bold' : 'normal'} 12px monospace`; const lines = wrapText(text, maxWidth); - + for (const line of lines) { let x = padding; const textWidth = ctx.measureText(line).width; - + if (align === 'center') { x = (width - textWidth) / 2; } else if (align === 'right') { x = width - textWidth - padding; } - + ctx.fillText(line, x, y); y += lineHeight; } }; - + // Helper to draw a line const drawLine = () => { ctx.beginPath(); @@ -492,40 +568,40 @@ export class HtmlToImageConverter { ctx.stroke(); y += lineHeight; }; - + // Extract and render content recursively const processElement = (el) => { if (!el) return; - + const style = window.getComputedStyle(el); const display = style.display; - + // Skip hidden elements if (display === 'none' || style.visibility === 'hidden') { return; } - + const tagName = el.tagName; const textAlign = style.textAlign || 'left'; const fontWeight = style.fontWeight; const isBold = fontWeight === 'bold' || parseInt(fontWeight) >= 600; - + // Handle special elements if (tagName === 'BR') { y += lineHeight; return; } - + if (tagName === 'HR') { drawLine(); return; } - + // Check if element has only text content (no child elements) const hasOnlyText = Array.from(el.childNodes).every( node => node.nodeType === Node.TEXT_NODE ); - + if (hasOnlyText) { const text = el.textContent.trim(); if (text) { @@ -544,17 +620,17 @@ export class HtmlToImageConverter { } } } - + // Add spacing after block elements if (display === 'block' || tagName === 'DIV' || tagName === 'P' || tagName === 'TABLE') { y += lineHeight / 2; } }; - + // Start processing try { processElement(element); - console.log('[HtmlToImage] Rendering complete, height used:', y); + } catch (error) { console.error('[HtmlToImage] Error during rendering:', error); // Ultimate fallback - just print all text @@ -574,55 +650,55 @@ export class HtmlToImageConverter { * @returns {Uint8Array} Bitmap data */ canvasToBitmap(canvas) { - console.log('[HtmlToImage] Converting canvas to bitmap (optimized)...'); + const startTime = performance.now(); - + const ctx = canvas.getContext('2d', { willReadFrequently: true }); const width = canvas.width; const height = canvas.height; - + // Get image data const imageData = ctx.getImageData(0, 0, width, height); const pixels = imageData.data; - + // Convert to monochrome bitmap // Each byte represents 8 pixels (1 bit per pixel) const bytesPerLine = Math.ceil(width / 8); const bitmapData = new Uint8Array(bytesPerLine * height); - + // Optimized conversion using lookup table and bitwise operations // Pre-calculate grayscale weights for faster conversion const rWeight = 0.299; const gWeight = 0.587; const bWeight = 0.114; - + let byteIndex = 0; let currentByte = 0; let bitPosition = 7; - + for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const pixelIndex = (y * width + x) * 4; - + // Fast grayscale conversion using weighted average - const gray = pixels[pixelIndex] * rWeight + - pixels[pixelIndex + 1] * gWeight + - pixels[pixelIndex + 2] * bWeight; - + const gray = pixels[pixelIndex] * rWeight + + pixels[pixelIndex + 1] * gWeight + + pixels[pixelIndex + 2] * bWeight; + // Threshold to black or white if (gray < 128) { currentByte |= (1 << bitPosition); } - + bitPosition--; - + if (bitPosition < 0) { bitmapData[byteIndex++] = currentByte; currentByte = 0; bitPosition = 7; } } - + // Handle remaining bits at end of line if (bitPosition !== 7) { bitmapData[byteIndex++] = currentByte; @@ -630,14 +706,10 @@ export class HtmlToImageConverter { bitPosition = 7; } } - + const endTime = performance.now(); - console.log('[HtmlToImage] Bitmap conversion took:', (endTime - startTime).toFixed(2), 'ms'); - console.log('[HtmlToImage] Bitmap dimensions:', width, 'pixels x', height, 'pixels'); - console.log('[HtmlToImage] Bytes per line:', bytesPerLine, 'bytes'); - console.log('[HtmlToImage] Total bitmap size:', bitmapData.length, 'bytes'); - console.log('[HtmlToImage] Expected size:', bytesPerLine * height, 'bytes'); - + + return { data: bitmapData, width: width, @@ -653,16 +725,16 @@ export class HtmlToImageConverter { * @returns {Promise} Bitmap data with dimensions */ async htmlToBitmap(element) { - console.log('[HtmlToImage] Converting HTML to bitmap...'); - + + try { // Convert HTML to canvas const canvas = await this.htmlToCanvas(element); - + // Convert canvas to bitmap const bitmap = this.canvasToBitmap(canvas); - - console.log('[HtmlToImage] Conversion complete'); + + return bitmap; } catch (error) { console.error('[HtmlToImage] Conversion failed:', error); @@ -679,26 +751,29 @@ export class HtmlToImageConverter { setPaperWidth(widthMm, dpi = 203) { this.paperWidthMm = widthMm; this.dpi = dpi; - + // Calculate dots per mm based on DPI // 203 DPI = 8 dots/mm (203 / 25.4) // 304 DPI = 12 dots/mm (304 / 25.4) const dotsPerMm = Math.round(dpi / 25.4); - - // Calculate pixel width based on paper size and DPI - // Use FULL width without margins for maximum usage + + // Calculate pixel width based on PRINTABLE width (essential for thermal printers) + // 58mm Paper -> ~48mm Printable + // 80mm Paper -> ~72mm Printable + let printableWidthMm; + if (widthMm === 58) { - // 58mm paper at selected DPI - this.paperWidth = widthMm * dotsPerMm; + printableWidthMm = 48; // Standard printable width for 58mm } else if (widthMm === 80) { - // 80mm paper at selected DPI - this.paperWidth = widthMm * dotsPerMm; + printableWidthMm = 72; // Standard printable width for 80mm } else { - // Custom width: use full width - this.paperWidth = widthMm * dotsPerMm; + // Custom width: subtract ~10mm margin + printableWidthMm = Math.max(widthMm - 10, widthMm * 0.8); } - - console.log('[HtmlToImage] Setting paper width to', widthMm, 'mm at', dpi, 'DPI (', dotsPerMm, 'dots/mm,', this.paperWidth, 'pixels)'); + + this.paperWidth = Math.floor(printableWidthMm * dotsPerMm); + + } } diff --git a/static/src/js/pos_receipt_printer.js b/static/src/js/pos_receipt_printer.js index 0f424ed..0666264 100644 --- a/static/src/js/pos_receipt_printer.js +++ b/static/src/js/pos_receipt_printer.js @@ -60,69 +60,63 @@ patch(PosPrinterService.prototype, { async printHtml(el) { // Wrap entire method in try-catch to catch any errors try { - console.log('[BluetoothPrint] printHtml() called'); - console.log('[BluetoothPrint] Element type:', el?.constructor?.name); - console.log('[BluetoothPrint] Element tag:', el?.tagName); - console.log('[BluetoothPrint] Element classes:', el?.className); - + + // Check if Web Bluetooth API is available if (!navigator.bluetooth) { - console.log('[BluetoothPrint] Web Bluetooth API not available, using browser print dialog'); + await this._printViaBrowserDialog(el); return true; } - console.log('[BluetoothPrint] Web Bluetooth API available'); - + + // Check if a Bluetooth printer is configured (from localStorage) // We don't need POS config - we check if user has configured a printer const storage = new BluetoothPrinterStorage(); const config = storage.loadConfiguration(1); // Default POS config ID - + if (!config || !config.deviceId) { - console.log('[BluetoothPrint] No Bluetooth printer configured, using standard print'); - console.log('[BluetoothPrint] Calling originalPrintHtml with:', el); + try { const result = await originalPrintHtml.call(this, el); - console.log('[BluetoothPrint] originalPrintHtml returned:', result); - + + // If original method returned false, it didn't handle the print // So we need to handle it ourselves if (result === false) { - console.log('[BluetoothPrint] originalPrintHtml returned false, opening browser print dialog directly'); + await this._printViaBrowserDialog(el); return true; } - + return result; } catch (error) { console.error('[BluetoothPrint] Error calling originalPrintHtml:', error); // Fallback to browser print dialog - console.log('[BluetoothPrint] Falling back to browser print dialog'); + await this._printViaBrowserDialog(el); return true; } } - - console.log('[BluetoothPrint] Bluetooth printer configured:', config.deviceName); + + // Initialize bluetooth services - console.log('[BluetoothPrint] Initializing bluetooth services...'); - const services = initializeBluetoothPrinting(null); - console.log('[BluetoothPrint] Bluetooth services initialized'); - + const services = initializeBluetoothPrinting(null); // Restored + // Check if printer is actually connected const connectionStatus = services.bluetoothManager.getConnectionStatus(); - console.log('[BluetoothPrint] Current connection status:', connectionStatus); - + + if (connectionStatus !== 'connected') { - console.log('[BluetoothPrint] Printer not connected, attempting to reconnect...'); + // Try to reconnect try { await services.bluetoothManager.autoReconnect(); - console.log('[BluetoothPrint] Reconnection successful'); + } catch (reconnectError) { console.error('[BluetoothPrint] Reconnection failed:', reconnectError); - console.log('[BluetoothPrint] Falling back to browser print dialog'); + await this._printViaBrowserDialog(el); return true; } @@ -130,17 +124,17 @@ patch(PosPrinterService.prototype, { try { // Attempt bluetooth printing - console.log('[BluetoothPrint] Attempting bluetooth print...'); + await this._printViaBluetoothFromHtml(el, services, config); - - console.log('[BluetoothPrint] Print completed successfully'); + + return true; } catch (printError) { console.error('[BluetoothPrint] Bluetooth print failed:', printError); console.error('[BluetoothPrint] Error stack:', printError.stack); - + // Fallback to browser print dialog - console.log('[BluetoothPrint] Falling back to browser print dialog after error'); + await this._printViaBrowserDialog(el); return true; } @@ -149,7 +143,7 @@ patch(PosPrinterService.prototype, { console.error('[BluetoothPrint] Error name:', error.name); console.error('[BluetoothPrint] Error message:', error.message); console.error('[BluetoothPrint] Error stack:', error.stack); - console.log('[BluetoothPrint] Falling back to browser print dialog due to critical error'); + try { await this._printViaBrowserDialog(el); return true; @@ -169,8 +163,8 @@ patch(PosPrinterService.prototype, { * @returns {Promise} */ async _printViaBrowserDialog(el) { - console.log('[BluetoothPrint] Opening browser print dialog...'); - + + return new Promise((resolve) => { // Create a hidden iframe for printing const printFrame = document.createElement('iframe'); @@ -181,32 +175,32 @@ patch(PosPrinterService.prototype, { printFrame.style.height = '0'; printFrame.style.border = '0'; document.body.appendChild(printFrame); - + let printTriggered = false; let timeoutId = null; - + const cleanup = () => { if (timeoutId) { clearTimeout(timeoutId); } if (printFrame.parentNode) { document.body.removeChild(printFrame); - console.log('[BluetoothPrint] Iframe cleaned up'); + } resolve(); }; - + const triggerPrint = () => { if (printTriggered) { return; } printTriggered = true; - + try { printFrame.contentWindow.focus(); printFrame.contentWindow.print(); - console.log('[BluetoothPrint] Print dialog opened'); - + + // Clean up after a delay setTimeout(cleanup, 1000); } catch (printError) { @@ -214,12 +208,12 @@ patch(PosPrinterService.prototype, { cleanup(); } }; - + try { // Write receipt content to iframe const frameDoc = printFrame.contentWindow.document; frameDoc.open(); - + // Add basic styling for print const printHtml = ` @@ -262,27 +256,27 @@ patch(PosPrinterService.prototype, { `; - + frameDoc.write(printHtml); frameDoc.close(); - - console.log('[BluetoothPrint] Receipt content written to iframe'); - + + + // Wait for content to load printFrame.contentWindow.addEventListener('load', () => { - console.log('[BluetoothPrint] Iframe loaded, triggering print...'); + // Small delay to ensure rendering is complete setTimeout(triggerPrint, 100); }); - + // Fallback if load event doesn't fire within 2 seconds timeoutId = setTimeout(() => { if (!printTriggered) { - console.log('[BluetoothPrint] Load timeout, attempting print anyway...'); + triggerPrint(); } }, 2000); - + } catch (error) { console.error('[BluetoothPrint] Error in _printViaBrowserDialog:', error); cleanup(); @@ -303,65 +297,46 @@ patch(PosPrinterService.prototype, { */ async _printViaBluetoothFromHtml(el, services, config) { const { bluetoothManager } = services; - - console.log('[BluetoothPrint] Starting bluetooth print from HTML...'); - console.log('[BluetoothPrint] Using GRAPHICS MODE for exact HTML layout'); - console.log('[BluetoothPrint] Element:', el); - console.log('[BluetoothPrint] Config:', config); - + + + // Check connection status const status = bluetoothManager.getConnectionStatus(); - console.log('[BluetoothPrint] Connection status:', status); - + + if (status !== 'connected') { throw new PrinterNotConnectedError('Bluetooth printer is not connected'); } try { // Convert HTML to bitmap image - console.log('[BluetoothPrint] Converting HTML to bitmap...'); - console.log('[BluetoothPrint] Creating HtmlToImageConverter...'); const converter = new HtmlToImageConverter(); - console.log('[BluetoothPrint] HtmlToImageConverter created successfully'); - + + // Get paper width and DPI from configuration + + // Get paper width and DPI from configuration const paperWidthMm = config?.settings?.paperWidthMm || 58; const dpi = config?.settings?.dpi || 203; - console.log('[BluetoothPrint] Using paper width:', paperWidthMm, 'mm'); - console.log('[BluetoothPrint] Using printer DPI:', dpi); - console.log('[BluetoothPrint] Setting paper width and DPI on converter...'); converter.setPaperWidth(paperWidthMm, dpi); - console.log('[BluetoothPrint] Paper width and DPI set successfully'); - - console.log('[BluetoothPrint] Converting HTML to bitmap...'); + const bitmap = await converter.htmlToBitmap(el); - console.log('[BluetoothPrint] Bitmap created:', bitmap.width, 'x', bitmap.height); - console.log('[BluetoothPrint] Bitmap data length:', bitmap.data.length); - + // Generate ESC/POS graphics commands - console.log('[BluetoothPrint] Generating ESC/POS graphics commands...'); - console.log('[BluetoothPrint] Creating EscPosGraphics...'); const graphicsGenerator = new EscPosGraphics(); - console.log('[BluetoothPrint] EscPosGraphics created successfully'); - - console.log('[BluetoothPrint] Generating bitmap commands...'); + const escposData = graphicsGenerator.generateBitmapCommands(bitmap); - console.log('[BluetoothPrint] Generated', escposData.length, 'bytes of graphics data'); - + // Send data to printer - console.log('[BluetoothPrint] Sending graphics to printer...'); - const startTime = performance.now(); await bluetoothManager.sendData(escposData); - const endTime = performance.now(); - console.log('[BluetoothPrint] Graphics print completed successfully in', (endTime - startTime).toFixed(0), 'ms'); - + } catch (error) { console.error('[BluetoothPrint] Graphics mode failed:', error); console.error('[BluetoothPrint] Error name:', error.name); console.error('[BluetoothPrint] Error message:', error.message); console.error('[BluetoothPrint] Error stack:', error.stack); - console.log('[BluetoothPrint] Falling back to text mode...'); - + + // Fallback to text mode if graphics fails await this._printViaBluetoothTextMode(el, services); } @@ -378,23 +353,17 @@ patch(PosPrinterService.prototype, { */ async _printViaBluetoothTextMode(el, services) { const { bluetoothManager, escposGenerator } = services; - - console.log('[BluetoothPrint] Using TEXT MODE (fallback)'); - + + + // Parse receipt data from HTML element - console.log('[BluetoothPrint] Parsing receipt data from HTML...'); const receiptData = this._parseReceiptDataFromHtml(el); - console.log('[BluetoothPrint] Parsed receipt data:', JSON.stringify(receiptData, null, 2)); - + // Generate ESC/POS commands - console.log('[BluetoothPrint] Generating ESC/POS text commands...'); const escposData = escposGenerator.generateReceipt(receiptData); - console.log('[BluetoothPrint] Generated', escposData.length, 'bytes of text data'); - + // Send data to printer - console.log('[BluetoothPrint] Sending text to printer...'); await bluetoothManager.sendData(escposData); - console.log('[BluetoothPrint] Text print completed successfully'); }, /** @@ -407,34 +376,25 @@ patch(PosPrinterService.prototype, { * @throws {Error} If printing fails */ async _printViaBluetooth(el, pos) { + const services = initializeBluetoothPrinting(); const { bluetoothManager, escposGenerator } = services; - - console.log('[BluetoothPrint] Starting bluetooth print...'); - console.log('[BluetoothPrint] Element:', el); - + // Check connection status const status = bluetoothManager.getConnectionStatus(); - console.log('[BluetoothPrint] Connection status:', status); - + if (status !== 'connected') { throw new PrinterNotConnectedError('Bluetooth printer is not connected'); } // Parse receipt data from POS order - console.log('[BluetoothPrint] Parsing receipt data...'); const receiptData = this._parseReceiptDataFromPos(pos); - console.log('[BluetoothPrint] Receipt data:', receiptData); - + // Generate ESC/POS commands - console.log('[BluetoothPrint] Generating ESC/POS commands...'); const escposData = escposGenerator.generateReceipt(receiptData); - console.log('[BluetoothPrint] Generated', escposData.length, 'bytes of ESC/POS data'); - + // Send data to printer - console.log('[BluetoothPrint] Sending to printer...'); await bluetoothManager.sendData(escposData); - console.log('[BluetoothPrint] Print completed successfully'); }, /** @@ -447,12 +407,12 @@ patch(PosPrinterService.prototype, { async _printViaFallback(receipt) { // Use the standard Odoo POS printing mechanism // This typically opens the browser print dialog - + // Create a hidden iframe for printing const printFrame = document.createElement('iframe'); printFrame.style.display = 'none'; document.body.appendChild(printFrame); - + try { // Write receipt content to iframe const frameDoc = printFrame.contentWindow.document; @@ -481,7 +441,7 @@ patch(PosPrinterService.prototype, { `); frameDoc.close(); - + // Wait for content to load await new Promise(resolve => { if (printFrame.contentWindow.document.readyState === 'complete') { @@ -490,10 +450,10 @@ patch(PosPrinterService.prototype, { printFrame.contentWindow.addEventListener('load', resolve); } }); - + // Trigger print dialog printFrame.contentWindow.print(); - + // Clean up after a delay setTimeout(() => { document.body.removeChild(printFrame); @@ -520,7 +480,7 @@ patch(PosPrinterService.prototype, { const timeoutPromise = new Promise((_, reject) => { setTimeout(() => reject(new TimeoutError(`Print operation timed out after ${timeoutMs}ms`)), timeoutMs); }); - + try { await Promise.race([sendFunction(), timeoutPromise]); } catch (error) { @@ -545,25 +505,19 @@ patch(PosPrinterService.prototype, { * @returns {Object} Structured receipt data */ _parseReceiptDataFromHtml(el) { - console.log('[BluetoothPrint] _parseReceiptDataFromHtml called'); - console.log('[BluetoothPrint] Receipt HTML structure:', el.outerHTML.substring(0, 500)); - console.log('[BluetoothPrint] Receipt classes:', el.className); - console.log('[BluetoothPrint] Receipt children count:', el.children.length); - + // Extract text content from HTML const getText = (selector) => { const element = el.querySelector(selector); const text = element ? element.textContent.trim() : ''; - console.log(`[BluetoothPrint] getText('${selector}'):`, text || '(empty)'); return text; }; - + const getAll = (selector) => { const elements = Array.from(el.querySelectorAll(selector)); - console.log(`[BluetoothPrint] getAll('${selector}'):`, elements.length, 'elements found'); return elements; }; - + // Parse header (company info) const headerData = { companyName: getText('.pos-receipt-company-name') || getText('h2') || getText('h3') || 'Receipt', @@ -571,7 +525,7 @@ patch(PosPrinterService.prototype, { phone: getText('.pos-receipt-phone') || '', taxId: getText('.pos-receipt-tax-id') || '' }; - + // Parse order info const orderData = { orderName: getText('.pos-receipt-order-name') || getText('.order-name') || getText('.pos-receipt-order-data') || '', @@ -579,9 +533,8 @@ patch(PosPrinterService.prototype, { cashier: getText('.pos-receipt-cashier') || getText('.cashier') || '', customer: getText('.pos-receipt-customer') || getText('.customer') || null }; - + // Parse order lines - try multiple selectors - console.log('[BluetoothPrint] Searching for order lines...'); let lineElements = getAll('.orderline'); if (lineElements.length === 0) { lineElements = getAll('.pos-receipt-orderline'); @@ -596,72 +549,66 @@ patch(PosPrinterService.prototype, { // Try to find any table rows lineElements = getAll('tbody tr'); } - - console.log('[BluetoothPrint] Found', lineElements.length, 'line elements'); - + const lines = lineElements.map((line, index) => { - console.log(`[BluetoothPrint] Processing line ${index}:`, line.outerHTML.substring(0, 200)); - + const productName = line.querySelector('.product-name, td:first-child, .pos-receipt-left-align')?.textContent.trim() || ''; const qtyText = line.querySelector('.qty, .quantity')?.textContent.trim() || '1'; const priceText = line.querySelector('.price, .price-unit')?.textContent.trim() || '0'; const totalText = line.querySelector('.price-total, .total, td:last-child, .pos-receipt-right-align')?.textContent.trim() || '0'; - - console.log(`[BluetoothPrint] Line ${index} raw data:`, { productName, qtyText, priceText, totalText }); - + + + // Parse numbers (remove currency symbols and commas) const parseNumber = (str) => { const cleaned = str.replace(/[^0-9.-]/g, ''); return parseFloat(cleaned) || 0; }; - + const parsedLine = { productName: productName, quantity: parseNumber(qtyText), price: parseNumber(priceText), total: parseNumber(totalText) }; - - console.log(`[BluetoothPrint] Line ${index} parsed:`, parsedLine); - + + + return parsedLine; }).filter(line => line.productName); // Filter out empty lines - - console.log('[BluetoothPrint] Parsed', lines.length, 'lines from HTML'); - console.log('[BluetoothPrint] All lines:', JSON.stringify(lines, null, 2)); - + + + // Parse totals - console.log('[BluetoothPrint] Parsing totals...'); + const totals = { subtotal: this._parseAmount(getText('.pos-receipt-subtotal, .subtotal')), tax: this._parseAmount(getText('.pos-receipt-tax, .tax')), discount: this._parseAmount(getText('.pos-receipt-discount, .discount')), total: this._parseAmount(getText('.pos-receipt-total, .total, .amount-total')) }; - - console.log('[BluetoothPrint] Parsed totals:', totals); - + + + // If totals not found in specific elements, calculate from lines if (totals.total === 0 && lines.length > 0) { - console.log('[BluetoothPrint] Total is 0, calculating from lines...'); totals.total = lines.reduce((sum, line) => sum + line.total, 0); totals.subtotal = totals.total; - console.log('[BluetoothPrint] Calculated totals:', totals); } - + // Parse payment info const paymentData = { method: getText('.pos-receipt-payment-method, .payment-method') || 'Cash', amount: this._parseAmount(getText('.pos-receipt-payment-amount, .payment-amount')) || totals.total, change: this._parseAmount(getText('.pos-receipt-change, .change')) || 0 }; - + // Footer const footerData = { message: getText('.pos-receipt-footer, .receipt-footer') || 'Thank you for your business!', barcode: orderData.orderName || null }; - + const receiptData = { headerData, orderData, @@ -670,8 +617,8 @@ patch(PosPrinterService.prototype, { paymentData, footerData }; - - console.log('[BluetoothPrint] Parsed receipt data from HTML:', receiptData); + + return receiptData; }, @@ -697,35 +644,28 @@ patch(PosPrinterService.prototype, { * @returns {Object} Structured receipt data */ _parseReceiptDataFromPos(pos) { - console.log('[BluetoothPrint] _parseReceiptDataFromPos called'); - console.log('[BluetoothPrint] POS object keys:', Object.keys(pos)); - + // In Odoo 18, get current order from POS // Try multiple ways to access the order let order = null; - + if (typeof pos.get_order === 'function') { order = pos.get_order(); - console.log('[BluetoothPrint] Got order via get_order()'); } else if (pos.selectedOrder) { order = pos.selectedOrder; - console.log('[BluetoothPrint] Got order via selectedOrder'); } else if (pos.orders && pos.orders.length > 0) { order = pos.orders[pos.orders.length - 1]; - console.log('[BluetoothPrint] Got order via orders array'); } - - console.log('[BluetoothPrint] Order:', order); - console.log('[BluetoothPrint] Order keys:', order ? Object.keys(order) : 'null'); - + + + if (!order) { throw new Error('No active order found'); } // Get company info const company = pos.company || {}; - console.log('[BluetoothPrint] Company:', company); - + // Get cashier info - try multiple ways let cashierName = ''; if (typeof pos.get_cashier === 'function') { @@ -735,8 +675,8 @@ patch(PosPrinterService.prototype, { } else if (pos.user) { cashierName = pos.user.name || ''; } - console.log('[BluetoothPrint] Cashier name:', cashierName); - + + // Get customer info - try multiple ways let customerName = null; if (typeof order.get_partner === 'function') { @@ -746,7 +686,7 @@ patch(PosPrinterService.prototype, { } else if (order.partner_id) { customerName = order.partner_id[1] || null; } - console.log('[BluetoothPrint] Customer name:', customerName); + // Build receipt data structure const receiptData = { @@ -771,7 +711,7 @@ patch(PosPrinterService.prototype, { } }; - console.log('[BluetoothPrint] Parsed receipt data:', receiptData); + return receiptData; }, @@ -784,19 +724,19 @@ patch(PosPrinterService.prototype, { */ _formatAddress(company) { const parts = []; - + if (company.street) parts.push(company.street); if (company.street2) parts.push(company.street2); - + const cityLine = []; if (company.zip) cityLine.push(company.zip); if (company.city) cityLine.push(company.city); if (cityLine.length > 0) parts.push(cityLine.join(' ')); - + if (company.country_id && company.country_id[1]) { parts.push(company.country_id[1]); } - + return parts.join(', '); }, @@ -820,12 +760,12 @@ patch(PosPrinterService.prototype, { */ _formatTotals(order) { console.log('[BluetoothPrint] Formatting totals...'); - + let subtotal = 0; let tax = 0; let discount = 0; let total = 0; - + // Try multiple ways to get totals if (typeof order.get_total_without_tax === 'function') { subtotal = order.get_total_without_tax(); @@ -846,9 +786,9 @@ patch(PosPrinterService.prototype, { }); subtotal = total; } - + console.log('[BluetoothPrint] Totals:', { subtotal, tax, discount, total }); - + return { subtotal: subtotal, tax: tax, @@ -884,18 +824,18 @@ patch(PosPrinterService.prototype, { */ _formatOrderLines(order) { console.log('[BluetoothPrint] Formatting order lines...'); - + const lines = this._getOrderLines(order); console.log('[BluetoothPrint] Found', lines.length, 'lines'); - + return lines.map((line, index) => { console.log(`[BluetoothPrint] Processing line ${index}:`, line); - + let productName = ''; let quantity = 0; let price = 0; let total = 0; - + // Get product name if (typeof line.get_product === 'function') { const product = line.get_product(); @@ -905,7 +845,7 @@ patch(PosPrinterService.prototype, { } else if (line.full_product_name) { productName = line.full_product_name; } - + // Get quantity if (typeof line.get_quantity === 'function') { quantity = line.get_quantity(); @@ -914,7 +854,7 @@ patch(PosPrinterService.prototype, { } else if (line.quantity !== undefined) { quantity = line.quantity; } - + // Get price if (typeof line.get_unit_price === 'function') { price = line.get_unit_price(); @@ -923,7 +863,7 @@ patch(PosPrinterService.prototype, { } else if (line.price !== undefined) { price = line.price; } - + // Get total if (typeof line.get_price_with_tax === 'function') { total = line.get_price_with_tax(); @@ -934,9 +874,9 @@ patch(PosPrinterService.prototype, { } else { total = quantity * price; } - + console.log(`[BluetoothPrint] Line ${index} formatted:`, { productName, quantity, price, total }); - + return { productName: productName, quantity: quantity, @@ -955,9 +895,9 @@ patch(PosPrinterService.prototype, { */ _formatPaymentData(order) { console.log('[BluetoothPrint] Formatting payment data...'); - + let paymentlines = []; - + // Get payment lines if (typeof order.get_paymentlines === 'function') { paymentlines = order.get_paymentlines(); @@ -966,9 +906,9 @@ patch(PosPrinterService.prototype, { } else if (order.payment_ids) { paymentlines = order.payment_ids; } - + console.log('[BluetoothPrint] Found', paymentlines.length, 'payment lines'); - + if (paymentlines.length === 0) { return { method: 'Unknown', @@ -979,7 +919,7 @@ patch(PosPrinterService.prototype, { // Use first payment method (or combine if multiple) const payment = paymentlines[0]; - + // Get payment method name let methodName = 'Cash'; if (payment.payment_method) { @@ -987,12 +927,12 @@ patch(PosPrinterService.prototype, { } else if (payment.name) { methodName = payment.name; } - + // Calculate total paid const totalPaid = paymentlines.reduce((sum, p) => { return sum + (p.amount || 0); }, 0); - + // Get order total let orderTotal = 0; if (typeof order.get_total_with_tax === 'function') { @@ -1000,9 +940,9 @@ patch(PosPrinterService.prototype, { } else if (order.amount_total !== undefined) { orderTotal = order.amount_total; } - + const change = Math.max(0, totalPaid - orderTotal); - + console.log('[BluetoothPrint] Payment data:', { methodName, totalPaid, orderTotal, change }); return { @@ -1038,7 +978,7 @@ patch(PosPrinterService.prototype, { */ _showFallbackNotification(error) { let message = 'Bluetooth printer unavailable. Receipt sent to default printer.'; - + if (error instanceof TimeoutError) { message = 'Bluetooth printer timeout. Receipt sent to default printer.'; } else if (error instanceof PrinterNotConnectedError) { @@ -1069,9 +1009,9 @@ patch(PosPrinterService.prototype, { errorMessage: error.message || 'Unknown error', stack: error.stack }; - + console.error('Print error details:', errorInfo); - + // Could send to server for logging if needed // this.env.services.rpc(...); } diff --git a/static/src/js/pos_session_integration.js b/static/src/js/pos_session_integration.js index 97cf9fc..0e2948a 100644 --- a/static/src/js/pos_session_integration.js +++ b/static/src/js/pos_session_integration.js @@ -22,7 +22,7 @@ patch(PosStore.prototype, { */ async setup() { await super.setup(...arguments); - + // Initialize bluetooth printing if enabled if (this.config.bluetooth_printer_enabled) { await this._initializeBluetoothPrinter(); @@ -36,14 +36,14 @@ patch(PosStore.prototype, { * @returns {Promise} */ async _initializeBluetoothPrinter() { - console.log('Initializing bluetooth printer for POS session...'); - + + try { const notificationService = this.env?.services?.notification || null; const errorService = getErrorNotificationService(notificationService); const services = getBluetoothPrintingServices(notificationService); const { bluetoothManager, storageManager } = services; - + // Check if Web Bluetooth API is available if (!bluetoothManager.isBluetoothAvailable()) { console.warn('Web Bluetooth API not available in this browser'); @@ -56,23 +56,23 @@ patch(PosStore.prototype, { // Load printer configuration from local storage const config = storageManager.loadConfiguration(this.config.id); - + if (!config) { // No printer configured for this device - console.log('No bluetooth printer configured for this device'); + // console.log('No bluetooth printer configured for this device'); this._promptPrinterConfiguration(); return; } - console.log('Found printer configuration:', config.deviceName); - + // console.log('Found printer configuration:', config.deviceName); + // Enable auto-reconnect based on saved settings const autoReconnect = config.settings?.autoReconnect !== false; bluetoothManager.setAutoReconnect(autoReconnect); - + // Attempt to connect to the configured printer await this._connectToConfiguredPrinter(config); - + } catch (error) { console.error('Failed to initialize bluetooth printer:', error); const errorService = getErrorNotificationService(); @@ -92,17 +92,17 @@ patch(PosStore.prototype, { const errorService = getErrorNotificationService(notificationService); const services = getBluetoothPrintingServices(notificationService); const { bluetoothManager } = services; - + try { - console.log('Attempting to connect to printer:', config.deviceName); - + // console.log('Attempting to connect to printer:', config.deviceName); + // Try to get the previously paired device const devices = await navigator.bluetooth.getDevices(); - const device = devices.find(d => - d.id === config.deviceId || + const device = devices.find(d => + d.id === config.deviceId || d.name === config.deviceName ); - + if (!device) { console.warn('Previously configured printer not found'); errorService.showNotification( @@ -115,13 +115,13 @@ patch(PosStore.prototype, { // Connect to the printer await bluetoothManager.connectToPrinter(device); - - console.log('Successfully connected to bluetooth printer'); + + // console.log('Successfully connected to bluetooth printer'); // Success notification is now handled by error service in bluetooth manager - + } catch (error) { console.error('Failed to connect to bluetooth printer:', error); - + // Error is now handled by error notification service in bluetooth manager // Just log additional context here errorService.logError(error, { @@ -139,13 +139,13 @@ patch(PosStore.prototype, { _promptPrinterConfiguration() { const notificationService = this.env?.services?.notification || null; const errorService = getErrorNotificationService(notificationService); - + // Show notification prompting user to configure printer errorService.showNotification( 'No bluetooth printer configured. Please configure a printer in POS settings.', 'info' ); - + // Could trigger opening the configuration dialog here // For now, we just notify the user }, @@ -160,7 +160,7 @@ patch(PosStore.prototype, { if (this.config.bluetooth_printer_enabled) { await this._cleanupBluetoothPrinter(); } - + // Call parent closePos return super.closePos(...arguments); }, @@ -172,19 +172,19 @@ patch(PosStore.prototype, { * @returns {Promise} */ async _cleanupBluetoothPrinter() { - console.log('Cleaning up bluetooth printer connection...'); - + // console.log('Cleaning up bluetooth printer connection...'); + try { const services = getBluetoothPrintingServices(); const { bluetoothManager } = services; - + // Check if there's an active connection const status = bluetoothManager.getConnectionStatus(); - + if (status === 'connected' || status === 'connecting') { - console.log('Disconnecting bluetooth printer...'); + // console.log('Disconnecting bluetooth printer...'); await bluetoothManager.disconnect(); - console.log('Bluetooth printer disconnected'); + // console.log('Bluetooth printer disconnected'); } } catch (error) { console.error('Error cleaning up bluetooth printer:', error); @@ -208,7 +208,7 @@ patch(PosStore.prototype, { try { const services = getBluetoothPrintingServices(); const { bluetoothManager } = services; - + return { enabled: true, ...bluetoothManager.getConnectionInfo() @@ -258,7 +258,7 @@ patch(PosStore.prototype, { sticky: false }); } else { - console.log(`[${type.toUpperCase()}] ${message}`); + // console.log(`[${type.toUpperCase()}] ${message}`); } } });