539 lines
19 KiB
PHP
539 lines
19 KiB
PHP
<?php
|
|
session_start();
|
|
require '../config.php';
|
|
require '../include_header.php';
|
|
?>
|
|
|
|
<body>
|
|
<?php require '../include_topbar.php'; ?>
|
|
<?php require '../include_master_sidebar.php'; ?>
|
|
|
|
<main id="content" class="content py-15">
|
|
<div class="container-fluid">
|
|
<div class="row mb-6">
|
|
<div class="col-12 d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<h1 class="fs-3 mb-1">SKU Barcode Labels</h1>
|
|
<p class="mb-0 text-muted">Print or reprint product identity labels from the lot master list.</p>
|
|
</div>
|
|
<div class="d-flex gap-2">
|
|
<button class="btn btn-success" onclick="open_create_label_modal()">
|
|
<i class="ti ti-plus me-1"></i>New Barcode
|
|
</button>
|
|
<button id="btn_print" class="btn btn-outline-success" onclick="print_selected_label()" disabled>
|
|
<i class="ti ti-printer me-1"></i>Print
|
|
</button>
|
|
<button id="btn_disable" class="btn btn-outline-danger" onclick="disable_selected_label()" disabled>
|
|
<i class="ti ti-ban me-1"></i>Disable
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-5">
|
|
<div class="col-lg-7">
|
|
<div class="card">
|
|
<div class="card-body p-5">
|
|
<div class="position-relative mb-4">
|
|
<input type="search" id="search_lot" class="form-control ps-9"
|
|
placeholder="Search SKU, product, lot, expiry, or description...">
|
|
<span class="position-absolute top-25 ms-4">
|
|
<i class="ti ti-search text-muted"></i>
|
|
</span>
|
|
</div>
|
|
<div class="table-responsive table-scroll">
|
|
<table class="table table-hover mb-0">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>SKU</th>
|
|
<th>Product</th>
|
|
<th>Lot No</th>
|
|
<th>Expiry</th>
|
|
<th>Source</th>
|
|
<th class="text-end">Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="lot_tbody">
|
|
<tr><td colspan="7" class="text-center text-muted py-4">Loading lots...</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-lg-5">
|
|
<div class="card">
|
|
<div class="card-body p-5">
|
|
<div id="label_preview" class="border rounded p-4 text-center text-muted">
|
|
Select a lot to preview its label.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<!-- Manage Labels Modal (view existing + add new) -->
|
|
<div class="modal fade" id="skuLabelModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<div>
|
|
<h5 class="modal-title mb-0">Barcode Labels</h5>
|
|
<p class="text-muted small mb-0 mt-1" id="modal_lot_info"></p>
|
|
</div>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<input type="hidden" id="modal_lot_index">
|
|
|
|
<p class="fw-semibold small text-uppercase text-muted mb-2">Existing Labels</p>
|
|
<div id="existing_label_list" class="small text-muted mb-4">Loading...</div>
|
|
|
|
<hr class="my-3">
|
|
|
|
<p class="fw-semibold small text-uppercase text-muted mb-2">Add New Label</p>
|
|
<div class="input-group">
|
|
<input type="text" id="modal_serial_number" class="form-control" placeholder="Serial No"
|
|
autocomplete="off">
|
|
<button class="btn btn-primary" type="button" onclick="generate_label_from_modal()">
|
|
<i class="ti ti-barcode me-1"></i>Generate
|
|
</button>
|
|
</div>
|
|
<small class="text-muted">Enter a new serial to create a label, or enter an existing serial to reprint.</small>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- New Barcode Modal (free-form, lot need not exist yet) -->
|
|
<div class="modal fade" id="createSkuLabelModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">New Barcode</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label">SKU</label>
|
|
<select id="create_sku" class="form-select" onchange="sync_create_product_name()"></select>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Product</label>
|
|
<input type="text" id="create_product_name" class="form-control" readonly>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Lot No</label>
|
|
<input type="text" id="create_lot_number" class="form-control" autocomplete="off">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Serial No</label>
|
|
<input type="text" id="create_serial_number" class="form-control" autocomplete="off">
|
|
</div>
|
|
<div class="alert alert-light border mb-0 small">
|
|
Use this to prepare a label before the lot has been received into the system.
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="button" class="btn btn-primary" onclick="create_custom_label()">
|
|
<i class="ti ti-barcode me-1"></i>Create & Preview
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<?php require '../include_ending.php'; ?>
|
|
<script src="https://cdn.jsdelivr.net/npm/jsbarcode@3.11.6/dist/JsBarcode.all.min.js"></script>
|
|
<script>
|
|
var lots = [];
|
|
var products = [];
|
|
var active_labels = [];
|
|
var selected_payload = '';
|
|
var selected_label_data = null;
|
|
var selected_lot_index = -1;
|
|
var sku_label_modal = null;
|
|
var create_label_modal = null;
|
|
|
|
function escape_html(value) {
|
|
return String(value ?? '').replace(/[&<>"']/g, function(char) {
|
|
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char];
|
|
});
|
|
}
|
|
|
|
function refresh_header_buttons() {
|
|
var has = !!selected_payload;
|
|
$('#btn_print').prop('disabled', !has);
|
|
$('#btn_disable').prop('disabled', !has);
|
|
}
|
|
|
|
function highlight_selected_row() {
|
|
$('#lot_tbody tr').removeClass('table-active');
|
|
if (selected_lot_index >= 0) {
|
|
$('#lot_tbody tr[data-lot-index="' + selected_lot_index + '"]').addClass('table-active');
|
|
}
|
|
}
|
|
|
|
function load_lots() {
|
|
return ajax_request({
|
|
url: server_url + 'ics/api/engine/sku_label_lots.php',
|
|
autoPrepare: true,
|
|
checkRequired: 0,
|
|
action: 'read',
|
|
queueLock: false,
|
|
onSuccess: function(res) {
|
|
lots = res.output || [];
|
|
render_lots();
|
|
highlight_selected_row();
|
|
}
|
|
});
|
|
}
|
|
|
|
function load_products() {
|
|
return ajax_request({
|
|
url: server_url + 'ics/api/engine/sku_label_products.php',
|
|
autoPrepare: true,
|
|
checkRequired: 0,
|
|
action: 'read',
|
|
queueLock: false,
|
|
onSuccess: function(res) {
|
|
products = res.output || [];
|
|
var opts = '<option value="">Select SKU...</option>';
|
|
products.forEach(function(item) {
|
|
opts += `<option value="${escape_html(item.sku)}">${escape_html(item.sku)} - ${escape_html(item.product_name)}</option>`;
|
|
});
|
|
$('#create_sku').html(opts);
|
|
}
|
|
});
|
|
}
|
|
|
|
function selected_create_product() {
|
|
var sku = $('#create_sku').val();
|
|
return products.find(function(item) { return item.sku === sku; }) || {};
|
|
}
|
|
|
|
function sync_create_product_name() {
|
|
var item = selected_create_product();
|
|
$('#create_product_name').val(item.product_name || '');
|
|
}
|
|
|
|
function open_create_label_modal() {
|
|
$('#create_sku').val('');
|
|
$('#create_product_name').val('');
|
|
$('#create_lot_number').val('');
|
|
$('#create_serial_number').val('');
|
|
create_label_modal.show();
|
|
}
|
|
|
|
function render_lots() {
|
|
var keyword = $('#search_lot').val().toLowerCase();
|
|
var rows = lots.filter(function(item) {
|
|
if (!keyword) return true;
|
|
return [
|
|
item.product_sku,
|
|
item.product_name,
|
|
item.lot_number,
|
|
item.expiry_date,
|
|
item.description
|
|
].join(' ').toLowerCase().includes(keyword);
|
|
});
|
|
|
|
if (!rows.length) {
|
|
$('#lot_tbody').html('<tr><td colspan="7" class="text-center text-muted py-4">No lots found.</td></tr>');
|
|
return;
|
|
}
|
|
|
|
var body = '';
|
|
rows.forEach(function(item) {
|
|
var idx = lots.indexOf(item);
|
|
var source = '<span class="badge bg-success">Lot master</span>';
|
|
if (item.source === 'active_stock') {
|
|
source = '<span class="badge bg-warning text-dark">Stock only</span>';
|
|
}
|
|
if (item.source === 'md_barcode') {
|
|
source = '<span class="badge bg-info text-dark">Barcode only</span>';
|
|
}
|
|
|
|
body += `<tr data-lot-index="${idx}">
|
|
<td class="fw-semibold">${escape_html(item.product_sku)}</td>
|
|
<td>${escape_html(item.product_name)}</td>
|
|
<td>${escape_html(item.lot_number)}</td>
|
|
<td>${item.expiry_date ? escape_html(item.expiry_date) : '<span class="text-muted">—</span>'}</td>
|
|
<td>${source}</td>
|
|
<td class="text-end">
|
|
<button class="btn btn-sm btn-outline-primary" onclick="open_label_modal(${idx})">
|
|
<i class="ti ti-list me-1"></i>Labels
|
|
</button>
|
|
</td>
|
|
</tr>`;
|
|
});
|
|
$('#lot_tbody').html(body);
|
|
}
|
|
|
|
function open_label_modal(index) {
|
|
var item = lots[index];
|
|
if (!item) return;
|
|
|
|
$('#modal_lot_index').val(index);
|
|
var info = escape_html(item.product_sku) + ' · ' + escape_html(item.lot_number);
|
|
if (item.expiry_date) info += ' · Exp ' + escape_html(item.expiry_date);
|
|
$('#modal_lot_info').html(info);
|
|
$('#modal_serial_number').val('');
|
|
$('#existing_label_list').html('Loading...');
|
|
sku_label_modal.show();
|
|
load_existing_labels(item);
|
|
setTimeout(function() { $('#modal_serial_number').trigger('focus'); }, 300);
|
|
}
|
|
|
|
function load_existing_labels(item) {
|
|
ajax_request({
|
|
url: server_url + 'ics/api/engine/sku_barcode_label.php',
|
|
autoPrepare: true,
|
|
checkRequired: 0,
|
|
action: 'read',
|
|
queueLock: false,
|
|
data: {
|
|
product_sku: item.product_sku,
|
|
lot_number: item.lot_number
|
|
},
|
|
onSuccess: function(res) {
|
|
active_labels = res.output || [];
|
|
if (!active_labels.length) {
|
|
$('#existing_label_list').html('<span class="text-muted">No labels prepared for this lot yet.</span>');
|
|
return;
|
|
}
|
|
|
|
var html = '<div class="list-group list-group-flush">';
|
|
active_labels.forEach(function(label, index) {
|
|
var statusBadge = parseInt(label.status) === 1
|
|
? '<span class="badge bg-success ms-2">Active</span>'
|
|
: '<span class="badge bg-danger ms-2">Disabled</span>';
|
|
var actionButtons = parseInt(label.status) === 1
|
|
? `<button type="button" class="btn btn-sm btn-outline-primary" onclick="preview_existing_label(${index})">
|
|
<i class="ti ti-eye me-1"></i>Select
|
|
</button>
|
|
<button type="button" class="btn btn-sm btn-outline-danger" onclick="disable_existing_label(${index})">
|
|
<i class="ti ti-ban me-1"></i>Disable
|
|
</button>`
|
|
: `<button type="button" class="btn btn-sm btn-outline-success" onclick="reenable_existing_label(${index})">
|
|
<i class="ti ti-refresh me-1"></i>Re-enable
|
|
</button>`;
|
|
html += `<div class="list-group-item d-flex justify-content-between align-items-center px-0">
|
|
<span class="small">${escape_html(label.serial_number)} ${statusBadge}</span>
|
|
<div class="d-flex gap-2">${actionButtons}</div>
|
|
</div>`;
|
|
});
|
|
html += '</div>';
|
|
$('#existing_label_list').html(html);
|
|
}
|
|
});
|
|
}
|
|
|
|
function preview_label(data, lot_index) {
|
|
selected_payload = data.barcode;
|
|
selected_label_data = data;
|
|
if (lot_index !== undefined) selected_lot_index = lot_index;
|
|
$('#label_preview').html(`
|
|
<div class="barcode-label mx-auto">
|
|
<div class="fw-bold fs-5">${escape_html(data.product_sku)}</div>
|
|
<div class="small text-muted">${escape_html(data.product_name)}</div>
|
|
<svg id="barcode_svg"></svg>
|
|
<div class="small">Lot: ${escape_html(data.lot_number)} Serial: ${escape_html(data.serial_number)}</div>
|
|
<div class="small text-muted">${escape_html(data.barcode)}</div>
|
|
</div>`);
|
|
JsBarcode('#barcode_svg', data.barcode, { format: 'CODE128', width: 1.6, height: 70, displayValue: false });
|
|
sku_label_modal.hide();
|
|
refresh_header_buttons();
|
|
highlight_selected_row();
|
|
}
|
|
|
|
function preview_existing_label(index) {
|
|
var label = active_labels[index];
|
|
if (!label) return;
|
|
var lot_index = parseInt($('#modal_lot_index').val(), 10);
|
|
preview_label(label, lot_index);
|
|
}
|
|
|
|
function clear_selected_label() {
|
|
selected_payload = '';
|
|
selected_label_data = null;
|
|
selected_lot_index = -1;
|
|
$('#label_preview').html('Select a lot to preview its label.');
|
|
refresh_header_buttons();
|
|
highlight_selected_row();
|
|
}
|
|
|
|
function disable_barcode_label(barcode, onDone) {
|
|
bootbox.confirm({
|
|
message: 'Disable this SKU barcode label? It will no longer be accepted by stock scans.',
|
|
buttons: {
|
|
confirm: { label: 'Disable', className: 'btn-danger' },
|
|
cancel: { label: 'Back', className: 'btn-secondary' }
|
|
},
|
|
callback: function(result) {
|
|
if (!result) return;
|
|
ajax_request({
|
|
url: server_url + 'ics/api/engine/sku_barcode_label.php',
|
|
autoPrepare: true,
|
|
checkRequired: 0,
|
|
action: 'read',
|
|
queueLock: false,
|
|
data: { label_action: 'disable', barcode: barcode },
|
|
onSuccess: function() {
|
|
clear_selected_label();
|
|
load_lots();
|
|
onDone?.();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
function disable_existing_label(index) {
|
|
var label = active_labels[index];
|
|
if (!label) return;
|
|
disable_barcode_label(label.barcode, function() {
|
|
var item = lots[parseInt($('#modal_lot_index').val(), 10)];
|
|
if (item) load_existing_labels(item);
|
|
});
|
|
}
|
|
|
|
function reenable_existing_label(index) {
|
|
var label = active_labels[index];
|
|
if (!label) return;
|
|
ajax_request({
|
|
url: server_url + 'ics/api/engine/sku_barcode_label.php',
|
|
autoPrepare: true,
|
|
checkRequired: 0,
|
|
action: 'read',
|
|
queueLock: false,
|
|
data: {
|
|
label_action: 'create',
|
|
allow_unlisted: 1,
|
|
product_sku: label.product_sku,
|
|
lot_number: label.lot_number,
|
|
serial_number: label.serial_number
|
|
},
|
|
onSuccess: function() {
|
|
var item = lots[parseInt($('#modal_lot_index').val(), 10)];
|
|
if (item) load_existing_labels(item);
|
|
load_lots();
|
|
}
|
|
});
|
|
}
|
|
|
|
function disable_selected_label() {
|
|
disable_barcode_label(selected_payload);
|
|
}
|
|
|
|
function generate_label_from_modal() {
|
|
var item = lots[parseInt($('#modal_lot_index').val(), 10)];
|
|
var serial = $('#modal_serial_number').val().trim();
|
|
if (!item || !serial) {
|
|
bootbox.alert('Serial no is required.');
|
|
return;
|
|
}
|
|
|
|
ajax_request({
|
|
url: server_url + 'ics/api/engine/sku_barcode_label.php',
|
|
autoPrepare: true,
|
|
checkRequired: 0,
|
|
action: 'read',
|
|
queueLock: false,
|
|
data: {
|
|
label_action: 'create',
|
|
product_sku: item.product_sku,
|
|
lot_number: item.lot_number,
|
|
serial_number: serial
|
|
},
|
|
onSuccess: function(res) {
|
|
var lot_index = parseInt($('#modal_lot_index').val(), 10);
|
|
preview_label(res.output, lot_index);
|
|
load_lots();
|
|
}
|
|
});
|
|
}
|
|
|
|
function create_custom_label() {
|
|
var sku = $('#create_sku').val();
|
|
var lot = $('#create_lot_number').val().trim();
|
|
var serial = $('#create_serial_number').val().trim();
|
|
|
|
if (!sku || !lot || !serial) {
|
|
bootbox.alert('SKU, lot no, and serial no are required.');
|
|
return;
|
|
}
|
|
|
|
ajax_request({
|
|
url: server_url + 'ics/api/engine/sku_barcode_label.php',
|
|
autoPrepare: true,
|
|
checkRequired: 0,
|
|
action: 'read',
|
|
queueLock: false,
|
|
data: {
|
|
label_action: 'create',
|
|
allow_unlisted: 1,
|
|
product_sku: sku,
|
|
lot_number: lot,
|
|
serial_number: serial
|
|
},
|
|
onSuccess: function(res) {
|
|
selected_lot_index = -1;
|
|
create_label_modal.hide();
|
|
preview_label(res.output);
|
|
load_lots();
|
|
}
|
|
});
|
|
}
|
|
|
|
function print_selected_label() {
|
|
if (!selected_payload) return;
|
|
ajax_request({
|
|
url: server_url + 'ics/api/engine/sku_barcode_label.php',
|
|
autoPrepare: true,
|
|
checkRequired: 0,
|
|
action: 'read',
|
|
queueLock: false,
|
|
data: { label_action: 'print', barcode: selected_payload },
|
|
onSuccess: function() { window.print(); }
|
|
});
|
|
}
|
|
|
|
$('#search_lot').on('input', render_lots);
|
|
|
|
$(function() {
|
|
sku_label_modal = new bootstrap.Modal(document.getElementById('skuLabelModal'));
|
|
create_label_modal = new bootstrap.Modal(document.getElementById('createSkuLabelModal'));
|
|
load_lots();
|
|
load_products();
|
|
});
|
|
</script>
|
|
<style>
|
|
.barcode-label { max-width: 420px; background: #fff; color: #111; }
|
|
.table-scroll {
|
|
max-height: calc(100vh - 320px);
|
|
overflow-y: auto;
|
|
}
|
|
.table-scroll thead th {
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 10;
|
|
background: #f8f9fa !important;
|
|
box-shadow: inset 0 -1px 0 #dee2e6;
|
|
}
|
|
@media print {
|
|
body * { visibility: hidden; }
|
|
#label_preview, #label_preview * { visibility: visible; }
|
|
#label_preview { position: absolute; left: 0; top: 0; width: 100%; border: 0 !important; }
|
|
}
|
|
</style>
|
|
</body>
|
|
</html>
|