pos_bluetooth_thermal_printer/static/src/js/bluetooth_printer_config.js
2025-12-07 21:36:21 +07:00

474 lines
15 KiB
JavaScript

/** @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<boolean>}
*/
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;