- Add boxes_crates database table with BIGINT IDs and 8-digit auto-numbered box_numbers - Implement boxes CRUD operations (add, edit, update, delete, delete_multiple) - Create boxes route handlers with POST actions for all operations - Add boxes.html template with 3-panel layout matching warehouse locations module - Implement barcode generation and printing with JsBarcode and QZ Tray integration - Add browser print fallback for when QZ Tray is not available - Simplify create box form to single button with auto-generation - Fix JavaScript null reference errors with proper element validation - Convert tuple data to dictionaries for Jinja2 template compatibility - Register boxes blueprint in Flask app initialization
296 lines
11 KiB
JavaScript
296 lines
11 KiB
JavaScript
/**
|
|
* QZ Tray Printer Module
|
|
* Shared printer functionality for all pages
|
|
* Provides printer detection, selection, and printing capabilities
|
|
*/
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
// Global printer state
|
|
window.qzPrinter = {
|
|
connected: false,
|
|
availablePrinters: [],
|
|
selectedPrinter: '',
|
|
|
|
/**
|
|
* Initialize QZ Tray connection
|
|
* @returns {Promise<boolean>} True if connected, false otherwise
|
|
*/
|
|
initialize: async function() {
|
|
try {
|
|
console.log('Initializing QZ Tray...');
|
|
|
|
if (typeof qz === 'undefined') {
|
|
console.warn('QZ Tray library not loaded');
|
|
return false;
|
|
}
|
|
|
|
// Try to connect
|
|
await qz.websocket.connect();
|
|
this.connected = true;
|
|
console.log('✅ QZ Tray connected');
|
|
|
|
// Load available printers
|
|
await this.loadPrinters();
|
|
|
|
return true;
|
|
|
|
} catch (error) {
|
|
console.warn('QZ Tray not available:', error.message);
|
|
this.connected = false;
|
|
return false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Load available printers from QZ Tray
|
|
* @returns {Promise<Array>} Array of printer names
|
|
*/
|
|
loadPrinters: async function() {
|
|
try {
|
|
if (!this.connected) return [];
|
|
|
|
const printers = await qz.printers.find();
|
|
this.availablePrinters = printers;
|
|
|
|
console.log('Loaded printers:', printers);
|
|
|
|
// Auto-select first thermal printer if available
|
|
const thermalPrinter = printers.find(p =>
|
|
p.toLowerCase().includes('thermal') ||
|
|
p.toLowerCase().includes('label') ||
|
|
p.toLowerCase().includes('zebra')
|
|
);
|
|
|
|
if (thermalPrinter) {
|
|
this.selectedPrinter = thermalPrinter;
|
|
console.log('Auto-selected thermal printer:', thermalPrinter);
|
|
}
|
|
|
|
return printers;
|
|
|
|
} catch (error) {
|
|
console.error('Error loading printers:', error);
|
|
return [];
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Get printer selection dropdown HTML
|
|
* @param {string} selectId - ID for the select element
|
|
* @returns {string} HTML string for printer select dropdown
|
|
*/
|
|
getPrinterSelectHTML: function(selectId = 'printer-select') {
|
|
let html = `<select id="${selectId}" class="form-select form-select-sm">
|
|
<option value="">Default Printer</option>`;
|
|
|
|
this.availablePrinters.forEach(printer => {
|
|
const selected = printer === this.selectedPrinter ? ' selected' : '';
|
|
html += `<option value="${printer}"${selected}>${printer}</option>`;
|
|
});
|
|
|
|
html += '</select>';
|
|
return html;
|
|
},
|
|
|
|
/**
|
|
* Update printer selection
|
|
* @param {string} printerName - Name of printer to select
|
|
*/
|
|
selectPrinter: function(printerName) {
|
|
this.selectedPrinter = printerName || '';
|
|
console.log('Selected printer:', this.selectedPrinter);
|
|
},
|
|
|
|
/**
|
|
* Test QZ Tray connection
|
|
* @returns {boolean} True if connected
|
|
*/
|
|
test: function() {
|
|
if (!this.connected) {
|
|
alert('QZ Tray is not connected.\nBrowser print will be used instead.');
|
|
return false;
|
|
}
|
|
|
|
const printerList = this.availablePrinters.length > 0
|
|
? this.availablePrinters.join('\n• ')
|
|
: 'No printers found';
|
|
|
|
alert('✅ QZ Tray is connected and ready!\n\nAvailable printers:\n• ' + printerList);
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* Print barcode using QZ Tray
|
|
* @param {string} barcodeData - Barcode value
|
|
* @param {string} printerName - Printer to use (optional, uses selected)
|
|
* @param {Object} options - Print options
|
|
* @returns {Promise<void>}
|
|
*/
|
|
printBarcode: async function(barcodeData, printerName, options = {}) {
|
|
try {
|
|
if (!this.connected) {
|
|
throw new Error('QZ Tray not connected');
|
|
}
|
|
|
|
const targetPrinter = printerName || this.selectedPrinter;
|
|
console.log('Printing to:', targetPrinter, 'Data:', barcodeData);
|
|
|
|
// Default print options
|
|
const printConfig = {
|
|
printer: targetPrinter,
|
|
colorType: options.colorType || 'color',
|
|
copies: options.copies || 1
|
|
};
|
|
|
|
// Barcode data with default configuration
|
|
const printData = [{
|
|
type: 'barcode',
|
|
format: options.format || 'CODE128',
|
|
data: barcodeData,
|
|
width: options.width || 2,
|
|
height: options.height || 100,
|
|
displayValue: options.displayValue !== false
|
|
}];
|
|
|
|
// Add optional label
|
|
if (options.label) {
|
|
printData.push({
|
|
type: 'text',
|
|
data: options.label,
|
|
position: options.labelPosition || {x: 0.5, y: 2.2},
|
|
font: options.font || {family: 'Arial', size: 12, weight: 'bold'}
|
|
});
|
|
}
|
|
|
|
// Send to printer
|
|
await qz.print(printConfig, printData);
|
|
console.log('✅ Print job sent to', targetPrinter);
|
|
return true;
|
|
|
|
} catch (error) {
|
|
console.error('QZ Tray printing error:', error);
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Print SVG/HTML barcode using QZ Tray
|
|
* @param {string} svgElement - SVG element or selector
|
|
* @param {string} barcodeText - Text to display with barcode
|
|
* @param {string} printerName - Printer to use (optional)
|
|
* @returns {Promise<void>}
|
|
*/
|
|
printSVGBarcode: async function(svgElement, barcodeText, printerName) {
|
|
try {
|
|
if (!this.connected) {
|
|
throw new Error('QZ Tray not connected');
|
|
}
|
|
|
|
// If no printer specified, use the selected printer or default
|
|
let targetPrinter = printerName || this.selectedPrinter;
|
|
|
|
// Get SVG element
|
|
let svgEl = typeof svgElement === 'string'
|
|
? document.querySelector(svgElement)
|
|
: svgElement;
|
|
|
|
if (!svgEl) {
|
|
throw new Error('Barcode SVG element not found');
|
|
}
|
|
|
|
// Serialize SVG to string and encode as base64
|
|
const svgString = new XMLSerializer().serializeToString(svgEl);
|
|
const svgBase64 = btoa(svgString);
|
|
|
|
// If still no printer, get the device default
|
|
if (!targetPrinter) {
|
|
try {
|
|
const defaultPrinter = await qz.printers.getDefault();
|
|
targetPrinter = defaultPrinter;
|
|
console.log('Using device default printer:', targetPrinter);
|
|
} catch (err) {
|
|
console.warn('Could not get default printer, using system default');
|
|
targetPrinter = ''; // Empty string uses system default
|
|
}
|
|
}
|
|
|
|
const printConfig = {
|
|
printer: targetPrinter,
|
|
colorType: 'color',
|
|
copies: 1
|
|
};
|
|
|
|
const printData = [{
|
|
type: 'image',
|
|
format: 'base64',
|
|
data: svgBase64,
|
|
width: 3,
|
|
height: 1.5,
|
|
position: {x: 0.5, y: 0.5}
|
|
}];
|
|
|
|
if (barcodeText) {
|
|
printData.push({
|
|
type: 'text',
|
|
data: barcodeText,
|
|
position: {x: 0.5, y: 2.2},
|
|
font: {family: 'Arial', size: 12, weight: 'bold'}
|
|
});
|
|
}
|
|
|
|
console.log('Printing to thermal printer:', targetPrinter);
|
|
await qz.print(printConfig, printData);
|
|
console.log('✅ SVG print job sent to', targetPrinter);
|
|
return true;
|
|
|
|
} catch (error) {
|
|
console.error('QZ Tray SVG printing error:', error);
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Fallback to browser print
|
|
* @param {string} title - Print document title
|
|
* @param {string} content - HTML content to print
|
|
*/
|
|
printBrowser: function(title, content) {
|
|
const printWindow = window.open('', '', 'height=400,width=600');
|
|
printWindow.document.write('<html><head><title>' + title + '</title>');
|
|
printWindow.document.write('<style>');
|
|
printWindow.document.write('body { font-family: Arial, sans-serif; text-align: center; padding: 20px; }');
|
|
printWindow.document.write('h2 { margin-bottom: 30px; }');
|
|
printWindow.document.write('svg { max-width: 100%; height: auto; margin: 20px 0; }');
|
|
printWindow.document.write('.content { margin: 20px 0; }');
|
|
printWindow.document.write('</style></head><body>');
|
|
printWindow.document.write('<h2>' + title + '</h2>');
|
|
printWindow.document.write('<div class="content">' + content + '</div>');
|
|
printWindow.document.write('<p style="margin-top: 40px; font-size: 14px; color: #666;">');
|
|
printWindow.document.write('Printed on: ' + new Date().toLocaleString());
|
|
printWindow.document.write('</p></body></html>');
|
|
printWindow.document.close();
|
|
|
|
setTimeout(() => {
|
|
printWindow.print();
|
|
}, 250);
|
|
}
|
|
};
|
|
|
|
// Auto-initialize when document is ready
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
if (typeof qz !== 'undefined') {
|
|
window.qzPrinter.initialize();
|
|
}
|
|
});
|
|
} else {
|
|
// Document already loaded
|
|
if (typeof qz !== 'undefined') {
|
|
window.qzPrinter.initialize();
|
|
}
|
|
}
|
|
|
|
})();
|