Features: - Real-time progress modal with visual progress bar and counter - Detailed event log with timestamps and color-coded status - Pause/Resume functionality for checking printer/paper - Reprint Last button for damaged labels - Cancel button for emergency stops - Automatic error detection and recovery - Auto-pause on print failures (paper out/jam) - Automatic retry of failed labels after resume - Smart state management tracking current/last/failed labels - Sequential label numbering maintained through errors - Database update only on successful completion - Proper modal display with .show class toggle
1816 lines
76 KiB
HTML
1816 lines
76 KiB
HTML
{% 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;
|
|
}
|
|
|
|
/* 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.7);
|
|
justify-content: center;
|
|
align-items: center;
|
|
}
|
|
|
|
.print-progress-modal.show {
|
|
display: flex !important;
|
|
}
|
|
|
|
.print-progress-content {
|
|
background-color: white;
|
|
padding: 30px;
|
|
border-radius: 15px;
|
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
|
min-width: 500px;
|
|
max-width: 600px;
|
|
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;
|
|
min-height: 24px;
|
|
}
|
|
|
|
.progress-bar-container {
|
|
width: 100%;
|
|
height: 35px;
|
|
background-color: #e9ecef;
|
|
border-radius: 18px;
|
|
overflow: hidden;
|
|
margin-bottom: 15px;
|
|
box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.15);
|
|
position: relative;
|
|
}
|
|
|
|
.progress-bar {
|
|
height: 100%;
|
|
background: linear-gradient(90deg, #28a745 0%, #20c997 100%);
|
|
width: 0%;
|
|
transition: width 0.4s ease;
|
|
border-radius: 18px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: white;
|
|
font-weight: bold;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.progress-bar.error {
|
|
background: linear-gradient(90deg, #dc3545 0%, #c82333 100%);
|
|
}
|
|
|
|
.progress-bar.paused {
|
|
background: linear-gradient(90deg, #ffc107 0%, #ff9800 100%);
|
|
}
|
|
|
|
.progress-details {
|
|
font-size: 20px;
|
|
font-weight: bold;
|
|
color: #28a745;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.progress-details.error {
|
|
color: #dc3545;
|
|
}
|
|
|
|
.print-status-log {
|
|
max-height: 150px;
|
|
overflow-y: auto;
|
|
background: #f8f9fa;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 8px;
|
|
padding: 10px;
|
|
margin: 15px 0;
|
|
text-align: left;
|
|
font-family: monospace;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.print-status-log div {
|
|
padding: 3px 0;
|
|
color: #495057;
|
|
}
|
|
|
|
.print-status-log div.success {
|
|
color: #28a745;
|
|
}
|
|
|
|
.print-status-log div.error {
|
|
color: #dc3545;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.print-status-log div.warning {
|
|
color: #ffc107;
|
|
}
|
|
|
|
.print-control-buttons {
|
|
display: flex;
|
|
gap: 10px;
|
|
justify-content: center;
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.print-control-btn {
|
|
padding: 10px 20px;
|
|
border: none;
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.print-control-btn:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
|
}
|
|
|
|
.print-control-btn.pause {
|
|
background: #ffc107;
|
|
color: #000;
|
|
}
|
|
|
|
.print-control-btn.resume {
|
|
background: #28a745;
|
|
color: white;
|
|
}
|
|
|
|
.print-control-btn.reprint {
|
|
background: #17a2b8;
|
|
color: white;
|
|
}
|
|
|
|
.print-control-btn.cancel {
|
|
background: #dc3545;
|
|
color: white;
|
|
}
|
|
|
|
.print-control-btn:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
</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>
|
|
|
|
<!-- Printing Progress Modal -->
|
|
<div id="print-progress-modal" class="print-progress-modal">
|
|
<div class="print-progress-content">
|
|
<h3>🖨️ Print Controller</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">0%</div>
|
|
</div>
|
|
<div class="progress-details">
|
|
<span id="progress-count">0 / 0</span>
|
|
</div>
|
|
|
|
<!-- Print Status Log -->
|
|
<div class="print-status-log" id="print-status-log">
|
|
<div>Waiting to start...</div>
|
|
</div>
|
|
|
|
<!-- Control Buttons -->
|
|
<div class="print-control-buttons">
|
|
<button id="pause-print-btn" class="print-control-btn pause" style="display: none;">
|
|
⏸️ Pause
|
|
</button>
|
|
<button id="resume-print-btn" class="print-control-btn resume" style="display: none;">
|
|
▶️ Resume
|
|
</button>
|
|
<button id="reprint-last-btn" class="print-control-btn reprint" style="display: none;">
|
|
🔄 Reprint Last
|
|
</button>
|
|
<button id="cancel-print-btn" class="print-control-btn cancel" style="display: none;">
|
|
❌ Cancel
|
|
</button>
|
|
</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>
|
|
<!-- 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
|
|
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;">×</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');
|
|
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)');
|
|
|
|
// 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';
|
|
|
|
// 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 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 {
|
|
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... (${pieceNumber}/${totalPieces})`);
|
|
|
|
// Prepare data for PDF generation
|
|
const pdfData = {
|
|
...orderData,
|
|
quantity: 1, // Single label per print job
|
|
piece_number: pieceNumber,
|
|
total_pieces: totalPieces
|
|
};
|
|
|
|
// Call backend to generate PDF
|
|
const response = await fetch('/generate_label_pdf', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(pdfData)
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json();
|
|
throw new Error(errorData.error || 'Failed to generate PDF');
|
|
}
|
|
|
|
// Get PDF as blob
|
|
const pdfBlob = await response.blob();
|
|
|
|
// 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);
|
|
});
|
|
|
|
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',
|
|
data: pdfBase64
|
|
}];
|
|
|
|
await qz.print(config, data);
|
|
console.log(`✅ PDF sent to printer successfully (${pieceNumber}/${totalPieces})`);
|
|
|
|
} 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 with enhanced controller
|
|
let printController = {
|
|
isPaused: false,
|
|
isCancelled: false,
|
|
currentLabel: 0,
|
|
totalLabels: 0,
|
|
lastPrintedLabel: 0,
|
|
failedLabels: [],
|
|
orderData: null,
|
|
printerName: null
|
|
};
|
|
|
|
function addLogEntry(message, type = 'info') {
|
|
const log = document.getElementById('print-status-log');
|
|
const entry = document.createElement('div');
|
|
entry.className = type;
|
|
const timestamp = new Date().toLocaleTimeString();
|
|
entry.textContent = `[${timestamp}] ${message}`;
|
|
log.appendChild(entry);
|
|
log.scrollTop = log.scrollHeight;
|
|
}
|
|
|
|
function updateProgressBar(current, total, status = '') {
|
|
const progressBar = document.getElementById('progress-bar');
|
|
const progressCount = document.getElementById('progress-count');
|
|
const progressText = document.getElementById('progress-text');
|
|
const percentage = Math.round((current / total) * 100);
|
|
|
|
progressBar.style.width = `${percentage}%`;
|
|
progressBar.textContent = `${percentage}%`;
|
|
progressCount.textContent = `${current} / ${total}`;
|
|
|
|
if (status) {
|
|
progressText.textContent = status;
|
|
}
|
|
}
|
|
|
|
async function handleQZTrayPrint(selectedRow) {
|
|
const modal = document.getElementById('print-progress-modal');
|
|
const progressBar = document.getElementById('progress-bar');
|
|
const progressText = document.getElementById('progress-text');
|
|
const log = document.getElementById('print-status-log');
|
|
|
|
const pauseBtn = document.getElementById('pause-print-btn');
|
|
const resumeBtn = document.getElementById('resume-print-btn');
|
|
const reprintBtn = document.getElementById('reprint-last-btn');
|
|
const cancelBtn = document.getElementById('cancel-print-btn');
|
|
|
|
// Reset controller state
|
|
printController = {
|
|
isPaused: false,
|
|
isCancelled: false,
|
|
currentLabel: 0,
|
|
totalLabels: 0,
|
|
lastPrintedLabel: 0,
|
|
failedLabels: [],
|
|
orderData: null,
|
|
printerName: null
|
|
};
|
|
|
|
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;
|
|
printController.orderData = orderData;
|
|
printController.printerName = selectedPrinter;
|
|
printController.totalLabels = quantity;
|
|
|
|
console.log(`🖨️ Printing ${quantity} labels via QZ Tray to ${selectedPrinter}`);
|
|
|
|
// Show modal with show class for proper display
|
|
modal.classList.add('show');
|
|
log.innerHTML = '';
|
|
addLogEntry(`Starting print job: ${quantity} labels`, 'info');
|
|
addLogEntry(`Printer: ${selectedPrinter}`, 'info');
|
|
addLogEntry(`Order: ${orderData.comanda_productie}`, 'info');
|
|
|
|
updateProgressBar(0, quantity, 'Preparing to print...');
|
|
progressBar.classList.remove('error', 'paused');
|
|
|
|
// Show control buttons
|
|
pauseBtn.style.display = 'inline-block';
|
|
cancelBtn.style.display = 'inline-block';
|
|
reprintBtn.style.display = 'none';
|
|
resumeBtn.style.display = 'none';
|
|
|
|
// Setup button handlers
|
|
pauseBtn.onclick = () => {
|
|
printController.isPaused = true;
|
|
pauseBtn.style.display = 'none';
|
|
resumeBtn.style.display = 'inline-block';
|
|
progressBar.classList.add('paused');
|
|
addLogEntry('Print job paused by user', 'warning');
|
|
updateProgressBar(printController.currentLabel, quantity, '⏸️ Paused - Check printer paper');
|
|
};
|
|
|
|
resumeBtn.onclick = () => {
|
|
printController.isPaused = false;
|
|
resumeBtn.style.display = 'none';
|
|
pauseBtn.style.display = 'inline-block';
|
|
progressBar.classList.remove('paused');
|
|
addLogEntry('Print job resumed', 'success');
|
|
};
|
|
|
|
reprintBtn.onclick = async () => {
|
|
if (printController.lastPrintedLabel > 0) {
|
|
addLogEntry(`Reprinting label ${printController.lastPrintedLabel}...`, 'info');
|
|
try {
|
|
await generatePDFAndPrint(selectedPrinter, orderData, printController.lastPrintedLabel, quantity);
|
|
addLogEntry(`Label ${printController.lastPrintedLabel} reprinted successfully`, 'success');
|
|
} catch (error) {
|
|
addLogEntry(`Reprint failed: ${error.message}`, 'error');
|
|
}
|
|
}
|
|
};
|
|
|
|
cancelBtn.onclick = () => {
|
|
printController.isCancelled = true;
|
|
addLogEntry('Print job cancelled by user', 'error');
|
|
progressBar.classList.add('error');
|
|
updateProgressBar(printController.currentLabel, quantity, '❌ Cancelled');
|
|
};
|
|
|
|
try {
|
|
// Print each label sequentially
|
|
for (let i = 1; i <= quantity; i++) {
|
|
if (printController.isCancelled) {
|
|
addLogEntry('Print job terminated', 'error');
|
|
break;
|
|
}
|
|
|
|
// Wait while paused
|
|
while (printController.isPaused && !printController.isCancelled) {
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
}
|
|
|
|
if (printController.isCancelled) break;
|
|
|
|
printController.currentLabel = i;
|
|
updateProgressBar(i - 1, quantity, `Printing label ${i} of ${quantity}...`);
|
|
addLogEntry(`Sending label ${i} to printer...`, 'info');
|
|
|
|
try {
|
|
// Generate PDF and send to printer
|
|
await generatePDFAndPrint(selectedPrinter, orderData, i, quantity);
|
|
|
|
printController.lastPrintedLabel = i;
|
|
updateProgressBar(i, quantity, `Label ${i} printed successfully`);
|
|
addLogEntry(`✓ Label ${i} printed successfully`, 'success');
|
|
|
|
// Show reprint button after each successful print
|
|
reprintBtn.style.display = 'inline-block';
|
|
|
|
} catch (printError) {
|
|
// Print error detected
|
|
progressBar.classList.add('error');
|
|
printController.failedLabels.push(i);
|
|
addLogEntry(`✗ Label ${i} failed: ${printError.message}`, 'error');
|
|
|
|
// Pause automatically on error
|
|
printController.isPaused = true;
|
|
pauseBtn.style.display = 'none';
|
|
resumeBtn.style.display = 'inline-block';
|
|
progressBar.classList.add('paused');
|
|
updateProgressBar(i - 1, quantity, '⚠️ ERROR - Check printer (paper jam/out of paper)');
|
|
|
|
// Wait for user to resume or cancel
|
|
while (printController.isPaused && !printController.isCancelled) {
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
}
|
|
|
|
if (!printController.isCancelled) {
|
|
// Retry failed label
|
|
addLogEntry(`Retrying label ${i}...`, 'warning');
|
|
await generatePDFAndPrint(selectedPrinter, orderData, i, quantity);
|
|
addLogEntry(`✓ Label ${i} printed successfully (retry)`, 'success');
|
|
printController.lastPrintedLabel = i;
|
|
progressBar.classList.remove('error');
|
|
}
|
|
}
|
|
|
|
// Small delay between labels for printer processing
|
|
if (i < quantity && !printController.isCancelled) {
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
}
|
|
}
|
|
|
|
if (!printController.isCancelled) {
|
|
// All labels printed successfully
|
|
progressBar.classList.remove('paused', 'error');
|
|
updateProgressBar(quantity, quantity, '✅ All labels printed! Updating database...');
|
|
addLogEntry('All labels completed successfully', 'success');
|
|
|
|
// Hide control buttons
|
|
pauseBtn.style.display = 'none';
|
|
resumeBtn.style.display = 'none';
|
|
cancelBtn.style.display = 'none';
|
|
|
|
// 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');
|
|
addLogEntry('Database update failed', 'warning');
|
|
updateProgressBar(quantity, quantity, '⚠️ Labels printed but database update failed');
|
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
} else {
|
|
addLogEntry('Database updated successfully', 'success');
|
|
updateProgressBar(quantity, quantity, '✅ Complete! Refreshing table...');
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
}
|
|
} catch (dbError) {
|
|
console.error('Database update error:', dbError);
|
|
addLogEntry(`Database error: ${dbError.message}`, 'error');
|
|
updateProgressBar(quantity, quantity, '⚠️ Labels printed but database update failed');
|
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
}
|
|
|
|
// Close modal
|
|
modal.classList.remove('show');
|
|
|
|
// Show success notification
|
|
showNotification(`✅ Successfully printed ${quantity} labels!`, 'success');
|
|
|
|
// Refresh table to show updated status
|
|
document.getElementById('check-db-btn').click();
|
|
} else {
|
|
// Job was cancelled
|
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
modal.classList.remove('show');
|
|
showNotification(`⚠️ Print job cancelled. ${printController.lastPrintedLabel} of ${quantity} labels printed.`, 'warning');
|
|
}
|
|
|
|
} catch (printError) {
|
|
modal.classList.remove('show');
|
|
throw new Error(`Print failed: ${printError.message}`);
|
|
}
|
|
|
|
} catch (error) {
|
|
modal.classList.remove('show');
|
|
console.error('QZ Tray print error:', error);
|
|
showNotification('❌ QZ Tray print error: ' + error.message, 'error');
|
|
}
|
|
}
|
|
|
|
// 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 %} |