Files
quality_recticel/backup/print_module.html.old 2backup2

1374 lines
61 KiB
Plaintext
Raw Blame History

{% extends "base.html" %}
{% block head %}
<!-- JsBarcode library for real barcode generation -->
<script src="https://cdn.jsdelivr.net/npm/jsbarcode@3.11.5/dist/JsBarcode.all.min.js"></script>
<style>
#label-preview {
background: #fafafa;
position: relative;
overflow: hidden;
}
/* Enhanced table styling */
.card.scan-table-card table.print-module-table.scan-table thead th {
border-bottom: 2px solid #dee2e6 !important;
background-color: #f8f9fa !important;
padding: 0.25rem 0.4rem !important;
text-align: left !important;
font-weight: 600 !important;
font-size: 10px !important;
line-height: 1.2 !important;
}
.card.scan-table-card table.print-module-table.scan-table {
width: 100% !important;
border-collapse: collapse !important;
}
.card.scan-table-card table.print-module-table.scan-table tbody tr:hover td {
background-color: #f8f9fa !important;
cursor: pointer !important;
}
.card.scan-table-card table.print-module-table.scan-table tbody tr.selected td {
background-color: #007bff !important;
color: white !important;
}
</style>
{% endblock %}
{% block content %}
<div class="scan-container" style="display: flex; flex-direction: row; gap: 20px; width: 100%; align-items: flex-start;">
<!-- Label Preview Card -->
<div class="card scan-form-card" style="display: flex; flex-direction: column; justify-content: flex-start; align-items: center; min-height: 700px; width: 330px; flex-shrink: 0; position: relative; padding: 15px;">
<div class="label-view-title" style="width: 100%; text-align: center; padding: 0 0 15px 0; font-size: 18px; font-weight: bold; letter-spacing: 0.5px;">Label View</div>
<!-- Add link to pairing key management -->
<div style="width: 100%; text-align: center; margin-bottom: 10px;">
<a href="{{ url_for('main.download_extension') }}" class="btn btn-info btn-sm" target="_blank">🔑 Manage Pairing Keys</a>
</div>
<!-- Client/Printer selection dropdown -->
<div style="width: 100%; text-align: center; margin-bottom: 10px;">
<label for="client-select" style="font-weight: 600;">Select Printer/Client:</label>
<select id="client-select" class="form-control form-control-sm" style="width: 80%; margin: 0 auto;"></select>
</div>
<!-- Display pairing key for selected client -->
<div id="pairing-key-display" style="width: 100%; text-align: center; margin-bottom: 15px; font-size: 13px; color: #007bff; font-family: monospace;"></div>
<!-- Label Preview Section -->
<div id="label-preview" style="border: 1px solid #ddd; padding: 10px; position: relative; background: #fafafa; width: 301px; height: 434.7px;">
<!-- Label content rectangle -->
<div id="label-content" style="position: absolute; top: 65.7px; left: 11.34px; width: 227.4px; height: 321.3px; border: 2px solid #333; background: white;">
<!-- Top row content: Company name -->
<div style="position: absolute; top: 0; left: 0; right: 0; height: 32.13px; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 12px; color: #000; z-index: 10;">
INNOFA ROMANIA SRL
</div>
<!-- Row 2 content: Customer Name -->
<div id="customer-name-row" style="position: absolute; top: 32.13px; left: 0; right: 0; height: 32.13px; display: flex; align-items: center; justify-content: center; font-size: 11px; color: #000;">
<!-- Customer name will be populated here -->
</div>
<!-- Horizontal dividing lines -->
<div style="position: absolute; top: 32.13px; left: 0; right: 0; height: 1px; background: #999;"></div>
<div style="position: absolute; top: 64.26px; left: 0; right: 0; height: 1px; background: #999;"></div>
<div style="position: absolute; top: 96.39px; left: 0; right: 0; height: 1px; background: #999;"></div>
<div style="position: absolute; top: 128.52px; left: 0; right: 0; height: 1px; background: #999;"></div>
<div style="position: absolute; top: 160.65px; left: 0; right: 0; height: 1px; background: #999;"></div>
<div style="position: absolute; top: 224.91px; left: 0; right: 0; height: 1px; background: #999;"></div>
<div style="position: absolute; top: 257.04px; left: 0; right: 0; height: 1px; background: #999;"></div>
<div style="position: absolute; top: 289.17px; left: 0; right: 0; height: 1px; background: #999;"></div>
<!-- Vertical dividing line -->
<div style="position: absolute; left: 90.96px; top: 64.26px; width: 1px; height: 257.04px; background: #999;"></div>
<!-- Row 3: Quantity ordered -->
<div style="position: absolute; top: 64.26px; left: 0; width: 90.96px; height: 32.13px; display: flex; align-items: center; padding-left: 5px; font-size: 10px; color: #000;">
Quantity ordered
</div>
<div id="quantity-ordered-value" style="position: absolute; top: 64.26px; left: 90.96px; width: 136.44px; height: 32.13px; display: flex; align-items: center; justify-content: center; font-size: 13px; font-weight: bold; color: #000;">
<!-- Quantity value will be populated here -->
</div>
<!-- Row 4: Customer order -->
<div style="position: absolute; top: 96.39px; left: 0; width: 90.96px; height: 32.13px; display: flex; align-items: center; padding-left: 5px; font-size: 10px; color: #000;">
Customer order
</div>
<div id="client-order-info" style="position: absolute; top: 96.39px; left: 90.96px; width: 136.44px; height: 32.13px; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: bold; color: #000;">
<!-- Client order info will be populated here -->
</div>
<!-- Row 5: Delivery date -->
<div style="position: absolute; top: 128.52px; left: 0; width: 90.96px; height: 32.13px; display: flex; align-items: center; padding-left: 5px; font-size: 10px; color: #000;">
Delivery date
</div>
<div id="delivery-date-value" style="position: absolute; top: 128.52px; left: 90.96px; width: 136.44px; height: 32.13px; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: bold; color: #000;">
<!-- Delivery date value will be populated here -->
</div>
<!-- Row 6: Description (double height) -->
<div style="position: absolute; top: 160.65px; left: 0; width: 90.96px; height: 64.26px; display: flex; align-items: center; padding-left: 5px; font-size: 10px; color: #000;">
Product description
</div>
<div id="description-value" style="position: absolute; top: 160.65px; left: 90.96px; width: 136.44px; height: 64.26px; display: flex; align-items: center; justify-content: center; font-size: 8px; color: #000; text-align: center; padding: 2px; overflow: hidden;">
<!-- Description will be populated here -->
</div>
<!-- Row 7: Size -->
<div style="position: absolute; top: 224.91px; left: 0; width: 90.96px; height: 32.13px; display: flex; align-items: center; padding-left: 5px; font-size: 10px; color: #000;">
Size
</div>
<div id="size-value" style="position: absolute; top: 224.91px; left: 90.96px; width: 136.44px; height: 32.13px; display: flex; align-items: center; justify-content: center; font-size: 10px; font-weight: bold; color: #000;">
<!-- Size value will be populated here -->
</div>
<!-- Row 8: Article Code -->
<div style="position: absolute; top: 257.04px; left: 0; width: 90.96px; height: 32.13px; display: flex; align-items: center; padding-left: 5px; font-size: 10px; color: #000;">
Article code
</div>
<div id="article-code-value" style="position: absolute; top: 257.04px; left: 90.96px; width: 136.44px; height: 32.13px; display: flex; align-items: center; justify-content: center; font-size: 9px; font-weight: bold; color: #000;">
<!-- Article code will be populated here -->
</div>
<!-- Row 9: Production Order -->
<div style="position: absolute; top: 289.17px; left: 0; width: 90.96px; height: 32.13px; display: flex; align-items: center; padding-left: 5px; font-size: 10px; color: #000;">
Prod. order
</div>
<div id="prod-order-value" style="position: absolute; top: 289.17px; left: 90.96px; width: 136.44px; height: 32.13px; display: flex; align-items: center; justify-content: center; font-size: 10px; font-weight: bold; color: #000;">
<!-- Production order will be populated here -->
</div>
</div>
<!-- Barcode Frame - positioned 10px below rectangle with 2mm side margins -->
<div id="barcode-frame" style="position: absolute; top: 395px; left: 7.56px; width: 295.44px; height: 50px; background: white; display: flex; flex-direction: column; align-items: center; justify-content: center;">
<!-- Code 128 Barcode representation -->
<svg id="barcode-display" style="width: 280px; height: 40px;"></svg>
<!-- Barcode text below the bars -->
<div id="barcode-text" style="font-size: 8px; font-family: 'Courier New', monospace; margin-top: 2px; text-align: center; font-weight: bold;">
<!-- Barcode text will be populated here -->
</div>
</div>
<!-- Vertical Barcode Frame - positioned on the right side of the label -->
<div id="vertical-barcode-frame" style="position: absolute; top: 70px; left: 245px; width: 50px; height: 309.96px; background: white; display: flex; flex-direction: column; align-items: center; justify-content: center;">
<!-- Vertical Code 128 Barcode representation -->
<svg id="vertical-barcode-display" style="width: 45px; height: 280px;"></svg>
<!-- Vertical barcode text -->
<div id="vertical-barcode-text" style="font-size: 6px; font-family: 'Courier New', monospace; margin-top: 5px; text-align: center; font-weight: bold; writing-mode: vertical-rl; text-orientation: mixed;">
<!-- Vertical barcode text will be populated here -->
</div>
</div>
</div>
<!-- Print Options -->
<div style="width: 100%; margin-top: 20px;">
<!-- Print Method Selection -->
<div style="margin-bottom: 15px;">
<label style="font-size: 12px; font-weight: 600; color: #495057; margin-bottom: 8px; display: block;">
📄 Print Method:
</label>
<div class="form-check mb-2">
<input class="form-check-input" type="radio" name="printMethod" id="qzTrayPrint" value="qztray">
<label class="form-check-label" for="qzTrayPrint" style="font-size: 11px; line-height: 1.3;">
<strong>QZ Tray Direct Print</strong> <span id="qztray-status" class="badge badge-secondary">Checking...</span><br>
<span class="text-muted">Direct print to thermal label printer</span>
</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="radio" name="printMethod" id="pdfGenerate" value="pdf" checked>
<label class="form-check-label" for="pdfGenerate" style="font-size: 11px; line-height: 1.3;">
<strong>Generate PDF</strong><br>
<span class="text-muted">Create PDF for manual printing (recommended)</span>
</label>
</div>
</div>
<!-- Printer Selection for QZ Tray -->
<div id="qztray-printer-selection" style="margin-bottom: 15px; display: none;">
<label style="font-size: 12px; font-weight: 600; color: #495057; margin-bottom: 5px; display: block;">
🖨️ Select Printer:
</label>
<select id="qztray-printer-select" class="form-control form-control-sm" style="font-size: 11px; padding: 4px 8px;">
<option value="">Loading printers...</option>
</select>
<small class="text-muted" style="font-size: 10px;">Choose your thermal label printer</small>
</div>
<!-- Print Button -->
<div style="width: 100%; text-align: center; margin-bottom: 15px;">
<button id="print-label-btn" class="btn btn-success" style="font-size: 14px; padding: 10px 30px; border-radius: 6px; font-weight: 600;">
📄 Generate PDF Labels
</button>
</div>
<!-- Print Information -->
<div style="width: 100%; text-align: center; color: #6c757d; font-size: 11px; line-height: 1.4;">
<div style="margin-bottom: 5px;">Creates sequential labels based on quantity</div>
<small>(e.g., CP00000711-001 to CP00000711-063)</small>
</div>
<!-- QZ Tray Installation Info -->
<div id="qztray-info" style="width: 100%; margin-top: 15px; padding-top: 15px; border-top: 1px solid #e9ecef; display: none;">
<div style="background: #e8f4fd; border: 1px solid #bee5eb; border-radius: 6px; padding: 12px; margin-bottom: 10px;">
<div style="font-size: 11px; color: #0c5460; margin-bottom: 8px;">
<strong>🖨️ QZ Tray Direct Printing</strong>
</div>
<div style="font-size: 10px; color: #495057; margin-bottom: 10px; line-height: 1.3;">
Professional printing solution • ZPL thermal labels • Direct hardware access
</div>
<div style="margin-bottom: 10px;">
<button onclick="initializeQZTray()" class="btn btn-primary btn-sm" style="font-size: 10px; padding: 4px 12px;">
🔄 Reconnect to QZ Tray
</button>
<button onclick="testQZConnection()" class="btn btn-info btn-sm" style="font-size: 10px; padding: 4px 12px;">
🔍 Test Connection
</button>
</div>
<a href="https://qz.io/download/" target="_blank" class="btn btn-secondary btn-sm" style="font-size: 10px; padding: 4px 12px; text-decoration: none;">
📥 Download QZ Tray (Free)
</a>
<div style="font-size: 9px; color: #6c757d; margin-top: 8px; line-height: 1.2;">
<strong>Setup:</strong> 1. Install QZ Tray → 2. Start the service → 3. Click "Reconnect"
</div>
</div>
</div>
</div>
</div>
<!-- Data Preview Card -->
<div class="card scan-table-card" style="min-height: 700px; width: calc(100% - 350px); margin: 0;">
<h3>Data Preview (Unprinted Orders)</h3>
<button id="check-db-btn" class="btn btn-primary mb-3">Load Orders</button>
<div class="report-table-container">
<table class="scan-table print-module-table">
<thead>
<tr>
<th>ID</th>
<th>Comanda Productie</th>
<th>Cod Articol</th>
<th>Descr. Com. Prod</th>
<th>Cantitate</th>
<th>Data Livrare</th>
<th>Dimensiune</th>
<th>Com. Achiz. Client</th>
<th>Nr. Linie</th>
<th>Customer Name</th>
<th>Customer Art. Nr.</th>
<th>Open Order</th>
<th>Line</th>
<th>Printed</th>
<th>Created</th>
</tr>
</thead>
<tbody id="unprinted-orders-table">
<!-- Data will be dynamically loaded here -->
</tbody>
</table>
</div>
</div>
</div>
<!-- QZ Tray JavaScript Library -->
<!-- Add html2canvas library for capturing preview as image -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/qz-tray@2.2.4/qz-tray.js"></script>
<script>
// Simplified notification system
function showNotification(message, type = 'info') {
const existingNotifications = document.querySelectorAll('.notification');
existingNotifications.forEach(n => n.remove());
const notification = document.createElement('div');
notification.className = `notification alert alert-${type === 'error' ? 'danger' : type === 'success' ? 'success' : type === 'warning' ? 'warning' : 'info'}`;
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
z-index: 9999;
max-width: 450px;
padding: 15px;
border-radius: 5px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
`;
// Convert newlines to <br> tags for proper display
const formattedMessage = message.replace(/\n/g, '<br>');
notification.innerHTML = `
<div style="display: flex; align-items: flex-start; justify-content: space-between;">
<span style="flex: 1; padding-right: 10px; white-space: pre-wrap; font-family: monospace; font-size: 12px;">${formattedMessage}</span>
<button type="button" onclick="this.parentElement.parentElement.remove()" style="background: none; border: none; font-size: 20px; cursor: pointer; flex-shrink: 0;">&times;</button>
</div>
`;
document.body.appendChild(notification);
// Longer timeout for error messages
const timeout = type === 'error' ? 15000 : 5000;
setTimeout(() => {
if (notification.parentElement) {
notification.remove();
}
}, timeout);
}
// Database loading functionality
document.getElementById('check-db-btn').addEventListener('click', function() {
const button = this;
const originalText = button.textContent;
button.textContent = 'Loading...';
button.disabled = true;
fetch('/get_unprinted_orders')
.then(response => {
if (response.status === 403) {
return response.json().then(errorData => {
throw new Error(`Access Denied: ${errorData.error}`);
});
} else if (!response.ok) {
return response.text().then(text => {
throw new Error(`HTTP ${response.status}: ${text}`);
});
}
return response.json();
})
.then(data => {
console.log('Received data:', data);
const tbody = document.getElementById('unprinted-orders-table');
tbody.innerHTML = '';
if (data.length === 0) {
tbody.innerHTML = '<tr><td colspan="15" style="text-align: center; padding: 20px; color: #28a745;"><strong>✅ All orders have been printed!</strong><br><small>No unprinted orders remaining.</small></td></tr>';
clearLabelPreview();
return;
}
data.forEach((order, index) => {
const tr = document.createElement('tr');
tr.dataset.orderId = order.id;
tr.dataset.orderIndex = index;
tr.style.cursor = 'pointer';
tr.innerHTML = `
<td style="font-size: 9px;">${order.id}</td>
<td style="font-size: 9px;"><strong>${order.comanda_productie}</strong></td>
<td style="font-size: 9px;">${order.cod_articol || '-'}</td>
<td style="font-size: 9px;">${order.descr_com_prod}</td>
<td style="text-align: right; font-weight: 600; font-size: 9px;">${order.cantitate}</td>
<td style="text-align: center; font-size: 9px;">
${order.data_livrare ? new Date(order.data_livrare).toLocaleDateString() : '-'}
</td>
<td style="text-align: center; font-size: 9px;">${order.dimensiune || '-'}</td>
<td style="font-size: 9px;">${order.com_achiz_client || '-'}</td>
<td style="text-align: right; font-size: 9px;">${order.nr_linie_com_client || '-'}</td>
<td style="font-size: 9px;">${order.customer_name || '-'}</td>
<td style="font-size: 9px;">${order.customer_article_number || '-'}</td>
<td style="font-size: 9px;">${order.open_for_order || '-'}</td>
<td style="text-align: right; font-size: 9px;">${order.line_number || '-'}</td>
<td style="text-align: center; font-size: 9px;">
${order.printed_labels == 1 ?
'<span style="color: #28a745; font-weight: bold;">✅ Yes</span>' :
'<span style="color: #dc3545;">❌ No</span>'}
</td>
<td style="font-size: 9px; color: #6c757d;">
${order.created_at ? new Date(order.created_at).toLocaleString() : '-'}
</td>
`;
tr.addEventListener('click', function() {
console.log('Row clicked:', order.id);
// Remove selection from other rows
document.querySelectorAll('.print-module-table tbody tr').forEach(row => {
row.classList.remove('selected');
const cells = row.querySelectorAll('td');
cells.forEach(cell => {
cell.style.backgroundColor = '';
cell.style.color = '';
});
});
// Select this row
this.classList.add('selected');
const cells = this.querySelectorAll('td');
cells.forEach(cell => {
cell.style.backgroundColor = '#007bff';
cell.style.color = 'white';
});
// Update label preview with selected order data
updateLabelPreview(order);
});
tbody.appendChild(tr);
});
// Auto-select first row
setTimeout(() => {
const firstRow = document.querySelector('.print-module-table tbody tr');
if (firstRow && !firstRow.querySelector('td[colspan]')) {
firstRow.click();
}
}, 100);
showNotification(`✅ Loaded ${data.length} unprinted orders`, 'success');
})
.catch(error => {
console.error('Error loading orders:', error);
const tbody = document.getElementById('unprinted-orders-table');
tbody.innerHTML = '<tr><td colspan="15" style="text-align: center; padding: 20px; color: #dc3545;"><strong>❌ Failed to load data</strong><br><small>' + error.message + '</small></td></tr>';
showNotification('❌ Failed to load orders: ' + error.message, 'error');
})
.finally(() => {
button.textContent = originalText;
button.disabled = false;
});
});
// Fetch pairing keys and populate dropdown
fetch('/get_pairing_keys')
.then(response => response.json())
.then(keys => {
const select = document.getElementById('client-select');
select.innerHTML = '';
keys.forEach(key => {
const option = document.createElement('option');
option.value = key.pairing_key;
option.textContent = key.printer_name;
select.appendChild(option);
});
// Show first key by default
if (keys.length > 0) {
document.getElementById('pairing-key-display').textContent = 'Pairing Key: ' + keys[0].pairing_key;
}
});
// Update pairing key display on selection
document.getElementById('client-select').addEventListener('change', function() {
document.getElementById('pairing-key-display').textContent = 'Pairing Key: ' + this.value;
});
// Update label preview with order data
function updateLabelPreview(order) {
const customerName = order.customer_name || 'N/A';
document.getElementById('customer-name-row').textContent = customerName;
// Update quantity ordered value
const quantity = order.cantitate || '0';
document.getElementById('quantity-ordered-value').textContent = quantity;
// Update client order info (Com.Achiz.Client - Nr. Linie)
const comAchizClient = order.com_achiz_client || '';
const nrLinie = order.nr_linie_com_client || '';
const clientOrderInfo = comAchizClient && nrLinie ? `${comAchizClient}-${nrLinie}` : 'N/A';
document.getElementById('client-order-info').textContent = clientOrderInfo;
// Update delivery date (using data_livrare column)
const deliveryDate = order.data_livrare ? new Date(order.data_livrare).toLocaleDateString() : 'N/A';
document.getElementById('delivery-date-value').textContent = deliveryDate;
// Update size (using dimensiune column)
const size = order.dimensiune || 'N/A';
document.getElementById('size-value').textContent = size;
// Update description (Descr. Com. Prod)
const description = order.descr_com_prod || 'N/A';
document.getElementById('description-value').textContent = description;
// Update article code (using cod_articol column)
const articleCode = order.cod_articol || 'N/A';
document.getElementById('article-code-value').textContent = articleCode;
// Update prod order (comanda_productie - cantitate)
const comandaProductie = order.comanda_productie || '';
const cantitate = order.cantitate || '';
const prodOrder = comandaProductie && cantitate ? `${comandaProductie}-${cantitate}` : 'N/A';
document.getElementById('prod-order-value').textContent = prodOrder;
// Update horizontal barcode with CP format (e.g., CP00000711/001)
// Show the first piece number (001) in preview
const horizontalBarcodeData = comandaProductie ? `${comandaProductie}/001` : 'N/A';
document.getElementById('barcode-text').textContent = horizontalBarcodeData;
// Update vertical barcode with client order format (e.g., Abcderd/65)
const verticalBarcodeData = comAchizClient && nrLinie ? `${comAchizClient}/${nrLinie}` : '000000/00';
document.getElementById('vertical-barcode-text').textContent = verticalBarcodeData;
}
// Clear label preview when no orders are available
function clearLabelPreview() {
document.getElementById('customer-name-row').textContent = 'No orders available';
document.getElementById('quantity-ordered-value').textContent = '0';
document.getElementById('client-order-info').textContent = 'N/A';
document.getElementById('delivery-date-value').textContent = 'N/A';
document.getElementById('size-value').textContent = 'N/A';
document.getElementById('description-value').textContent = 'N/A';
document.getElementById('article-code-value').textContent = 'N/A';
document.getElementById('prod-order-value').textContent = 'N/A';
document.getElementById('barcode-text').textContent = 'N/A';
document.getElementById('vertical-barcode-text').textContent = '000000/00';
}
// QZ Tray Integration
let qzTray = null;
let availablePrinters = [];
// Initialize QZ Tray connection
async function initializeQZTray() {
try {
console.log('🔍 Checking for QZ Tray...');
// Check if QZ Tray library is loaded
if (typeof qz === 'undefined') {
console.error('❌ QZ Tray library not loaded from CDN');
const errorMsg = '❌ QZ Tray Library Not Loaded\n\n' +
'The QZ Tray JavaScript library failed to load from CDN.\n' +
'Check your internet connection and refresh the page.\n\n' +
'CDN: https://cdn.jsdelivr.net/npm/qz-tray@2.2.4/qz-tray.js';
document.getElementById('qztray-status').textContent = 'Library Error';
document.getElementById('qztray-status').className = 'badge badge-danger';
showNotification(errorMsg, 'error');
return false;
}
console.log('✅ QZ Tray library loaded');
// For our custom QZ Tray build with pairing key authentication,
// we DON'T need certificate/signature validation.
// We'll connect to the insecure WebSocket port (8181) and skip certificate setup entirely.
console.log('<27> Skipping certificate setup - using pairing key authentication instead');
// Configure QZ to NOT require certificates/signatures
// This tells QZ Tray to accept the connection without signature validation
// IMPORTANT: Do NOT set signature algorithm or signature promise
// This prevents the "Invalid function call: NONE" error
// Our custom QZ Tray build uses pairing keys instead
console.log('✅ Configured for pairing-key based authentication');
console.log('🔌 Attempting to connect to QZ Tray on client PC...');
console.log('📍 Connection info:', {
isActive: qz.websocket.isActive(),
defaultPorts: 'ws://localhost:8181 or wss://localhost:8182'
});
// Set connection timeout
qz.websocket.setClosedCallbacks(function() {
console.warn('⚠️ QZ Tray connection closed');
document.getElementById('qztray-status').textContent = 'Disconnected';
document.getElementById('qztray-status').className = 'badge badge-warning';
});
// Connect to QZ Tray running on client PC
console.log('⏳ Connecting...');
await qz.websocket.connect();
qzTray = qz;
const version = await qz.api.getVersion();
console.log('✅ QZ Tray connected successfully');
console.log('📋 QZ Tray Version:', version);
// Update status
document.getElementById('qztray-status').textContent = 'Connected';
document.getElementById('qztray-status').className = 'badge badge-success';
// Load available printers
await loadQZTrayPrinters();
showNotification(`🖨️ QZ Tray v${version} connected successfully!`, 'success');
return true;
} catch (error) {
console.error('❌ QZ Tray connection failed:', error);
console.error('Error details:', {
message: error.message,
type: typeof error,
errorName: error.name,
stack: error.stack
});
// Detailed error messages based on actual error
let errorMsg = '❌ Cannot Connect to QZ Tray\n\n';
let statusText = 'Not Connected';
const errorStr = error.toString().toLowerCase();
const messageStr = (error.message || '').toLowerCase();
if (messageStr.includes('unable to establish') ||
errorStr.includes('unable to establish') ||
messageStr.includes('failed to connect') ||
errorStr.includes('websocket') ||
messageStr.includes('econnrefused')) {
errorMsg += '🔌 Connection Refused\n\n';
errorMsg += 'QZ Tray is not responding on this computer.\n\n';
errorMsg += '✅ Steps to fix:\n';
errorMsg += '1. Check if QZ Tray is installed on THIS computer\n';
errorMsg += '2. Look for QZ Tray icon in system tray (bottom-right)\n';
errorMsg += '3. If not running, start QZ Tray application\n';
errorMsg += '4. If installed but not working, restart QZ Tray\n';
errorMsg += '5. Download from: https://qz.io/download/\n\n';
errorMsg += '🔍 Technical: Trying to connect to ports 8181/8182';
statusText = 'Not Running';
} else if (messageStr.includes('certificate') ||
errorStr.includes('certificate') ||
messageStr.includes('security') ||
messageStr.includes('ssl') ||
messageStr.includes('tls')) {
errorMsg += '🔒 Certificate Security Issue\n\n';
errorMsg += 'QZ Tray uses a self-signed certificate for security.\n\n';
errorMsg += '✅ Steps to fix:\n';
errorMsg += '1. Open new tab: https://localhost:8182\n';
errorMsg += '2. Accept/Trust the security certificate\n';
errorMsg += '3. Come back and click "Reconnect to QZ Tray"\n\n';
errorMsg += '🔍 This is normal and safe for QZ Tray';
statusText = 'Certificate Error';
} else {
errorMsg += '⚠️ Unexpected Error\n\n';
errorMsg += 'Error: ' + error.message + '\n\n';
errorMsg += '🔍 Troubleshooting:\n';
errorMsg += '1. Open browser console (F12) for details\n';
errorMsg += '2. Click "Test Connection" for diagnostics\n';
errorMsg += '3. Make sure QZ Tray is running\n';
errorMsg += '4. Try restarting your browser';
}
document.getElementById('qztray-status').textContent = statusText;
document.getElementById('qztray-status').className = 'badge badge-danger';
showNotification(errorMsg, 'error');
return false;
}
}
// Manual test connection function
async function testQZConnection() {
console.log('🔍 ========== QZ TRAY CONNECTION TEST ==========');
const statusElement = document.getElementById('qztray-status');
const originalStatus = statusElement.textContent;
statusElement.textContent = 'Testing...';
statusElement.className = 'badge badge-warning';
let testResults = [];
try {
// Test 1: Check if library is loaded
console.log('Test 1: Checking library...');
if (typeof qz === 'undefined') {
testResults.push('❌ Test 1: Library NOT loaded from CDN');
throw new Error('QZ Tray library not loaded from CDN');
}
testResults.push('✅ Test 1: Library loaded successfully');
console.log('✅ Test 1 PASSED: Library loaded from CDN');
// Setup certificate handling - skip signature to avoid "Invalid function call: NONE"
// DO NOT set signature algorithm or promise - this causes the NONE error
console.log('✅ Configured for pairing-key authentication');
// Test 2: Check if already connected
console.log('Test 2: Checking existing connection...');
if (qz.websocket.isActive()) {
testResults.push('✅ Test 2: Already connected!');
console.log('✅ Test 2 PASSED: Already connected');
const version = await qz.api.getVersion();
testResults.push(`✅ Version: ${version}`);
console.log(`📋 QZ Tray Version: ${version}`);
const printers = await qz.printers.find();
testResults.push(`✅ Found ${printers.length} printer(s)`);
console.log(`🖨️ Printers found: ${printers.length}`);
showNotification(testResults.join('\n'), 'success');
statusElement.textContent = 'Connected';
statusElement.className = 'badge badge-success';
console.log('========== TEST COMPLETED: ALL PASSED ==========');
return;
}
testResults.push('⚠️ Test 2: Not currently connected');
console.log('⚠️ Test 2: Not connected, will attempt connection...');
// Test 3: Try to connect
console.log('Test 3: Attempting WebSocket connection...');
testResults.push('🔌 Test 3: Connecting to ws://localhost:8181...');
console.log('🔌 Connecting to WebSocket (ports 8181/8182)...');
await qz.websocket.connect();
testResults.push('✅ Test 3: WebSocket connected!');
console.log('✅ Test 3 PASSED: WebSocket connected');
// Test 4: Get version
console.log('Test 4: Getting QZ Tray version...');
const version = await qz.api.getVersion();
testResults.push(`✅ Test 4: QZ Tray v${version}`);
console.log(`✅ Test 4 PASSED: Version ${version}`);
// Test 5: List printers
console.log('Test 5: Fetching printer list...');
const printers = await qz.printers.find();
testResults.push(`✅ Test 5: Found ${printers.length} printer(s)`);
console.log(`✅ Test 5 PASSED: ${printers.length} printers found`);
if (printers.length > 0) {
console.log('📋 Available printers:', printers);
testResults.push('📋 Printers: ' + printers.slice(0, 3).join(', ') + (printers.length > 3 ? '...' : ''));
}
// Success!
qzTray = qz;
statusElement.textContent = 'Connected';
statusElement.className = 'badge badge-success';
console.log('========== TEST COMPLETED: ALL PASSED ==========');
showNotification(testResults.join('\n') + '\n\n✅ All tests passed!', 'success');
// Reload printers
await loadQZTrayPrinters();
} catch (error) {
console.error('❌ TEST FAILED:', error);
console.error('Error type:', typeof error);
console.error('Error name:', error.name);
console.error('Error message:', error.message);
console.error('Error stack:', error.stack);
statusElement.textContent = originalStatus;
statusElement.className = 'badge badge-danger';
const errorStr = error.toString().toLowerCase();
const messageStr = (error.message || '').toLowerCase();
let errorMsg = '❌ Connection Test Results:\n\n';
errorMsg += testResults.join('\n') + '\n\n';
if (typeof qz === 'undefined') {
errorMsg += '❌ FAILED: Library Load Error\n\n';
errorMsg += '📚 The QZ Tray JavaScript library did not load.\n\n';
errorMsg += 'Fix:\n';
errorMsg += '• Check internet connection\n';
errorMsg += '• Refresh the page (F5)\n';
errorMsg += '• Check if CDN is accessible:\n';
errorMsg += ' https://cdn.jsdelivr.net/npm/qz-tray@2.2.4/qz-tray.js';
} else if (messageStr.includes('unable to establish') ||
errorStr.includes('unable to establish') ||
messageStr.includes('failed to connect') ||
messageStr.includes('websocket') ||
errorStr.includes('websocket error')) {
errorMsg += '❌ FAILED: Cannot Connect to QZ Tray\n\n';
errorMsg += '🔌 QZ Tray is not running on this computer.\n\n';
errorMsg += 'Fix:\n';
errorMsg += '1. Check if QZ Tray is installed\n';
errorMsg += '2. Look in system tray (bottom-right) for QZ icon\n';
errorMsg += '3. If not there, launch QZ Tray app\n';
errorMsg += '4. Download: https://qz.io/download/\n\n';
errorMsg += '💡 QZ Tray must be running on THIS computer,\n';
errorMsg += ' not on the server!';
} else if (messageStr.includes('certificate') ||
errorStr.includes('certificate') ||
messageStr.includes('security')) {
errorMsg += '❌ FAILED: Certificate Error\n\n';
errorMsg += '🔒 Browser security blocking connection.\n\n';
errorMsg += 'Fix:\n';
errorMsg += '1. Open: https://localhost:8182\n';
errorMsg += '2. Click "Advanced" or "Proceed"\n';
errorMsg += '3. Accept the certificate\n';
errorMsg += '4. Return here and test again\n\n';
errorMsg += '💡 Self-signed certificates are normal for QZ Tray';
} else {
errorMsg += '❌ FAILED: Unexpected Error\n\n';
errorMsg += 'Error: ' + error.message + '\n\n';
errorMsg += 'Next steps:\n';
errorMsg += '1. Check browser console (F12)\n';
errorMsg += '2. Verify QZ Tray is running\n';
errorMsg += '3. Restart browser and QZ Tray\n';
errorMsg += '4. Check firewall settings';
}
console.log('========== TEST COMPLETED: FAILED ==========');
showNotification(errorMsg, 'error');
}
}
// Load available printers from QZ Tray
async function loadQZTrayPrinters() {
try {
if (!qzTray) return;
const printers = await qzTray.printers.find();
availablePrinters = printers;
const printerSelect = document.getElementById('qztray-printer-select');
printerSelect.innerHTML = '<option value="">Select a printer...</option>';
printers.forEach(printer => {
const option = document.createElement('option');
option.value = printer;
option.textContent = printer;
printerSelect.appendChild(option);
});
console.log(`📄 Found ${printers.length} 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') ||
p.toLowerCase().includes('epson')
);
if (thermalPrinter) {
printerSelect.value = thermalPrinter;
}
} catch (error) {
console.error('Error loading printers:', error);
showNotification('⚠️ Failed to load printers: ' + error.message, 'warning');
}
}
// Generate PDF and send to thermal printer
async function generatePDFAndPrint(selectedPrinter, orderData, pieceNumber, totalPieces) {
try {
console.log('📄 Generating PDF for thermal printing...');
// Prepare data for PDF generation
const pdfData = {
...orderData,
quantity: 1, // Single label per print job
piece_number: pieceNumber,
total_pieces: totalPieces
};
await qz.print(config, data);
console.log('✅ PDF sent to printer successfully');
} catch (error) {
console.error('❌ Error generating/printing PDF:', error);
throw error;
}
}
// Helper function to convert array buffer to base64
function arrayBufferToBase64(buffer) {
const bytes = new Uint8Array(buffer);
const binary = bytes.reduce((data, byte) => data + String.fromCharCode(byte), '');
return btoa(binary);
}
// Helper function to convert blob to base64 (kept for compatibility)
function blobToBase64(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
const base64 = reader.result.split(',')[1]; // Remove data URL prefix
resolve(base64);
};
reader.onerror = reject;
reader.readAsDataURL(blob);
});
}
// Update preview with order data for specific piece
function updatePreview(orderData, pieceNumber, totalPieces) {
// Extract and format the data
const {
customer_name = 'N/A',
cantitate = '0',
com_achiz_client = '',
nr_linie_com_client = '',
data_livrare = null,
dimensiune = 'N/A',
descr_com_prod = 'N/A',
cod_articol = 'N/A',
comanda_productie = ''
} = orderData;
const deliveryDate = data_livrare ? new Date(data_livrare).toLocaleDateString() : 'N/A';
const clientOrder = com_achiz_client && nr_linie_com_client ? `${com_achiz_client}-${nr_linie_com_client}` : 'N/A';
const horizontalBarcode = comanda_productie ? `${comanda_productie}-${String(pieceNumber).padStart(3, '0')}` : 'N/A';
const verticalBarcode = clientOrder;
const prodOrder = comanda_productie && cantitate ? `${comanda_productie}-${cantitate}` : 'N/A';
// Update preview elements with correct IDs
const customerRowElement = document.getElementById('customer-name-row');
if (customerRowElement) customerRowElement.textContent = customer_name;
const quantityElement = document.getElementById('quantity-ordered-value');
if (quantityElement) quantityElement.textContent = cantitate;
const clientOrderElement = document.getElementById('client-order-info');
if (clientOrderElement) clientOrderElement.textContent = clientOrder;
const deliveryDateElement = document.getElementById('delivery-date-value');
if (deliveryDateElement) deliveryDateElement.textContent = deliveryDate;
const descriptionElement = document.getElementById('description-value');
if (descriptionElement) descriptionElement.textContent = descr_com_prod;
const sizeElement = document.getElementById('size-value');
if (sizeElement) sizeElement.textContent = dimensiune;
const articleCodeElement = document.getElementById('article-code-value');
if (articleCodeElement) articleCodeElement.textContent = cod_articol;
const prodOrderElement = document.getElementById('prod-order-value');
if (prodOrderElement) prodOrderElement.textContent = prodOrder;
const barcodeTextElement = document.getElementById('barcode-text');
if (barcodeTextElement) barcodeTextElement.textContent = horizontalBarcode;
const verticalBarcodeTextElement = document.getElementById('vertical-barcode-text');
if (verticalBarcodeTextElement) verticalBarcodeTextElement.textContent = verticalBarcode;
}
function generateHTMLLabel(orderData, pieceNumber, totalPieces) {
const {
customer_name = 'N/A',
cantitate = '0',
com_achiz_client = '',
nr_linie_com_client = '',
data_livrare = null,
dimensiune = 'N/A',
descr_com_prod = 'N/A',
cod_articol = 'N/A',
comanda_productie = ''
} = orderData;
// Format data for label (matching preview format)
const deliveryDate = data_livrare ? new Date(data_livrare).toLocaleDateString() : 'N/A';
const clientOrder = com_achiz_client && nr_linie_com_client ? `${com_achiz_client}-${nr_linie_com_client}` : 'N/A';
const horizontalBarcode = comanda_productie ? `${comanda_productie}-${String(pieceNumber).padStart(3, '0')}` : 'N/A';
const verticalBarcode = clientOrder;
const prodOrder = comanda_productie && cantitate ? `${comanda_productie}-${cantitate}` : 'N/A';
const htmlContent = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
@page {
size: 80mm 110mm;
margin: 0;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;
font-family: Arial, sans-serif;
width: 80mm;
height: 110mm;
position: relative;
background: white;
font-size: 12px;
}
.label-container {
position: absolute;
top: 5mm;
left: 2mm;
width: 76mm;
height: 100mm;
border: 2px solid black;
display: flex;
flex-direction: column;
}
.header {
text-align: center;
font-size: 14px;
font-weight: bold;
padding: 3mm 2mm;
border-bottom: 1px solid black;
}
.customer-row {
text-align: center;
font-size: 12px;
padding: 2mm;
border-bottom: 1px solid black;
min-height: 8mm;
display: flex;
align-items: center;
justify-content: center;
}
.data-section {
display: flex;
flex: 1;
flex-direction: column;
}
.data-row {
display: flex;
height: 8mm;
border-bottom: 1px solid black;
}
.data-row.double {
height: 16mm;
}
.label-column {
width: 30mm;
padding: 1mm 2mm;
font-size: 8px;
border-right: 1px solid black;
display: flex;
align-items: center;
}
.value-column {
flex: 1;
padding: 1mm 2mm;
font-size: 10px;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
}
.description-value {
font-size: 8px !important;
line-height: 1.2;
overflow: hidden;
}
.barcode-section {
position: absolute;
bottom: 2mm;
left: 2mm;
width: 50mm;
height: 15mm;
border: 2px solid black;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background: white;
}
.barcode-visual {
font-family: 'Courier New', monospace;
font-size: 16px;
font-weight: bold;
line-height: 1;
margin-bottom: 2mm;
}
.barcode-text {
font-family: 'Courier New', monospace;
font-size: 8px;
font-weight: bold;
}
.vertical-barcode {
position: absolute;
right: 2mm;
top: 25mm;
width: 12mm;
height: 60mm;
border: 2px solid black;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: white;
}
.vertical-bars {
font-family: 'Courier New', monospace;
font-size: 12px;
font-weight: bold;
writing-mode: vertical-rl;
text-orientation: sideways;
margin: 2mm 0;
}
.vertical-text {
font-family: 'Courier New', monospace;
font-size: 6px;
font-weight: bold;
writing-mode: vertical-rl;
text-orientation: mixed;
transform: rotate(180deg);
}
</style>
</head>
<body>
<div class="label-container">
<div class="header">INNOFA ROMANIA SRL</div>
<div class="customer-row">${customer_name}</div>
<div class="data-section">
<div class="data-row">
<div class="label-column">Quantity ordered</div>
<div class="value-column">${cantitate}</div>
</div>
<div class="data-row">
<div class="label-column">Customer order</div>
<div class="value-column">${clientOrder}</div>
</div>
<div class="data-row">
<div class="label-column">Delivery date</div>
<div class="value-column">${deliveryDate}</div>
</div>
<div class="data-row double">
<div class="label-column">Product description</div>
<div class="value-column description-value">${descr_com_prod}</div>
</div>
<div class="data-row">
<div class="label-column">Size</div>
<div class="value-column">${dimensiune}</div>
</div>
<div class="data-row">
<div class="label-column">Article code</div>
<div class="value-column">${cod_articol}</div>
</div>
<div class="data-row">
<div class="label-column">Prod. order</div>
<div class="value-column">${prodOrder}</div>
</div>
</div>
</div>
<div class="barcode-section">
<div class="barcode-visual">||||| || ||| || ||||| ||| || |||||</div>
<div class="barcode-text">${horizontalBarcode}</div>
</div>
<div class="vertical-barcode">
<div class="vertical-bars">| | | | | | |</div>
<div class="vertical-text">${verticalBarcode}</div>
</div>
</body>
</html>`;
return htmlContent;
}
// Handle QZ Tray printing
async function handleQZTrayPrint(selectedRow) {
try {
if (!qzTray) {
await initializeQZTray();
if (!qzTray) {
throw new Error('QZ Tray not available');
}
}
const selectedPrinter = document.getElementById('qztray-printer-select').value;
if (!selectedPrinter) {
showNotification('⚠️ Please select a printer first', 'warning');
return;
}
// Extract order data from row
const cells = selectedRow.querySelectorAll('td');
const orderData = {
id: cells[0].textContent,
comanda_productie: cells[1].textContent.trim(),
cod_articol: cells[2].textContent.trim(),
descr_com_prod: cells[3].textContent.trim(),
cantitate: parseInt(cells[4].textContent.trim()),
data_livrare: cells[5].textContent.trim(),
dimensiune: cells[6].textContent.trim(),
com_achiz_client: cells[7].textContent.trim(),
nr_linie_com_client: cells[8].textContent.trim(),
customer_name: cells[9].textContent.trim()
};
const quantity = orderData.cantitate;
console.log(`🖨️ Printing ${quantity} labels via QZ Tray to ${selectedPrinter}`);
const button = document.getElementById('print-label-btn');
const originalText = button.textContent;
button.textContent = `Printing 0/${quantity}...`;
button.disabled = true;
try {
// Print each label sequentially
for (let i = 1; i <= quantity; i++) {
button.textContent = `Printing ${i}/${quantity}...`;
// Generate PDF and send to printer
await generatePDFAndPrint(selectedPrinter, orderData, i, quantity);
// Small delay between labels for printer processing
if (i < quantity) {
await new Promise(resolve => setTimeout(resolve, 500));
}
}
showNotification(`✅ Successfully printed ${quantity} labels!`, 'success');
// Mark order as printed and refresh table
setTimeout(() => {
document.getElementById('check-db-btn').click();
}, 1000);
} catch (printError) {
throw new Error(`Print failed: ${printError.message}`);
}
} catch (error) {
console.error('QZ Tray print error:', error);
showNotification('❌ QZ Tray print error: ' + error.message, 'error');
} finally {
const button = document.getElementById('print-label-btn');
button.textContent = originalText;
button.disabled = false;
}
}
// Print Button Handler - Routes to appropriate print method
document.getElementById('print-label-btn').addEventListener('click', function(e) {
e.preventDefault();
// Get selected order
const selectedRow = document.querySelector('.print-module-table tbody tr.selected');
if (!selectedRow) {
showNotification('⚠️ Please select an order first from the table below.', 'warning');
return;
}
// Get selected print method
const printMethod = document.querySelector('input[name="printMethod"]:checked').value;
if (printMethod === 'qztray') {
handleQZTrayPrint(selectedRow);
} else {
handlePDFGeneration(selectedRow);
}
});
// Handle PDF generation
function handlePDFGeneration(selectedRow) {
const orderId = selectedRow.dataset.orderId;
const quantityCell = selectedRow.querySelector('td:nth-child(5)');
const quantity = quantityCell ? parseInt(quantityCell.textContent) : 1;
const prodOrderCell = selectedRow.querySelector('td:nth-child(2)');
const prodOrder = prodOrderCell ? prodOrderCell.textContent.trim() : 'N/A';
const button = document.getElementById('print-label-btn');
const originalText = button.textContent;
button.textContent = 'Generating PDF...';
button.disabled = true;
console.log(`Generating PDF for order ${orderId} with ${quantity} labels`);
// Generate PDF with paper-saving mode enabled (optimized for thermal printers)
fetch(`/generate_labels_pdf/${orderId}/true`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.blob();
})
.then(blob => {
// Create blob URL for PDF
const url = window.URL.createObjectURL(blob);
// Create download link for PDF
const a = document.createElement('a');
a.href = url;
a.download = `labels_${prodOrder}_${quantity}pcs.pdf`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
// Also open PDF in new tab for printing
const printWindow = window.open(url, '_blank');
if (printWindow) {
printWindow.focus();
// Wait for PDF to load, then show print dialog
setTimeout(() => {
printWindow.print();
// Clean up blob URL after print dialog is shown
setTimeout(() => {
window.URL.revokeObjectURL(url);
}, 2000);
}, 1500);
} else {
// If popup was blocked, clean up immediately
setTimeout(() => {
window.URL.revokeObjectURL(url);
}, 1000);
}
// Show success message
showNotification(`✅ PDF generated successfully!\n📊 Order: ${prodOrder}\n📦 Labels: ${quantity} pieces`, 'success');
// Refresh the orders table to reflect printed status
setTimeout(() => {
document.getElementById('check-db-btn').click();
}, 1000);
})
.catch(error => {
console.error('Error generating PDF:', error);
showNotification('❌ Failed to generate PDF labels. Error: ' + error.message, 'error');
})
.finally(() => {
// Reset button state
button.textContent = originalText;
button.disabled = false;
});
}
// UI Control Functions
function updatePrintMethodUI() {
const printMethod = document.querySelector('input[name="printMethod"]:checked').value;
const printerSelection = document.getElementById('qztray-printer-selection');
const qztrayInfo = document.getElementById('qztray-info');
const printButton = document.getElementById('print-label-btn');
if (printMethod === 'qztray') {
printerSelection.style.display = 'block';
qztrayInfo.style.display = 'block';
printButton.textContent = '🖨️ Print Labels Directly';
printButton.className = 'btn btn-primary';
} else {
printerSelection.style.display = 'none';
qztrayInfo.style.display = 'none';
printButton.textContent = '📄 Generate PDF Labels';
printButton.className = 'btn btn-success';
}
}
// Add event listeners for print method radio buttons
document.addEventListener('DOMContentLoaded', function() {
// Add change listeners to radio buttons
document.querySelectorAll('input[name="printMethod"]').forEach(radio => {
radio.addEventListener('change', updatePrintMethodUI);
});
// Initialize UI
updatePrintMethodUI();
// Initialize QZ Tray
setTimeout(initializeQZTray, 1000);
// Load orders
setTimeout(() => {
document.getElementById('check-db-btn').click();
}, 500);
});
</script>
{% endblock %}