fix the width scaling error, fix the font scaling, removing debug logging

This commit is contained in:
Suherdy Yacob 2025-12-07 22:40:50 +07:00
parent 9483f369d7
commit 54729b1a51
5 changed files with 557 additions and 535 deletions

View File

@ -70,22 +70,22 @@ export class TimeoutError extends Error {
export class BluetoothPrinterManager { export class BluetoothPrinterManager {
constructor(errorNotificationService = null) { constructor(errorNotificationService = null) {
// Debug logging // Debug logging
this.debugMode = true; // Set to false to disable verbose logging this.debugMode = false; // Set to false to disable verbose logging
// Connection state // Connection state
this.device = null; this.device = null;
this.server = null; this.server = null;
this.service = null; this.service = null;
this.characteristic = null; this.characteristic = null;
this.connectionStatus = 'disconnected'; this.connectionStatus = 'disconnected';
// Reconnection state // Reconnection state
this.reconnectAttempts = 0; this.reconnectAttempts = 0;
this.maxReconnectAttempts = 3; this.maxReconnectAttempts = 3;
this.reconnectDelays = [1000, 2000, 4000]; // Exponential backoff in milliseconds this.reconnectDelays = [1000, 2000, 4000]; // Exponential backoff in milliseconds
this.isReconnecting = false; this.isReconnecting = false;
this.autoReconnectEnabled = true; this.autoReconnectEnabled = true;
// Event listeners // Event listeners
this.eventListeners = { this.eventListeners = {
'connection-status-changed': [], 'connection-status-changed': [],
@ -95,21 +95,21 @@ export class BluetoothPrinterManager {
'reconnection-success': [], 'reconnection-success': [],
'reconnection-failure': [] 'reconnection-failure': []
}; };
// Printer state // Printer state
this.lastError = null; this.lastError = null;
this.isPrinting = false; this.isPrinting = false;
// Error notification service // Error notification service
this.errorNotificationService = errorNotificationService; this.errorNotificationService = errorNotificationService;
// Bluetooth service UUID for serial port profile (commonly used by thermal printers) // Bluetooth service UUID for serial port profile (commonly used by thermal printers)
// Using the standard Serial Port Profile UUID (SPP) // Using the standard Serial Port Profile UUID (SPP)
// Most thermal printers use SPP for communication // Most thermal printers use SPP for communication
this.serviceUUID = '00001101-0000-1000-8000-00805f9b34fb'; // Serial Port Profile this.serviceUUID = '00001101-0000-1000-8000-00805f9b34fb'; // Serial Port Profile
this.characteristicUUID = '00002af1-0000-1000-8000-00805f9b34fb'; this.characteristicUUID = '00002af1-0000-1000-8000-00805f9b34fb';
} }
/** /**
* Set error notification service * Set error notification service
* @param {Object} errorNotificationService - Error notification service instance * @param {Object} errorNotificationService - Error notification service instance
@ -139,8 +139,8 @@ export class BluetoothPrinterManager {
* @returns {boolean} True if available * @returns {boolean} True if available
*/ */
isBluetoothAvailable() { isBluetoothAvailable() {
const available = typeof navigator !== 'undefined' && const available = typeof navigator !== 'undefined' &&
navigator.bluetooth !== undefined; navigator.bluetooth !== undefined;
this._log(`Bluetooth API available: ${available}`); this._log(`Bluetooth API available: ${available}`);
return available; return available;
} }
@ -162,7 +162,7 @@ export class BluetoothPrinterManager {
try { try {
this._log('Starting device scan...'); this._log('Starting device scan...');
// Request bluetooth device with filters for thermal printers // Request bluetooth device with filters for thermal printers
// Many thermal printers advertise with specific name patterns // Many thermal printers advertise with specific name patterns
const device = await navigator.bluetooth.requestDevice({ const device = await navigator.bluetooth.requestDevice({
@ -190,7 +190,7 @@ export class BluetoothPrinterManager {
return [device]; return [device];
} catch (error) { } catch (error) {
this._log('Filtered scan failed, trying acceptAllDevices fallback', error.name); this._log('Filtered scan failed, trying acceptAllDevices fallback', error.name);
// If filtered search fails, try acceptAllDevices as fallback // If filtered search fails, try acceptAllDevices as fallback
if (error.name === 'NotFoundError') { if (error.name === 'NotFoundError') {
try { try {
@ -219,7 +219,7 @@ export class BluetoothPrinterManager {
} }
throw cancelError; throw cancelError;
} }
// Log unexpected errors // Log unexpected errors
if (this.errorNotificationService) { if (this.errorNotificationService) {
this.errorNotificationService.logError(error, { operation: 'scanDevices' }); this.errorNotificationService.logError(error, { operation: 'scanDevices' });
@ -247,7 +247,7 @@ export class BluetoothPrinterManager {
try { try {
this._log('Attempting to connect to printer...', deviceId); this._log('Attempting to connect to printer...', deviceId);
this._setConnectionStatus('connecting'); this._setConnectionStatus('connecting');
// If deviceId is actually a device object from scanDevices, use it directly // If deviceId is actually a device object from scanDevices, use it directly
let device; let device;
if (typeof deviceId === 'object' && deviceId.gatt) { if (typeof deviceId === 'object' && deviceId.gatt) {
@ -259,15 +259,15 @@ export class BluetoothPrinterManager {
const devices = await navigator.bluetooth.getDevices(); const devices = await navigator.bluetooth.getDevices();
this._log(`Found ${devices.length} previously paired devices`); this._log(`Found ${devices.length} previously paired devices`);
device = devices.find(d => d.id === deviceId || d.name === deviceId); device = devices.find(d => d.id === deviceId || d.name === deviceId);
if (!device) { if (!device) {
this._log('Device not found in paired devices'); this._log('Device not found in paired devices');
const error = new DeviceNotFoundError(`Device ${deviceId} not found`); const error = new DeviceNotFoundError(`Device ${deviceId} not found`);
if (this.errorNotificationService) { if (this.errorNotificationService) {
this.errorNotificationService.handleError(error, { this.errorNotificationService.handleError(error, {
operation: 'connectToPrinter', operation: 'connectToPrinter',
deviceId: deviceId deviceId: deviceId
}); });
} }
throw error; throw error;
@ -276,7 +276,7 @@ export class BluetoothPrinterManager {
this.device = device; this.device = device;
this._log('Device selected', { name: this.device.name, id: this.device.id }); this._log('Device selected', { name: this.device.name, id: this.device.id });
// Set up disconnect handler // Set up disconnect handler
this.device.addEventListener('gattserverdisconnected', () => { this.device.addEventListener('gattserverdisconnected', () => {
this._log('GATT server disconnected event fired'); this._log('GATT server disconnected event fired');
@ -287,7 +287,7 @@ export class BluetoothPrinterManager {
this._log('Connecting to GATT server...'); this._log('Connecting to GATT server...');
this.server = await this.device.gatt.connect(); this.server = await this.device.gatt.connect();
this._log('GATT server connected successfully'); this._log('GATT server connected successfully');
// Try to get the printer service - try multiple common UUIDs // Try to get the printer service - try multiple common UUIDs
const serviceUUIDs = [ const serviceUUIDs = [
this.serviceUUID, // Serial Port Profile this.serviceUUID, // Serial Port Profile
@ -296,7 +296,7 @@ export class BluetoothPrinterManager {
'0000ffe0-0000-1000-8000-00805f9b34fb', // Common serial service '0000ffe0-0000-1000-8000-00805f9b34fb', // Common serial service
'6e400001-b5a3-f393-e0a9-e50e24dcca9e', // Nordic UART Service '6e400001-b5a3-f393-e0a9-e50e24dcca9e', // Nordic UART Service
]; ];
const characteristicUUIDs = [ const characteristicUUIDs = [
this.characteristicUUID, // Default characteristic this.characteristicUUID, // Default characteristic
'00002af1-0000-1000-8000-00805f9b34fb', // Write characteristic '00002af1-0000-1000-8000-00805f9b34fb', // Write characteristic
@ -304,19 +304,19 @@ export class BluetoothPrinterManager {
'0000ffe1-0000-1000-8000-00805f9b34fb', // Common write characteristic '0000ffe1-0000-1000-8000-00805f9b34fb', // Common write characteristic
'6e400002-b5a3-f393-e0a9-e50e24dcca9e', // Nordic UART TX '6e400002-b5a3-f393-e0a9-e50e24dcca9e', // Nordic UART TX
]; ];
let serviceFound = false; let serviceFound = false;
for (const serviceUUID of serviceUUIDs) { for (const serviceUUID of serviceUUIDs) {
try { try {
console.log(`Trying service UUID: ${serviceUUID}`); this._log(`Trying service UUID: ${serviceUUID}`);
this.service = await this.server.getPrimaryService(serviceUUID); this.service = await this.server.getPrimaryService(serviceUUID);
// Try to find a writable characteristic // Try to find a writable characteristic
for (const charUUID of characteristicUUIDs) { for (const charUUID of characteristicUUIDs) {
try { try {
this.characteristic = await this.service.getCharacteristic(charUUID); this.characteristic = await this.service.getCharacteristic(charUUID);
console.log(`Found characteristic: ${charUUID}`); this._log(`Found characteristic: ${charUUID}`);
serviceFound = true; serviceFound = true;
break; break;
} catch (charError) { } catch (charError) {
@ -324,7 +324,7 @@ export class BluetoothPrinterManager {
continue; continue;
} }
} }
if (serviceFound) { if (serviceFound) {
break; break;
} }
@ -333,10 +333,10 @@ export class BluetoothPrinterManager {
continue; continue;
} }
} }
if (!serviceFound) { if (!serviceFound) {
console.warn('Standard printer services not found, attempting to use any available writable characteristic'); console.warn('Standard printer services not found, attempting to use any available writable characteristic');
// Last resort: try to find any writable characteristic // Last resort: try to find any writable characteristic
try { try {
const services = await this.server.getPrimaryServices(); const services = await this.server.getPrimaryServices();
@ -346,7 +346,7 @@ export class BluetoothPrinterManager {
if (char.properties.write || char.properties.writeWithoutResponse) { if (char.properties.write || char.properties.writeWithoutResponse) {
this.service = service; this.service = service;
this.characteristic = char; this.characteristic = char;
console.log(`Using fallback characteristic: ${char.uuid}`); this._log(`Using fallback characteristic: ${char.uuid}`);
serviceFound = true; serviceFound = true;
break; break;
} }
@ -356,9 +356,9 @@ export class BluetoothPrinterManager {
} catch (fallbackError) { } catch (fallbackError) {
console.error('Failed to find any writable characteristic:', fallbackError); console.error('Failed to find any writable characteristic:', fallbackError);
if (this.errorNotificationService) { if (this.errorNotificationService) {
this.errorNotificationService.logError(fallbackError, { this.errorNotificationService.logError(fallbackError, {
operation: 'findWritableCharacteristic', operation: 'findWritableCharacteristic',
deviceName: this.device.name deviceName: this.device.name
}); });
} }
} }
@ -376,16 +376,16 @@ export class BluetoothPrinterManager {
} catch (error) { } catch (error) {
this._setConnectionStatus('error'); this._setConnectionStatus('error');
this.lastError = error.message; this.lastError = error.message;
if (error instanceof DeviceNotFoundError) { if (error instanceof DeviceNotFoundError) {
throw error; throw error;
} }
const connectionError = new ConnectionFailedError(`Failed to connect: ${error.message}`); const connectionError = new ConnectionFailedError(`Failed to connect: ${error.message}`);
if (this.errorNotificationService) { if (this.errorNotificationService) {
this.errorNotificationService.handleError(connectionError, { this.errorNotificationService.handleError(connectionError, {
operation: 'connectToPrinter', operation: 'connectToPrinter',
originalError: error.message originalError: error.message
}); });
} }
throw connectionError; throw connectionError;
@ -398,7 +398,7 @@ export class BluetoothPrinterManager {
*/ */
async disconnect() { async disconnect() {
this.autoReconnectEnabled = false; this.autoReconnectEnabled = false;
if (this.server && this.server.connected) { if (this.server && this.server.connected) {
try { try {
await this.server.disconnect(); await this.server.disconnect();
@ -411,7 +411,7 @@ export class BluetoothPrinterManager {
this.server = null; this.server = null;
this.service = null; this.service = null;
this.characteristic = null; this.characteristic = null;
this._setConnectionStatus('disconnected'); this._setConnectionStatus('disconnected');
this.lastError = null; this.lastError = null;
} }
@ -444,9 +444,9 @@ export class BluetoothPrinterManager {
if (!this.characteristic) { if (!this.characteristic) {
const error = new TransmissionError('Printer characteristic not available'); const error = new TransmissionError('Printer characteristic not available');
if (this.errorNotificationService) { if (this.errorNotificationService) {
this.errorNotificationService.handleError(error, { this.errorNotificationService.handleError(error, {
operation: 'sendData', operation: 'sendData',
reason: 'characteristic_unavailable' reason: 'characteristic_unavailable'
}); });
} }
throw error; throw error;
@ -455,36 +455,36 @@ export class BluetoothPrinterManager {
try { try {
this.isPrinting = true; this.isPrinting = true;
const startTime = performance.now(); const startTime = performance.now();
// OPTIMIZED: Use larger chunks for graphics data (faster transmission) // OPTIMIZED: Use larger chunks for graphics data (faster transmission)
// Graphics data can handle larger chunks than text commands // Graphics data can handle larger chunks than text commands
const isLargeData = escposData.length > 1000; const isLargeData = escposData.length > 1000;
const chunkSize = isLargeData ? 512 : 20; // Much larger chunks for graphics const chunkSize = isLargeData ? 512 : 20; // Much larger chunks for graphics
const chunks = []; const chunks = [];
for (let i = 0; i < escposData.length; i += chunkSize) { for (let i = 0; i < escposData.length; i += chunkSize) {
chunks.push(escposData.slice(i, 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 // Determine write method based on characteristic properties
const useWriteWithoutResponse = this.characteristic.properties.writeWithoutResponse; const useWriteWithoutResponse = this.characteristic.properties.writeWithoutResponse;
const useWrite = this.characteristic.properties.write; const useWrite = this.characteristic.properties.write;
if (!useWrite && !useWriteWithoutResponse) { if (!useWrite && !useWriteWithoutResponse) {
throw new Error('Characteristic does not support write operations'); throw new Error('Characteristic does not support write operations');
} }
// OPTIMIZED: Reduce delays for faster transmission // OPTIMIZED: Reduce delays for faster transmission
const delay = isLargeData ? const delay = isLargeData ?
(useWriteWithoutResponse ? 10 : 5) : // Much shorter delays for graphics (useWriteWithoutResponse ? 10 : 5) : // Much shorter delays for graphics
(useWriteWithoutResponse ? 50 : 25); // Normal delays for text (useWriteWithoutResponse ? 50 : 25); // Normal delays for text
// Send each chunk // Send each chunk
for (let i = 0; i < chunks.length; i++) { for (let i = 0; i < chunks.length; i++) {
const chunk = chunks[i]; const chunk = chunks[i];
try { try {
if (useWriteWithoutResponse) { if (useWriteWithoutResponse) {
// Faster but no acknowledgment // Faster but no acknowledgment
@ -493,16 +493,16 @@ export class BluetoothPrinterManager {
// Slower but with acknowledgment // Slower but with acknowledgment
await this.characteristic.writeValue(chunk); await this.characteristic.writeValue(chunk);
} }
// OPTIMIZED: Minimal delay between chunks // OPTIMIZED: Minimal delay between chunks
if (delay > 0) { if (delay > 0) {
await this._sleep(delay); await this._sleep(delay);
} }
// Progress logging every 20% // Progress logging every 20%
if (i % Math.ceil(chunks.length / 5) === 0) { if (i % Math.ceil(chunks.length / 5) === 0) {
const progress = Math.round((i / chunks.length) * 100); const progress = Math.round((i / chunks.length) * 100);
console.log(`[Bluetooth] Progress: ${progress}%`); this._log(`Progress: ${progress}%`);
} }
} catch (chunkError) { } catch (chunkError) {
console.error(`Failed to send chunk ${i + 1}/${chunks.length}:`, chunkError); console.error(`Failed to send chunk ${i + 1}/${chunks.length}:`, chunkError);
@ -513,22 +513,22 @@ export class BluetoothPrinterManager {
const endTime = performance.now(); const endTime = performance.now();
const duration = ((endTime - startTime) / 1000).toFixed(2); const duration = ((endTime - startTime) / 1000).toFixed(2);
const speed = (escposData.length / 1024 / (duration || 1)).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.isPrinting = false;
this._emit('print-completed', { success: true }); this._emit('print-completed', { success: true });
return true; return true;
} catch (error) { } catch (error) {
this.isPrinting = false; this.isPrinting = false;
this._emit('print-failed', { error: error.message }); this._emit('print-failed', { error: error.message });
const transmissionError = new TransmissionError(`Failed to send data: ${error.message}`); const transmissionError = new TransmissionError(`Failed to send data: ${error.message}`);
if (this.errorNotificationService) { if (this.errorNotificationService) {
this.errorNotificationService.handleError(transmissionError, { this.errorNotificationService.handleError(transmissionError, {
operation: 'sendData', operation: 'sendData',
dataSize: escposData.length, dataSize: escposData.length,
originalError: error.message originalError: error.message
}); });
} }
throw transmissionError; throw transmissionError;
@ -578,13 +578,13 @@ export class BluetoothPrinterManager {
for (let attempt = 0; attempt < this.maxReconnectAttempts; attempt++) { for (let attempt = 0; attempt < this.maxReconnectAttempts; attempt++) {
this.reconnectAttempts = attempt + 1; this.reconnectAttempts = attempt + 1;
// Emit reconnection attempt event // Emit reconnection attempt event
this._emit('reconnection-attempt', { this._emit('reconnection-attempt', {
attempt: this.reconnectAttempts, attempt: this.reconnectAttempts,
maxAttempts: this.maxReconnectAttempts maxAttempts: this.maxReconnectAttempts
}); });
// Notify via error service // Notify via error service
if (this.errorNotificationService) { if (this.errorNotificationService) {
this.errorNotificationService.handleReconnectionAttempt( this.errorNotificationService.handleReconnectionAttempt(
@ -592,32 +592,32 @@ export class BluetoothPrinterManager {
this.maxReconnectAttempts this.maxReconnectAttempts
); );
} }
try { try {
console.log(`Reconnection attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts}`); this._log(`Reconnection attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts}`);
// Try to reconnect // Try to reconnect
await this.connectToPrinter(this.device); await this.connectToPrinter(this.device);
this.isReconnecting = false; this.isReconnecting = false;
this.reconnectAttempts = 0; this.reconnectAttempts = 0;
console.log('Reconnection successful'); this._log('Reconnection successful');
// Emit reconnection success event // Emit reconnection success event
this._emit('reconnection-success', { this._emit('reconnection-success', {
deviceName: this.device.name deviceName: this.device.name
}); });
// Notify via error service // Notify via error service
if (this.errorNotificationService) { if (this.errorNotificationService) {
this.errorNotificationService.handleReconnectionSuccess(this.device.name); this.errorNotificationService.handleReconnectionSuccess(this.device.name);
} }
return true; return true;
} catch (error) { } catch (error) {
console.error(`Reconnection attempt ${this.reconnectAttempts} failed:`, error); console.error(`Reconnection attempt ${this.reconnectAttempts} failed:`, error);
// Log the error // Log the error
if (this.errorNotificationService) { if (this.errorNotificationService) {
this.errorNotificationService.logError(error, { this.errorNotificationService.logError(error, {
@ -626,11 +626,11 @@ export class BluetoothPrinterManager {
maxAttempts: this.maxReconnectAttempts maxAttempts: this.maxReconnectAttempts
}); });
} }
// Wait before next attempt (exponential backoff) // Wait before next attempt (exponential backoff)
if (attempt < this.maxReconnectAttempts - 1) { if (attempt < this.maxReconnectAttempts - 1) {
const delay = this.reconnectDelays[attempt]; 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); await this._sleep(delay);
} }
} }
@ -640,19 +640,19 @@ export class BluetoothPrinterManager {
this.isReconnecting = false; this.isReconnecting = false;
this._setConnectionStatus('error'); this._setConnectionStatus('error');
this.lastError = 'Reconnection failed after maximum attempts'; this.lastError = 'Reconnection failed after maximum attempts';
console.error('All reconnection attempts failed'); console.error('All reconnection attempts failed');
// Emit reconnection failure event // Emit reconnection failure event
this._emit('reconnection-failure', { this._emit('reconnection-failure', {
attempts: this.maxReconnectAttempts attempts: this.maxReconnectAttempts
}); });
// Notify via error service // Notify via error service
if (this.errorNotificationService) { if (this.errorNotificationService) {
this.errorNotificationService.handleReconnectionFailure(); this.errorNotificationService.handleReconnectionFailure();
} }
return false; return false;
} }
@ -694,12 +694,12 @@ export class BluetoothPrinterManager {
* @private * @private
*/ */
_onDisconnected() { _onDisconnected() {
console.log('Bluetooth device disconnected'); this._log('Bluetooth device disconnected');
this._setConnectionStatus('disconnected'); this._setConnectionStatus('disconnected');
// Attempt auto-reconnection if enabled // Attempt auto-reconnection if enabled
if (this.autoReconnectEnabled) { if (this.autoReconnectEnabled) {
console.log('Starting auto-reconnection...'); this._log('Starting auto-reconnection...');
this.autoReconnect().catch(error => { this.autoReconnect().catch(error => {
console.error('Auto-reconnection failed:', error); console.error('Auto-reconnection failed:', error);
}); });
@ -714,7 +714,7 @@ export class BluetoothPrinterManager {
_setConnectionStatus(status) { _setConnectionStatus(status) {
const oldStatus = this.connectionStatus; const oldStatus = this.connectionStatus;
this.connectionStatus = status; this.connectionStatus = status;
if (oldStatus !== status) { if (oldStatus !== status) {
const statusData = { const statusData = {
oldStatus, oldStatus,
@ -722,9 +722,9 @@ export class BluetoothPrinterManager {
deviceName: this.device ? this.device.name : null, deviceName: this.device ? this.device.name : null,
timestamp: Date.now() timestamp: Date.now()
}; };
this._emit('connection-status-changed', statusData); this._emit('connection-status-changed', statusData);
// Notify error service about status change // Notify error service about status change
if (this.errorNotificationService) { if (this.errorNotificationService) {
this.errorNotificationService.handleStatusChange(statusData); this.errorNotificationService.handleStatusChange(statusData);

View File

@ -35,34 +35,39 @@ export class EscPosGraphics {
* @returns {Uint8Array} Complete ESC/POS command sequence * @returns {Uint8Array} Complete ESC/POS command sequence
*/ */
generateBitmapCommands(bitmap) { generateBitmapCommands(bitmap) {
console.log('[EscPosGraphics] Generating bitmap commands (optimized)...');
console.log('[EscPosGraphics] Original dimensions:', bitmap.width, 'x', bitmap.height);
const startTime = performance.now(); const startTime = performance.now();
// OPTIMIZATION: Remove blank lines from top and bottom // OPTIMIZATION: Remove blank lines from top and bottom
const optimizedBitmap = this._removeBlankLines(bitmap); 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 // Initialize printer
commands.push(...this.initialize()); const initCmd = this.initialize();
// Print bitmap using raster graphics mode // Print bitmap using raster graphics mode
const rasterCommands = this._generateRasterGraphics(optimizedBitmap); const rasterCommands = this._generateRasterGraphics(optimizedBitmap);
commands.push(...rasterCommands);
// Feed paper and cut // Feed paper and cut
commands.push(...this._feedAndCut(4)); const feedCmd = this._feedAndCut(4);
const result = new Uint8Array(commands); // 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(); 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; return result;
} }
@ -75,14 +80,14 @@ export class EscPosGraphics {
*/ */
_removeBlankLines(bitmap) { _removeBlankLines(bitmap) {
const { data, width, height, bytesPerLine } = bitmap; const { data, width, height, bytesPerLine } = bitmap;
// Find first non-blank line from top // Find first non-blank line from top
let firstLine = 0; let firstLine = 0;
for (let y = 0; y < height; y++) { for (let y = 0; y < height; y++) {
const lineStart = y * bytesPerLine; const lineStart = y * bytesPerLine;
const lineEnd = lineStart + bytesPerLine; const lineEnd = lineStart + bytesPerLine;
const lineData = data.slice(lineStart, lineEnd); const lineData = data.slice(lineStart, lineEnd);
// Check if line has any black pixels // Check if line has any black pixels
const hasContent = lineData.some(byte => byte !== 0); const hasContent = lineData.some(byte => byte !== 0);
if (hasContent) { if (hasContent) {
@ -90,14 +95,14 @@ export class EscPosGraphics {
break; break;
} }
} }
// Find last non-blank line from bottom // Find last non-blank line from bottom
let lastLine = height - 1; let lastLine = height - 1;
for (let y = height - 1; y >= firstLine; y--) { for (let y = height - 1; y >= firstLine; y--) {
const lineStart = y * bytesPerLine; const lineStart = y * bytesPerLine;
const lineEnd = lineStart + bytesPerLine; const lineEnd = lineStart + bytesPerLine;
const lineData = data.slice(lineStart, lineEnd); const lineData = data.slice(lineStart, lineEnd);
// Check if line has any black pixels // Check if line has any black pixels
const hasContent = lineData.some(byte => byte !== 0); const hasContent = lineData.some(byte => byte !== 0);
if (hasContent) { if (hasContent) {
@ -105,18 +110,18 @@ export class EscPosGraphics {
break; break;
} }
} }
// Extract only the content lines // Extract only the content lines
const newHeight = lastLine - firstLine + 1; const newHeight = lastLine - firstLine + 1;
const newData = new Uint8Array(bytesPerLine * newHeight); const newData = new Uint8Array(bytesPerLine * newHeight);
for (let y = 0; y < newHeight; y++) { for (let y = 0; y < newHeight; y++) {
const srcStart = (firstLine + y) * bytesPerLine; const srcStart = (firstLine + y) * bytesPerLine;
const srcEnd = srcStart + bytesPerLine; const srcEnd = srcStart + bytesPerLine;
const dstStart = y * bytesPerLine; const dstStart = y * bytesPerLine;
newData.set(data.slice(srcStart, srcEnd), dstStart); newData.set(data.slice(srcStart, srcEnd), dstStart);
} }
return { return {
data: newData, data: newData,
width: width, width: width,
@ -134,37 +139,39 @@ export class EscPosGraphics {
* @returns {Array} Command bytes * @returns {Array} Command bytes
*/ */
_generateRasterGraphics(bitmap) { _generateRasterGraphics(bitmap) {
const commands = [];
const { data, width, height, bytesPerLine } = bitmap; const { data, width, height, bytesPerLine } = bitmap;
// Calculate dimensions // Calculate dimensions
const widthBytes = bytesPerLine; const widthBytes = bytesPerLine;
const widthLow = widthBytes & 0xFF; const widthLow = widthBytes & 0xFF;
const widthHigh = (widthBytes >> 8) & 0xFF; const widthHigh = (widthBytes >> 8) & 0xFF;
const heightLow = height & 0xFF; const heightLow = height & 0xFF;
const heightHigh = (height >> 8) & 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 // GS v 0 - Print raster bitmap
// Format: GS v 0 m xL xH yL yH d1...dk // Format: GS v 0 m xL xH yL yH d1...dk
// m = mode (0 = normal, 1 = double width, 2 = double height, 3 = quadruple) // Header length: 8 bytes
commands.push(GS, 0x76, 0x30, 0x00); // GS v 0 m (m=0 for normal) const header = [
commands.push(widthLow, widthHigh); // xL xH (width in bytes) GS, 0x76, 0x30, 0x00, // GS v 0 m (m=0 for normal)
commands.push(heightLow, heightHigh); // yL yH (height in dots) widthLow, widthHigh, // xL xH (width in bytes)
heightLow, heightHigh // yL yH (height in dots)
// Add bitmap data ];
commands.push(...data);
// Create Uint8Array for the complete command
// Add line feed after image const totalLength = header.length + data.length + 1; // +1 for LF
commands.push(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; return commands;
} }
@ -179,7 +186,7 @@ export class EscPosGraphics {
_generateBitImageCommands(bitmap) { _generateBitImageCommands(bitmap) {
const commands = []; const commands = [];
const { data, width, height, bytesPerLine } = bitmap; const { data, width, height, bytesPerLine } = bitmap;
// Print line by line using ESC * command // Print line by line using ESC * command
for (let y = 0; y < height; y++) { for (let y = 0; y < height; y++) {
// ESC * m nL nH d1...dk // ESC * m nL nH d1...dk
@ -187,18 +194,18 @@ export class EscPosGraphics {
const mode = 33; const mode = 33;
const nL = width & 0xFF; const nL = width & 0xFF;
const nH = (width >> 8) & 0xFF; const nH = (width >> 8) & 0xFF;
commands.push(ESC, 0x2A, mode, nL, nH); commands.push(ESC, 0x2A, mode, nL, nH);
// Add line data // Add line data
const lineStart = y * bytesPerLine; const lineStart = y * bytesPerLine;
const lineEnd = lineStart + bytesPerLine; const lineEnd = lineStart + bytesPerLine;
commands.push(...data.slice(lineStart, lineEnd)); commands.push(...data.slice(lineStart, lineEnd));
// Line feed // Line feed
commands.push(LF); commands.push(LF);
} }
return commands; return commands;
} }
@ -211,16 +218,16 @@ export class EscPosGraphics {
*/ */
_feedAndCut(lines = 3) { _feedAndCut(lines = 3) {
const commands = []; const commands = [];
// Feed lines // Feed lines
for (let i = 0; i < lines; i++) { for (let i = 0; i < lines; i++) {
commands.push(LF); commands.push(LF);
} }
// Cut paper (GS V m) // Cut paper (GS V m)
// m = 0 (full cut), 1 (partial cut) // m = 0 (full cut), 1 (partial cut)
commands.push(GS, 0x56, 0x00); commands.push(GS, 0x56, 0x00);
return commands; return commands;
} }
@ -234,13 +241,13 @@ export class EscPosGraphics {
*/ */
splitIntoChunks(commands, chunkSize = 1024) { splitIntoChunks(commands, chunkSize = 1024) {
const chunks = []; const chunks = [];
for (let i = 0; i < commands.length; i += chunkSize) { for (let i = 0; i < commands.length; i += chunkSize) {
const chunk = commands.slice(i, i + chunkSize); const chunk = commands.slice(i, i + chunkSize);
chunks.push(chunk); chunks.push(chunk);
} }
console.log('[EscPosGraphics] Split into', chunks.length, 'chunks');
return chunks; return chunks;
} }
@ -254,12 +261,12 @@ export class EscPosGraphics {
const height = 200; const height = 200;
const bytesPerLine = Math.ceil(width / 8); const bytesPerLine = Math.ceil(width / 8);
const data = new Uint8Array(bytesPerLine * height); const data = new Uint8Array(bytesPerLine * height);
// Create a test pattern (checkerboard) // Create a test pattern (checkerboard)
for (let y = 0; y < height; y++) { for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) { for (let x = 0; x < width; x++) {
const isBlack = ((Math.floor(x / 8) + Math.floor(y / 8)) % 2) === 0; const isBlack = ((Math.floor(x / 8) + Math.floor(y / 8)) % 2) === 0;
if (isBlack) { if (isBlack) {
const byteIndex = y * bytesPerLine + Math.floor(x / 8); const byteIndex = y * bytesPerLine + Math.floor(x / 8);
const bitIndex = 7 - (x % 8); const bitIndex = 7 - (x % 8);
@ -267,14 +274,14 @@ export class EscPosGraphics {
} }
} }
} }
const bitmap = { const bitmap = {
data: data, data: data,
width: width, width: width,
height: height, height: height,
bytesPerLine: bytesPerLine bytesPerLine: bytesPerLine
}; };
return this.generateBitmapCommands(bitmap); return this.generateBitmapCommands(bitmap);
} }
} }

View File

@ -31,12 +31,11 @@ export class HtmlToImageConverter {
* @returns {Promise<HTMLCanvasElement>} Canvas with rendered HTML * @returns {Promise<HTMLCanvasElement>} Canvas with rendered HTML
*/ */
async htmlToCanvas(element) { 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 // Clone the element to avoid modifying the original
const clone = element.cloneNode(true); const clone = element.cloneNode(true);
// Apply receipt styling to clone for proper rendering // Apply receipt styling to clone for proper rendering
clone.style.width = `${this.paperWidth}px`; clone.style.width = `${this.paperWidth}px`;
clone.style.maxWidth = `${this.paperWidth}px`; clone.style.maxWidth = `${this.paperWidth}px`;
@ -47,47 +46,12 @@ export class HtmlToImageConverter {
clone.style.backgroundColor = 'white'; clone.style.backgroundColor = 'white';
clone.style.color = 'black'; clone.style.color = 'black';
clone.style.overflow = 'visible'; 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 // Test print uses ESC/POS text mode which has larger fonts
const fontScale = 2.0; const fontScale = 1.2;
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');
// Create a temporary container to measure height // Create a temporary container to measure height
// Container must be large enough to hold the scaled content // Container must be large enough to hold the scaled content
const container = document.createElement('div'); const container = document.createElement('div');
@ -101,14 +65,66 @@ export class HtmlToImageConverter {
container.style.overflow = 'visible'; container.style.overflow = 'visible';
container.style.zIndex = '-1000'; container.style.zIndex = '-1000';
container.style.boxSizing = 'border-box'; container.style.boxSizing = 'border-box';
container.appendChild(clone); container.appendChild(clone);
document.body.appendChild(container); 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 { try {
// Wait for layout to settle, fonts to load, and images to load // Wait for layout to settle, fonts to load, and images to load
await new Promise(resolve => setTimeout(resolve, 200)); await new Promise(resolve => setTimeout(resolve, 200));
// Load all images in the clone // Load all images in the clone
const images = clone.querySelectorAll('img'); const images = clone.querySelectorAll('img');
await Promise.all(Array.from(images).map(img => { await Promise.all(Array.from(images).map(img => {
@ -123,36 +139,32 @@ export class HtmlToImageConverter {
} }
}); });
})); }));
// Get the actual rendered dimensions // Get the actual rendered dimensions
const rect = clone.getBoundingClientRect(); const rect = clone.getBoundingClientRect();
const width = this.paperWidth; const width = this.paperWidth;
const height = Math.ceil(rect.height); 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 // Create canvas with exact dimensions
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
canvas.width = width; canvas.width = width;
canvas.height = height; canvas.height = height;
const ctx = canvas.getContext('2d', { alpha: false, willReadFrequently: false }); const ctx = canvas.getContext('2d', { alpha: false, willReadFrequently: false });
// Fill with white background // Fill with white background
ctx.fillStyle = 'white'; ctx.fillStyle = 'white';
ctx.fillRect(0, 0, width, height); ctx.fillRect(0, 0, width, height);
// Render the DOM to canvas using improved method // Render the DOM to canvas using improved method
// Content is already at the correct width, no scaling needed // 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); await this._renderDomToCanvasImproved(clone, ctx, 0, 0, width, height);
console.log('[HtmlToImage] Canvas rendering complete');
return canvas; return canvas;
} finally { } finally {
// Clean up // Clean up
document.body.removeChild(container); document.body.removeChild(container);
@ -173,17 +185,17 @@ export class HtmlToImageConverter {
* @returns {Promise<number>} Height used * @returns {Promise<number>} Height used
*/ */
async _renderDomToCanvasImproved(element, ctx, offsetX, offsetY, canvasWidth, canvasHeight) { 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 // Background is already filled, no need to fill again
// Get the bounding rect of the root element to calculate offsets // Get the bounding rect of the root element to calculate offsets
const rootRect = element.getBoundingClientRect(); const rootRect = element.getBoundingClientRect();
// Render element recursively with proper offset calculation // Render element recursively with proper offset calculation
await this._renderElement(element, ctx, -rootRect.left, -rootRect.top, rootRect); await this._renderElement(element, ctx, -rootRect.left, -rootRect.top, rootRect);
console.log('[HtmlToImage] Rendering complete');
return canvasHeight; return canvasHeight;
} }
@ -201,50 +213,114 @@ export class HtmlToImageConverter {
if (!element || element.nodeType !== Node.ELEMENT_NODE) { if (!element || element.nodeType !== Node.ELEMENT_NODE) {
return; return;
} }
const style = window.getComputedStyle(element); const style = window.getComputedStyle(element);
// Skip hidden elements // Skip hidden elements
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') { if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
return; return;
} }
const rect = element.getBoundingClientRect(); const rect = element.getBoundingClientRect();
const x = rect.left + offsetX; const x = rect.left + offsetX;
const y = rect.top + offsetY; const y = rect.top + offsetY;
// Render background // Render background
const bgColor = style.backgroundColor; const bgColor = style.backgroundColor;
if (bgColor && bgColor !== 'rgba(0, 0, 0, 0)' && bgColor !== 'transparent') { if (bgColor && bgColor !== 'rgba(0, 0, 0, 0)' && bgColor !== 'transparent') {
ctx.fillStyle = bgColor; ctx.fillStyle = bgColor;
ctx.fillRect(x, y, rect.width, rect.height); ctx.fillRect(x, y, rect.width, rect.height);
} }
// Render borders // Render borders
this._renderBorders(ctx, x, y, rect.width, rect.height, style); this._renderBorders(ctx, x, y, rect.width, rect.height, style);
// Render images // Render images
if (element.tagName === 'IMG') { if (element.tagName === 'IMG') {
await this._renderImage(element, ctx, x, y, rect.width, rect.height); await this._renderImage(element, ctx, x, y, rect.width, rect.height);
} }
// Render text content // Render children (text and elements)
if (element.childNodes.length > 0) { 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( const hasOnlyText = Array.from(element.childNodes).every(
node => node.nodeType === Node.TEXT_NODE || (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'BR') 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) { if (hasOnlyText) {
this._renderText(element, ctx, x, y, rect.width, rect.height, style); this._renderText(element, ctx, x, y, rect.width, rect.height, style);
} else { } else {
// Render children recursively // Mixed content or complex structure
for (const child of element.children) { for (const node of element.childNodes) {
await this._renderElement(child, ctx, offsetX, offsetY, rootRect); 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 * Render text content of an element
* *
@ -260,25 +336,25 @@ export class HtmlToImageConverter {
_renderText(element, ctx, x, y, width, height, style) { _renderText(element, ctx, x, y, width, height, style) {
const text = element.textContent.trim(); const text = element.textContent.trim();
if (!text) return; if (!text) return;
// Set font // Set font
const fontSize = parseFloat(style.fontSize) || 12; const fontSize = parseFloat(style.fontSize) || 12;
const fontWeight = style.fontWeight; const fontWeight = style.fontWeight;
const fontFamily = style.fontFamily.split(',')[0].replace(/['"]/g, '') || 'monospace'; const fontFamily = style.fontFamily.split(',')[0].replace(/['"]/g, '') || 'monospace';
const isBold = fontWeight === 'bold' || parseInt(fontWeight) >= 600; const isBold = fontWeight === 'bold' || parseInt(fontWeight) >= 600;
ctx.font = `${isBold ? 'bold' : 'normal'} ${fontSize}px ${fontFamily}`; ctx.font = `${isBold ? 'bold' : 'normal'} ${fontSize}px ${fontFamily}`;
ctx.fillStyle = style.color || 'black'; ctx.fillStyle = style.color || 'black';
ctx.textBaseline = 'top'; ctx.textBaseline = 'top';
// Handle text alignment // Handle text alignment
const textAlign = style.textAlign || 'left'; const textAlign = style.textAlign || 'left';
const paddingLeft = parseFloat(style.paddingLeft || 0); const paddingLeft = parseFloat(style.paddingLeft || 0);
const paddingRight = parseFloat(style.paddingRight || 0); const paddingRight = parseFloat(style.paddingRight || 0);
const paddingTop = parseFloat(style.paddingTop || 0); const paddingTop = parseFloat(style.paddingTop || 0);
let textX = x + paddingLeft; let textX = x + paddingLeft;
if (textAlign === 'center') { if (textAlign === 'center') {
ctx.textAlign = 'center'; ctx.textAlign = 'center';
textX = x + width / 2; textX = x + width / 2;
@ -288,15 +364,15 @@ export class HtmlToImageConverter {
} else { } else {
ctx.textAlign = 'left'; ctx.textAlign = 'left';
} }
const textY = y + paddingTop; const textY = y + paddingTop;
// Word wrap if needed // Word wrap if needed
const maxWidth = width - paddingLeft - paddingRight; const maxWidth = width - paddingLeft - paddingRight;
const lineHeight = parseFloat(style.lineHeight) || fontSize * 1.4; const lineHeight = parseFloat(style.lineHeight) || fontSize * 1.4;
this._wrapAndDrawText(ctx, text, textX, textY, maxWidth, lineHeight); this._wrapAndDrawText(ctx, text, textX, textY, maxWidth, lineHeight);
// Reset text align // Reset text align
ctx.textAlign = 'left'; ctx.textAlign = 'left';
} }
@ -316,11 +392,11 @@ export class HtmlToImageConverter {
const words = text.split(' '); const words = text.split(' ');
let line = ''; let line = '';
let currentY = y; let currentY = y;
for (let i = 0; i < words.length; i++) { for (let i = 0; i < words.length; i++) {
const testLine = line + (line ? ' ' : '') + words[i]; const testLine = line + (line ? ' ' : '') + words[i];
const metrics = ctx.measureText(testLine); const metrics = ctx.measureText(testLine);
if (metrics.width > maxWidth && line) { if (metrics.width > maxWidth && line) {
ctx.fillText(line, x, currentY); ctx.fillText(line, x, currentY);
line = words[i]; line = words[i];
@ -329,7 +405,7 @@ export class HtmlToImageConverter {
line = testLine; line = testLine;
} }
} }
if (line) { if (line) {
ctx.fillText(line, x, currentY); ctx.fillText(line, x, currentY);
} }
@ -351,7 +427,7 @@ export class HtmlToImageConverter {
const borderRight = parseFloat(style.borderRightWidth) || 0; const borderRight = parseFloat(style.borderRightWidth) || 0;
const borderBottom = parseFloat(style.borderBottomWidth) || 0; const borderBottom = parseFloat(style.borderBottomWidth) || 0;
const borderLeft = parseFloat(style.borderLeftWidth) || 0; const borderLeft = parseFloat(style.borderLeftWidth) || 0;
if (borderTop > 0) { if (borderTop > 0) {
ctx.strokeStyle = style.borderTopColor || 'black'; ctx.strokeStyle = style.borderTopColor || 'black';
ctx.lineWidth = borderTop; ctx.lineWidth = borderTop;
@ -360,7 +436,7 @@ export class HtmlToImageConverter {
ctx.lineTo(x + width, y); ctx.lineTo(x + width, y);
ctx.stroke(); ctx.stroke();
} }
if (borderRight > 0) { if (borderRight > 0) {
ctx.strokeStyle = style.borderRightColor || 'black'; ctx.strokeStyle = style.borderRightColor || 'black';
ctx.lineWidth = borderRight; ctx.lineWidth = borderRight;
@ -369,7 +445,7 @@ export class HtmlToImageConverter {
ctx.lineTo(x + width, y + height); ctx.lineTo(x + width, y + height);
ctx.stroke(); ctx.stroke();
} }
if (borderBottom > 0) { if (borderBottom > 0) {
ctx.strokeStyle = style.borderBottomColor || 'black'; ctx.strokeStyle = style.borderBottomColor || 'black';
ctx.lineWidth = borderBottom; ctx.lineWidth = borderBottom;
@ -378,7 +454,7 @@ export class HtmlToImageConverter {
ctx.lineTo(x + width, y + height); ctx.lineTo(x + width, y + height);
ctx.stroke(); ctx.stroke();
} }
if (borderLeft > 0) { if (borderLeft > 0) {
ctx.strokeStyle = style.borderLeftColor || 'black'; ctx.strokeStyle = style.borderLeftColor || 'black';
ctx.lineWidth = borderLeft; ctx.lineWidth = borderLeft;
@ -421,30 +497,30 @@ export class HtmlToImageConverter {
* @param {number} height - Canvas height * @param {number} height - Canvas height
*/ */
async _renderDomToCanvas(element, ctx, width, height) { async _renderDomToCanvas(element, ctx, width, height) {
console.log('[HtmlToImage] Rendering DOM to canvas (simple fallback method)...');
// Set default styles // Set default styles
ctx.fillStyle = 'white'; ctx.fillStyle = 'white';
ctx.fillRect(0, 0, width, height); ctx.fillRect(0, 0, width, height);
ctx.fillStyle = 'black'; ctx.fillStyle = 'black';
ctx.textBaseline = 'top'; ctx.textBaseline = 'top';
ctx.font = '12px monospace'; ctx.font = '12px monospace';
const padding = 10; const padding = 10;
const lineHeight = 16; const lineHeight = 16;
const maxWidth = width - (padding * 2); const maxWidth = width - (padding * 2);
let y = padding; let y = padding;
// Helper function to wrap text // Helper function to wrap text
const wrapText = (text, maxWidth) => { const wrapText = (text, maxWidth) => {
const words = text.split(' '); const words = text.split(' ');
const lines = []; const lines = [];
let currentLine = ''; let currentLine = '';
for (const word of words) { for (const word of words) {
const testLine = currentLine + (currentLine ? ' ' : '') + word; const testLine = currentLine + (currentLine ? ' ' : '') + word;
const metrics = ctx.measureText(testLine); const metrics = ctx.measureText(testLine);
if (metrics.width > maxWidth && currentLine) { if (metrics.width > maxWidth && currentLine) {
lines.push(currentLine); lines.push(currentLine);
currentLine = word; currentLine = word;
@ -452,36 +528,36 @@ export class HtmlToImageConverter {
currentLine = testLine; currentLine = testLine;
} }
} }
if (currentLine) { if (currentLine) {
lines.push(currentLine); lines.push(currentLine);
} }
return lines; return lines;
}; };
// Helper to draw text with alignment // Helper to draw text with alignment
const drawText = (text, align = 'left', bold = false) => { const drawText = (text, align = 'left', bold = false) => {
if (!text || !text.trim()) return; if (!text || !text.trim()) return;
ctx.font = `${bold ? 'bold' : 'normal'} 12px monospace`; ctx.font = `${bold ? 'bold' : 'normal'} 12px monospace`;
const lines = wrapText(text, maxWidth); const lines = wrapText(text, maxWidth);
for (const line of lines) { for (const line of lines) {
let x = padding; let x = padding;
const textWidth = ctx.measureText(line).width; const textWidth = ctx.measureText(line).width;
if (align === 'center') { if (align === 'center') {
x = (width - textWidth) / 2; x = (width - textWidth) / 2;
} else if (align === 'right') { } else if (align === 'right') {
x = width - textWidth - padding; x = width - textWidth - padding;
} }
ctx.fillText(line, x, y); ctx.fillText(line, x, y);
y += lineHeight; y += lineHeight;
} }
}; };
// Helper to draw a line // Helper to draw a line
const drawLine = () => { const drawLine = () => {
ctx.beginPath(); ctx.beginPath();
@ -492,40 +568,40 @@ export class HtmlToImageConverter {
ctx.stroke(); ctx.stroke();
y += lineHeight; y += lineHeight;
}; };
// Extract and render content recursively // Extract and render content recursively
const processElement = (el) => { const processElement = (el) => {
if (!el) return; if (!el) return;
const style = window.getComputedStyle(el); const style = window.getComputedStyle(el);
const display = style.display; const display = style.display;
// Skip hidden elements // Skip hidden elements
if (display === 'none' || style.visibility === 'hidden') { if (display === 'none' || style.visibility === 'hidden') {
return; return;
} }
const tagName = el.tagName; const tagName = el.tagName;
const textAlign = style.textAlign || 'left'; const textAlign = style.textAlign || 'left';
const fontWeight = style.fontWeight; const fontWeight = style.fontWeight;
const isBold = fontWeight === 'bold' || parseInt(fontWeight) >= 600; const isBold = fontWeight === 'bold' || parseInt(fontWeight) >= 600;
// Handle special elements // Handle special elements
if (tagName === 'BR') { if (tagName === 'BR') {
y += lineHeight; y += lineHeight;
return; return;
} }
if (tagName === 'HR') { if (tagName === 'HR') {
drawLine(); drawLine();
return; return;
} }
// Check if element has only text content (no child elements) // Check if element has only text content (no child elements)
const hasOnlyText = Array.from(el.childNodes).every( const hasOnlyText = Array.from(el.childNodes).every(
node => node.nodeType === Node.TEXT_NODE node => node.nodeType === Node.TEXT_NODE
); );
if (hasOnlyText) { if (hasOnlyText) {
const text = el.textContent.trim(); const text = el.textContent.trim();
if (text) { if (text) {
@ -544,17 +620,17 @@ export class HtmlToImageConverter {
} }
} }
} }
// Add spacing after block elements // Add spacing after block elements
if (display === 'block' || tagName === 'DIV' || tagName === 'P' || tagName === 'TABLE') { if (display === 'block' || tagName === 'DIV' || tagName === 'P' || tagName === 'TABLE') {
y += lineHeight / 2; y += lineHeight / 2;
} }
}; };
// Start processing // Start processing
try { try {
processElement(element); processElement(element);
console.log('[HtmlToImage] Rendering complete, height used:', y);
} catch (error) { } catch (error) {
console.error('[HtmlToImage] Error during rendering:', error); console.error('[HtmlToImage] Error during rendering:', error);
// Ultimate fallback - just print all text // Ultimate fallback - just print all text
@ -574,55 +650,55 @@ export class HtmlToImageConverter {
* @returns {Uint8Array} Bitmap data * @returns {Uint8Array} Bitmap data
*/ */
canvasToBitmap(canvas) { canvasToBitmap(canvas) {
console.log('[HtmlToImage] Converting canvas to bitmap (optimized)...');
const startTime = performance.now(); const startTime = performance.now();
const ctx = canvas.getContext('2d', { willReadFrequently: true }); const ctx = canvas.getContext('2d', { willReadFrequently: true });
const width = canvas.width; const width = canvas.width;
const height = canvas.height; const height = canvas.height;
// Get image data // Get image data
const imageData = ctx.getImageData(0, 0, width, height); const imageData = ctx.getImageData(0, 0, width, height);
const pixels = imageData.data; const pixels = imageData.data;
// Convert to monochrome bitmap // Convert to monochrome bitmap
// Each byte represents 8 pixels (1 bit per pixel) // Each byte represents 8 pixels (1 bit per pixel)
const bytesPerLine = Math.ceil(width / 8); const bytesPerLine = Math.ceil(width / 8);
const bitmapData = new Uint8Array(bytesPerLine * height); const bitmapData = new Uint8Array(bytesPerLine * height);
// Optimized conversion using lookup table and bitwise operations // Optimized conversion using lookup table and bitwise operations
// Pre-calculate grayscale weights for faster conversion // Pre-calculate grayscale weights for faster conversion
const rWeight = 0.299; const rWeight = 0.299;
const gWeight = 0.587; const gWeight = 0.587;
const bWeight = 0.114; const bWeight = 0.114;
let byteIndex = 0; let byteIndex = 0;
let currentByte = 0; let currentByte = 0;
let bitPosition = 7; let bitPosition = 7;
for (let y = 0; y < height; y++) { for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) { for (let x = 0; x < width; x++) {
const pixelIndex = (y * width + x) * 4; const pixelIndex = (y * width + x) * 4;
// Fast grayscale conversion using weighted average // Fast grayscale conversion using weighted average
const gray = pixels[pixelIndex] * rWeight + const gray = pixels[pixelIndex] * rWeight +
pixels[pixelIndex + 1] * gWeight + pixels[pixelIndex + 1] * gWeight +
pixels[pixelIndex + 2] * bWeight; pixels[pixelIndex + 2] * bWeight;
// Threshold to black or white // Threshold to black or white
if (gray < 128) { if (gray < 128) {
currentByte |= (1 << bitPosition); currentByte |= (1 << bitPosition);
} }
bitPosition--; bitPosition--;
if (bitPosition < 0) { if (bitPosition < 0) {
bitmapData[byteIndex++] = currentByte; bitmapData[byteIndex++] = currentByte;
currentByte = 0; currentByte = 0;
bitPosition = 7; bitPosition = 7;
} }
} }
// Handle remaining bits at end of line // Handle remaining bits at end of line
if (bitPosition !== 7) { if (bitPosition !== 7) {
bitmapData[byteIndex++] = currentByte; bitmapData[byteIndex++] = currentByte;
@ -630,14 +706,10 @@ export class HtmlToImageConverter {
bitPosition = 7; bitPosition = 7;
} }
} }
const endTime = performance.now(); 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 { return {
data: bitmapData, data: bitmapData,
width: width, width: width,
@ -653,16 +725,16 @@ export class HtmlToImageConverter {
* @returns {Promise<Object>} Bitmap data with dimensions * @returns {Promise<Object>} Bitmap data with dimensions
*/ */
async htmlToBitmap(element) { async htmlToBitmap(element) {
console.log('[HtmlToImage] Converting HTML to bitmap...');
try { try {
// Convert HTML to canvas // Convert HTML to canvas
const canvas = await this.htmlToCanvas(element); const canvas = await this.htmlToCanvas(element);
// Convert canvas to bitmap // Convert canvas to bitmap
const bitmap = this.canvasToBitmap(canvas); const bitmap = this.canvasToBitmap(canvas);
console.log('[HtmlToImage] Conversion complete');
return bitmap; return bitmap;
} catch (error) { } catch (error) {
console.error('[HtmlToImage] Conversion failed:', error); console.error('[HtmlToImage] Conversion failed:', error);
@ -679,26 +751,29 @@ export class HtmlToImageConverter {
setPaperWidth(widthMm, dpi = 203) { setPaperWidth(widthMm, dpi = 203) {
this.paperWidthMm = widthMm; this.paperWidthMm = widthMm;
this.dpi = dpi; this.dpi = dpi;
// Calculate dots per mm based on DPI // Calculate dots per mm based on DPI
// 203 DPI = 8 dots/mm (203 / 25.4) // 203 DPI = 8 dots/mm (203 / 25.4)
// 304 DPI = 12 dots/mm (304 / 25.4) // 304 DPI = 12 dots/mm (304 / 25.4)
const dotsPerMm = Math.round(dpi / 25.4); const dotsPerMm = Math.round(dpi / 25.4);
// Calculate pixel width based on paper size and DPI // Calculate pixel width based on PRINTABLE width (essential for thermal printers)
// Use FULL width without margins for maximum usage // 58mm Paper -> ~48mm Printable
// 80mm Paper -> ~72mm Printable
let printableWidthMm;
if (widthMm === 58) { if (widthMm === 58) {
// 58mm paper at selected DPI printableWidthMm = 48; // Standard printable width for 58mm
this.paperWidth = widthMm * dotsPerMm;
} else if (widthMm === 80) { } else if (widthMm === 80) {
// 80mm paper at selected DPI printableWidthMm = 72; // Standard printable width for 80mm
this.paperWidth = widthMm * dotsPerMm;
} else { } else {
// Custom width: use full width // Custom width: subtract ~10mm margin
this.paperWidth = widthMm * dotsPerMm; 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);
} }
} }

View File

@ -60,69 +60,63 @@ patch(PosPrinterService.prototype, {
async printHtml(el) { async printHtml(el) {
// Wrap entire method in try-catch to catch any errors // Wrap entire method in try-catch to catch any errors
try { 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 // Check if Web Bluetooth API is available
if (!navigator.bluetooth) { if (!navigator.bluetooth) {
console.log('[BluetoothPrint] Web Bluetooth API not available, using browser print dialog');
await this._printViaBrowserDialog(el); await this._printViaBrowserDialog(el);
return true; return true;
} }
console.log('[BluetoothPrint] Web Bluetooth API available');
// Check if a Bluetooth printer is configured (from localStorage) // Check if a Bluetooth printer is configured (from localStorage)
// We don't need POS config - we check if user has configured a printer // We don't need POS config - we check if user has configured a printer
const storage = new BluetoothPrinterStorage(); const storage = new BluetoothPrinterStorage();
const config = storage.loadConfiguration(1); // Default POS config ID const config = storage.loadConfiguration(1); // Default POS config ID
if (!config || !config.deviceId) { if (!config || !config.deviceId) {
console.log('[BluetoothPrint] No Bluetooth printer configured, using standard print');
console.log('[BluetoothPrint] Calling originalPrintHtml with:', el);
try { try {
const result = await originalPrintHtml.call(this, el); const result = await originalPrintHtml.call(this, el);
console.log('[BluetoothPrint] originalPrintHtml returned:', result);
// If original method returned false, it didn't handle the print // If original method returned false, it didn't handle the print
// So we need to handle it ourselves // So we need to handle it ourselves
if (result === false) { if (result === false) {
console.log('[BluetoothPrint] originalPrintHtml returned false, opening browser print dialog directly');
await this._printViaBrowserDialog(el); await this._printViaBrowserDialog(el);
return true; return true;
} }
return result; return result;
} catch (error) { } catch (error) {
console.error('[BluetoothPrint] Error calling originalPrintHtml:', error); console.error('[BluetoothPrint] Error calling originalPrintHtml:', error);
// Fallback to browser print dialog // Fallback to browser print dialog
console.log('[BluetoothPrint] Falling back to browser print dialog');
await this._printViaBrowserDialog(el); await this._printViaBrowserDialog(el);
return true; return true;
} }
} }
console.log('[BluetoothPrint] Bluetooth printer configured:', config.deviceName);
// Initialize bluetooth services // Initialize bluetooth services
console.log('[BluetoothPrint] Initializing bluetooth services...'); const services = initializeBluetoothPrinting(null); // Restored
const services = initializeBluetoothPrinting(null);
console.log('[BluetoothPrint] Bluetooth services initialized');
// Check if printer is actually connected // Check if printer is actually connected
const connectionStatus = services.bluetoothManager.getConnectionStatus(); const connectionStatus = services.bluetoothManager.getConnectionStatus();
console.log('[BluetoothPrint] Current connection status:', connectionStatus);
if (connectionStatus !== 'connected') { if (connectionStatus !== 'connected') {
console.log('[BluetoothPrint] Printer not connected, attempting to reconnect...');
// Try to reconnect // Try to reconnect
try { try {
await services.bluetoothManager.autoReconnect(); await services.bluetoothManager.autoReconnect();
console.log('[BluetoothPrint] Reconnection successful');
} catch (reconnectError) { } catch (reconnectError) {
console.error('[BluetoothPrint] Reconnection failed:', reconnectError); console.error('[BluetoothPrint] Reconnection failed:', reconnectError);
console.log('[BluetoothPrint] Falling back to browser print dialog');
await this._printViaBrowserDialog(el); await this._printViaBrowserDialog(el);
return true; return true;
} }
@ -130,17 +124,17 @@ patch(PosPrinterService.prototype, {
try { try {
// Attempt bluetooth printing // Attempt bluetooth printing
console.log('[BluetoothPrint] Attempting bluetooth print...');
await this._printViaBluetoothFromHtml(el, services, config); await this._printViaBluetoothFromHtml(el, services, config);
console.log('[BluetoothPrint] Print completed successfully');
return true; return true;
} catch (printError) { } catch (printError) {
console.error('[BluetoothPrint] Bluetooth print failed:', printError); console.error('[BluetoothPrint] Bluetooth print failed:', printError);
console.error('[BluetoothPrint] Error stack:', printError.stack); console.error('[BluetoothPrint] Error stack:', printError.stack);
// Fallback to browser print dialog // Fallback to browser print dialog
console.log('[BluetoothPrint] Falling back to browser print dialog after error');
await this._printViaBrowserDialog(el); await this._printViaBrowserDialog(el);
return true; return true;
} }
@ -149,7 +143,7 @@ patch(PosPrinterService.prototype, {
console.error('[BluetoothPrint] Error name:', error.name); console.error('[BluetoothPrint] Error name:', error.name);
console.error('[BluetoothPrint] Error message:', error.message); console.error('[BluetoothPrint] Error message:', error.message);
console.error('[BluetoothPrint] Error stack:', error.stack); console.error('[BluetoothPrint] Error stack:', error.stack);
console.log('[BluetoothPrint] Falling back to browser print dialog due to critical error');
try { try {
await this._printViaBrowserDialog(el); await this._printViaBrowserDialog(el);
return true; return true;
@ -169,8 +163,8 @@ patch(PosPrinterService.prototype, {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async _printViaBrowserDialog(el) { async _printViaBrowserDialog(el) {
console.log('[BluetoothPrint] Opening browser print dialog...');
return new Promise((resolve) => { return new Promise((resolve) => {
// Create a hidden iframe for printing // Create a hidden iframe for printing
const printFrame = document.createElement('iframe'); const printFrame = document.createElement('iframe');
@ -181,32 +175,32 @@ patch(PosPrinterService.prototype, {
printFrame.style.height = '0'; printFrame.style.height = '0';
printFrame.style.border = '0'; printFrame.style.border = '0';
document.body.appendChild(printFrame); document.body.appendChild(printFrame);
let printTriggered = false; let printTriggered = false;
let timeoutId = null; let timeoutId = null;
const cleanup = () => { const cleanup = () => {
if (timeoutId) { if (timeoutId) {
clearTimeout(timeoutId); clearTimeout(timeoutId);
} }
if (printFrame.parentNode) { if (printFrame.parentNode) {
document.body.removeChild(printFrame); document.body.removeChild(printFrame);
console.log('[BluetoothPrint] Iframe cleaned up');
} }
resolve(); resolve();
}; };
const triggerPrint = () => { const triggerPrint = () => {
if (printTriggered) { if (printTriggered) {
return; return;
} }
printTriggered = true; printTriggered = true;
try { try {
printFrame.contentWindow.focus(); printFrame.contentWindow.focus();
printFrame.contentWindow.print(); printFrame.contentWindow.print();
console.log('[BluetoothPrint] Print dialog opened');
// Clean up after a delay // Clean up after a delay
setTimeout(cleanup, 1000); setTimeout(cleanup, 1000);
} catch (printError) { } catch (printError) {
@ -214,12 +208,12 @@ patch(PosPrinterService.prototype, {
cleanup(); cleanup();
} }
}; };
try { try {
// Write receipt content to iframe // Write receipt content to iframe
const frameDoc = printFrame.contentWindow.document; const frameDoc = printFrame.contentWindow.document;
frameDoc.open(); frameDoc.open();
// Add basic styling for print // Add basic styling for print
const printHtml = ` const printHtml = `
<!DOCTYPE html> <!DOCTYPE html>
@ -262,27 +256,27 @@ patch(PosPrinterService.prototype, {
</body> </body>
</html> </html>
`; `;
frameDoc.write(printHtml); frameDoc.write(printHtml);
frameDoc.close(); frameDoc.close();
console.log('[BluetoothPrint] Receipt content written to iframe');
// Wait for content to load // Wait for content to load
printFrame.contentWindow.addEventListener('load', () => { printFrame.contentWindow.addEventListener('load', () => {
console.log('[BluetoothPrint] Iframe loaded, triggering print...');
// Small delay to ensure rendering is complete // Small delay to ensure rendering is complete
setTimeout(triggerPrint, 100); setTimeout(triggerPrint, 100);
}); });
// Fallback if load event doesn't fire within 2 seconds // Fallback if load event doesn't fire within 2 seconds
timeoutId = setTimeout(() => { timeoutId = setTimeout(() => {
if (!printTriggered) { if (!printTriggered) {
console.log('[BluetoothPrint] Load timeout, attempting print anyway...');
triggerPrint(); triggerPrint();
} }
}, 2000); }, 2000);
} catch (error) { } catch (error) {
console.error('[BluetoothPrint] Error in _printViaBrowserDialog:', error); console.error('[BluetoothPrint] Error in _printViaBrowserDialog:', error);
cleanup(); cleanup();
@ -303,65 +297,46 @@ patch(PosPrinterService.prototype, {
*/ */
async _printViaBluetoothFromHtml(el, services, config) { async _printViaBluetoothFromHtml(el, services, config) {
const { bluetoothManager } = services; 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 // Check connection status
const status = bluetoothManager.getConnectionStatus(); const status = bluetoothManager.getConnectionStatus();
console.log('[BluetoothPrint] Connection status:', status);
if (status !== 'connected') { if (status !== 'connected') {
throw new PrinterNotConnectedError('Bluetooth printer is not connected'); throw new PrinterNotConnectedError('Bluetooth printer is not connected');
} }
try { try {
// Convert HTML to bitmap image // Convert HTML to bitmap image
console.log('[BluetoothPrint] Converting HTML to bitmap...');
console.log('[BluetoothPrint] Creating HtmlToImageConverter...');
const converter = new 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 // Get paper width and DPI from configuration
const paperWidthMm = config?.settings?.paperWidthMm || 58; const paperWidthMm = config?.settings?.paperWidthMm || 58;
const dpi = config?.settings?.dpi || 203; 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); 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); 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 // Generate ESC/POS graphics commands
console.log('[BluetoothPrint] Generating ESC/POS graphics commands...');
console.log('[BluetoothPrint] Creating EscPosGraphics...');
const graphicsGenerator = new EscPosGraphics(); const graphicsGenerator = new EscPosGraphics();
console.log('[BluetoothPrint] EscPosGraphics created successfully');
console.log('[BluetoothPrint] Generating bitmap commands...');
const escposData = graphicsGenerator.generateBitmapCommands(bitmap); const escposData = graphicsGenerator.generateBitmapCommands(bitmap);
console.log('[BluetoothPrint] Generated', escposData.length, 'bytes of graphics data');
// Send data to printer // Send data to printer
console.log('[BluetoothPrint] Sending graphics to printer...');
const startTime = performance.now();
await bluetoothManager.sendData(escposData); await bluetoothManager.sendData(escposData);
const endTime = performance.now();
console.log('[BluetoothPrint] Graphics print completed successfully in', (endTime - startTime).toFixed(0), 'ms');
} catch (error) { } catch (error) {
console.error('[BluetoothPrint] Graphics mode failed:', error); console.error('[BluetoothPrint] Graphics mode failed:', error);
console.error('[BluetoothPrint] Error name:', error.name); console.error('[BluetoothPrint] Error name:', error.name);
console.error('[BluetoothPrint] Error message:', error.message); console.error('[BluetoothPrint] Error message:', error.message);
console.error('[BluetoothPrint] Error stack:', error.stack); console.error('[BluetoothPrint] Error stack:', error.stack);
console.log('[BluetoothPrint] Falling back to text mode...');
// Fallback to text mode if graphics fails // Fallback to text mode if graphics fails
await this._printViaBluetoothTextMode(el, services); await this._printViaBluetoothTextMode(el, services);
} }
@ -378,23 +353,17 @@ patch(PosPrinterService.prototype, {
*/ */
async _printViaBluetoothTextMode(el, services) { async _printViaBluetoothTextMode(el, services) {
const { bluetoothManager, escposGenerator } = services; const { bluetoothManager, escposGenerator } = services;
console.log('[BluetoothPrint] Using TEXT MODE (fallback)');
// Parse receipt data from HTML element // Parse receipt data from HTML element
console.log('[BluetoothPrint] Parsing receipt data from HTML...');
const receiptData = this._parseReceiptDataFromHtml(el); const receiptData = this._parseReceiptDataFromHtml(el);
console.log('[BluetoothPrint] Parsed receipt data:', JSON.stringify(receiptData, null, 2));
// Generate ESC/POS commands // Generate ESC/POS commands
console.log('[BluetoothPrint] Generating ESC/POS text commands...');
const escposData = escposGenerator.generateReceipt(receiptData); const escposData = escposGenerator.generateReceipt(receiptData);
console.log('[BluetoothPrint] Generated', escposData.length, 'bytes of text data');
// Send data to printer // Send data to printer
console.log('[BluetoothPrint] Sending text to printer...');
await bluetoothManager.sendData(escposData); await bluetoothManager.sendData(escposData);
console.log('[BluetoothPrint] Text print completed successfully');
}, },
/** /**
@ -407,34 +376,25 @@ patch(PosPrinterService.prototype, {
* @throws {Error} If printing fails * @throws {Error} If printing fails
*/ */
async _printViaBluetooth(el, pos) { async _printViaBluetooth(el, pos) {
const services = initializeBluetoothPrinting(); const services = initializeBluetoothPrinting();
const { bluetoothManager, escposGenerator } = services; const { bluetoothManager, escposGenerator } = services;
console.log('[BluetoothPrint] Starting bluetooth print...');
console.log('[BluetoothPrint] Element:', el);
// Check connection status // Check connection status
const status = bluetoothManager.getConnectionStatus(); const status = bluetoothManager.getConnectionStatus();
console.log('[BluetoothPrint] Connection status:', status);
if (status !== 'connected') { if (status !== 'connected') {
throw new PrinterNotConnectedError('Bluetooth printer is not connected'); throw new PrinterNotConnectedError('Bluetooth printer is not connected');
} }
// Parse receipt data from POS order // Parse receipt data from POS order
console.log('[BluetoothPrint] Parsing receipt data...');
const receiptData = this._parseReceiptDataFromPos(pos); const receiptData = this._parseReceiptDataFromPos(pos);
console.log('[BluetoothPrint] Receipt data:', receiptData);
// Generate ESC/POS commands // Generate ESC/POS commands
console.log('[BluetoothPrint] Generating ESC/POS commands...');
const escposData = escposGenerator.generateReceipt(receiptData); const escposData = escposGenerator.generateReceipt(receiptData);
console.log('[BluetoothPrint] Generated', escposData.length, 'bytes of ESC/POS data');
// Send data to printer // Send data to printer
console.log('[BluetoothPrint] Sending to printer...');
await bluetoothManager.sendData(escposData); await bluetoothManager.sendData(escposData);
console.log('[BluetoothPrint] Print completed successfully');
}, },
/** /**
@ -447,12 +407,12 @@ patch(PosPrinterService.prototype, {
async _printViaFallback(receipt) { async _printViaFallback(receipt) {
// Use the standard Odoo POS printing mechanism // Use the standard Odoo POS printing mechanism
// This typically opens the browser print dialog // This typically opens the browser print dialog
// Create a hidden iframe for printing // Create a hidden iframe for printing
const printFrame = document.createElement('iframe'); const printFrame = document.createElement('iframe');
printFrame.style.display = 'none'; printFrame.style.display = 'none';
document.body.appendChild(printFrame); document.body.appendChild(printFrame);
try { try {
// Write receipt content to iframe // Write receipt content to iframe
const frameDoc = printFrame.contentWindow.document; const frameDoc = printFrame.contentWindow.document;
@ -481,7 +441,7 @@ patch(PosPrinterService.prototype, {
</html> </html>
`); `);
frameDoc.close(); frameDoc.close();
// Wait for content to load // Wait for content to load
await new Promise(resolve => { await new Promise(resolve => {
if (printFrame.contentWindow.document.readyState === 'complete') { if (printFrame.contentWindow.document.readyState === 'complete') {
@ -490,10 +450,10 @@ patch(PosPrinterService.prototype, {
printFrame.contentWindow.addEventListener('load', resolve); printFrame.contentWindow.addEventListener('load', resolve);
} }
}); });
// Trigger print dialog // Trigger print dialog
printFrame.contentWindow.print(); printFrame.contentWindow.print();
// Clean up after a delay // Clean up after a delay
setTimeout(() => { setTimeout(() => {
document.body.removeChild(printFrame); document.body.removeChild(printFrame);
@ -520,7 +480,7 @@ patch(PosPrinterService.prototype, {
const timeoutPromise = new Promise((_, reject) => { const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new TimeoutError(`Print operation timed out after ${timeoutMs}ms`)), timeoutMs); setTimeout(() => reject(new TimeoutError(`Print operation timed out after ${timeoutMs}ms`)), timeoutMs);
}); });
try { try {
await Promise.race([sendFunction(), timeoutPromise]); await Promise.race([sendFunction(), timeoutPromise]);
} catch (error) { } catch (error) {
@ -545,25 +505,19 @@ patch(PosPrinterService.prototype, {
* @returns {Object} Structured receipt data * @returns {Object} Structured receipt data
*/ */
_parseReceiptDataFromHtml(el) { _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 // Extract text content from HTML
const getText = (selector) => { const getText = (selector) => {
const element = el.querySelector(selector); const element = el.querySelector(selector);
const text = element ? element.textContent.trim() : ''; const text = element ? element.textContent.trim() : '';
console.log(`[BluetoothPrint] getText('${selector}'):`, text || '(empty)');
return text; return text;
}; };
const getAll = (selector) => { const getAll = (selector) => {
const elements = Array.from(el.querySelectorAll(selector)); const elements = Array.from(el.querySelectorAll(selector));
console.log(`[BluetoothPrint] getAll('${selector}'):`, elements.length, 'elements found');
return elements; return elements;
}; };
// Parse header (company info) // Parse header (company info)
const headerData = { const headerData = {
companyName: getText('.pos-receipt-company-name') || getText('h2') || getText('h3') || 'Receipt', companyName: getText('.pos-receipt-company-name') || getText('h2') || getText('h3') || 'Receipt',
@ -571,7 +525,7 @@ patch(PosPrinterService.prototype, {
phone: getText('.pos-receipt-phone') || '', phone: getText('.pos-receipt-phone') || '',
taxId: getText('.pos-receipt-tax-id') || '' taxId: getText('.pos-receipt-tax-id') || ''
}; };
// Parse order info // Parse order info
const orderData = { const orderData = {
orderName: getText('.pos-receipt-order-name') || getText('.order-name') || getText('.pos-receipt-order-data') || '', 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') || '', cashier: getText('.pos-receipt-cashier') || getText('.cashier') || '',
customer: getText('.pos-receipt-customer') || getText('.customer') || null customer: getText('.pos-receipt-customer') || getText('.customer') || null
}; };
// Parse order lines - try multiple selectors // Parse order lines - try multiple selectors
console.log('[BluetoothPrint] Searching for order lines...');
let lineElements = getAll('.orderline'); let lineElements = getAll('.orderline');
if (lineElements.length === 0) { if (lineElements.length === 0) {
lineElements = getAll('.pos-receipt-orderline'); lineElements = getAll('.pos-receipt-orderline');
@ -596,72 +549,66 @@ patch(PosPrinterService.prototype, {
// Try to find any table rows // Try to find any table rows
lineElements = getAll('tbody tr'); lineElements = getAll('tbody tr');
} }
console.log('[BluetoothPrint] Found', lineElements.length, 'line elements');
const lines = lineElements.map((line, index) => { 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 productName = line.querySelector('.product-name, td:first-child, .pos-receipt-left-align')?.textContent.trim() || '';
const qtyText = line.querySelector('.qty, .quantity')?.textContent.trim() || '1'; const qtyText = line.querySelector('.qty, .quantity')?.textContent.trim() || '1';
const priceText = line.querySelector('.price, .price-unit')?.textContent.trim() || '0'; 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'; 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) // Parse numbers (remove currency symbols and commas)
const parseNumber = (str) => { const parseNumber = (str) => {
const cleaned = str.replace(/[^0-9.-]/g, ''); const cleaned = str.replace(/[^0-9.-]/g, '');
return parseFloat(cleaned) || 0; return parseFloat(cleaned) || 0;
}; };
const parsedLine = { const parsedLine = {
productName: productName, productName: productName,
quantity: parseNumber(qtyText), quantity: parseNumber(qtyText),
price: parseNumber(priceText), price: parseNumber(priceText),
total: parseNumber(totalText) total: parseNumber(totalText)
}; };
console.log(`[BluetoothPrint] Line ${index} parsed:`, parsedLine);
return parsedLine; return parsedLine;
}).filter(line => line.productName); // Filter out empty lines }).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 // Parse totals
console.log('[BluetoothPrint] Parsing totals...');
const totals = { const totals = {
subtotal: this._parseAmount(getText('.pos-receipt-subtotal, .subtotal')), subtotal: this._parseAmount(getText('.pos-receipt-subtotal, .subtotal')),
tax: this._parseAmount(getText('.pos-receipt-tax, .tax')), tax: this._parseAmount(getText('.pos-receipt-tax, .tax')),
discount: this._parseAmount(getText('.pos-receipt-discount, .discount')), discount: this._parseAmount(getText('.pos-receipt-discount, .discount')),
total: this._parseAmount(getText('.pos-receipt-total, .total, .amount-total')) 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 not found in specific elements, calculate from lines
if (totals.total === 0 && lines.length > 0) { 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.total = lines.reduce((sum, line) => sum + line.total, 0);
totals.subtotal = totals.total; totals.subtotal = totals.total;
console.log('[BluetoothPrint] Calculated totals:', totals);
} }
// Parse payment info // Parse payment info
const paymentData = { const paymentData = {
method: getText('.pos-receipt-payment-method, .payment-method') || 'Cash', method: getText('.pos-receipt-payment-method, .payment-method') || 'Cash',
amount: this._parseAmount(getText('.pos-receipt-payment-amount, .payment-amount')) || totals.total, amount: this._parseAmount(getText('.pos-receipt-payment-amount, .payment-amount')) || totals.total,
change: this._parseAmount(getText('.pos-receipt-change, .change')) || 0 change: this._parseAmount(getText('.pos-receipt-change, .change')) || 0
}; };
// Footer // Footer
const footerData = { const footerData = {
message: getText('.pos-receipt-footer, .receipt-footer') || 'Thank you for your business!', message: getText('.pos-receipt-footer, .receipt-footer') || 'Thank you for your business!',
barcode: orderData.orderName || null barcode: orderData.orderName || null
}; };
const receiptData = { const receiptData = {
headerData, headerData,
orderData, orderData,
@ -670,8 +617,8 @@ patch(PosPrinterService.prototype, {
paymentData, paymentData,
footerData footerData
}; };
console.log('[BluetoothPrint] Parsed receipt data from HTML:', receiptData);
return receiptData; return receiptData;
}, },
@ -697,35 +644,28 @@ patch(PosPrinterService.prototype, {
* @returns {Object} Structured receipt data * @returns {Object} Structured receipt data
*/ */
_parseReceiptDataFromPos(pos) { _parseReceiptDataFromPos(pos) {
console.log('[BluetoothPrint] _parseReceiptDataFromPos called');
console.log('[BluetoothPrint] POS object keys:', Object.keys(pos));
// In Odoo 18, get current order from POS // In Odoo 18, get current order from POS
// Try multiple ways to access the order // Try multiple ways to access the order
let order = null; let order = null;
if (typeof pos.get_order === 'function') { if (typeof pos.get_order === 'function') {
order = pos.get_order(); order = pos.get_order();
console.log('[BluetoothPrint] Got order via get_order()');
} else if (pos.selectedOrder) { } else if (pos.selectedOrder) {
order = pos.selectedOrder; order = pos.selectedOrder;
console.log('[BluetoothPrint] Got order via selectedOrder');
} else if (pos.orders && pos.orders.length > 0) { } else if (pos.orders && pos.orders.length > 0) {
order = pos.orders[pos.orders.length - 1]; 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) { if (!order) {
throw new Error('No active order found'); throw new Error('No active order found');
} }
// Get company info // Get company info
const company = pos.company || {}; const company = pos.company || {};
console.log('[BluetoothPrint] Company:', company);
// Get cashier info - try multiple ways // Get cashier info - try multiple ways
let cashierName = ''; let cashierName = '';
if (typeof pos.get_cashier === 'function') { if (typeof pos.get_cashier === 'function') {
@ -735,8 +675,8 @@ patch(PosPrinterService.prototype, {
} else if (pos.user) { } else if (pos.user) {
cashierName = pos.user.name || ''; cashierName = pos.user.name || '';
} }
console.log('[BluetoothPrint] Cashier name:', cashierName);
// Get customer info - try multiple ways // Get customer info - try multiple ways
let customerName = null; let customerName = null;
if (typeof order.get_partner === 'function') { if (typeof order.get_partner === 'function') {
@ -746,7 +686,7 @@ patch(PosPrinterService.prototype, {
} else if (order.partner_id) { } else if (order.partner_id) {
customerName = order.partner_id[1] || null; customerName = order.partner_id[1] || null;
} }
console.log('[BluetoothPrint] Customer name:', customerName);
// Build receipt data structure // Build receipt data structure
const receiptData = { const receiptData = {
@ -771,7 +711,7 @@ patch(PosPrinterService.prototype, {
} }
}; };
console.log('[BluetoothPrint] Parsed receipt data:', receiptData);
return receiptData; return receiptData;
}, },
@ -784,19 +724,19 @@ patch(PosPrinterService.prototype, {
*/ */
_formatAddress(company) { _formatAddress(company) {
const parts = []; const parts = [];
if (company.street) parts.push(company.street); if (company.street) parts.push(company.street);
if (company.street2) parts.push(company.street2); if (company.street2) parts.push(company.street2);
const cityLine = []; const cityLine = [];
if (company.zip) cityLine.push(company.zip); if (company.zip) cityLine.push(company.zip);
if (company.city) cityLine.push(company.city); if (company.city) cityLine.push(company.city);
if (cityLine.length > 0) parts.push(cityLine.join(' ')); if (cityLine.length > 0) parts.push(cityLine.join(' '));
if (company.country_id && company.country_id[1]) { if (company.country_id && company.country_id[1]) {
parts.push(company.country_id[1]); parts.push(company.country_id[1]);
} }
return parts.join(', '); return parts.join(', ');
}, },
@ -820,12 +760,12 @@ patch(PosPrinterService.prototype, {
*/ */
_formatTotals(order) { _formatTotals(order) {
console.log('[BluetoothPrint] Formatting totals...'); console.log('[BluetoothPrint] Formatting totals...');
let subtotal = 0; let subtotal = 0;
let tax = 0; let tax = 0;
let discount = 0; let discount = 0;
let total = 0; let total = 0;
// Try multiple ways to get totals // Try multiple ways to get totals
if (typeof order.get_total_without_tax === 'function') { if (typeof order.get_total_without_tax === 'function') {
subtotal = order.get_total_without_tax(); subtotal = order.get_total_without_tax();
@ -846,9 +786,9 @@ patch(PosPrinterService.prototype, {
}); });
subtotal = total; subtotal = total;
} }
console.log('[BluetoothPrint] Totals:', { subtotal, tax, discount, total }); console.log('[BluetoothPrint] Totals:', { subtotal, tax, discount, total });
return { return {
subtotal: subtotal, subtotal: subtotal,
tax: tax, tax: tax,
@ -884,18 +824,18 @@ patch(PosPrinterService.prototype, {
*/ */
_formatOrderLines(order) { _formatOrderLines(order) {
console.log('[BluetoothPrint] Formatting order lines...'); console.log('[BluetoothPrint] Formatting order lines...');
const lines = this._getOrderLines(order); const lines = this._getOrderLines(order);
console.log('[BluetoothPrint] Found', lines.length, 'lines'); console.log('[BluetoothPrint] Found', lines.length, 'lines');
return lines.map((line, index) => { return lines.map((line, index) => {
console.log(`[BluetoothPrint] Processing line ${index}:`, line); console.log(`[BluetoothPrint] Processing line ${index}:`, line);
let productName = ''; let productName = '';
let quantity = 0; let quantity = 0;
let price = 0; let price = 0;
let total = 0; let total = 0;
// Get product name // Get product name
if (typeof line.get_product === 'function') { if (typeof line.get_product === 'function') {
const product = line.get_product(); const product = line.get_product();
@ -905,7 +845,7 @@ patch(PosPrinterService.prototype, {
} else if (line.full_product_name) { } else if (line.full_product_name) {
productName = line.full_product_name; productName = line.full_product_name;
} }
// Get quantity // Get quantity
if (typeof line.get_quantity === 'function') { if (typeof line.get_quantity === 'function') {
quantity = line.get_quantity(); quantity = line.get_quantity();
@ -914,7 +854,7 @@ patch(PosPrinterService.prototype, {
} else if (line.quantity !== undefined) { } else if (line.quantity !== undefined) {
quantity = line.quantity; quantity = line.quantity;
} }
// Get price // Get price
if (typeof line.get_unit_price === 'function') { if (typeof line.get_unit_price === 'function') {
price = line.get_unit_price(); price = line.get_unit_price();
@ -923,7 +863,7 @@ patch(PosPrinterService.prototype, {
} else if (line.price !== undefined) { } else if (line.price !== undefined) {
price = line.price; price = line.price;
} }
// Get total // Get total
if (typeof line.get_price_with_tax === 'function') { if (typeof line.get_price_with_tax === 'function') {
total = line.get_price_with_tax(); total = line.get_price_with_tax();
@ -934,9 +874,9 @@ patch(PosPrinterService.prototype, {
} else { } else {
total = quantity * price; total = quantity * price;
} }
console.log(`[BluetoothPrint] Line ${index} formatted:`, { productName, quantity, price, total }); console.log(`[BluetoothPrint] Line ${index} formatted:`, { productName, quantity, price, total });
return { return {
productName: productName, productName: productName,
quantity: quantity, quantity: quantity,
@ -955,9 +895,9 @@ patch(PosPrinterService.prototype, {
*/ */
_formatPaymentData(order) { _formatPaymentData(order) {
console.log('[BluetoothPrint] Formatting payment data...'); console.log('[BluetoothPrint] Formatting payment data...');
let paymentlines = []; let paymentlines = [];
// Get payment lines // Get payment lines
if (typeof order.get_paymentlines === 'function') { if (typeof order.get_paymentlines === 'function') {
paymentlines = order.get_paymentlines(); paymentlines = order.get_paymentlines();
@ -966,9 +906,9 @@ patch(PosPrinterService.prototype, {
} else if (order.payment_ids) { } else if (order.payment_ids) {
paymentlines = order.payment_ids; paymentlines = order.payment_ids;
} }
console.log('[BluetoothPrint] Found', paymentlines.length, 'payment lines'); console.log('[BluetoothPrint] Found', paymentlines.length, 'payment lines');
if (paymentlines.length === 0) { if (paymentlines.length === 0) {
return { return {
method: 'Unknown', method: 'Unknown',
@ -979,7 +919,7 @@ patch(PosPrinterService.prototype, {
// Use first payment method (or combine if multiple) // Use first payment method (or combine if multiple)
const payment = paymentlines[0]; const payment = paymentlines[0];
// Get payment method name // Get payment method name
let methodName = 'Cash'; let methodName = 'Cash';
if (payment.payment_method) { if (payment.payment_method) {
@ -987,12 +927,12 @@ patch(PosPrinterService.prototype, {
} else if (payment.name) { } else if (payment.name) {
methodName = payment.name; methodName = payment.name;
} }
// Calculate total paid // Calculate total paid
const totalPaid = paymentlines.reduce((sum, p) => { const totalPaid = paymentlines.reduce((sum, p) => {
return sum + (p.amount || 0); return sum + (p.amount || 0);
}, 0); }, 0);
// Get order total // Get order total
let orderTotal = 0; let orderTotal = 0;
if (typeof order.get_total_with_tax === 'function') { if (typeof order.get_total_with_tax === 'function') {
@ -1000,9 +940,9 @@ patch(PosPrinterService.prototype, {
} else if (order.amount_total !== undefined) { } else if (order.amount_total !== undefined) {
orderTotal = order.amount_total; orderTotal = order.amount_total;
} }
const change = Math.max(0, totalPaid - orderTotal); const change = Math.max(0, totalPaid - orderTotal);
console.log('[BluetoothPrint] Payment data:', { methodName, totalPaid, orderTotal, change }); console.log('[BluetoothPrint] Payment data:', { methodName, totalPaid, orderTotal, change });
return { return {
@ -1038,7 +978,7 @@ patch(PosPrinterService.prototype, {
*/ */
_showFallbackNotification(error) { _showFallbackNotification(error) {
let message = 'Bluetooth printer unavailable. Receipt sent to default printer.'; let message = 'Bluetooth printer unavailable. Receipt sent to default printer.';
if (error instanceof TimeoutError) { if (error instanceof TimeoutError) {
message = 'Bluetooth printer timeout. Receipt sent to default printer.'; message = 'Bluetooth printer timeout. Receipt sent to default printer.';
} else if (error instanceof PrinterNotConnectedError) { } else if (error instanceof PrinterNotConnectedError) {
@ -1069,9 +1009,9 @@ patch(PosPrinterService.prototype, {
errorMessage: error.message || 'Unknown error', errorMessage: error.message || 'Unknown error',
stack: error.stack stack: error.stack
}; };
console.error('Print error details:', errorInfo); console.error('Print error details:', errorInfo);
// Could send to server for logging if needed // Could send to server for logging if needed
// this.env.services.rpc(...); // this.env.services.rpc(...);
} }

View File

@ -22,7 +22,7 @@ patch(PosStore.prototype, {
*/ */
async setup() { async setup() {
await super.setup(...arguments); await super.setup(...arguments);
// Initialize bluetooth printing if enabled // Initialize bluetooth printing if enabled
if (this.config.bluetooth_printer_enabled) { if (this.config.bluetooth_printer_enabled) {
await this._initializeBluetoothPrinter(); await this._initializeBluetoothPrinter();
@ -36,14 +36,14 @@ patch(PosStore.prototype, {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async _initializeBluetoothPrinter() { async _initializeBluetoothPrinter() {
console.log('Initializing bluetooth printer for POS session...');
try { try {
const notificationService = this.env?.services?.notification || null; const notificationService = this.env?.services?.notification || null;
const errorService = getErrorNotificationService(notificationService); const errorService = getErrorNotificationService(notificationService);
const services = getBluetoothPrintingServices(notificationService); const services = getBluetoothPrintingServices(notificationService);
const { bluetoothManager, storageManager } = services; const { bluetoothManager, storageManager } = services;
// Check if Web Bluetooth API is available // Check if Web Bluetooth API is available
if (!bluetoothManager.isBluetoothAvailable()) { if (!bluetoothManager.isBluetoothAvailable()) {
console.warn('Web Bluetooth API not available in this browser'); console.warn('Web Bluetooth API not available in this browser');
@ -56,23 +56,23 @@ patch(PosStore.prototype, {
// Load printer configuration from local storage // Load printer configuration from local storage
const config = storageManager.loadConfiguration(this.config.id); const config = storageManager.loadConfiguration(this.config.id);
if (!config) { if (!config) {
// No printer configured for this device // 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(); this._promptPrinterConfiguration();
return; return;
} }
console.log('Found printer configuration:', config.deviceName); // console.log('Found printer configuration:', config.deviceName);
// Enable auto-reconnect based on saved settings // Enable auto-reconnect based on saved settings
const autoReconnect = config.settings?.autoReconnect !== false; const autoReconnect = config.settings?.autoReconnect !== false;
bluetoothManager.setAutoReconnect(autoReconnect); bluetoothManager.setAutoReconnect(autoReconnect);
// Attempt to connect to the configured printer // Attempt to connect to the configured printer
await this._connectToConfiguredPrinter(config); await this._connectToConfiguredPrinter(config);
} catch (error) { } catch (error) {
console.error('Failed to initialize bluetooth printer:', error); console.error('Failed to initialize bluetooth printer:', error);
const errorService = getErrorNotificationService(); const errorService = getErrorNotificationService();
@ -92,17 +92,17 @@ patch(PosStore.prototype, {
const errorService = getErrorNotificationService(notificationService); const errorService = getErrorNotificationService(notificationService);
const services = getBluetoothPrintingServices(notificationService); const services = getBluetoothPrintingServices(notificationService);
const { bluetoothManager } = services; const { bluetoothManager } = services;
try { 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 // Try to get the previously paired device
const devices = await navigator.bluetooth.getDevices(); const devices = await navigator.bluetooth.getDevices();
const device = devices.find(d => const device = devices.find(d =>
d.id === config.deviceId || d.id === config.deviceId ||
d.name === config.deviceName d.name === config.deviceName
); );
if (!device) { if (!device) {
console.warn('Previously configured printer not found'); console.warn('Previously configured printer not found');
errorService.showNotification( errorService.showNotification(
@ -115,13 +115,13 @@ patch(PosStore.prototype, {
// Connect to the printer // Connect to the printer
await bluetoothManager.connectToPrinter(device); 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 // Success notification is now handled by error service in bluetooth manager
} catch (error) { } catch (error) {
console.error('Failed to connect to bluetooth printer:', error); console.error('Failed to connect to bluetooth printer:', error);
// Error is now handled by error notification service in bluetooth manager // Error is now handled by error notification service in bluetooth manager
// Just log additional context here // Just log additional context here
errorService.logError(error, { errorService.logError(error, {
@ -139,13 +139,13 @@ patch(PosStore.prototype, {
_promptPrinterConfiguration() { _promptPrinterConfiguration() {
const notificationService = this.env?.services?.notification || null; const notificationService = this.env?.services?.notification || null;
const errorService = getErrorNotificationService(notificationService); const errorService = getErrorNotificationService(notificationService);
// Show notification prompting user to configure printer // Show notification prompting user to configure printer
errorService.showNotification( errorService.showNotification(
'No bluetooth printer configured. Please configure a printer in POS settings.', 'No bluetooth printer configured. Please configure a printer in POS settings.',
'info' 'info'
); );
// Could trigger opening the configuration dialog here // Could trigger opening the configuration dialog here
// For now, we just notify the user // For now, we just notify the user
}, },
@ -160,7 +160,7 @@ patch(PosStore.prototype, {
if (this.config.bluetooth_printer_enabled) { if (this.config.bluetooth_printer_enabled) {
await this._cleanupBluetoothPrinter(); await this._cleanupBluetoothPrinter();
} }
// Call parent closePos // Call parent closePos
return super.closePos(...arguments); return super.closePos(...arguments);
}, },
@ -172,19 +172,19 @@ patch(PosStore.prototype, {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async _cleanupBluetoothPrinter() { async _cleanupBluetoothPrinter() {
console.log('Cleaning up bluetooth printer connection...'); // console.log('Cleaning up bluetooth printer connection...');
try { try {
const services = getBluetoothPrintingServices(); const services = getBluetoothPrintingServices();
const { bluetoothManager } = services; const { bluetoothManager } = services;
// Check if there's an active connection // Check if there's an active connection
const status = bluetoothManager.getConnectionStatus(); const status = bluetoothManager.getConnectionStatus();
if (status === 'connected' || status === 'connecting') { if (status === 'connected' || status === 'connecting') {
console.log('Disconnecting bluetooth printer...'); // console.log('Disconnecting bluetooth printer...');
await bluetoothManager.disconnect(); await bluetoothManager.disconnect();
console.log('Bluetooth printer disconnected'); // console.log('Bluetooth printer disconnected');
} }
} catch (error) { } catch (error) {
console.error('Error cleaning up bluetooth printer:', error); console.error('Error cleaning up bluetooth printer:', error);
@ -208,7 +208,7 @@ patch(PosStore.prototype, {
try { try {
const services = getBluetoothPrintingServices(); const services = getBluetoothPrintingServices();
const { bluetoothManager } = services; const { bluetoothManager } = services;
return { return {
enabled: true, enabled: true,
...bluetoothManager.getConnectionInfo() ...bluetoothManager.getConnectionInfo()
@ -258,7 +258,7 @@ patch(PosStore.prototype, {
sticky: false sticky: false
}); });
} else { } else {
console.log(`[${type.toUpperCase()}] ${message}`); // console.log(`[${type.toUpperCase()}] ${message}`);
} }
} }
}); });