Implement boxes management module with auto-numbered box creation
- 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
This commit is contained in:
295
app/static/js/qz-printer.js
Normal file
295
app/static/js/qz-printer.js
Normal file
@@ -0,0 +1,295 @@
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user