it prints

This commit is contained in:
2025-10-02 02:00:40 +03:00
parent b2b225049d
commit 7dc688d972
12 changed files with 9185 additions and 64 deletions

View File

@@ -35,6 +35,71 @@
background-color: #007bff !important;
color: white !important;
}
/* Print Progress Modal Styles */
.print-progress-modal {
display: none;
position: fixed;
z-index: 9999;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.6);
justify-content: center;
align-items: center;
}
.print-progress-content {
background-color: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
min-width: 400px;
max-width: 500px;
text-align: center;
}
.print-progress-content h3 {
margin: 0 0 20px 0;
color: #333;
font-size: 24px;
}
.progress-info {
margin-bottom: 15px;
color: #666;
font-size: 16px;
}
.progress-bar-container {
width: 100%;
height: 30px;
background-color: #f0f0f0;
border-radius: 15px;
overflow: hidden;
margin-bottom: 15px;
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
}
.progress-bar {
height: 100%;
background: linear-gradient(90deg, #007bff 0%, #0056b3 100%);
width: 0%;
transition: width 0.3s ease;
border-radius: 15px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
}
.progress-details {
font-size: 18px;
font-weight: bold;
color: #007bff;
}
</style>
{% endblock %}
@@ -44,6 +109,20 @@
<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 -->
@@ -209,11 +288,19 @@
<div style="font-size: 10px; color: #495057; margin-bottom: 10px; line-height: 1.3;">
Professional printing solution • ZPL thermal labels • Direct hardware access
</div>
<a href="https://qz.io/download/" target="_blank" class="btn btn-info btn-sm" style="font-size: 10px; padding: 4px 12px; text-decoration: none;">
<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. Refresh this page
<strong>Setup:</strong> 1. Install QZ Tray → 2. Start the service → 3. Click "Reconnect"
</div>
</div>
</div>
@@ -253,10 +340,28 @@
</div>
</div>
<!-- Printing Progress Modal -->
<div id="print-progress-modal" class="print-progress-modal" style="display: none;">
<div class="print-progress-content">
<h3>🖨️ Printing Labels</h3>
<div class="progress-info">
<span id="progress-text">Preparing to print...</span>
</div>
<div class="progress-bar-container">
<div id="progress-bar" class="progress-bar"></div>
</div>
<div class="progress-details">
<span id="progress-count">0 / 0</span>
</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>
<!-- PATCHED QZ Tray library - works with custom server using pairing key authentication -->
<script src="{{ url_for('static', filename='qz-tray.js') }}"></script>
<!-- Original CDN version (disabled): <script src="https://cdn.jsdelivr.net/npm/qz-tray@2.2.4/qz-tray.js"></script> -->
<script>
// Simplified notification system
@@ -271,25 +376,31 @@ function showNotification(message, type = 'info') {
top: 20px;
right: 20px;
z-index: 9999;
max-width: 350px;
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: center; justify-content: space-between;">
<span style="flex: 1; padding-right: 10px;">${message}</span>
<button type="button" onclick="this.parentElement.parentElement.remove()" style="background: none; border: none; font-size: 20px; cursor: pointer;">&times;</button>
<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();
}
}, 5000);
}, timeout);
}
// Database loading functionality
@@ -404,6 +515,28 @@ document.getElementById('check-db-btn').addEventListener('click', function() {
});
});
// 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';
@@ -472,15 +605,51 @@ let availablePrinters = [];
// Initialize QZ Tray connection
async function initializeQZTray() {
try {
// Check if QZ Tray is available
console.log('🔍 Checking for QZ Tray...');
// Check if QZ Tray library is loaded
if (typeof qz === 'undefined') {
throw new Error('QZ Tray library not loaded');
console.error('QZ Tray library not loaded');
const errorMsg = '❌ QZ Tray Library Not Loaded\n\n' +
'The patched QZ Tray JavaScript library failed to load.\n' +
'Check the browser console for errors and refresh the page.\n\n' +
'Path: /static/qz-tray.js (patched version for pairing key auth)';
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');
// Using PATCHED qz-tray.js that works with our custom QZ Tray server
// The patched library automatically skips certificate validation
// Our custom server uses ONLY pairing key (HMAC) authentication
console.log('🔒 Using patched qz-tray.js for pairing-key authentication...');
// No security setup needed - the patched library handles it
// Original qz-tray.js required setCertificatePromise, but our patch bypasses it
console.log('✅ Ready to connect to custom QZ Tray server');
console.log('🔌 Attempting to connect to QZ Tray on client PC...');
console.log('📍 Will try: ws://localhost:8181 (insecure) then wss://localhost:8182 (secure)');
// Connect to QZ Tray
// Set connection closed callback
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';
@@ -488,20 +657,227 @@ async function initializeQZTray() {
// Load available printers
await loadQZTrayPrinters();
console.log('✅ QZ Tray connected successfully');
showNotification('🖨️ QZ Tray connected successfully!', 'success');
showNotification(`🖨️ QZ Tray v${version} connected successfully!`, 'success');
return true;
} catch (error) {
console.error('❌ QZ Tray connection failed:', error);
document.getElementById('qztray-status').textContent = 'Not Connected';
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('❌ QZ Tray not available. Please install and start QZ Tray.', 'error');
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 local (patched version)');
// Using patched qz-tray.js - no security setup needed
console.log('🔒 Using patched library for custom QZ Tray...');
// 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 QZ Tray...');
console.log('🔌 Connecting to WebSocket...');
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 browser console for errors (F12)\n';
errorMsg += '• Refresh the page (F5)\n';
errorMsg += '• Check if library loaded:\n';
errorMsg += ' /static/qz-tray.js (patched version)';
} 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 {
@@ -543,7 +919,7 @@ async function loadQZTrayPrinters() {
// Generate PDF and send to thermal printer
async function generatePDFAndPrint(selectedPrinter, orderData, pieceNumber, totalPieces) {
try {
console.log('📄 Generating PDF for thermal printing...');
console.log(`📄 Generating PDF for thermal printing... (${pieceNumber}/${totalPieces})`);
// Prepare data for PDF generation
const pdfData = {
@@ -553,37 +929,44 @@ async function generatePDFAndPrint(selectedPrinter, orderData, pieceNumber, tota
total_pieces: totalPieces
};
// Generate PDF via server endpoint
// Call backend to generate PDF
const response = await fetch('/generate_label_pdf', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(pdfData)
});
if (!response.ok) {
throw new Error(`PDF generation failed: ${response.statusText}`);
const errorData = await response.json();
throw new Error(errorData.error || 'Failed to generate PDF');
}
// Get PDF as array buffer and convert to base64
const pdfArrayBuffer = await response.arrayBuffer();
const pdfBase64 = arrayBufferToBase64(pdfArrayBuffer);
// Get PDF as blob
const pdfBlob = await response.blob();
console.log('🖨️ Sending PDF to thermal printer with correct sizing...');
// Create a more specific thermal printer configuration
const config = qz.configs.create(selectedPrinter, {
margins: { top: 0, right: 0, bottom: 0, left: 0 },
size: {
width: 3.15, // 80mm = 3.15 inches
height: 4.33, // 110mm = 4.33 inches
units: 'in'
},
orientation: 'portrait'
// Convert blob to base64 for QZ Tray
const pdfBase64 = await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => {
// Remove data:application/pdf;base64, prefix
const base64 = reader.result.split(',')[1];
resolve(base64);
};
reader.onerror = reject;
reader.readAsDataURL(pdfBlob);
});
// Send PDF with explicit sizing for thermal labels - use raw base64
console.log(`🖨️ Sending PDF to printer ${selectedPrinter}...`);
// Configure QZ Tray for PDF printing
const config = qz.configs.create(selectedPrinter, {
scaleContent: false,
rasterize: false
});
// Prepare PDF data for QZ Tray
const data = [{
type: 'pdf',
format: 'base64',
@@ -591,7 +974,7 @@ async function generatePDFAndPrint(selectedPrinter, orderData, pieceNumber, tota
}];
await qz.print(config, data);
console.log('✅ PDF sent to printer successfully');
console.log(`✅ PDF sent to printer successfully (${pieceNumber}/${totalPieces})`);
} catch (error) {
console.error('❌ Error generating/printing PDF:', error);
@@ -895,6 +1278,11 @@ function generateHTMLLabel(orderData, pieceNumber, totalPieces) {
// Handle QZ Tray printing
async function handleQZTrayPrint(selectedRow) {
const modal = document.getElementById('print-progress-modal');
const progressBar = document.getElementById('progress-bar');
const progressText = document.getElementById('progress-text');
const progressCount = document.getElementById('progress-count');
try {
if (!qzTray) {
await initializeQZTray();
@@ -927,43 +1315,76 @@ async function handleQZTrayPrint(selectedRow) {
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;
// Show progress modal
modal.style.display = 'flex';
progressText.textContent = 'Preparing to print...';
progressBar.style.width = '0%';
progressCount.textContent = `0 / ${quantity}`;
try {
// Print each label sequentially
for (let i = 1; i <= quantity; i++) {
button.textContent = `Printing ${i}/${quantity}...`;
progressText.textContent = `Printing label ${i} of ${quantity}...`;
progressCount.textContent = `${i - 1} / ${quantity}`;
progressBar.style.width = `${((i - 1) / quantity) * 100}%`;
// Generate PDF and send to printer
await generatePDFAndPrint(selectedPrinter, orderData, i, quantity);
// Update progress after successful print
progressCount.textContent = `${i} / ${quantity}`;
progressBar.style.width = `${(i / quantity) * 100}%`;
// Small delay between labels for printer processing
if (i < quantity) {
await new Promise(resolve => setTimeout(resolve, 500));
}
}
// All labels printed successfully
progressText.textContent = '✅ All labels printed! Updating database...';
// Update database to mark order as printed
try {
const updateResponse = await fetch(`/update_printed_status/${orderData.id}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
if (!updateResponse.ok) {
console.error('Failed to update printed status in database');
progressText.textContent = '⚠️ Labels printed but database update failed';
await new Promise(resolve => setTimeout(resolve, 2000));
} else {
progressText.textContent = '✅ Complete! Refreshing table...';
await new Promise(resolve => setTimeout(resolve, 1000));
}
} catch (dbError) {
console.error('Database update error:', dbError);
progressText.textContent = '⚠️ Labels printed but database update failed';
await new Promise(resolve => setTimeout(resolve, 2000));
}
// Close modal
modal.style.display = 'none';
// Show success notification
showNotification(`✅ Successfully printed ${quantity} labels!`, 'success');
// Mark order as printed and refresh table
setTimeout(() => {
document.getElementById('check-db-btn').click();
}, 1000);
// Refresh table to show updated status
document.getElementById('check-db-btn').click();
} catch (printError) {
modal.style.display = 'none';
throw new Error(`Print failed: ${printError.message}`);
}
} catch (error) {
modal.style.display = 'none';
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;
}
}