/** @odoo-module **/ import { Component, useState, onWillStart } from "@odoo/owl"; import { useService } from "@web/core/utils/hooks"; import { Dialog } from "@web/core/dialog/dialog"; import { BluetoothPrinterManager } from "./bluetooth_printer_manager"; import { BluetoothPrinterStorage } from "./storage_manager"; import { EscPosGenerator } from "./escpos_generator"; /** * Bluetooth Printer Configuration Component * * Provides UI for: * - Scanning and discovering bluetooth devices * - Selecting and pairing with a printer * - Configuring printer settings (character set, paper width) * - Testing printer connection * - Managing printer connections */ export class BluetoothPrinterConfig extends Component { static template = "pos_bluetooth_thermal_printer.BluetoothPrinterConfig"; static components = { Dialog }; setup() { this.notification = useService("notification"); this.state = useState({ // Scanning state isScanning: false, availableDevices: [], selectedDevice: null, // Connection state isConnecting: false, isConnected: false, connectedDevice: null, // Configuration state characterSet: 'CP437', paperWidth: 48, paperWidthMm: 58, // Paper width in millimeters (58mm or 80mm) dpi: 203, // Printer DPI (203 or 304) autoReconnect: true, timeout: 10000, // UI state showConfiguration: false, isTesting: false, // Error state lastError: null }); // Initialize services // Use the bluetoothManager from props if provided, otherwise create new one this.bluetoothManager = this.props.bluetoothManager || new BluetoothPrinterManager(); this.storageManager = new BluetoothPrinterStorage(); this.escposGenerator = new EscPosGenerator(); // Get POS config ID from props this.posConfigId = this.props.posConfigId || 1; onWillStart(() => { this._loadSavedConfiguration(); }); } /** * Load saved configuration from local storage * @private */ async _loadSavedConfiguration() { try { const config = this.storageManager.loadConfiguration(this.posConfigId); if (config) { this.state.connectedDevice = { id: config.deviceId, name: config.deviceName, macAddress: config.macAddress }; if (config.settings) { this.state.characterSet = config.settings.characterSet || 'CP437'; this.state.paperWidth = config.settings.paperWidth || 48; this.state.paperWidthMm = config.settings.paperWidthMm || 58; this.state.dpi = config.settings.dpi || 203; this.state.autoReconnect = config.settings.autoReconnect !== false; this.state.timeout = config.settings.timeout || 10000; } this.state.showConfiguration = true; } } catch (error) { console.error('Failed to load saved configuration:', error); this._showNotification('Failed to load saved configuration', 'warning'); } } /** * Handle scan devices button click */ async onScanDevices() { this.state.isScanning = true; this.state.lastError = null; this.state.availableDevices = []; try { const devices = await this.bluetoothManager.scanDevices(); // Map devices to include display information this.state.availableDevices = devices.map(device => ({ id: device.id, name: device.name || 'Unknown Device', device: device, // Signal strength would require additional BLE APIs // For now, we'll show a placeholder signalStrength: 'Good' })); if (this.state.availableDevices.length === 0) { this._showNotification('No bluetooth devices found', 'warning'); } else { this._showNotification( `Found ${this.state.availableDevices.length} device(s)`, 'success' ); } } catch (error) { console.error('Device scan failed:', error); this.state.lastError = error.message; if (error.name === 'BluetoothNotAvailableError') { this._showNotification( 'Bluetooth is not available in this browser. Please use Chrome, Edge, or Opera.', 'danger' ); } else if (error.name === 'UserCancelledError') { this._showNotification('Device selection cancelled', 'info'); } else { this._showNotification(`Scan failed: ${error.message}`, 'danger'); } } finally { this.state.isScanning = false; } } /** * Handle device selection * @param {Object} device - Selected device object */ async onSelectDevice(device) { this.state.selectedDevice = device; this.state.isConnecting = true; this.state.lastError = null; try { // Connect to the selected device const connection = await this.bluetoothManager.connectToPrinter(device.device); this.state.isConnected = true; this.state.connectedDevice = { id: connection.deviceId, name: connection.deviceName, macAddress: device.id // Using device ID as MAC address placeholder }; // Save configuration await this._saveConfiguration(); this.state.showConfiguration = true; this._showNotification( `Connected to ${connection.deviceName}`, 'success' ); } catch (error) { console.error('Connection failed:', error); this.state.lastError = error.message; this.state.isConnected = false; this._showNotification( `Failed to connect: ${error.message}`, 'danger' ); } finally { this.state.isConnecting = false; } } /** * Handle test print button click */ async onTestPrint() { if (!this.state.isConnected) { this._showNotification('Please connect to a printer first', 'warning'); return; } this.state.isTesting = true; this.state.lastError = null; try { // Generate test receipt data const testReceiptData = this._generateTestReceiptData(); // Convert to ESC/POS commands const escposData = this.escposGenerator.generateReceipt(testReceiptData); // Send to printer with timeout await this._sendWithTimeout(escposData, this.state.timeout); this._showNotification('Test print sent successfully!', 'success'); } catch (error) { console.error('Test print failed:', error); this.state.lastError = error.message; if (error.name === 'TimeoutError') { this._showNotification( 'Test print timed out. Please check printer connection.', 'danger' ); } else if (error.name === 'PrinterNotConnectedError') { this.state.isConnected = false; this._showNotification( 'Printer disconnected. Please reconnect.', 'danger' ); } else { this._showNotification( `Test print failed: ${error.message}`, 'danger' ); } } finally { this.state.isTesting = false; } } /** * Handle disconnect button click */ async onDisconnect() { try { await this.bluetoothManager.disconnect(); this.state.isConnected = false; this.state.connectedDevice = null; this.state.selectedDevice = null; this.state.showConfiguration = false; this._showNotification('Printer disconnected', 'info'); } catch (error) { console.error('Disconnect failed:', error); this._showNotification(`Disconnect failed: ${error.message}`, 'danger'); } } /** * Handle character set change * @param {Event} event - Change event */ onCharacterSetChange(event) { this.state.characterSet = event.target.value; this._saveConfiguration(); } /** * Handle paper width change * @param {Event} event - Change event */ onPaperWidthChange(event) { this.state.paperWidth = parseInt(event.target.value, 10); this._saveConfiguration(); } /** * Handle paper width (mm) change * @param {Event} event - Change event */ onPaperWidthMmChange(event) { this.state.paperWidthMm = parseInt(event.target.value, 10); // Auto-adjust character width based on paper size if (this.state.paperWidthMm === 58) { this.state.paperWidth = 32; } else if (this.state.paperWidthMm === 80) { this.state.paperWidth = 48; } this._saveConfiguration(); } /** * Handle DPI change * @param {Event} event - Change event */ onDpiChange(event) { this.state.dpi = parseInt(event.target.value, 10); this._saveConfiguration(); } /** * Handle auto-reconnect toggle * @param {Event} event - Change event */ onAutoReconnectChange(event) { this.state.autoReconnect = event.target.checked; this.bluetoothManager.setAutoReconnect(this.state.autoReconnect); this._saveConfiguration(); } /** * Handle timeout change * @param {Event} event - Change event */ onTimeoutChange(event) { this.state.timeout = parseInt(event.target.value, 10); this._saveConfiguration(); } /** * Save current configuration to local storage * @private */ async _saveConfiguration() { if (!this.state.connectedDevice) { return; } try { const config = { deviceId: this.state.connectedDevice.id, deviceName: this.state.connectedDevice.name, macAddress: this.state.connectedDevice.macAddress, lastConnected: Date.now(), settings: { characterSet: this.state.characterSet, paperWidth: this.state.paperWidth, paperWidthMm: this.state.paperWidthMm, dpi: this.state.dpi, autoReconnect: this.state.autoReconnect, timeout: this.state.timeout } }; this.storageManager.saveConfiguration(this.posConfigId, config); } catch (error) { console.error('Failed to save configuration:', error); this._showNotification('Failed to save configuration', 'warning'); } } /** * Generate test receipt data * @private * @returns {Object} Test receipt data */ _generateTestReceiptData() { return { headerData: { companyName: 'TEST RECEIPT', address: '123 Test Street, Test City', phone: 'Tel: +1 234 567 8900', taxId: 'Tax ID: TEST123456' }, orderData: { orderName: 'TEST-001', date: new Date().toLocaleString(), cashier: 'Test Cashier', customer: 'Test Customer' }, lines: [ { productName: 'Test Product 1', quantity: 2, price: 10.00, total: 20.00 }, { productName: 'Test Product 2', quantity: 1, price: 15.50, total: 15.50 }, { productName: 'Test Product 3', quantity: 3, price: 5.00, total: 15.00 } ], totals: { subtotal: 50.50, tax: 5.05, discount: 0, total: 55.55 }, paymentData: { method: 'Cash', amount: 60.00, change: 4.45 }, footerData: { message: 'Thank you for your purchase!\nThis is a test receipt.', barcode: null } }; } /** * Send data to printer with timeout * @private * @param {Uint8Array} data - Data to send * @param {number} timeoutMs - Timeout in milliseconds * @returns {Promise} */ async _sendWithTimeout(data, timeoutMs) { const timeoutPromise = new Promise((_, reject) => { setTimeout(() => { const error = new Error('Print operation timed out'); error.name = 'TimeoutError'; reject(error); }, timeoutMs); }); const sendPromise = this.bluetoothManager.sendData(data); return Promise.race([sendPromise, timeoutPromise]); } /** * Show notification to user * @private * @param {string} message - Notification message * @param {string} type - Notification type: 'success', 'danger', 'warning', 'info' */ _showNotification(message, type = 'info') { this.notification.add(message, { type: type, sticky: false }); } /** * Get CSS class for device item * @param {Object} device - Device object * @returns {string} CSS class */ getDeviceItemClass(device) { const baseClass = 'bluetooth-device-item'; const selectedClass = this.state.selectedDevice?.id === device.id ? 'selected' : ''; return `${baseClass} ${selectedClass}`.trim(); } /** * Check if a device is selected * @param {Object} device - Device object * @returns {boolean} */ isDeviceSelected(device) { return this.state.selectedDevice?.id === device.id; } /** * Close the dialog */ close() { if (this.props.close) { this.props.close(); } } } export default BluetoothPrinterConfig;