474 lines
15 KiB
JavaScript
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;
|