- Add boxes_crates database table with BIGINT IDs and 8-digit auto-numbered box_numbers - Implement boxes CRUD operations (add, edit, update, delete, delete_multiple) - Create boxes route handlers with POST actions for all operations - Add boxes.html template with 3-panel layout matching warehouse locations module - Implement barcode generation and printing with JsBarcode and QZ Tray integration - Add browser print fallback for when QZ Tray is not available - Simplify create box form to single button with auto-generation - Fix JavaScript null reference errors with proper element validation - Convert tuple data to dictionaries for Jinja2 template compatibility - Register boxes blueprint in Flask app initialization
680 lines
27 KiB
HTML
680 lines
27 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid mt-5">
|
|
<div class="row mb-4">
|
|
<div class="col-12">
|
|
<h2 class="mb-3">
|
|
<i class="fas fa-cube me-2"></i>Manage Boxes
|
|
</h2>
|
|
</div>
|
|
</div>
|
|
|
|
{% if message %}
|
|
<div class="alert alert-{{ message_type }} alert-dismissible fade show" role="alert">
|
|
{{ message }}
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="row">
|
|
<!-- Left Panel: Add Box Form -->
|
|
<div class="col-md-3">
|
|
<div class="card shadow-sm">
|
|
<div class="card-header bg-primary text-white">
|
|
<h5 class="mb-0">
|
|
<i class="fas fa-plus me-2"></i>Create New Box
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<form method="POST" id="addBoxForm">
|
|
<input type="hidden" name="action" value="add_box">
|
|
|
|
<button type="submit" class="btn btn-primary w-100" name="add_box" value="1">
|
|
<i class="fas fa-plus me-2"></i>Create Box
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Statistics Card -->
|
|
<div class="card shadow-sm mt-3">
|
|
<div class="card-header bg-info text-white">
|
|
<h5 class="mb-0">
|
|
<i class="fas fa-chart-bar me-2"></i>Statistics
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="mb-3">
|
|
<small class="text-muted d-block">Total Boxes</small>
|
|
<h4 class="text-primary mb-0">{{ stats.total }}</h4>
|
|
</div>
|
|
<div class="mb-3">
|
|
<small class="text-muted d-block">Open</small>
|
|
<h5 class="text-success mb-0">{{ stats.open }}</h5>
|
|
</div>
|
|
<div>
|
|
<small class="text-muted d-block">Closed</small>
|
|
<h5 class="text-danger mb-0">{{ stats.closed }}</h5>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Delete Multiple Button -->
|
|
<div class="card shadow-sm mt-3 border-warning">
|
|
<div class="card-header bg-warning">
|
|
<h5 class="mb-0">
|
|
<i class="fas fa-trash me-2"></i>Delete Selected
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<form method="POST" id="deleteForm">
|
|
<input type="hidden" name="action" value="delete_multiple">
|
|
<button type="button" class="btn btn-danger w-100" id="deleteSelectedBtn"
|
|
onclick="deleteSelectedBoxes()">
|
|
<i class="fas fa-trash me-2"></i>Delete Selected
|
|
</button>
|
|
<input type="hidden" id="delete_ids" name="delete_ids" value="">
|
|
</form>
|
|
<small class="text-muted d-block mt-2">Select boxes in table to delete</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Center Panel: Boxes Table -->
|
|
<div class="col-md-6">
|
|
<div class="card shadow-sm">
|
|
<div class="card-header bg-info text-white">
|
|
<h5 class="mb-0">
|
|
<i class="fas fa-list me-2"></i>All Boxes ({{ boxes|length }})
|
|
</h5>
|
|
</div>
|
|
<div class="card-body" style="max-height: 600px; overflow-y: auto;">
|
|
{% if boxes %}
|
|
<div class="table-responsive">
|
|
<table class="table table-striped table-hover" id="boxesTable">
|
|
<thead class="table-light sticky-top">
|
|
<tr>
|
|
<th width="30">
|
|
<input type="checkbox" id="selectAll" onchange="toggleSelectAll(this)">
|
|
</th>
|
|
<th>Box Number</th>
|
|
<th>Status</th>
|
|
<th>Location</th>
|
|
<th>Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for box in boxes %}
|
|
<tr class="box-row" data-box-id="{{ box.id }}" data-box-number="{{ box.box_number }}"
|
|
data-box-status="{{ box.status }}" data-location-id="{{ box.location_id or '' }}">
|
|
<td>
|
|
<input type="checkbox" class="box-checkbox"
|
|
value="{{ box.id }}"
|
|
onchange="updateDeleteBtn()">
|
|
</td>
|
|
<td>
|
|
<strong>{{ box.box_number }}</strong>
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-{{ 'success' if box.status == 'open' else 'danger' }}">
|
|
{{ box.status|upper }}
|
|
</span>
|
|
</td>
|
|
<td>{{ box.location_code or '-' }}</td>
|
|
<td>
|
|
<button type="button" class="btn btn-sm btn-outline-primary"
|
|
onclick="editBox({{ box.id }}, event)">
|
|
<i class="fas fa-edit"></i>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<div class="alert alert-info text-center" role="alert">
|
|
<i class="fas fa-info-circle me-2"></i>
|
|
No boxes found. Create one using the form on the left.
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right Panel: Edit/Print -->
|
|
<div class="col-md-3">
|
|
<div class="card shadow-sm">
|
|
<div class="card-header bg-success text-white">
|
|
<h5 class="mb-0">
|
|
<i class="fas fa-edit me-2"></i>Edit Box
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div id="editSection" style="display: none;">
|
|
<form method="POST" id="editBoxForm">
|
|
<input type="hidden" name="action" value="edit_box">
|
|
<input type="hidden" id="edit_box_id" name="box_id">
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Box Number</label>
|
|
<input type="text" class="form-control" id="edit_box_number"
|
|
placeholder="Box number" readonly>
|
|
<small class="text-muted">Cannot be changed</small>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="edit_status" class="form-label">Status</label>
|
|
<select class="form-select" id="edit_status" name="status">
|
|
<option value="open">Open</option>
|
|
<option value="closed">Closed</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="edit_location" class="form-label">Location</label>
|
|
<select class="form-select" id="edit_location" name="location_id">
|
|
<option value="">No Location</option>
|
|
{% for location in locations %}
|
|
<option value="{{ location.id }}">{{ location.location_code }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
|
|
<button type="button" class="btn btn-success w-100 mb-2"
|
|
onclick="saveEditBox()">
|
|
<i class="fas fa-save me-2"></i>Save Changes
|
|
</button>
|
|
<button type="button" class="btn btn-danger w-100 mb-2"
|
|
onclick="deleteBoxConfirm()">
|
|
<i class="fas fa-trash me-2"></i>Delete Box
|
|
</button>
|
|
<button type="button" class="btn btn-secondary w-100"
|
|
onclick="cancelEdit()">
|
|
Cancel
|
|
</button>
|
|
</form>
|
|
</div>
|
|
|
|
<div id="noEditSection" class="alert alert-info text-center">
|
|
<i class="fas fa-arrow-left me-2"></i>
|
|
Click edit button in table to modify a box
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Barcode Print Section -->
|
|
<div class="card shadow-sm mt-3">
|
|
<div class="card-header bg-secondary text-white">
|
|
<h5 class="mb-0">
|
|
<i class="fas fa-barcode me-2"></i>Print Label
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div id="printSection" style="display: none;">
|
|
<div class="mb-3">
|
|
<label class="form-label">Box Number:</label>
|
|
<p id="print_box_number" class="fw-bold mb-0" style="font-size: 1.2rem; color: #0d6efd;">-</p>
|
|
</div>
|
|
|
|
<!-- Printer Selection -->
|
|
<div class="mb-3">
|
|
<label for="printer-select" class="form-label">Select Printer:</label>
|
|
<select id="printer-select" class="form-select form-select-sm" style="font-size: 0.95rem;">
|
|
<option value="">Default Printer</option>
|
|
</select>
|
|
<small class="text-muted d-block mt-1">
|
|
<i class="fas fa-info-circle me-1"></i>
|
|
<span id="qz-status">QZ Tray: Initializing...</span>
|
|
</small>
|
|
</div>
|
|
|
|
<!-- Barcode Preview -->
|
|
<div id="barcodePreviewContainer" style="display: none; text-align: center; margin: 20px 0; padding: 15px; background-color: #f8f9fa; border-radius: 4px;">
|
|
<svg id="boxBarcode" style="max-width: 100%; height: auto;"></svg>
|
|
<p class="text-muted small mt-2 mb-0">Scan this barcode to identify the box</p>
|
|
</div>
|
|
|
|
<button type="button" class="btn btn-primary w-100 mb-2"
|
|
onclick="generateBarcodePreview()">
|
|
<i class="fas fa-eye me-2"></i>Generate Preview
|
|
</button>
|
|
<button type="button" class="btn btn-success w-100 mb-2"
|
|
id="printBoxBtn" style="display: none;" onclick="printBarcode()">
|
|
<i class="fas fa-print me-2"></i>Print Label
|
|
</button>
|
|
<button type="button" class="btn btn-info btn-sm w-100 mb-2"
|
|
onclick="testQZTrayConnection()">
|
|
<i class="fas fa-cog me-2"></i>Test QZ Tray
|
|
</button>
|
|
<small class="text-muted d-block">
|
|
<i class="fas fa-info-circle me-1"></i>
|
|
Requires QZ Tray installed for thermal printing
|
|
</small>
|
|
</div>
|
|
|
|
<div id="noPrintSection" class="alert alert-info text-center">
|
|
<i class="fas fa-arrow-left me-2"></i>
|
|
Select a box to print
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Delete Confirmation Modal -->
|
|
<div class="modal fade" id="deleteConfirmModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header bg-danger text-white">
|
|
<h5 class="modal-title">Confirm Delete</h5>
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p id="deleteConfirmMessage">Are you sure you want to delete this box?</p>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="button" class="btn btn-danger" id="confirmDeleteBtn"
|
|
onclick="confirmDelete()">Delete</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- QZ Tray and Barcode Libraries -->
|
|
<script src="https://cdn.jsdelivr.net/npm/jsbarcode@3.11.5/dist/JsBarcode.all.min.js"></script>
|
|
<script src="{{ url_for('static', filename='js/qz-tray.js') }}"></script>
|
|
<script src="{{ url_for('static', filename='js/qz-printer.js') }}"></script>
|
|
|
|
<script>
|
|
// Box editing state
|
|
let currentEditingBoxId = null;
|
|
let currentDeleteId = null;
|
|
|
|
// Toggle select all checkboxes
|
|
function toggleSelectAll(checkbox) {
|
|
const checkboxes = document.querySelectorAll('.box-checkbox');
|
|
checkboxes.forEach(cb => cb.checked = checkbox.checked);
|
|
updateDeleteBtn();
|
|
}
|
|
|
|
// Update delete button visibility
|
|
function updateDeleteBtn() {
|
|
const checkedCount = document.querySelectorAll('.box-checkbox:checked').length;
|
|
const deleteBtn = document.getElementById('deleteSelectedBtn');
|
|
deleteBtn.disabled = checkedCount === 0;
|
|
if (checkedCount > 0) {
|
|
deleteBtn.textContent = `Delete ${checkedCount} Selected`;
|
|
} else {
|
|
deleteBtn.innerHTML = '<i class="fas fa-trash me-2"></i>Delete Selected';
|
|
}
|
|
}
|
|
|
|
// Delete selected boxes
|
|
function deleteSelectedBoxes() {
|
|
const selectedIds = Array.from(document.querySelectorAll('.box-checkbox:checked'))
|
|
.map(cb => cb.value);
|
|
|
|
if (selectedIds.length === 0) {
|
|
alert('Please select boxes to delete');
|
|
return;
|
|
}
|
|
|
|
document.getElementById('deleteConfirmMessage').textContent =
|
|
`Are you sure you want to delete ${selectedIds.length} box(es)?`;
|
|
currentDeleteId = selectedIds.join(',');
|
|
new bootstrap.Modal(document.getElementById('deleteConfirmModal')).show();
|
|
}
|
|
|
|
// Edit box
|
|
function editBox(boxId, evt) {
|
|
try {
|
|
const row = evt.target.closest('tr');
|
|
if (!row) {
|
|
console.error('Could not find table row');
|
|
return;
|
|
}
|
|
|
|
currentEditingBoxId = boxId;
|
|
const boxNumber = row.dataset.boxNumber;
|
|
const status = row.dataset.boxStatus;
|
|
const locationId = row.dataset.locationId || '';
|
|
|
|
console.log('Editing box:', {boxId, boxNumber, status, locationId});
|
|
|
|
// Populate form fields
|
|
const editBoxIdEl = document.getElementById('edit_box_id');
|
|
const editBoxNumberEl = document.getElementById('edit_box_number');
|
|
const editStatusEl = document.getElementById('edit_status');
|
|
const editLocationEl = document.getElementById('edit_location');
|
|
|
|
if (editBoxIdEl) editBoxIdEl.value = boxId;
|
|
if (editBoxNumberEl) editBoxNumberEl.value = boxNumber;
|
|
if (editStatusEl) editStatusEl.value = status;
|
|
if (editLocationEl) editLocationEl.value = locationId;
|
|
|
|
// Show/hide sections
|
|
const editSectionEl = document.getElementById('editSection');
|
|
const noEditSectionEl = document.getElementById('noEditSection');
|
|
const printSectionEl = document.getElementById('printSection');
|
|
const noPrintSectionEl = document.getElementById('noPrintSection');
|
|
|
|
if (editSectionEl) editSectionEl.style.display = 'block';
|
|
if (noEditSectionEl) noEditSectionEl.style.display = 'none';
|
|
if (printSectionEl) printSectionEl.style.display = 'block';
|
|
if (noPrintSectionEl) noPrintSectionEl.style.display = 'none';
|
|
|
|
// Update print section
|
|
const printBoxNumberEl = document.getElementById('print_box_number');
|
|
if (printBoxNumberEl) printBoxNumberEl.textContent = boxNumber;
|
|
|
|
// Reset barcode preview
|
|
const barcodeEl = document.getElementById('boxBarcode');
|
|
if (barcodeEl) {
|
|
barcodeEl.innerHTML = '';
|
|
}
|
|
const barcodePreviewEl = document.getElementById('barcodePreviewContainer');
|
|
const printBoxBtnEl = document.getElementById('printBoxBtn');
|
|
if (barcodePreviewEl) barcodePreviewEl.style.display = 'none';
|
|
if (printBoxBtnEl) printBoxBtnEl.style.display = 'none';
|
|
|
|
// Highlight selected row
|
|
document.querySelectorAll('.box-row').forEach(r => r.style.backgroundColor = '');
|
|
if (row) row.style.backgroundColor = '#e3f2fd';
|
|
|
|
console.log('Box edit section displayed');
|
|
|
|
} catch (error) {
|
|
console.error('Error in editBox:', error);
|
|
alert('Error loading box: ' + error.message);
|
|
}
|
|
}
|
|
|
|
// Save edit
|
|
function saveEditBox() {
|
|
if (!currentEditingBoxId) return;
|
|
|
|
const statusEl = document.getElementById('edit_status');
|
|
const locationEl = document.getElementById('edit_location');
|
|
|
|
const status = statusEl ? statusEl.value : '';
|
|
const location_id = locationEl ? locationEl.value : '';
|
|
|
|
const form2 = document.createElement('form');
|
|
form2.method = 'POST';
|
|
form2.innerHTML = `
|
|
<input type="hidden" name="action" value="edit_box">
|
|
<input type="hidden" name="box_id" value="${currentEditingBoxId}">
|
|
<input type="hidden" name="status" value="${status}">
|
|
<input type="hidden" name="location_id" value="${location_id}">
|
|
`;
|
|
document.body.appendChild(form2);
|
|
form2.submit();
|
|
}
|
|
|
|
// Delete box confirmation
|
|
function deleteBoxConfirm() {
|
|
if (!currentEditingBoxId) return;
|
|
|
|
document.getElementById('deleteConfirmMessage').textContent =
|
|
'Are you sure you want to delete this box?';
|
|
currentDeleteId = currentEditingBoxId;
|
|
new bootstrap.Modal(document.getElementById('deleteConfirmModal')).show();
|
|
}
|
|
|
|
// Confirm delete
|
|
function confirmDelete() {
|
|
const form = document.createElement('form');
|
|
form.method = 'POST';
|
|
form.innerHTML = `
|
|
<input type="hidden" name="action" value="delete_box">
|
|
<input type="hidden" name="box_id" value="${currentDeleteId}">
|
|
`;
|
|
document.body.appendChild(form);
|
|
form.submit();
|
|
|
|
bootstrap.Modal.getInstance(document.getElementById('deleteConfirmModal')).hide();
|
|
}
|
|
|
|
// Cancel edit
|
|
function cancelEdit() {
|
|
currentEditingBoxId = null;
|
|
|
|
const editSectionEl = document.getElementById('editSection');
|
|
const noEditSectionEl = document.getElementById('noEditSection');
|
|
const printSectionEl = document.getElementById('printSection');
|
|
const noPrintSectionEl = document.getElementById('noPrintSection');
|
|
|
|
if (editSectionEl) editSectionEl.style.display = 'none';
|
|
if (noEditSectionEl) noEditSectionEl.style.display = 'block';
|
|
if (printSectionEl) printSectionEl.style.display = 'none';
|
|
if (noPrintSectionEl) noPrintSectionEl.style.display = 'block';
|
|
|
|
document.querySelectorAll('.box-row').forEach(r => r.style.backgroundColor = '');
|
|
}
|
|
|
|
// Barcode generation
|
|
function generateBarcodePreview() {
|
|
const boxNumber = document.getElementById('print_box_number').textContent.trim();
|
|
|
|
if (!boxNumber || boxNumber === '-') {
|
|
alert('Please select a box first');
|
|
return;
|
|
}
|
|
|
|
const barcodeEl = document.getElementById('boxBarcode');
|
|
const containerEl = document.getElementById('barcodePreviewContainer');
|
|
const printBtn = document.getElementById('printBoxBtn');
|
|
|
|
if (barcodeEl) {
|
|
barcodeEl.innerHTML = '';
|
|
|
|
try {
|
|
JsBarcode("#boxBarcode", boxNumber, {
|
|
format: "CODE128",
|
|
width: 2,
|
|
height: 100,
|
|
displayValue: true,
|
|
margin: 10
|
|
});
|
|
|
|
console.log('Barcode generated for box:', boxNumber);
|
|
|
|
if (containerEl) {
|
|
containerEl.style.display = 'block';
|
|
}
|
|
if (printBtn) {
|
|
printBtn.style.display = 'block';
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Error generating barcode:', error);
|
|
alert('Error generating barcode. Make sure jsbarcode library is loaded.');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Print barcode
|
|
function printBarcode() {
|
|
const boxNumber = document.getElementById('print_box_number').textContent.trim();
|
|
|
|
if (!boxNumber || boxNumber === '-') {
|
|
alert('Please select a box first');
|
|
return;
|
|
}
|
|
|
|
console.log('Printing barcode for box:', boxNumber);
|
|
|
|
if (window.qzPrinter && window.qzPrinter.connected) {
|
|
printWithQZTray(boxNumber);
|
|
} else {
|
|
printWithBrowserDialog(boxNumber);
|
|
}
|
|
}
|
|
|
|
// Print with QZ Tray
|
|
function printWithQZTray(boxNumber) {
|
|
try {
|
|
if (!window.qzPrinter || !window.qzPrinter.connected) {
|
|
console.log('QZ Tray not connected, falling back to browser print');
|
|
printWithBrowserDialog(boxNumber);
|
|
return;
|
|
}
|
|
|
|
const svgElement = document.getElementById('boxBarcode');
|
|
if (!svgElement) {
|
|
console.error('Barcode element not found');
|
|
printWithBrowserDialog(boxNumber);
|
|
return;
|
|
}
|
|
|
|
const printerSelect = document.getElementById('printer-select');
|
|
const selectedPrinter = printerSelect ? printerSelect.value : '';
|
|
|
|
console.log('Printing to QZ Tray - Selected printer:', selectedPrinter || 'Default');
|
|
|
|
window.qzPrinter.printSVGBarcode(svgElement, 'Box: ' + boxNumber, selectedPrinter)
|
|
.then(() => {
|
|
console.log('✅ Print job sent successfully');
|
|
const printerName = selectedPrinter || 'default printer';
|
|
alert('Print job sent to ' + printerName + ' successfully!');
|
|
})
|
|
.catch(error => {
|
|
console.error('Print error:', error);
|
|
console.log('Falling back to browser print');
|
|
printWithBrowserDialog(boxNumber);
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('QZ Tray printing error:', error);
|
|
printWithBrowserDialog(boxNumber);
|
|
}
|
|
}
|
|
|
|
// Browser print fallback
|
|
function printWithBrowserDialog(boxNumber) {
|
|
const printWindow = window.open('', '', 'height=400,width=600');
|
|
const barcodeSvg = document.getElementById('boxBarcode').innerHTML;
|
|
|
|
printWindow.document.write('<html><head><title>Print Box Label</title>');
|
|
printWindow.document.write('<style>');
|
|
printWindow.document.write('body { font-family: Arial, sans-serif; text-align: center; padding: 20px; }');
|
|
printWindow.document.write('h2 { margin-bottom: 30px; }');
|
|
printWindow.document.write('svg { max-width: 100%; height: auto; margin: 20px 0; }');
|
|
printWindow.document.write('</style></head><body>');
|
|
printWindow.document.write('<h2>Box: ' + boxNumber + '</h2>');
|
|
printWindow.document.write('<svg id="barcode">' + barcodeSvg + '</svg>');
|
|
printWindow.document.write('<p style="margin-top: 40px; font-size: 14px; color: #666;">');
|
|
printWindow.document.write('Printed on: ' + new Date().toLocaleString());
|
|
printWindow.document.write('</p></body></html>');
|
|
printWindow.document.close();
|
|
|
|
setTimeout(function() {
|
|
printWindow.print();
|
|
}, 250);
|
|
}
|
|
|
|
// QZ Tray status update
|
|
function updateQZStatus(message, status = 'info') {
|
|
const statusEl = document.getElementById('qz-status');
|
|
if (statusEl) {
|
|
statusEl.textContent = 'QZ Tray: ' + message;
|
|
statusEl.className = status === 'success' ? 'text-success fw-bold' :
|
|
status === 'warning' ? 'text-warning' : 'text-muted';
|
|
}
|
|
}
|
|
|
|
// Test QZ Tray
|
|
function testQZTrayConnection() {
|
|
if (window.qzPrinter && window.qzPrinter.connected) {
|
|
const printers = window.qzPrinter.availablePrinters;
|
|
const printerList = printers.length > 0
|
|
? printers.join('\n• ')
|
|
: 'No printers found';
|
|
alert('✅ QZ Tray is connected and ready!\n\nAvailable printers:\n• ' + printerList);
|
|
} else {
|
|
alert('⚠️ QZ Tray is not connected.\nBrowser print will be used instead.');
|
|
}
|
|
}
|
|
|
|
// Initialize on page load
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
updateDeleteBtn();
|
|
|
|
// Initialize QZ Tray
|
|
setTimeout(() => {
|
|
if (window.qzPrinter) {
|
|
if (window.qzPrinter.connected) {
|
|
updateQZStatus('Connected ✅', 'success');
|
|
populatePrinterSelect();
|
|
} else {
|
|
updateQZStatus('Not Available (will use browser print)', 'warning');
|
|
}
|
|
}
|
|
}, 500);
|
|
|
|
// Handle printer selection change
|
|
const printerSelect = document.getElementById('printer-select');
|
|
if (printerSelect) {
|
|
printerSelect.addEventListener('change', function() {
|
|
if (window.qzPrinter) {
|
|
window.qzPrinter.selectPrinter(this.value);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// Populate printer select
|
|
function populatePrinterSelect() {
|
|
if (!window.qzPrinter || !window.qzPrinter.availablePrinters.length) {
|
|
return;
|
|
}
|
|
|
|
const printerSelect = document.getElementById('printer-select');
|
|
if (!printerSelect) return;
|
|
|
|
printerSelect.innerHTML = '<option value="">Default Printer</option>';
|
|
|
|
window.qzPrinter.availablePrinters.forEach(printer => {
|
|
const option = document.createElement('option');
|
|
option.value = printer;
|
|
option.textContent = printer;
|
|
if (printer === window.qzPrinter.selectedPrinter) {
|
|
option.selected = true;
|
|
}
|
|
printerSelect.appendChild(option);
|
|
});
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
.sticky-top {
|
|
z-index: 10;
|
|
}
|
|
|
|
.table-hover tbody tr:hover {
|
|
background-color: #f8f9fa;
|
|
}
|
|
|
|
.card {
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.card-header {
|
|
border-radius: 8px 8px 0 0;
|
|
}
|
|
|
|
#boxesTable {
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
.btn-outline-primary:hover {
|
|
transform: scale(1.05);
|
|
transition: all 0.2s;
|
|
}
|
|
</style>
|
|
{% endblock %}
|