547 lines
21 KiB
HTML
547 lines
21 KiB
HTML
{% extends 'base.html' %}
|
|
{% load static %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid mt-4 mb-5 no-print">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h2 class="h4 mb-0"><i class="bi bi-upc-scan me-2"></i>Barcode Label Printing</h2>
|
|
<button onclick="window.print()" class="btn btn-primary">
|
|
<i class="bi bi-printer me-2"></i>Print Labels
|
|
</button>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<!-- Product Selection -->
|
|
<div class="col-md-5">
|
|
<div class="card shadow-sm border-0 mb-4">
|
|
<div class="card-header bg-white py-3">
|
|
<h5 class="card-title mb-0">1. Select Products</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="input-group mb-3">
|
|
<span class="input-group-text bg-light border-end-0"><i class="bi bi-search"></i></span>
|
|
<input type="text" id="productSearch" class="form-control border-start-0 ps-0" placeholder="Search by name or SKU...">
|
|
</div>
|
|
<div class="table-responsive" style="max-height: 500px;">
|
|
<table class="table table-hover align-middle" id="productTable">
|
|
<thead class="table-light sticky-top">
|
|
<tr>
|
|
<th>Product</th>
|
|
<th>SKU</th>
|
|
<th>Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for product in products %}
|
|
<tr class="product-row" data-name="{{ product.name_en }}" data-sku="{{ product.sku }}">
|
|
<td>
|
|
<div class="fw-bold">{{ product.name_en }}</div>
|
|
<small class="text-muted">{{ product.name_ar }}</small>
|
|
</td>
|
|
<td><code>{{ product.sku }}</code></td>
|
|
<td>
|
|
<button class="btn btn-sm btn-outline-primary add-to-queue"
|
|
data-id="{{ product.id }}"
|
|
data-name="{{ product.name_en }}"
|
|
data-sku="{{ product.sku }}"
|
|
data-price="{{ product.price }}">
|
|
<i class="bi bi-plus"></i>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Label Queue & Settings -->
|
|
<div class="col-md-7">
|
|
<div class="card shadow-sm border-0 mb-4">
|
|
<div class="card-header bg-white py-3">
|
|
<h5 class="card-title mb-0">2. Label Queue & Settings</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row g-3 mb-4">
|
|
<div class="col-md-6">
|
|
<label class="form-label fw-bold">Label Size / Type</label>
|
|
<select id="labelType" class="form-select">
|
|
<option value="standard">Standard Sticker (50mm x 25mm)</option>
|
|
<option value="small">Small Sticker (38mm x 25mm)</option>
|
|
<option value="a4-24">A4 Sheet (3x8 = 24 labels)</option>
|
|
<option value="a4-40">A4 Sheet (4x10 = 40 labels)</option>
|
|
<option value="l7651">A4 Avery L7651 (5x13 = 65 labels)</option>
|
|
<option value="l7656">A4 Avery L7656 (4x21 = 84 labels)</option>
|
|
<option value="l7156">A4 Avery L7156 (3x7 = 21 labels)</option>
|
|
<option value="price-tag">Jewelry / Price Tag (Small)</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label fw-bold">Include Fields</label>
|
|
<div class="d-flex gap-3 mt-2">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="showName" checked>
|
|
<label class="form-check-label" for="showName">Name</label>
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="showPrice" checked>
|
|
<label class="form-check-label" for="showPrice">Price</label>
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="showSKU" checked>
|
|
<label class="form-check-label" for="showSKU">SKU Text</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="table-responsive">
|
|
<table class="table align-middle">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>Product</th>
|
|
<th style="width: 120px;">Qty of Labels</th>
|
|
<th style="width: 50px;"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="labelQueue">
|
|
<!-- Dynamic content -->
|
|
</tbody>
|
|
</table>
|
|
<div id="emptyQueue" class="text-center py-4 text-muted">
|
|
<i class="bi bi-cart-x display-4 d-block mb-2"></i>
|
|
Queue is empty. Select products to start.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Live Preview -->
|
|
<div class="card shadow-sm border-0">
|
|
<div class="card-header bg-white py-3">
|
|
<h5 class="card-title mb-0">3. Live Preview (Single Label)</h5>
|
|
</div>
|
|
<div class="card-body text-center bg-light p-5">
|
|
<div id="previewContainer" class="d-inline-block bg-white shadow-sm p-3 border">
|
|
<div id="previewLabelContent">
|
|
<div class="preview-name fw-bold small mb-1">Product Name</div>
|
|
<svg id="previewBarcode"></svg>
|
|
<div class="preview-footer d-flex justify-content-between mt-1 small">
|
|
<span class="preview-sku">SKU12345</span>
|
|
<span class="preview-price fw-bold">OMR 0.000</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="mt-3 text-muted small">
|
|
Note: This is a preview of the layout. Actual print layout depends on settings above.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- PRINT VIEW (Hidden by default) -->
|
|
<div id="printArea" class="print-only">
|
|
<!-- Generated labels will go here -->
|
|
</div>
|
|
|
|
<style>
|
|
/* Print Styles */
|
|
@media screen {
|
|
.print-only { display: none; }
|
|
}
|
|
@media print {
|
|
.no-print { display: none !important; }
|
|
.print-only { display: block; }
|
|
body { margin: 0; padding: 0; background: white; -webkit-print-color-adjust: exact; }
|
|
|
|
@page {
|
|
size: A4;
|
|
margin: 0;
|
|
}
|
|
|
|
.label-sheet {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
justify-content: flex-start;
|
|
padding: 5mm;
|
|
width: 210mm;
|
|
margin: 0 auto;
|
|
background: white;
|
|
}
|
|
|
|
/* Specific A4 Grid Layouts to ensure alignment */
|
|
.sheet-a4-24 {
|
|
display: grid !important;
|
|
grid-template-columns: repeat(3, 63.5mm);
|
|
grid-auto-rows: 33.9mm;
|
|
padding: 12.9mm 9.75mm !important;
|
|
gap: 0;
|
|
}
|
|
|
|
.sheet-a4-40 {
|
|
display: grid !important;
|
|
grid-template-columns: repeat(4, 48.5mm);
|
|
grid-auto-rows: 25.4mm;
|
|
padding: 21.5mm 8mm !important;
|
|
gap: 0;
|
|
}
|
|
|
|
/* Avery L7651 (5x13) */
|
|
.sheet-l7651 {
|
|
display: grid !important;
|
|
grid-template-columns: repeat(5, 38.1mm);
|
|
grid-auto-rows: 21.2mm;
|
|
padding: 10.7mm 9.75mm !important;
|
|
gap: 0;
|
|
}
|
|
|
|
/* Avery L7656 (4x21) */
|
|
.sheet-l7656 {
|
|
display: grid !important;
|
|
grid-template-columns: repeat(4, 46mm);
|
|
grid-auto-rows: 11.1mm;
|
|
padding: 31.95mm 13mm !important;
|
|
gap: 0;
|
|
}
|
|
|
|
/* Avery L7156 (3x7) */
|
|
.sheet-l7156 {
|
|
display: grid !important;
|
|
grid-template-columns: repeat(3, 63.5mm);
|
|
grid-auto-rows: 38.1mm;
|
|
padding: 15.15mm 9.75mm !important;
|
|
gap: 0;
|
|
}
|
|
|
|
/* Standard Sticker (50x25) - usually for thermal printers */
|
|
.label-standard {
|
|
width: 50mm;
|
|
height: 25mm;
|
|
border: 0.1mm solid #eee;
|
|
margin: 1mm;
|
|
padding: 2mm;
|
|
text-align: center;
|
|
overflow: hidden;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
align-items: center;
|
|
page-break-inside: avoid;
|
|
}
|
|
|
|
/* Small Sticker (38x25) */
|
|
.label-small {
|
|
width: 38mm;
|
|
height: 25mm;
|
|
border: 0.1mm solid #eee;
|
|
margin: 1mm;
|
|
padding: 1.5mm;
|
|
text-align: center;
|
|
overflow: hidden;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
align-items: center;
|
|
page-break-inside: avoid;
|
|
}
|
|
|
|
/* A4 24 Labels (3x8) */
|
|
.label-a4-24 {
|
|
width: 63.5mm;
|
|
height: 33.9mm;
|
|
padding: 2mm;
|
|
text-align: center;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
align-items: center;
|
|
page-break-inside: avoid;
|
|
}
|
|
|
|
/* A4 40 Labels (4x10) */
|
|
.label-a4-40 {
|
|
width: 48.5mm;
|
|
height: 25.4mm;
|
|
padding: 1mm;
|
|
text-align: center;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
align-items: center;
|
|
page-break-inside: avoid;
|
|
}
|
|
|
|
/* Avery L7651 (5x13) */
|
|
.label-l7651 {
|
|
width: 38.1mm;
|
|
height: 21.2mm;
|
|
padding: 1mm;
|
|
text-align: center;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
align-items: center;
|
|
page-break-inside: avoid;
|
|
}
|
|
|
|
/* Avery L7656 (4x21) */
|
|
.label-l7656 {
|
|
width: 46mm;
|
|
height: 11.1mm;
|
|
padding: 0.5mm;
|
|
text-align: center;
|
|
display: flex;
|
|
flex-direction: row;
|
|
justify-content: space-around;
|
|
align-items: center;
|
|
page-break-inside: avoid;
|
|
}
|
|
.label-l7656 svg {
|
|
height: 8mm !important;
|
|
width: auto;
|
|
}
|
|
.label-l7656 .label-text {
|
|
font-size: 5pt !important;
|
|
width: auto !important;
|
|
margin: 0 2px;
|
|
}
|
|
|
|
/* Avery L7156 (3x7) */
|
|
.label-l7156 {
|
|
width: 63.5mm;
|
|
height: 38.1mm;
|
|
padding: 2mm;
|
|
text-align: center;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
align-items: center;
|
|
page-break-inside: avoid;
|
|
}
|
|
|
|
/* Price Tag */
|
|
.label-price-tag {
|
|
width: 30mm;
|
|
height: 15mm;
|
|
margin: 1mm;
|
|
padding: 1mm;
|
|
text-align: center;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
align-items: center;
|
|
page-break-inside: avoid;
|
|
}
|
|
|
|
.label-item svg {
|
|
max-width: 100%;
|
|
height: auto;
|
|
}
|
|
|
|
.label-text {
|
|
font-family: Arial, sans-serif;
|
|
font-size: 7pt;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
width: 100%;
|
|
line-height: 1.2;
|
|
}
|
|
.label-price {
|
|
font-size: 9pt;
|
|
font-weight: bold;
|
|
}
|
|
}
|
|
|
|
#previewBarcode {
|
|
max-width: 100%;
|
|
height: auto;
|
|
}
|
|
</style>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/jsbarcode@3.11.5/dist/JsBarcode.all.min.js"></script>
|
|
<script>
|
|
const queue = [];
|
|
const labelQueueBody = document.getElementById('labelQueue');
|
|
const emptyQueue = document.getElementById('emptyQueue');
|
|
const printArea = document.getElementById('printArea');
|
|
|
|
// Add to Queue
|
|
document.querySelectorAll('.add-to-queue').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const product = {
|
|
id: btn.dataset.id,
|
|
name: btn.dataset.name,
|
|
sku: btn.dataset.sku,
|
|
price: btn.dataset.price,
|
|
qty: 1
|
|
};
|
|
|
|
const existing = queue.find(p => p.id === product.id);
|
|
if (existing) {
|
|
existing.qty++;
|
|
} else {
|
|
queue.push(product);
|
|
}
|
|
renderQueue();
|
|
updatePreview();
|
|
});
|
|
});
|
|
|
|
function renderQueue() {
|
|
if (queue.length === 0) {
|
|
labelQueueBody.innerHTML = '';
|
|
emptyQueue.classList.remove('d-none');
|
|
return;
|
|
}
|
|
emptyQueue.classList.add('d-none');
|
|
labelQueueBody.innerHTML = queue.map((p, index) => `
|
|
<tr>
|
|
<td>
|
|
<div class="fw-bold small">${p.name}</div>
|
|
<code class="text-muted small">${p.sku}</code>
|
|
</td>
|
|
<td>
|
|
<input type="number" class="form-control form-control-sm qty-input"
|
|
value="${p.qty}" min="1" onchange="updateQty(${index}, this.value)">
|
|
</td>
|
|
<td class="text-end">
|
|
<button class="btn btn-sm btn-link text-danger" onclick="removeFromQueue(${index})">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
`).join('');
|
|
preparePrint();
|
|
}
|
|
|
|
window.updateQty = (index, val) => {
|
|
queue[index].qty = parseInt(val) || 1;
|
|
preparePrint();
|
|
};
|
|
|
|
window.removeFromQueue = (index) => {
|
|
queue.splice(index, 1);
|
|
renderQueue();
|
|
updatePreview();
|
|
};
|
|
|
|
// Product Search
|
|
document.getElementById('productSearch').addEventListener('input', function() {
|
|
const query = this.value.toLowerCase();
|
|
document.querySelectorAll('.product-row').forEach(row => {
|
|
const name = row.dataset.name.toLowerCase();
|
|
const sku = row.dataset.sku.toLowerCase();
|
|
if (name.includes(query) || sku.includes(query)) {
|
|
row.style.display = '';
|
|
} else {
|
|
row.style.display = 'none';
|
|
}
|
|
});
|
|
});
|
|
|
|
// Preview Logic
|
|
function updatePreview() {
|
|
if (queue.length === 0) return;
|
|
const lastProduct = queue[queue.length - 1];
|
|
|
|
document.querySelector('.preview-name').innerText = lastProduct.name;
|
|
document.querySelector('.preview-sku').innerText = lastProduct.sku;
|
|
document.querySelector('.preview-price').innerText = 'OMR ' + parseFloat(lastProduct.price).toFixed(3);
|
|
|
|
JsBarcode("#previewBarcode", lastProduct.sku, {
|
|
format: "CODE128",
|
|
width: 2,
|
|
height: 40,
|
|
displayValue: false,
|
|
margin: 0
|
|
});
|
|
|
|
// Apply visibility toggles
|
|
document.querySelector('.preview-name').style.display = document.getElementById('showName').checked ? '' : 'none';
|
|
document.querySelector('.preview-price').style.display = document.getElementById('showPrice').checked ? '' : 'none';
|
|
document.querySelector('.preview-sku').style.display = document.getElementById('showSKU').checked ? '' : 'none';
|
|
}
|
|
|
|
// Prepare Print Area
|
|
function preparePrint() {
|
|
printArea.innerHTML = '';
|
|
const labelType = document.getElementById('labelType').value;
|
|
const showName = document.getElementById('showName').checked;
|
|
const showPrice = document.getElementById('showPrice').checked;
|
|
const showSKU = document.getElementById('showSKU').checked;
|
|
|
|
const sheet = document.createElement('div');
|
|
sheet.className = 'label-sheet';
|
|
|
|
// Add specific sheet class for grid layouts
|
|
if (labelType === 'a4-24') sheet.classList.add('sheet-a4-24');
|
|
else if (labelType === 'a4-40') sheet.classList.add('sheet-a4-40');
|
|
else if (labelType === 'l7651') sheet.classList.add('sheet-l7651');
|
|
else if (labelType === 'l7656') sheet.classList.add('sheet-l7656');
|
|
else if (labelType === 'l7156') sheet.classList.add('sheet-l7156');
|
|
|
|
queue.forEach(p => {
|
|
for (let i = 0; i < p.qty; i++) {
|
|
const label = document.createElement('div');
|
|
label.className = `label-item label-${labelType}`;
|
|
|
|
let content = '';
|
|
if (labelType === 'l7656') {
|
|
// Horizontal layout for very short labels
|
|
if (showName) content += `<div class="label-text" style="max-width: 40px">${p.name}</div>`;
|
|
content += `<svg id="barcode-${p.id}-${i}"></svg>`;
|
|
if (showPrice) content += `<div class="label-text label-price">${parseFloat(p.price).toFixed(3)}</div>`;
|
|
} else {
|
|
if (showName) content += `<div class="label-text">${p.name}</div>`;
|
|
content += `<svg id="barcode-${p.id}-${i}"></svg>`;
|
|
if (showSKU || showPrice) {
|
|
content += `<div class="label-text d-flex justify-content-between">`;
|
|
if (showSKU) content += `<span>${p.sku}</span>`;
|
|
if (showPrice) content += `<span class="label-price">OMR ${parseFloat(p.price).toFixed(3)}</span>`;
|
|
content += `</div>`;
|
|
}
|
|
}
|
|
|
|
label.innerHTML = content;
|
|
sheet.appendChild(label);
|
|
}
|
|
});
|
|
|
|
printArea.appendChild(sheet);
|
|
|
|
// Generate barcodes
|
|
queue.forEach(p => {
|
|
for (let i = 0; i < p.qty; i++) {
|
|
const svgId = `barcode-${p.id}-${i}`;
|
|
let bWidth = 1.5;
|
|
let bHeight = 30;
|
|
|
|
if (labelType === 'l7651') { bWidth = 1.0; bHeight = 20; }
|
|
else if (labelType === 'l7656') { bWidth = 0.8; bHeight = 8; }
|
|
else if (labelType === 'a4-40') { bWidth = 1.2; bHeight = 25; }
|
|
else if (labelType === 'price-tag') { bWidth = 1.0; bHeight = 15; }
|
|
|
|
JsBarcode(`#${svgId}`, p.sku, {
|
|
format: "CODE128",
|
|
width: bWidth,
|
|
height: bHeight,
|
|
displayValue: false,
|
|
margin: 0
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
document.getElementById('labelType').addEventListener('change', preparePrint);
|
|
document.getElementById('showName').addEventListener('change', () => { updatePreview(); preparePrint(); });
|
|
document.getElementById('showPrice').addEventListener('change', () => { updatePreview(); preparePrint(); });
|
|
document.getElementById('showSKU').addEventListener('change', () => { updatePreview(); preparePrint(); });
|
|
|
|
updatePreview();
|
|
</script>
|
|
{% endblock %} |