Barcode Operations

This commit is contained in:
Thanakorn S
2026-05-06 15:27:52 +07:00
parent d8131a01b7
commit ada701bd53
22 changed files with 2449 additions and 35 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 802 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

+173
View File
@@ -0,0 +1,173 @@
var scanner = (function() {
var _callback = null;
var _allow_raw = true;
var _allowed_types = null;
var _camera = null;
var _modal = null;
var _buffer = '';
var _last_key_ts = 0;
var _max_key_gap = 0;
var _HW_THRESHOLD = 50;
function _on_paste(e) {
if (!_callback) return;
var el = document.activeElement;
if (el && el.tagName === 'TEXTAREA') return;
var text = ((e.originalEvent || e).clipboardData || window.clipboardData).getData('text');
if (!text) return;
text = text.trim();
if (text.length < 3) return;
e.preventDefault();
_resolve(text);
}
function _on_keydown(e) {
var now = Date.now();
var gap = now - _last_key_ts;
if (gap > 500) {
_buffer = '';
_max_key_gap = 0;
} else if (_buffer.length > 0) {
_max_key_gap = Math.max(_max_key_gap, gap);
}
_last_key_ts = now;
if (e.key === 'Enter' && _buffer.length >= 3) {
var barcode = _buffer;
var is_scanner_input = _max_key_gap <= _HW_THRESHOLD;
_buffer = '';
_max_key_gap = 0;
if (is_scanner_input) {
e.preventDefault();
_remove_typed_barcode(barcode);
_resolve(barcode);
}
return;
}
if (e.key.length === 1) {
_buffer += e.key;
}
}
function _remove_typed_barcode(barcode) {
var el = document.activeElement;
if (!el || !('value' in el)) return;
if (el.tagName !== 'INPUT' && el.tagName !== 'TEXTAREA') return;
var value = String(el.value || '');
if (!value.endsWith(barcode)) return;
el.value = value.slice(0, value.length - barcode.length);
$(el).trigger('input');
}
function _resolve(barcode) {
if (!barcode || !_callback) return;
$.ajax({
url: server_url + 'ics/api/engine/barcode_lookup.php',
type: 'POST',
dataType: 'json',
data: { json: JSON.stringify({ barcode: barcode, action: 'read' }) },
headers: { 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') },
success: function(res) {
if (res && res.success) {
if ((!_allow_raw && res.type === 'raw') || (_allowed_types && _allowed_types.indexOf(res.type) === -1)) {
_show_error('This page only accepts generated barcode labels.');
return;
}
_callback(res.sku || '', res.product_name || '', res.uom || '', res);
return;
}
_show_error('Barcode not found: ' + barcode);
},
error: function(xhr) {
var msg = xhr.responseJSON && xhr.responseJSON.message ? xhr.responseJSON.message : 'Scanner error. Please try again.';
_show_error(msg === 'Barcode not found.' ? 'Barcode not found: ' + barcode : msg);
}
});
}
function _build_modal() {
if (_modal) return;
$('body').append(`
<div class="modal fade" id="scanner_modal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="ti ti-scan me-2"></i>Scan Barcode</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div id="scanner_qr_reader"></div>
<div id="scanner_error" class="alert alert-danger mt-3 d-none"></div>
</div>
</div>
</div>
</div>`);
_modal = new bootstrap.Modal(document.getElementById('scanner_modal'));
document.getElementById('scanner_modal').addEventListener('hidden.bs.modal', _stop_camera);
}
function _start_camera() {
if (_camera) return;
if (typeof Html5Qrcode === 'undefined') {
_show_error('Camera scanner library is not available.');
return;
}
_camera = new Html5Qrcode('scanner_qr_reader');
_camera.start(
{ facingMode: 'environment' },
{ fps: 10, qrbox: { width: 250, height: 120 } },
function(decodedText) {
_modal.hide();
_resolve(decodedText);
},
function() {}
).catch(function(err) {
_show_error('Camera error: ' + err);
});
}
function _stop_camera() {
if (!_camera) return;
_camera.stop().catch(function() {}).finally(function() {
_camera = null;
});
}
function _show_error(msg) {
var $err = $('#scanner_error');
if ($err.length) {
$err.text(msg).removeClass('d-none');
setTimeout(function() { $err.addClass('d-none'); }, 3000);
return;
}
if (typeof bootbox !== 'undefined') bootbox.alert(msg);
}
return {
init: function(options) {
options = options || {};
_callback = options.onScan || null;
_allow_raw = options.allowRaw !== false;
_allowed_types = Array.isArray(options.allowedTypes) ? options.allowedTypes : null;
document.removeEventListener('keydown', _on_keydown);
document.removeEventListener('paste', _on_paste);
document.addEventListener('keydown', _on_keydown);
document.addEventListener('paste', _on_paste);
_build_modal();
},
open_camera: function() {
if (!_modal) _build_modal();
_modal.show();
setTimeout(_start_camera, 400);
},
destroy: function() {
document.removeEventListener('keydown', _on_keydown);
document.removeEventListener('paste', _on_paste);
_stop_camera();
}
};
})();
+6 -3
View File
@@ -317,7 +317,8 @@ class ProductManager {
$sth = $this->pdo->prepare( $sth = $this->pdo->prepare(
"SELECT * FROM md_product "SELECT * FROM md_product
WHERE company_id = :company_id WHERE company_id = :company_id
AND sku LIKE :keyword AND (sku LIKE :keyword OR barcode LIKE :keyword)
AND status > 0
LIMIT 50" LIMIT 50"
); );
$sth->execute([ $sth->execute([
@@ -358,6 +359,7 @@ class ProductManager {
':company_id' => $this->company_id, ':company_id' => $this->company_id,
':product_name' => $data['product_name'], ':product_name' => $data['product_name'],
':sku' => $data['sku'], ':sku' => $data['sku'],
':barcode' => trim($data['barcode'] ?? '') ?: null,
':uom' => $data['uom'] ?? 'pcs', ':uom' => $data['uom'] ?? 'pcs',
':price' => $data['price'], ':price' => $data['price'],
':cost_price' => (float)($data['cost_price'] ?? 0), ':cost_price' => (float)($data['cost_price'] ?? 0),
@@ -376,6 +378,7 @@ class ProductManager {
"UPDATE md_product SET "UPDATE md_product SET
product_name = :product_name, product_name = :product_name,
sku = :sku, sku = :sku,
barcode = :barcode,
uom = :uom, uom = :uom,
price = :price, price = :price,
cost_price = :cost_price, cost_price = :cost_price,
@@ -391,10 +394,10 @@ class ProductManager {
} else { } else {
$this->pdo->prepare( $this->pdo->prepare(
"INSERT INTO md_product "INSERT INTO md_product
(company_id, product_name, sku, uom, price, cost_price, min_stock, reorder_point, (company_id, product_name, sku, barcode, uom, price, cost_price, min_stock, reorder_point,
category, product_image, `description`, `status`, `log`) category, product_image, `description`, `status`, `log`)
VALUES VALUES
(:company_id, :product_name, :sku, :uom, :price, :cost_price, :min_stock, :reorder_point, (:company_id, :product_name, :sku, :barcode, :uom, :price, :cost_price, :min_stock, :reorder_point,
:category, :product_image, :description, :status, :log)" :category, :product_image, :description, :status, :log)"
)->execute($params); )->execute($params);
} }
+205
View File
@@ -0,0 +1,205 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
$barcode = trim($data['barcode'] ?? '');
if (!$barcode) {
$answer['message'] = 'No barcode provided.';
http_response_code(400);
exit(json_encode($answer));
}
function findProduct(PDO $pdo, int $company_id, string $sku): array|false {
$sth = $pdo->prepare(
"SELECT sku, product_name, uom
FROM md_product
WHERE company_id = :company_id
AND sku = :sku
AND status > 0
LIMIT 1"
);
$sth->execute([':company_id' => $company_id, ':sku' => $sku]);
return $sth->fetch(PDO::FETCH_ASSOC);
}
function validateLotSerial(PDO $pdo, int $company_id, string $sku, string $lot, string $serial): void {
if ($serial === '') return;
$warehouses = $pdo->prepare(
"SELECT id FROM md_warehouse
WHERE company_id = :company_id AND status = 1"
);
$warehouses->execute([':company_id' => $company_id]);
foreach ($warehouses->fetchAll(PDO::FETCH_COLUMN) as $warehouse_id) {
$table = 'td_stock_' . (int)$warehouse_id;
$sth = $pdo->prepare(
"SELECT lot_number
FROM `{$table}`
WHERE company_id = :company_id
AND product_sku = :sku
AND serial_number = :serial
AND status != -1
LIMIT 1"
);
$sth->execute([':company_id' => $company_id, ':sku' => $sku, ':serial' => $serial]);
$existing_lot = $sth->fetchColumn();
if ($existing_lot !== false && (string)$existing_lot !== $lot) {
throw new Exception('Serial number belongs to a different lot.');
}
}
}
function findPreparedLabel(PDO $pdo, int $company_id, string $barcode, string $type): array|false {
$sth = $pdo->prepare(
"SELECT *
FROM md_barcode
WHERE company_id = :company_id
AND barcode = :barcode
AND barcode_type = :barcode_type
LIMIT 1"
);
$sth->execute([
':company_id' => $company_id,
':barcode' => $barcode,
':barcode_type' => $type,
]);
$row = $sth->fetch(PDO::FETCH_ASSOC);
if ($row && (int)$row['status'] !== 1) {
throw new Exception('Barcode is disabled.');
}
return $row ?: false;
}
function advancedLocation(PDO $pdo, int $company_id): bool {
$sth = $pdo->prepare(
"SELECT value FROM company_setting
WHERE company_id = :company_id AND setting_key = 'advanced_location'
LIMIT 1"
);
$sth->execute([':company_id' => $company_id]);
$value = $sth->fetchColumn();
return (int)($value === false ? 0 : $value) === 1;
}
function validateLocation(PDO $pdo, int $company_id, int $warehouse_id, string $zone, string $aisle, string $rack): void {
$sth = $pdo->prepare(
"SELECT 1 FROM md_rack
WHERE company_id = :company_id
AND warehouse = :warehouse
AND zone = :zone
AND aisle = :aisle
AND rack = :rack
LIMIT 1"
);
$sth->execute([
':company_id' => $company_id,
':warehouse' => $warehouse_id,
':zone' => $zone,
':aisle' => $aisle,
':rack' => $rack,
]);
if (!$sth->fetchColumn()) throw new Exception('Location not found.');
}
$parts = explode('|', $barcode);
$label_type = strtoupper($parts[0] ?? '');
try {
if ($label_type === 'SKU') {
if (count($parts) !== 4) throw new Exception('Invalid SKU label format.');
$sku = trim($parts[1]);
$lot = trim($parts[2]);
$serial = trim($parts[3]);
if ($sku === '' || $lot === '' || $serial === '') throw new Exception('SKU, lot, and serial are required.');
$label = findPreparedLabel($pdo2, $company_id, $barcode, 'sku');
if (!$label) throw new Exception('SKU barcode label was not prepared.');
if ($label['product_sku'] !== $sku || $label['lot_number'] !== $lot || $label['serial_number'] !== $serial) {
throw new Exception('SKU barcode label does not match its registry.');
}
$product = findProduct($pdo2, $company_id, $sku);
if (!$product) throw new Exception('SKU not found.');
validateLotSerial($pdo2, $company_id, $sku, $lot, $serial);
$answer['success'] = 1;
$answer['type'] = 'sku';
$answer['sku'] = $product['sku'];
$answer['product_name'] = $product['product_name'];
$answer['uom'] = $product['uom'] ?? 'pcs';
$answer['lot_number'] = $lot;
$answer['serial_number'] = $serial;
exit(json_encode($answer));
}
if ($label_type === 'LOC') {
$advanced = advancedLocation($pdo1, $company_id);
if ($advanced && count($parts) !== 5) throw new Exception('Invalid advanced location label format.');
if (!$advanced && count($parts) !== 3) throw new Exception('Invalid location label format.');
$warehouse_id = (int)($parts[1] ?? 0);
if ($advanced) {
$zone = trim($parts[2]);
$aisle = trim($parts[3]);
$rack = trim($parts[4]);
} else {
$rack = trim($parts[2]);
$zone = $rack;
$aisle = $rack;
}
if (!$warehouse_id || $zone === '' || $aisle === '' || $rack === '') throw new Exception('Location label is incomplete.');
$label = findPreparedLabel($pdo2, $company_id, $barcode, 'loc');
if (!$label) throw new Exception('Location barcode label was not prepared.');
if ((int)$label['warehouse_id'] !== $warehouse_id || $label['zone'] !== $zone || $label['aisle'] !== $aisle || $label['rack'] !== $rack) {
throw new Exception('Location barcode label does not match its registry.');
}
validateLocation($pdo2, $company_id, $warehouse_id, $zone, $aisle, $rack);
$answer['success'] = 1;
$answer['type'] = 'loc';
$answer['warehouse_id'] = $warehouse_id;
$answer['zone'] = $zone;
$answer['aisle'] = $aisle;
$answer['rack'] = $rack;
exit(json_encode($answer));
}
} catch (Exception $e) {
$answer['message'] = $e->getMessage();
http_response_code(400);
exit(json_encode($answer));
}
$sth = $pdo2->prepare(
"SELECT sku, product_name, uom
FROM md_product
WHERE company_id = :company_id
AND (barcode = :barcode OR sku = :sku)
AND status > 0
ORDER BY (barcode = :barcode_order) DESC
LIMIT 1"
);
$sth->execute([
':company_id' => $company_id,
':barcode' => $barcode,
':sku' => $barcode,
':barcode_order' => $barcode,
]);
$product = $sth->fetch(PDO::FETCH_ASSOC);
if (!$product) {
$answer['message'] = 'Barcode not found.';
http_response_code(404);
exit(json_encode($answer));
}
$answer['success'] = 1;
$answer['type'] = 'raw';
$answer['sku'] = $product['sku'];
$answer['product_name'] = $product['product_name'];
$answer['uom'] = $product['uom'] ?? 'pcs';
exit(json_encode($answer));
?>
@@ -0,0 +1,153 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
function validateLocation(PDO $pdo, int $company_id, int $warehouse_id, string $zone, string $aisle, string $rack): void {
$sth = $pdo->prepare(
"SELECT 1 FROM md_rack
WHERE company_id = :company_id
AND warehouse = :warehouse
AND zone = :zone
AND aisle = :aisle
AND rack = :rack
LIMIT 1"
);
$sth->execute([
':company_id' => $company_id,
':warehouse' => $warehouse_id,
':zone' => $zone,
':aisle' => $aisle,
':rack' => $rack,
]);
if (!$sth->fetchColumn()) {
$answer = ['success' => 0, 'message' => 'Location not found.'];
http_response_code(404);
exit(json_encode($answer));
}
}
$label_action = $data['label_action'] ?? 'create';
$barcode = trim($data['barcode'] ?? '');
$warehouse_id = (int)($data['warehouse_id'] ?? 0);
$zone = trim($data['zone'] ?? '');
$aisle = trim($data['aisle'] ?? '');
$rack = trim($data['rack'] ?? '');
if ($label_action === 'list') {
$sth = $pdo2->prepare(
"SELECT barcode, warehouse_id, zone, aisle, rack, status, print_count, last_printed_dt
FROM md_barcode
WHERE company_id = :company_id
AND barcode_type = 'loc'"
);
$sth->execute([':company_id' => $company_id]);
$answer['success'] = 1;
$answer['output'] = $sth->fetchAll(PDO::FETCH_ASSOC);
exit(json_encode($answer));
}
if ($label_action === 'print') {
if ($barcode === '') {
$answer['message'] = 'No barcode provided.';
http_response_code(400);
exit(json_encode($answer));
}
$sth = $pdo2->prepare(
"UPDATE md_barcode
SET print_count = print_count + 1,
last_printed_dt = NOW()
WHERE company_id = :company_id
AND barcode = :barcode
AND barcode_type = 'loc'
AND status = 1"
);
$sth->execute([':company_id' => $company_id, ':barcode' => $barcode]);
$answer['success'] = 1;
exit(json_encode($answer));
}
if ($label_action === 'disable') {
if ($barcode === '') {
$answer['message'] = 'No barcode provided.';
http_response_code(400);
exit(json_encode($answer));
}
$sth = $pdo2->prepare(
"UPDATE md_barcode
SET status = 0
WHERE company_id = :company_id
AND barcode = :barcode
AND barcode_type = 'loc'
AND status = 1"
);
$sth->execute([':company_id' => $company_id, ':barcode' => $barcode]);
$answer['success'] = 1;
exit(json_encode($answer));
}
if ($label_action === 'enable') {
if ($barcode === '') {
$answer['message'] = 'No barcode provided.';
http_response_code(400);
exit(json_encode($answer));
}
$sth = $pdo2->prepare(
"UPDATE md_barcode
SET status = 1
WHERE company_id = :company_id
AND barcode = :barcode
AND barcode_type = 'loc'
AND status <> 1"
);
$sth->execute([':company_id' => $company_id, ':barcode' => $barcode]);
$answer['success'] = 1;
exit(json_encode($answer));
}
if ($barcode === '' || !$warehouse_id || $zone === '' || $aisle === '' || $rack === '') {
$answer['message'] = 'Location barcode data is incomplete.';
http_response_code(400);
exit(json_encode($answer));
}
validateLocation($pdo2, $company_id, $warehouse_id, $zone, $aisle, $rack);
$sth = $pdo2->prepare(
"INSERT INTO md_barcode
(company_id, barcode_type, barcode, warehouse_id, zone, aisle, rack)
VALUES
(:company_id, 'loc', :barcode, :warehouse_id, :zone, :aisle, :rack)
ON DUPLICATE KEY UPDATE
barcode_type = VALUES(barcode_type),
warehouse_id = VALUES(warehouse_id),
zone = VALUES(zone),
aisle = VALUES(aisle),
rack = VALUES(rack),
status = 1"
);
$sth->execute([
':company_id' => $company_id,
':barcode' => $barcode,
':warehouse_id' => $warehouse_id,
':zone' => $zone,
':aisle' => $aisle,
':rack' => $rack,
]);
$answer['success'] = 1;
$answer['output'] = [
'barcode' => $barcode,
'warehouse_id' => $warehouse_id,
'zone' => $zone,
'aisle' => $aisle,
'rack' => $rack,
];
exit(json_encode($answer));
?>
+245
View File
@@ -0,0 +1,245 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
function stockTableNameFromWarehouseId(int $warehouse_id): string {
return 'td_stock_' . $warehouse_id;
}
function findProduct(PDO $pdo, int $company_id, string $sku): array|false {
$sth = $pdo->prepare(
"SELECT sku, product_name, uom
FROM md_product
WHERE company_id = :company_id
AND sku = :sku
AND status > 0
LIMIT 1"
);
$sth->execute([':company_id' => $company_id, ':sku' => $sku]);
return $sth->fetch(PDO::FETCH_ASSOC);
}
function activeStockLotExists(PDO $pdo, int $company_id, string $sku, string $lot): bool {
$warehouses = $pdo->prepare(
"SELECT id FROM md_warehouse
WHERE company_id = :company_id AND status = 1"
);
$warehouses->execute([':company_id' => $company_id]);
foreach ($warehouses->fetchAll(PDO::FETCH_COLUMN) as $warehouse_id) {
$table = stockTableNameFromWarehouseId((int)$warehouse_id);
$sth = $pdo->prepare(
"SELECT ROUND(SUM(COALESCE(`in`, 0)) - SUM(COALESCE(`out`, 0)), 2) AS balance
FROM `{$table}`
WHERE company_id = :company_id
AND product_sku = :product_sku
AND lot_number = :lot_number
AND status = 1"
);
$sth->execute([
':company_id' => $company_id,
':product_sku' => $sku,
':lot_number' => $lot,
]);
if ((float)$sth->fetchColumn() > 0) return true;
}
return false;
}
function findLot(PDO $pdo, int $company_id, string $sku, string $lot): array|false {
$sth = $pdo->prepare(
"SELECT
l.product_sku,
l.lot_number,
l.expiry_date,
p.product_name,
p.uom
FROM md_lot l
INNER JOIN md_product p
ON p.company_id = l.company_id
AND p.sku = l.product_sku
WHERE l.company_id = :company_id
AND l.product_sku = :product_sku
AND l.lot_number = :lot_number
AND p.status > 0
LIMIT 1"
);
$sth->execute([
':company_id' => $company_id,
':product_sku' => $sku,
':lot_number' => $lot,
]);
$row = $sth->fetch(PDO::FETCH_ASSOC);
if ($row) return $row;
$product = findProduct($pdo, $company_id, $sku);
if (!$product || !activeStockLotExists($pdo, $company_id, $sku, $lot)) return false;
return [
'product_sku' => $product['sku'],
'lot_number' => $lot,
'expiry_date' => null,
'product_name' => $product['product_name'],
'uom' => $product['uom'],
];
}
function listLabels(PDO $pdo, int $company_id, string $sku, string $lot): array {
$params = [':company_id' => $company_id];
$where = "WHERE b.company_id = :company_id";
if ($sku !== '') {
$where .= " AND b.product_sku = :product_sku";
$params[':product_sku'] = $sku;
}
if ($lot !== '') {
$where .= " AND b.lot_number = :lot_number";
$params[':lot_number'] = $lot;
}
$sth = $pdo->prepare(
"SELECT
b.id,
b.product_sku,
b.lot_number,
b.serial_number,
b.barcode,
b.status,
b.print_count,
b.last_printed_dt,
b.created_dt,
p.product_name
FROM md_barcode b
LEFT JOIN md_product p
ON p.company_id = b.company_id
AND p.sku = b.product_sku
{$where}
AND b.barcode_type = 'sku'
ORDER BY b.created_dt DESC, b.id DESC"
);
$sth->execute($params);
return $sth->fetchAll(PDO::FETCH_ASSOC);
}
$label_action = $data['label_action'] ?? 'read';
$product_sku = trim($data['product_sku'] ?? '');
$lot_number = trim($data['lot_number'] ?? '');
if ($label_action === 'print') {
$barcode = trim($data['barcode'] ?? '');
if ($barcode === '') {
$answer['message'] = 'No barcode provided.';
http_response_code(400);
exit(json_encode($answer));
}
$sth = $pdo2->prepare(
"UPDATE md_barcode
SET print_count = print_count + 1,
last_printed_dt = NOW()
WHERE company_id = :company_id
AND barcode = :barcode
AND barcode_type = 'sku'
AND status = 1"
);
$sth->execute([':company_id' => $company_id, ':barcode' => $barcode]);
$answer['success'] = 1;
exit(json_encode($answer));
}
if ($label_action === 'disable') {
$barcode = trim($data['barcode'] ?? '');
if ($barcode === '') {
$answer['message'] = 'No barcode provided.';
http_response_code(400);
exit(json_encode($answer));
}
$sth = $pdo2->prepare(
"UPDATE md_barcode
SET status = 0
WHERE company_id = :company_id
AND barcode = :barcode
AND barcode_type = 'sku'
AND status = 1"
);
$sth->execute([':company_id' => $company_id, ':barcode' => $barcode]);
$answer['success'] = 1;
exit(json_encode($answer));
}
if ($label_action === 'create') {
$serial_number = trim($data['serial_number'] ?? '');
$allow_unlisted = !empty($data['allow_unlisted']);
if ($product_sku === '' || $lot_number === '' || $serial_number === '') {
$answer['message'] = 'SKU, lot no, and serial no are required.';
http_response_code(400);
exit(json_encode($answer));
}
$lot = findLot($pdo2, $company_id, $product_sku, $lot_number);
if (!$lot) {
if (!$allow_unlisted) {
$answer['message'] = 'Lot not found.';
http_response_code(404);
exit(json_encode($answer));
}
$product = findProduct($pdo2, $company_id, $product_sku);
if (!$product) {
$answer['message'] = 'SKU not found.';
http_response_code(404);
exit(json_encode($answer));
}
$lot = [
'product_sku' => $product['sku'],
'lot_number' => $lot_number,
'expiry_date' => null,
'product_name' => $product['product_name'],
'uom' => $product['uom'],
];
}
$barcode = "SKU|{$product_sku}|{$lot_number}|{$serial_number}";
$sth = $pdo2->prepare(
"INSERT INTO md_barcode
(company_id, barcode_type, barcode, product_sku, lot_number, serial_number)
VALUES
(:company_id, 'sku', :barcode, :product_sku, :lot_number, :serial_number)
ON DUPLICATE KEY UPDATE
barcode_type = VALUES(barcode_type),
product_sku = VALUES(product_sku),
lot_number = VALUES(lot_number),
serial_number = VALUES(serial_number),
status = 1"
);
$sth->execute([
':company_id' => $company_id,
':product_sku' => $product_sku,
':lot_number' => $lot_number,
':serial_number' => $serial_number,
':barcode' => $barcode,
]);
$answer['success'] = 1;
$answer['output'] = [
'product_sku' => $product_sku,
'product_name' => $lot['product_name'],
'lot_number' => $lot_number,
'serial_number' => $serial_number,
'barcode' => $barcode,
];
exit(json_encode($answer));
}
$answer['success'] = 1;
$answer['output'] = listLabels($pdo2, $company_id, $product_sku, $lot_number);
exit(json_encode($answer));
?>
+159
View File
@@ -0,0 +1,159 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
function stockTableNameFromWarehouseId(int $warehouse_id): string {
return 'td_stock_' . $warehouse_id;
}
$sth = $pdo2->prepare(
"SELECT
l.id,
l.product_sku,
l.lot_number,
l.expiry_date,
l.description,
p.product_name,
p.uom,
'md_lot' AS source
FROM md_lot l
INNER JOIN md_product p
ON p.company_id = l.company_id
AND p.sku = l.product_sku
WHERE l.company_id = :company_id
AND p.status > 0
ORDER BY l.product_sku ASC, l.lot_number ASC"
);
$sth->execute([':company_id' => $company_id]);
$rows = $sth->fetchAll(PDO::FETCH_ASSOC);
$seen = [];
foreach ($rows as $row) {
$seen[$row['product_sku'] . '|' . $row['lot_number']] = true;
}
$warehouses = $pdo2->prepare(
"SELECT id FROM md_warehouse
WHERE company_id = :company_id AND status = 1"
);
$warehouses->execute([':company_id' => $company_id]);
foreach ($warehouses->fetchAll(PDO::FETCH_COLUMN) as $warehouse_id) {
$table = stockTableNameFromWarehouseId((int)$warehouse_id);
$stock = $pdo2->query(
"SELECT
s.product_sku,
s.lot_number,
p.product_name,
p.uom,
ROUND(SUM(COALESCE(s.`in`, 0)) - SUM(COALESCE(s.`out`, 0)), 2) AS balance
FROM `{$table}` s
INNER JOIN md_product p
ON p.company_id = s.company_id
AND p.sku = s.product_sku
WHERE s.company_id = " . (int)$company_id . "
AND s.status = 1
AND s.lot_number IS NOT NULL
AND s.lot_number <> ''
AND p.status > 0
GROUP BY s.product_sku, s.lot_number, p.product_name, p.uom
HAVING balance > 0"
);
foreach ($stock->fetchAll(PDO::FETCH_ASSOC) as $row) {
$key = $row['product_sku'] . '|' . $row['lot_number'];
if (isset($seen[$key])) continue;
$seen[$key] = true;
$rows[] = [
'id' => null,
'product_sku' => $row['product_sku'],
'lot_number' => $row['lot_number'],
'expiry_date' => null,
'description' => 'Found in active stock; missing md_lot row.',
'product_name'=> $row['product_name'],
'uom' => $row['uom'],
'source' => 'active_stock',
];
}
}
$barcodes = $pdo2->prepare(
"SELECT
b.product_sku,
b.lot_number,
b.serial_number,
b.barcode,
b.status,
b.created_dt,
p.product_name,
p.uom
FROM md_barcode b
INNER JOIN md_product p
ON p.company_id = b.company_id
AND p.sku = b.product_sku
WHERE b.company_id = :company_id
AND b.barcode_type = 'sku'
AND b.product_sku IS NOT NULL
AND b.product_sku <> ''
AND b.lot_number IS NOT NULL
AND b.lot_number <> ''
AND p.status > 0
ORDER BY b.created_dt DESC, b.id DESC"
);
$barcodes->execute([':company_id' => $company_id]);
foreach ($barcodes->fetchAll(PDO::FETCH_ASSOC) as $row) {
$key = $row['product_sku'] . '|' . $row['lot_number'];
if (isset($seen[$key])) continue;
$seen[$key] = true;
$rows[] = [
'id' => null,
'product_sku' => $row['product_sku'],
'lot_number' => $row['lot_number'],
'expiry_date' => null,
'description' => 'Prepared barcode exists; missing md_lot row.',
'product_name' => $row['product_name'],
'uom' => $row['uom'],
'source' => 'md_barcode',
'serial_number' => $row['serial_number'],
'barcode' => $row['barcode'],
];
}
$status_rows = $pdo2->prepare(
"SELECT
product_sku,
lot_number,
SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) AS active_count,
SUM(CASE WHEN status <> 1 THEN 1 ELSE 0 END) AS disabled_count
FROM md_barcode
WHERE company_id = :company_id
AND barcode_type = 'sku'
AND product_sku IS NOT NULL
AND product_sku <> ''
AND lot_number IS NOT NULL
AND lot_number <> ''
GROUP BY product_sku, lot_number"
);
$status_rows->execute([':company_id' => $company_id]);
$label_status = [];
foreach ($status_rows->fetchAll(PDO::FETCH_ASSOC) as $row) {
$label_status[$row['product_sku'] . '|' . $row['lot_number']] = [
'active_count' => (int)$row['active_count'],
'disabled_count' => (int)$row['disabled_count'],
];
}
foreach ($rows as &$row) {
$status = $label_status[$row['product_sku'] . '|' . $row['lot_number']] ?? null;
$row['barcode_active_count'] = $status['active_count'] ?? 0;
$row['barcode_disabled_count'] = $status['disabled_count'] ?? 0;
}
unset($row);
usort($rows, fn($a, $b) => [$a['product_sku'], $a['lot_number']] <=> [$b['product_sku'], $b['lot_number']]);
$answer['success'] = 1;
$answer['output'] = $rows;
exit(json_encode($answer));
?>
+17
View File
@@ -0,0 +1,17 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
$sth = $pdo2->prepare(
"SELECT sku, product_name, uom
FROM md_product
WHERE company_id = :company_id
AND status > 0
ORDER BY sku ASC"
);
$sth->execute([':company_id' => $company_id]);
$answer['success'] = 1;
$answer['output'] = $sth->fetchAll(PDO::FETCH_ASSOC);
exit(json_encode($answer));
?>
@@ -0,0 +1,86 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/WarehouseManager.php';
$whMgmt = new WarehouseManager($pdo2, $company_id);
$mode = $data['mode'] ?? '';
$warehouse_id = (int)($data['warehouse_id'] ?? 0);
$zone = trim($data['zone'] ?? '');
$aisle = trim($data['aisle'] ?? '');
$rack = trim($data['rack'] ?? '');
$product_sku = trim($data['product_sku'] ?? '');
$lot_number = trim($data['lot_number'] ?? '');
$serial = trim($data['serial_number'] ?? '');
if (!$warehouse_id || $zone === '' || $aisle === '' || $rack === '') {
$answer['message'] = 'Location label is incomplete.';
http_response_code(400);
exit(json_encode($answer));
}
function rackRow(PDO $pdo, int $company_id, int $warehouse_id, string $zone, string $aisle, string $rack): array|false {
$sth = $pdo->prepare(
"SELECT id, product_sku, td_stock_id
FROM md_rack
WHERE company_id = :company_id
AND warehouse = :warehouse
AND zone = :zone
AND aisle = :aisle
AND rack = :rack
LIMIT 1"
);
$sth->execute([
':company_id' => $company_id,
':warehouse' => $warehouse_id,
':zone' => $zone,
':aisle' => $aisle,
':rack' => $rack,
]);
return $sth->fetch(PDO::FETCH_ASSOC);
}
try {
$rack_row = rackRow($pdo2, $company_id, $warehouse_id, $zone, $aisle, $rack);
if (!$rack_row) throw new Exception("Location {$zone}-{$aisle}-{$rack} does not exist.");
if ($mode === 'in' || $mode === 'to') {
if ($rack_row['product_sku'] !== null) {
throw new Exception("Location {$zone}-{$aisle}-{$rack} is already occupied by {$rack_row['product_sku']}.");
}
if ($mode === 'in' && $product_sku !== '') {
$whMgmt->validateStockUnique($product_sku, $lot_number ?: null, $serial ?: null);
}
} elseif ($mode === 'out' || $mode === 'from') {
if ($product_sku === '') throw new Exception('Scan the SKU label first.');
$stock = $whMgmt->getRackStock($warehouse_id, $zone, $aisle, $rack);
if (!$stock) {
throw new Exception("Location {$zone}-{$aisle}-{$rack} has no approved stock to move out.");
}
if ($stock['product_sku'] !== $product_sku) {
throw new Exception("Location holds {$stock['product_sku']}, not {$product_sku}.");
}
if ($lot_number !== '' && (string)$stock['lot_number'] !== $lot_number) {
throw new Exception("Location holds lot '{$stock['lot_number']}', not '{$lot_number}'.");
}
if ($serial !== '' && (string)$stock['serial_number'] !== $serial) {
throw new Exception("Location holds serial '{$stock['serial_number']}', not '{$serial}'.");
}
} else {
throw new Exception('Invalid scan validation mode.');
}
$answer['success'] = 1;
exit(json_encode($answer));
} catch (Exception $e) {
$answer['message'] = $e->getMessage();
http_response_code(400);
exit(json_encode($answer));
}
?>
+388
View File
@@ -0,0 +1,388 @@
<?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">Location Barcode Labels</h1>
<p class="mb-0 text-muted">Print or reprint labels from the rack master list.</p>
</div>
<div class="d-flex gap-2">
<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>
<button id="btn_reenable" class="btn btn-outline-primary d-none" onclick="reenable_selected_label()">
<i class="ti ti-refresh me-1"></i>Re-enable
</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_location" class="form-control ps-9"
placeholder="Search warehouse, zone, aisle, rack, or SKU...">
<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>Warehouse</th>
<th class="th-zone">Zone</th>
<th class="th-aisle">Aisle</th>
<th id="th-rack">Rack</th>
<th>Status</th>
<th>Label Status</th>
<th class="text-end">Action</th>
</tr>
</thead>
<tbody id="rack_tbody">
<tr><td colspan="7" class="text-center text-muted py-4">Loading locations...</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 location to preview its label.
</div>
</div></div>
</div>
</div>
</div>
</main>
<?php require '../include_ending.php'; ?>
<script src="https://cdn.jsdelivr.net/npm/jsbarcode@3.11.6/dist/JsBarcode.all.min.js"></script>
<script>
var advanced = false;
var label_rack = 'Location';
var label_zone = 'Zone';
var label_aisle = 'Aisle';
var racks = [];
var barcode_status = {};
var selected_payload = '';
var selected_rack_index = -1;
function escape_html(value) {
return String(value ?? '').replace(/[&<>"']/g, function(char) {
return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;' }[char];
});
}
function refresh_header_buttons() {
var status = selected_payload ? barcode_status[selected_payload] : null;
var is_active = status === 'active';
var is_disabled = status === 'disabled';
$('#btn_print').prop('disabled', !is_active).toggleClass('d-none', is_disabled);
$('#btn_disable').prop('disabled', !is_active).toggleClass('d-none', is_disabled);
$('#btn_reenable').toggleClass('d-none', !is_disabled);
}
function highlight_selected_row() {
$('#rack_tbody tr').removeClass('table-active');
if (selected_rack_index >= 0) {
$('#rack_tbody tr[data-rack-index="' + selected_rack_index + '"]').addClass('table-active');
}
}
function load_config() {
return ajax_request({
url: server_url + 'setting/api/engine/company_setting.php',
autoPrepare: true,
checkRequired: 0,
action: 'read',
queueLock: false,
onSuccess: function(res) {
var s = res.output || {};
advanced = s.advanced_location == 1;
label_rack = s.location_label_rack || (advanced ? 'Rack' : 'Location');
label_zone = s.location_label_zone || 'Zone';
label_aisle = s.location_label_aisle || 'Aisle';
$('.th-zone').text(label_zone).toggleClass('d-none', !advanced);
$('.th-aisle').text(label_aisle).toggleClass('d-none', !advanced);
$('#th-rack').text(label_rack);
}
});
}
function load_racks() {
return ajax_request({
url: server_url + 'reports/api/engine_report/rack_occupancy.php',
autoPrepare: true,
checkRequired: 0,
action: 'read',
queueLock: false,
onSuccess: function(res) {
racks = res.output || [];
render_racks();
highlight_selected_row();
}
});
}
function load_barcode_statuses() {
return ajax_request({
url: server_url + 'ics/api/engine/location_barcode_label.php',
autoPrepare: true,
checkRequired: 0,
action: 'read',
queueLock: false,
data: { label_action: 'list' },
onSuccess: function(res) {
barcode_status = {};
(res.output || []).forEach(function(item) {
barcode_status[item.barcode] = parseInt(item.status) === 1 ? 'active' : 'disabled';
});
render_racks();
}
});
}
function location_payload(item) {
return advanced
? `LOC|${item.warehouse_id}|${item.zone}|${item.aisle}|${item.rack}`
: `LOC|${item.warehouse_id}|${item.rack}`;
}
function render_racks() {
var keyword = $('#search_location').val().toLowerCase();
var rows = racks.filter(function(item) {
if (!keyword) return true;
return [
item.warehouse_name,
item.zone,
item.aisle,
item.rack,
item.product_sku,
item.product_name,
item.status
].join(' ').toLowerCase().includes(keyword);
});
if (!rows.length) {
$('#rack_tbody').html('<tr><td colspan="7" class="text-center text-muted py-4">No locations found.</td></tr>');
return;
}
var body = '';
rows.forEach(function(item) {
var idx = racks.indexOf(item);
var status = item.status === 'occupied'
? '<span class="badge bg-warning text-dark">Occupied</span>'
: '<span class="badge bg-success">Empty</span>';
var payload = location_payload(item);
var labelStatus = '<span class="badge bg-secondary">Not prepared</span>';
if (barcode_status[payload] === 'active') {
labelStatus = '<span class="badge bg-success">Active</span>';
}
if (barcode_status[payload] === 'disabled') {
labelStatus = '<span class="badge bg-danger">Disabled</span>';
}
body += `<tr data-rack-index="${idx}">
<td>${escape_html(item.warehouse_name)}</td>
<td class="th-zone ${advanced ? '' : 'd-none'}">${escape_html(item.zone)}</td>
<td class="th-aisle ${advanced ? '' : 'd-none'}">${escape_html(item.aisle)}</td>
<td>${escape_html(item.rack)}</td>
<td>${status}</td>
<td>${labelStatus}</td>
<td class="text-end">
<button class="btn btn-sm btn-outline-primary" onclick="preview_label(${idx})">
<i class="ti ti-barcode me-1"></i>Preview
</button>
</td>
</tr>`;
});
$('#rack_tbody').html(body);
}
function render_preview(item, payload) {
$('#label_preview').html(`
<div class="barcode-label mx-auto">
<div class="fw-bold fs-5">${escape_html(item.warehouse_name)}</div>
<div class="small">${advanced
? `${escape_html(item.zone)} / ${escape_html(item.aisle)} / ${escape_html(item.rack)}`
: escape_html(item.rack)}</div>
<svg id="barcode_svg"></svg>
<div class="small text-muted">${escape_html(payload)}</div>
</div>`);
JsBarcode('#barcode_svg', payload, { format: 'CODE128', width: 1.6, height: 70, displayValue: false });
}
function preview_label(index) {
var item = racks[index];
if (!item) return;
var payload = location_payload(item);
ajax_request({
url: server_url + 'ics/api/engine/location_barcode_label.php',
autoPrepare: true,
checkRequired: 0,
action: 'read',
queueLock: false,
data: {
label_action: 'create',
barcode: payload,
warehouse_id: item.warehouse_id,
zone: advanced ? item.zone : item.rack,
aisle: advanced ? item.aisle : item.rack,
rack: item.rack
},
onSuccess: function() {
selected_payload = payload;
selected_rack_index = index;
if (!barcode_status[payload]) barcode_status[payload] = 'active';
render_racks();
refresh_header_buttons();
highlight_selected_row();
render_preview(item, payload);
}
});
}
function print_selected_label() {
if (!selected_payload) {
bootbox.alert('Select a location label first.');
return;
}
ajax_request({
url: server_url + 'ics/api/engine/location_barcode_label.php',
autoPrepare: true,
checkRequired: 0,
action: 'read',
queueLock: false,
data: { label_action: 'print', barcode: selected_payload },
onSuccess: function() { window.print(); }
});
}
function disable_selected_label() {
if (!selected_payload) {
bootbox.alert('Select a location label first.');
return;
}
bootbox.confirm({
message: 'Disable this location 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/location_barcode_label.php',
autoPrepare: true,
checkRequired: 0,
action: 'read',
queueLock: false,
data: { label_action: 'disable', barcode: selected_payload },
onSuccess: function() {
barcode_status[selected_payload] = 'disabled';
selected_payload = '';
selected_rack_index = -1;
$('#label_preview').html('Select a location to preview its label.');
render_racks();
refresh_header_buttons();
highlight_selected_row();
}
});
}
});
}
function reenable_selected_label() {
if (!selected_payload) return;
var index = selected_rack_index;
var item = racks[index];
if (!item) return;
ajax_request({
url: server_url + 'ics/api/engine/location_barcode_label.php',
autoPrepare: true,
checkRequired: 0,
action: 'read',
queueLock: false,
data: { label_action: 'enable', barcode: selected_payload },
onSuccess: function() {
barcode_status[selected_payload] = 'active';
render_racks();
refresh_header_buttons();
highlight_selected_row();
render_preview(item, selected_payload);
}
});
}
function enable_label_for_row(index) {
var item = racks[index];
if (!item) return;
var payload = location_payload(item);
ajax_request({
url: server_url + 'ics/api/engine/location_barcode_label.php',
autoPrepare: true,
checkRequired: 0,
action: 'read',
queueLock: false,
data: { label_action: 'enable', barcode: payload },
onSuccess: function() {
barcode_status[payload] = 'active';
selected_payload = payload;
selected_rack_index = index;
render_racks();
refresh_header_buttons();
highlight_selected_row();
render_preview(item, payload);
}
});
}
$('#search_location').on('input', render_racks);
$(async function() {
await load_config();
await load_racks();
await load_barcode_statuses();
});
</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>
+127 -2
View File
@@ -30,8 +30,13 @@
<!-- Product Name --> <!-- Product Name -->
<div class="mb-3 col-lg-3"> <div class="mb-3 col-lg-3">
<label for="product_sku" class="form-label">SKU</label> <label for="product_sku" class="form-label">SKU</label>
<input type="text" id="product_sku" class="form-control product_search" <div class="input-group">
placeholder="Type product sku..." autocomplete="off" required> <input type="text" id="product_sku" class="form-control product_search"
placeholder="Type product sku..." autocomplete="off" required>
<button class="btn btn-outline-secondary" type="button" onclick="scanner.open_camera()" title="Scan barcode label">
<i class="ti ti-scan"></i>
</button>
</div>
</div> </div>
<div class="mb-3 col-lg-3"> <div class="mb-3 col-lg-3">
<label for="product_name" class="form-label">Name</label> <label for="product_name" class="form-label">Name</label>
@@ -130,6 +135,8 @@
var label_rack = 'Location'; var label_rack = 'Location';
var label_zone = 'Zone'; var label_zone = 'Zone';
var label_aisle = 'Aisle'; var label_aisle = 'Aisle';
var scan_sku_ready = false;
var scan_location_ready = false;
function load_location_config(callback) { function load_location_config(callback) {
return ajax_request({ return ajax_request({
@@ -166,6 +173,100 @@
} }
} }
function set_select_value(selector, value, label) {
if (!value) return;
var $select = $(selector);
if (!$select.find(`option[value="${value}"]`).length) {
$select.append(`<option value="${value}">${label || value}</option>`);
}
$select.val(value);
}
function apply_sku_label_scan(data) {
$('#product_sku').val(data.sku).attr('secondary', data.sku);
$('#product_name').val(data.product_name || '').attr('secondary', data.product_name || '');
$('#lot_number').val(data.lot_number || '');
$('#serial_number').val(data.serial_number || '');
$('#quantity').val($('#quantity').val() || 1);
scan_sku_ready = true;
scan_location_ready = false;
$('#warehouse').val('');
$('#zone').val('');
$('#aisle').val('');
$('#rack').val('');
$('#product_sku').trigger('blur');
}
function apply_location_label_scan(data) {
if (!scan_sku_ready && !($('#product_sku').attr('secondary') || $('#product_sku').val())) {
bootbox.alert('Scan the SKU label first, then scan the location label.');
return;
}
validate_scanned_location('in', data, function() {
set_select_value('#warehouse', data.warehouse_id, data.warehouse_id);
if (advanced) {
set_select_value('#zone', data.zone, data.zone);
set_select_value('#aisle', data.aisle, data.aisle);
}
set_select_value('#rack', data.rack, data.rack);
scan_location_ready = true;
});
}
function validate_scanned_location(mode, data, onSuccess) {
ajax_request({
url: '<?php echo $server_url?>ics/api/engine/validate_scan_location.php',
autoPrepare: true,
checkRequired: 0,
action: 'read',
queueLock: false,
noLoading: true,
data: {
mode: mode,
warehouse_id: data.warehouse_id,
zone: data.zone,
aisle: data.aisle,
rack: data.rack,
product_sku: $('#product_sku').attr('secondary') || $('#product_sku').val(),
lot_number: $('#lot_number').val(),
serial_number: $('#serial_number').val()
},
onSuccess: onSuccess
});
}
function reset_stock_in_inputs() {
$('form')[0].reset();
$('#product_sku').removeAttr('secondary').prop('disabled', false);
$('#product_name').removeAttr('secondary').val('');
$('#lot_number, #serial_number, #expiry_date').prop('disabled', false);
$('#contact').removeAttr('secondary data-id');
if (expiryPicker) expiryPicker.clear();
if (!advanced) {
$('#zone').val('');
$('#aisle').val('');
}
$('#warehouse').val('');
$('#zone').val('');
$('#aisle').val('');
$('#rack').val('');
scan_sku_ready = false;
scan_location_ready = false;
$('#product_sku').focus();
}
function save_scanned_stock_in() {
if (!scan_sku_ready && !($('#product_sku').attr('secondary') || $('#product_sku').val())) {
bootbox.alert('Scan the SKU label first.');
return;
}
if (!scan_location_ready && !$('#rack').val()) {
bootbox.alert('Scan the location label before saving.');
return;
}
manage_stock_in();
}
function manage_stock_in() { function manage_stock_in() {
var rack_val = $('#rack').val(); var rack_val = $('#rack').val();
@@ -200,7 +301,11 @@
}, },
action: 'manage', action: 'manage',
onSuccess: function() { onSuccess: function() {
<?php if(empty($_GET["id"])){ ?>
reset_stock_in_inputs();
<?php } else { ?>
window.location.href = '<?php echo $server_url?>ics/stock_in.php'; window.location.href = '<?php echo $server_url?>ics/stock_in.php';
<?php } ?>
} }
}); });
@@ -238,6 +343,8 @@
url: "<?php echo $server_url?>ics/api/engine/retrieve_warehouse.php", url: "<?php echo $server_url?>ics/api/engine/retrieve_warehouse.php",
autoPrepare: true, autoPrepare: true,
checkRequired: 0, checkRequired: 0,
noLoading: true,
queueLock: false,
data: { data: {
"type": type "type": type
}, },
@@ -262,6 +369,7 @@
autoPrepare: true, autoPrepare: true,
checkRequired: 0, checkRequired: 0,
noLoading: true, noLoading: true,
queueLock: false,
data : { data : {
"type": type "type": type
}, },
@@ -289,6 +397,7 @@
autoPrepare: true, autoPrepare: true,
checkRequired: 0, checkRequired: 0,
noLoading: true, noLoading: true,
queueLock: false,
data: { data: {
"type": type "type": type
}, },
@@ -315,6 +424,7 @@
autoPrepare: true, autoPrepare: true,
checkRequired: 0, checkRequired: 0,
noLoading: true, noLoading: true,
queueLock: false,
data: { data: {
"type": type "type": type
}, },
@@ -462,6 +572,14 @@
await load_location_config(apply_location_labels); await load_location_config(apply_location_labels);
await retrieve_warehouse('to'); await retrieve_warehouse('to');
await retrieve_for_edit(); await retrieve_for_edit();
scanner.init({
allowRaw: false,
allowedTypes: ['sku', 'loc'],
onScan: function(sku, product_name, uom, data) {
if (data.type === 'sku') apply_sku_label_scan(data);
if (data.type === 'loc') apply_location_label_scan(data);
}
});
<?php if(!empty($_GET["sku"])){ ?> <?php if(!empty($_GET["sku"])){ ?>
prefill_from_restock(); prefill_from_restock();
@@ -488,6 +606,13 @@
<?php if(empty($_GET["id"])){ ?> <?php if(empty($_GET["id"])){ ?>
$(document).on('keydown', function(e) {
if (e.key === 'F2') {
e.preventDefault();
save_scanned_stock_in();
}
});
$("select#warehouse").change(function() { $("select#warehouse").change(function() {
if (advanced) { if (advanced) {
retrieve_zone('to'); retrieve_zone('to');
+141 -4
View File
@@ -28,8 +28,13 @@
<!-- Step 1: Product --> <!-- Step 1: Product -->
<div class="mb-3 col-lg-3"> <div class="mb-3 col-lg-3">
<label for="product_sku" class="form-label">SKU</label> <label for="product_sku" class="form-label">SKU</label>
<input type="text" id="product_sku" class="form-control product_search" <div class="input-group">
placeholder="Type product sku..." autocomplete="off" required> <input type="text" id="product_sku" class="form-control product_search"
placeholder="Type product sku..." autocomplete="off" required>
<button class="btn btn-outline-secondary" type="button" onclick="scanner.open_camera()" title="Scan barcode label">
<i class="ti ti-scan"></i>
</button>
</div>
</div> </div>
<div class="mb-3 col-lg-3"> <div class="mb-3 col-lg-3">
<label for="product_name" class="form-label">Name</label> <label for="product_name" class="form-label">Name</label>
@@ -117,6 +122,97 @@
var label_rack = 'Location'; var label_rack = 'Location';
var label_zone = 'Zone'; var label_zone = 'Zone';
var label_aisle = 'Aisle'; var label_aisle = 'Aisle';
var scan_sku_ready = false;
var scan_location_ready = false;
var pending_scan_lot = '';
var pending_scan_serial = '';
function set_select_value(selector, value, label) {
if (!value) return;
var $select = $(selector);
if (!$select.find(`option[value="${value}"]`).length) {
$select.append(`<option value="${value}">${label || value}</option>`);
}
$select.val(value).prop('disabled', false);
}
function apply_sku_label_scan(data) {
$('#product_sku').val(data.sku).attr('secondary', data.sku);
$('#product_name').val(data.product_name || '').attr('secondary', data.product_name || '');
pending_scan_lot = data.lot_number || '';
pending_scan_serial = data.serial_number || '';
set_select_value('#lot_number', data.lot_number, data.lot_number);
set_select_value('#serial_number', data.serial_number, data.serial_number);
scan_sku_ready = true;
scan_location_ready = false;
reset_location();
retrieve_active_lot();
}
function apply_location_label_scan(data) {
if (!scan_sku_ready && !($('#product_sku').attr('secondary') || $('#product_sku').val())) {
bootbox.alert('Scan the SKU label first, then scan the location label.');
return;
}
validate_scanned_location('out', data, function() {
set_select_value('#warehouse', data.warehouse_id, data.warehouse_id);
if (advanced) {
set_select_value('#zone', data.zone, data.zone);
set_select_value('#aisle', data.aisle, data.aisle);
}
set_select_value('#rack', data.rack, data.rack);
scan_location_ready = true;
});
}
function validate_scanned_location(mode, data, onSuccess) {
ajax_request({
url: '<?php echo $server_url?>ics/api/engine/validate_scan_location.php',
autoPrepare: true,
checkRequired: 0,
action: 'read',
queueLock: false,
noLoading: true,
data: {
mode: mode,
warehouse_id: data.warehouse_id,
zone: data.zone,
aisle: data.aisle,
rack: data.rack,
product_sku: $('#product_sku').attr('secondary') || $('#product_sku').val(),
lot_number: $('#lot_number').val(),
serial_number: $('#serial_number').val()
},
onSuccess: onSuccess
});
}
function reset_stock_out_inputs() {
$('form')[0].reset();
$('#product_sku').removeAttr('secondary').prop('disabled', false);
$('#product_name').removeAttr('secondary').val('');
$('#lot_number').html('<option value="">Select lot...</option>').prop('disabled', false);
$('#serial_number').html('<option value="">Select serial...</option>').prop('disabled', false);
$('#contact').removeAttr('secondary data-id');
reset_location();
scan_sku_ready = false;
scan_location_ready = false;
pending_scan_lot = '';
pending_scan_serial = '';
$('#product_sku').focus();
}
function save_scanned_stock_out() {
if (!scan_sku_ready && !($('#product_sku').attr('secondary') || $('#product_sku').val())) {
bootbox.alert('Scan the SKU label first.');
return;
}
if (!scan_location_ready && !$('#rack').val()) {
bootbox.alert('Scan the location label before saving.');
return;
}
manage_stock_out();
}
function load_location_config(callback) { function load_location_config(callback) {
return ajax_request({ return ajax_request({
@@ -171,10 +267,16 @@
'rack': rack_val, 'rack': rack_val,
'product_sku': sku_val, 'product_sku': sku_val,
'warehouse': wh_val, 'warehouse': wh_val,
'lot_number': $('#lot_number').val(),
'serial_number': $('#serial_number').val(),
}, },
action: 'manage', action: 'manage',
onSuccess: function() { onSuccess: function() {
<?php if(empty($_GET["id"])){ ?>
reset_stock_out_inputs();
<?php } else { ?>
window.location.href = '<?php echo $server_url?>ics/stock_out.php'; window.location.href = '<?php echo $server_url?>ics/stock_out.php';
<?php } ?>
} }
}); });
} }
@@ -215,6 +317,7 @@
autoPrepare: true, autoPrepare: true,
checkRequired: 0, checkRequired: 0,
noLoading: true, noLoading: true,
queueLock: false,
action: 'read', action: 'read',
data: { "product_sku": sku }, data: { "product_sku": sku },
onSuccess: function(res) { onSuccess: function(res) {
@@ -226,10 +329,18 @@
option += `<option value="${item.lot_number}">${label}</option>`; option += `<option value="${item.lot_number}">${label}</option>`;
}); });
$('#lot_number').html(option).prop('disabled', false); $('#lot_number').html(option).prop('disabled', false);
if (pending_scan_lot) {
set_select_value('#lot_number', pending_scan_lot, pending_scan_lot);
}
$('#serial_number').html('<option value="">Select serial... (optional)</option>').prop('disabled', false); $('#serial_number').html('<option value="">Select serial... (optional)</option>').prop('disabled', false);
// Reset downstream // Reset downstream
reset_location(); if (!scan_location_ready) reset_location();
if (pending_scan_lot) {
retrieve_active_serial();
} else {
retrieve_warehouse();
}
} }
}); });
} }
@@ -248,6 +359,7 @@
autoPrepare: true, autoPrepare: true,
checkRequired: 0, checkRequired: 0,
noLoading: true, noLoading: true,
queueLock: false,
action: 'read', action: 'read',
data: { "product_sku": sku, "lot_number": lot }, data: { "product_sku": sku, "lot_number": lot },
onSuccess: function(res) { onSuccess: function(res) {
@@ -257,9 +369,14 @@
option += `<option value="${serial}">${serial}</option>`; option += `<option value="${serial}">${serial}</option>`;
}); });
$('#serial_number').html(option).prop('disabled', false); $('#serial_number').html(option).prop('disabled', false);
if (pending_scan_serial) {
set_select_value('#serial_number', pending_scan_serial, pending_scan_serial);
pending_scan_serial = '';
}
// Reset downstream // Reset downstream
reset_location(); if (!scan_location_ready) reset_location();
retrieve_warehouse();
} }
}); });
} }
@@ -273,6 +390,7 @@
autoPrepare: true, autoPrepare: true,
checkRequired: 0, checkRequired: 0,
noLoading: true, noLoading: true,
queueLock: false,
data: { data: {
"type": 'from', "type": 'from',
"product_sku": $('#product_sku').attr('secondary') || $('#product_sku').val(), "product_sku": $('#product_sku').attr('secondary') || $('#product_sku').val(),
@@ -280,11 +398,13 @@
}, },
action: 'read', action: 'read',
onSuccess: function(res) { onSuccess: function(res) {
var current = $('#warehouse').val();
var option = '<option value="">Select warehouse...</option>'; var option = '<option value="">Select warehouse...</option>';
$.each(res.output, function(key, item) { $.each(res.output, function(key, item) {
option += `<option value='${item.id}'>${item.warehouse_name}</option>`; option += `<option value='${item.id}'>${item.warehouse_name}</option>`;
}); });
$('#warehouse').html(option).prop('disabled', false); $('#warehouse').html(option).prop('disabled', false);
if (current) set_select_value('#warehouse', current, current);
} }
}); });
} }
@@ -298,6 +418,7 @@
autoPrepare: true, autoPrepare: true,
checkRequired: 0, checkRequired: 0,
noLoading: true, noLoading: true,
queueLock: false,
data: { data: {
"warehouse": $('#warehouse').val(), "warehouse": $('#warehouse').val(),
"type": 'from', "type": 'from',
@@ -324,6 +445,7 @@
autoPrepare: true, autoPrepare: true,
checkRequired: 0, checkRequired: 0,
noLoading: true, noLoading: true,
queueLock: false,
data: { data: {
"warehouse": $('#warehouse').val(), "warehouse": $('#warehouse').val(),
"zone": $('#zone').val(), "zone": $('#zone').val(),
@@ -351,6 +473,7 @@
autoPrepare: true, autoPrepare: true,
checkRequired: 0, checkRequired: 0,
noLoading: true, noLoading: true,
queueLock: false,
data: { data: {
"warehouse": $('#warehouse').val(), "warehouse": $('#warehouse').val(),
"zone": $('#zone').val(), "zone": $('#zone').val(),
@@ -454,6 +577,14 @@
await load_location_config(apply_location_labels); await load_location_config(apply_location_labels);
await retrieve_warehouse(); await retrieve_warehouse();
await retrieve_for_edit(); await retrieve_for_edit();
scanner.init({
allowRaw: false,
allowedTypes: ['sku', 'loc'],
onScan: function(sku, product_name, uom, data) {
if (data.type === 'sku') apply_sku_label_scan(data);
if (data.type === 'loc') apply_location_label_scan(data);
}
});
} catch(e) { } catch(e) {
console.log(e); console.log(e);
} }
@@ -462,6 +593,12 @@
// ── Change handlers (create mode only) ─────────────────────────────── // ── Change handlers (create mode only) ───────────────────────────────
<?php if(empty($_GET["id"])){ ?> <?php if(empty($_GET["id"])){ ?>
$(document).on('keydown', function(e) {
if (e.key === 'F2') {
e.preventDefault();
save_scanned_stock_out();
}
});
// SKU selected → load lots // SKU selected → load lots
$('input#product_sku').on('blur', function() { $('input#product_sku').on('blur', function() {
+178 -4
View File
@@ -28,8 +28,13 @@
<!-- Step 1: Product --> <!-- Step 1: Product -->
<div class="mb-3 col-lg-3"> <div class="mb-3 col-lg-3">
<label for="product_sku" class="form-label">SKU</label> <label for="product_sku" class="form-label">SKU</label>
<input type="text" id="product_sku" class="form-control product_search" <div class="input-group">
placeholder="Type product sku..." autocomplete="off" required> <input type="text" id="product_sku" class="form-control product_search"
placeholder="Type product sku..." autocomplete="off" required>
<button class="btn btn-outline-secondary" type="button" onclick="scanner.open_camera()" title="Scan barcode label">
<i class="ti ti-scan"></i>
</button>
</div>
</div> </div>
<div class="mb-3 col-lg-3"> <div class="mb-3 col-lg-3">
<label for="product_name" class="form-label">Name</label> <label for="product_name" class="form-label">Name</label>
@@ -150,6 +155,133 @@
var label_rack = 'Location'; var label_rack = 'Location';
var label_zone = 'Zone'; var label_zone = 'Zone';
var label_aisle = 'Aisle'; var label_aisle = 'Aisle';
var scan_sku_ready = false;
var scan_from_ready = false;
var scan_to_ready = false;
var pending_scan_lot = '';
var pending_scan_serial = '';
function set_select_value(selector, value, label) {
if (!value) return;
var $select = $(selector);
if (!$select.find(`option[value="${value}"]`).length) {
$select.append(`<option value="${value}">${label || value}</option>`);
}
$select.val(value).prop('disabled', false);
}
function apply_sku_label_scan(data) {
$('#product_sku').val(data.sku).attr('secondary', data.sku);
$('#product_name').val(data.product_name || '').attr('secondary', data.product_name || '');
pending_scan_lot = data.lot_number || '';
pending_scan_serial = data.serial_number || '';
set_select_value('#lot_number', data.lot_number, data.lot_number);
set_select_value('#serial_number', data.serial_number, data.serial_number);
scan_sku_ready = true;
scan_from_ready = false;
scan_to_ready = false;
reset_from_location();
reset_to_location();
retrieve_active_lot();
}
function apply_location_label_scan(data) {
if (!scan_sku_ready && !($('#product_sku').attr('secondary') || $('#product_sku').val())) {
bootbox.alert('Scan the SKU label first, then scan the from-location label.');
return;
}
var role = scan_from_ready ? 'to' : 'from';
if (role === 'to' && same_location_as_from(data)) {
bootbox.alert('To-location must be different from from-location.');
return;
}
validate_scanned_location(role, data, function() {
set_select_value('#warehouse_' + role, data.warehouse_id, data.warehouse_id);
if (advanced) {
set_select_value('#zone_' + role, data.zone, data.zone);
set_select_value('#aisle_' + role, data.aisle, data.aisle);
}
set_select_value('#rack_' + role, data.rack, data.rack);
if (role === 'from') {
scan_from_ready = true;
} else {
scan_to_ready = true;
}
});
}
function same_location_as_from(data) {
return String($('#warehouse_from').val()) === String(data.warehouse_id)
&& String($('#zone_from').val()) === String(data.zone)
&& String($('#aisle_from').val()) === String(data.aisle)
&& String($('#rack_from').val()) === String(data.rack);
}
function validate_scanned_location(role, data, onSuccess) {
ajax_request({
url: '<?php echo $server_url?>ics/api/engine/validate_scan_location.php',
autoPrepare: true,
checkRequired: 0,
action: 'read',
queueLock: false,
noLoading: true,
data: {
mode: role,
warehouse_id: data.warehouse_id,
zone: data.zone,
aisle: data.aisle,
rack: data.rack,
product_sku: $('#product_sku').attr('secondary') || $('#product_sku').val(),
lot_number: $('#lot_number').val(),
serial_number: $('#serial_number').val()
},
onSuccess: onSuccess
});
}
function reset_to_location() {
$('#warehouse_to').val('');
$('#zone_to').html('<option value="">Please select ' + label_zone.toLowerCase() + '</option>').val('');
$('#aisle_to').html('<option value="">Please select ' + label_aisle.toLowerCase() + '</option>').val('');
$('#rack_to').html('<option value="">Please select ' + label_rack.toLowerCase() + '</option>').val('');
}
function reset_stock_transfer_inputs() {
$('form')[0].reset();
$('#product_sku').removeAttr('secondary').prop('disabled', false);
$('#product_name').removeAttr('secondary').val('');
$('#lot_number').html('<option value="">Select lot...</option>').prop('disabled', false);
$('#serial_number').html('<option value="">Select serial...</option>').prop('disabled', false);
$('#contact').removeAttr('secondary data-id');
reset_from_location();
reset_to_location();
retrieve_warehouse('to');
scan_sku_ready = false;
scan_from_ready = false;
scan_to_ready = false;
pending_scan_lot = '';
pending_scan_serial = '';
$('#product_sku').focus();
}
function save_scanned_stock_transfer() {
if (!scan_sku_ready && !($('#product_sku').attr('secondary') || $('#product_sku').val())) {
bootbox.alert('Scan the SKU label first.');
return;
}
if (!scan_from_ready && !$('#rack_from').val()) {
bootbox.alert('Scan the from-location label before saving.');
return;
}
if (!scan_to_ready && !$('#rack_to').val()) {
bootbox.alert('Scan the to-location label before saving.');
return;
}
manage_stock_transfer();
}
function load_location_config(callback) { function load_location_config(callback) {
return ajax_request({ return ajax_request({
@@ -224,10 +356,16 @@
'aisle_to': advanced ? $('#aisle_to').val() : rack_to_val, 'aisle_to': advanced ? $('#aisle_to').val() : rack_to_val,
'rack_to': rack_to_val, 'rack_to': rack_to_val,
'product_sku': sku_val, 'product_sku': sku_val,
'lot_number': $('#lot_number').val(),
'serial_number': $('#serial_number').val(),
}, },
action: 'manage', action: 'manage',
onSuccess: function() { onSuccess: function() {
<?php if(empty($_GET['id'])){ ?>
reset_stock_transfer_inputs();
<?php } else { ?>
window.location.href = '<?php echo $server_url?>ics/stock_transfer.php'; window.location.href = '<?php echo $server_url?>ics/stock_transfer.php';
<?php } ?>
} }
}); });
} }
@@ -266,6 +404,7 @@
autoPrepare: true, autoPrepare: true,
checkRequired: 0, checkRequired: 0,
noLoading: true, noLoading: true,
queueLock: false,
action: 'read', action: 'read',
data: { 'product_sku': sku }, data: { 'product_sku': sku },
onSuccess: function(res) { onSuccess: function(res) {
@@ -276,8 +415,16 @@
option += '<option value="' + item.lot_number + '">' + label + '</option>'; option += '<option value="' + item.lot_number + '">' + label + '</option>';
}); });
$('#lot_number').html(option).prop('disabled', false); $('#lot_number').html(option).prop('disabled', false);
if (pending_scan_lot) {
set_select_value('#lot_number', pending_scan_lot, pending_scan_lot);
}
$('#serial_number').html('<option value="">Select serial... (optional)</option>').prop('disabled', false); $('#serial_number').html('<option value="">Select serial... (optional)</option>').prop('disabled', false);
reset_from_location(); if (!scan_from_ready) reset_from_location();
if (pending_scan_lot) {
retrieve_active_serial();
} else {
retrieve_warehouse('from');
}
} }
}); });
} }
@@ -292,6 +439,7 @@
autoPrepare: true, autoPrepare: true,
checkRequired: 0, checkRequired: 0,
noLoading: true, noLoading: true,
queueLock: false,
action: 'read', action: 'read',
data: { 'product_sku': sku, 'lot_number': $('#lot_number').val() }, data: { 'product_sku': sku, 'lot_number': $('#lot_number').val() },
onSuccess: function(res) { onSuccess: function(res) {
@@ -300,7 +448,12 @@
option += '<option value="' + serial + '">' + serial + '</option>'; option += '<option value="' + serial + '">' + serial + '</option>';
}); });
$('#serial_number').html(option).prop('disabled', false); $('#serial_number').html(option).prop('disabled', false);
reset_from_location(); if (pending_scan_serial) {
set_select_value('#serial_number', pending_scan_serial, pending_scan_serial);
pending_scan_serial = '';
}
if (!scan_from_ready) reset_from_location();
retrieve_warehouse('from');
} }
}); });
} }
@@ -313,6 +466,7 @@
autoPrepare: true, autoPrepare: true,
checkRequired: 0, checkRequired: 0,
noLoading: true, noLoading: true,
queueLock: false,
data: { data: {
'type': type, 'type': type,
'product_sku': type === 'from' ? ($('#product_sku').attr('secondary') || $('#product_sku').val()) : '', 'product_sku': type === 'from' ? ($('#product_sku').attr('secondary') || $('#product_sku').val()) : '',
@@ -320,11 +474,13 @@
}, },
action: 'read', action: 'read',
onSuccess: function(res) { onSuccess: function(res) {
var current = $('#warehouse_' + type).val();
var option = '<option value="">Select warehouse...</option>'; var option = '<option value="">Select warehouse...</option>';
$.each(res.output, function(key, item) { $.each(res.output, function(key, item) {
option += '<option value="' + item.id + '">' + item.warehouse_name + '</option>'; option += '<option value="' + item.id + '">' + item.warehouse_name + '</option>';
}); });
$('select[name="warehouse"][role="' + type + '"]').html(option).prop('disabled', false); $('select[name="warehouse"][role="' + type + '"]').html(option).prop('disabled', false);
if (current) set_select_value('#warehouse_' + type, current, current);
} }
}); });
} }
@@ -348,6 +504,7 @@
autoPrepare: true, autoPrepare: true,
checkRequired: 0, checkRequired: 0,
noLoading: true, noLoading: true,
queueLock: false,
data: Object.assign({ 'type': type }, extra), data: Object.assign({ 'type': type }, extra),
action: 'read', action: 'read',
onSuccess: function(res) { onSuccess: function(res) {
@@ -380,6 +537,7 @@
autoPrepare: true, autoPrepare: true,
checkRequired: 0, checkRequired: 0,
noLoading: true, noLoading: true,
queueLock: false,
data: Object.assign({ 'type': type }, extra), data: Object.assign({ 'type': type }, extra),
action: 'read', action: 'read',
onSuccess: function(res) { onSuccess: function(res) {
@@ -414,6 +572,7 @@
autoPrepare: true, autoPrepare: true,
checkRequired: 0, checkRequired: 0,
noLoading: true, noLoading: true,
queueLock: false,
data: Object.assign({ 'type': type }, extra), data: Object.assign({ 'type': type }, extra),
action: 'read', action: 'read',
onSuccess: function(res) { onSuccess: function(res) {
@@ -535,6 +694,14 @@
await retrieve_warehouse('to'); await retrieve_warehouse('to');
<?php } ?> <?php } ?>
await retrieve_for_edit(); await retrieve_for_edit();
scanner.init({
allowRaw: false,
allowedTypes: ['sku', 'loc'],
onScan: function(sku, product_name, uom, data) {
if (data.type === 'sku') apply_sku_label_scan(data);
if (data.type === 'loc') apply_location_label_scan(data);
}
});
} catch(e) { } catch(e) {
console.log(e); console.log(e);
} }
@@ -544,6 +711,13 @@
// ── Change handlers (create mode only) ─────────────────────────────────── // ── Change handlers (create mode only) ───────────────────────────────────
<?php if(empty($_GET['id'])){ ?> <?php if(empty($_GET['id'])){ ?>
$(document).on('keydown', function(e) {
if (e.key === 'F2') {
e.preventDefault();
save_scanned_stock_transfer();
}
});
$('input#product_sku').on('blur', function() { $('input#product_sku').on('blur', function() {
var sku = $(this).attr('secondary'); var sku = $(this).attr('secondary');
if (!sku) return; if (!sku) return;
+538
View File
@@ -0,0 +1,538 @@
<?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 { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;' }[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) + ' &middot; ' + escape_html(item.lot_number);
if (item.expiry_date) info += ' &middot; 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)} &nbsp; 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>
+8 -4
View File
@@ -6,9 +6,9 @@
<title>ThreeWMS</title> <title>ThreeWMS</title>
<meta name="csrf-token" content="<?= htmlspecialchars($_SESSION['csrf_token'] ?? '') ?>"> <meta name="csrf-token" content="<?= htmlspecialchars($_SESSION['csrf_token'] ?? '') ?>">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="apple-touch-icon" sizes="180x180" href="<?php echo $server_url?>assets/images/apple-touch-icon.png"> <link rel="apple-touch-icon" sizes="180x180" href="<?php echo $server_url?>assets/images/favicon.png">
<link rel="icon" type="image/png" sizes="32x32" href="<?php echo $server_url?>assets/images/favicon-32x32.png"> <link rel="icon" type="image/png" sizes="32x32" href="<?php echo $server_url?>assets/images/favicon.png">
<link rel="icon" type="image/png" sizes="16x16" href="<?php echo $server_url?>assets/images/favicon-16x16.png"> <link rel="icon" type="image/png" sizes="16x16" href="<?php echo $server_url?>assets/images/favicon.png">
<link rel="manifest" href="<?php echo $server_url?>assets/site.webmanifest"> <link rel="manifest" href="<?php echo $server_url?>assets/site.webmanifest">
<!-- jquery --> <!-- jquery -->
@@ -58,4 +58,8 @@
<link rel="stylesheet" href="<?php echo $server_url?>assets/css/custom.css"> <link rel="stylesheet" href="<?php echo $server_url?>assets/css/custom.css">
<script src="<?php echo $server_url?>assets/js/custom.js"></script> <script src="<?php echo $server_url?>assets/js/custom.js"></script>
</head> <script src="https://cdnjs.cloudflare.com/ajax/libs/html5-qrcode/2.3.8/html5-qrcode.min.js"
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="<?php echo $server_url?>assets/js/scanner.js"></script>
</head>
+15 -5
View File
@@ -6,11 +6,7 @@
<aside id="sidebar" class="sidebar overflow-y-auto d-flex flex-column"> <aside id="sidebar" class="sidebar overflow-y-auto d-flex flex-column">
<div class="logo-area"> <div class="logo-area">
<a href="<?php echo $server_url?>index.php" class="d-inline-flex"> <a href="<?php echo $server_url?>index.php" class="d-inline-flex">
<img <img src="<?php echo $server_url?>assets/images/favicon.png" alt="" width="40"/>
src="data:image/svg+xml,%3csvg%20width='62'%20height='67'%20viewBox='0%200%2062%2067'%20fill='none'%20xmlns='http://www.w3.org/2000/svg'%3e%3cpath%20d='M30.604%2066.378L0.00805664%2048.1582V35.7825L30.604%2054.0023V66.378Z'%20fill='%23302C4D'/%3e%3cpath%20d='M61.1996%2048.1582L30.604%2066.378V54.0023L61.1996%2035.7825V48.1582Z'%20fill='%23E66239'/%3e%3cpath%20d='M30.5955%200L0%2018.2198V30.5955L30.5955%2012.3757V0Z'%20fill='%23657E92'/%3e%3cpath%20d='M61.191%2018.2198L30.5955%200V12.3757L61.191%2030.5955V18.2198Z'%20fill='%23A3B2BE'/%3e%3cpath%20d='M30.604%2048.8457L0.00805664%2030.6259V18.2498L30.604%2036.47V48.8457Z'%20fill='%23302C4D'/%3e%3cpath%20d='M61.1996%2030.6259L30.604%2048.8457V36.47L61.1996%2018.2498V30.6259Z'%20fill='%23E66239'/%3e%3c/svg%3e"
alt=""
width="24"
/>
<span class="logo-text ms-2"> <span class="logo-text ms-2">
<img src="<?php echo $server_url?>assets/images/logo.svg" alt="" /> <img src="<?php echo $server_url?>assets/images/logo.svg" alt="" />
</span> </span>
@@ -32,6 +28,13 @@
</a> </a>
</li> </li>
<li>
<a class="nav-link" href="<?php echo $server_url?>ics/sku_barcode_label.php">
<i class="ti ti-barcode"></i>
<span class="nav-text">SKU Labels</span>
</a>
</li>
<li> <li>
<a class="nav-link <?php echo in_array($current_page, ['warehouse.php', 'manage_warehouse.php', 'manage_storage.php']) ? 'active' : ''; ?>" <a class="nav-link <?php echo in_array($current_page, ['warehouse.php', 'manage_warehouse.php', 'manage_storage.php']) ? 'active' : ''; ?>"
href="<?php echo $server_url?>inventory/warehouse.php"> href="<?php echo $server_url?>inventory/warehouse.php">
@@ -40,6 +43,13 @@
</a> </a>
</li> </li>
<li>
<a class="nav-link" href="<?php echo $server_url?>ics/location_barcode_label.php">
<i class="ti ti-map-pin-code"></i>
<span class="nav-text">Location Labels</span>
</a>
</li>
<li> <li>
<a class="nav-link <?php echo in_array($current_page, ['contact.php', 'manage_contact.php', 'manage_contact_type.php']) ? 'active' : ''; ?>" <a class="nav-link <?php echo in_array($current_page, ['contact.php', 'manage_contact.php', 'manage_contact_type.php']) ? 'active' : ''; ?>"
href="<?php echo $server_url?>contact/contact.php"> href="<?php echo $server_url?>contact/contact.php">
+1 -5
View File
@@ -6,11 +6,7 @@
<aside id="sidebar" class="sidebar overflow-y-auto d-flex flex-column"> <aside id="sidebar" class="sidebar overflow-y-auto d-flex flex-column">
<div class="logo-area"> <div class="logo-area">
<a href="<?php echo $server_url?>index.php" class="d-inline-flex"> <a href="<?php echo $server_url?>index.php" class="d-inline-flex">
<img <img src="<?php echo $server_url?>assets/images/favicon.png" alt="" width="40"/>
src="data:image/svg+xml,%3csvg%20width='62'%20height='67'%20viewBox='0%200%2062%2067'%20fill='none'%20xmlns='http://www.w3.org/2000/svg'%3e%3cpath%20d='M30.604%2066.378L0.00805664%2048.1582V35.7825L30.604%2054.0023V66.378Z'%20fill='%23302C4D'/%3e%3cpath%20d='M61.1996%2048.1582L30.604%2066.378V54.0023L61.1996%2035.7825V48.1582Z'%20fill='%23E66239'/%3e%3cpath%20d='M30.5955%200L0%2018.2198V30.5955L30.5955%2012.3757V0Z'%20fill='%23657E92'/%3e%3cpath%20d='M61.191%2018.2198L30.5955%200V12.3757L61.191%2030.5955V18.2198Z'%20fill='%23A3B2BE'/%3e%3cpath%20d='M30.604%2048.8457L0.00805664%2030.6259V18.2498L30.604%2036.47V48.8457Z'%20fill='%23302C4D'/%3e%3cpath%20d='M61.1996%2030.6259L30.604%2048.8457V36.47L61.1996%2018.2498V30.6259Z'%20fill='%23E66239'/%3e%3c/svg%3e"
alt=""
width="24"
/>
<span class="logo-text ms-2"> <span class="logo-text ms-2">
<img src="<?php echo $server_url?>assets/images/logo.svg" alt="" /> <img src="<?php echo $server_url?>assets/images/logo.svg" alt="" />
</span> </span>
+3 -7
View File
@@ -2,13 +2,9 @@
<aside id="sidebar" class="sidebar overflow-y-auto"> <aside id="sidebar" class="sidebar overflow-y-auto">
<div class="logo-area"> <div class="logo-area">
<a href="index.html" class="d-inline-flex"> <a href="index.html" class="d-inline-flex">
<img <img src="<?php echo $server_url?>assets/images/favicon.png" alt="" width="40"/>
src="data:image/svg+xml,%3csvg%20width='62'%20height='67'%20viewBox='0%200%2062%2067'%20fill='none'%20xmlns='http://www.w3.org/2000/svg'%3e%3cpath%20d='M30.604%2066.378L0.00805664%2048.1582V35.7825L30.604%2054.0023V66.378Z'%20fill='%23302C4D'/%3e%3cpath%20d='M61.1996%2048.1582L30.604%2066.378V54.0023L61.1996%2035.7825V48.1582Z'%20fill='%23E66239'/%3e%3cpath%20d='M30.5955%200L0%2018.2198V30.5955L30.5955%2012.3757V0Z'%20fill='%23657E92'/%3e%3cpath%20d='M61.191%2018.2198L30.5955%200V12.3757L61.191%2030.5955V18.2198Z'%20fill='%23A3B2BE'/%3e%3cpath%20d='M30.604%2048.8457L0.00805664%2030.6259V18.2498L30.604%2036.47V48.8457Z'%20fill='%23302C4D'/%3e%3cpath%20d='M61.1996%2030.6259L30.604%2048.8457V36.47L61.1996%2018.2498V30.6259Z'%20fill='%23E66239'/%3e%3c/svg%3e"
alt=""
width="24"
/>
<span class="logo-text ms-2"> <span class="logo-text ms-2">
<img src="./assets/images/logo.svg" alt="" /> <img src="<?php echo $server_url?>assets/images/logo.svg" alt="" />
</span> </span>
</a> </a>
</div> </div>
@@ -136,4 +132,4 @@
<li><br><br><br><br></li> <li><br><br><br><br></li>
</ul> </ul>
</aside> </aside>
+6 -1
View File
@@ -33,10 +33,14 @@
<label for="product_name" class="form-label">Product Name</label> <label for="product_name" class="form-label">Product Name</label>
<input type="text" class="form-control" id="product_name" placeholder="Enter product name" required> <input type="text" class="form-control" id="product_name" placeholder="Enter product name" required>
</div> </div>
<div class="col-md-6 mb-3"> <div class="col-md-3 mb-3">
<label for="sku" class="form-label">SKU</label> <label for="sku" class="form-label">SKU</label>
<input type="text" class="form-control" id="sku" placeholder="Enter SKU" required> <input type="text" class="form-control" id="sku" placeholder="Enter SKU" required>
</div> </div>
<div class="col-md-3 mb-3">
<label for="barcode" class="form-label">Barcode <span class="text-muted fw-normal">(optional)</span></label>
<input type="text" class="form-control" id="barcode" placeholder="e.g. 8850999001234" autocomplete="off" maxlength="64">
</div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-3 mb-3"> <div class="col-md-3 mb-3">
@@ -149,6 +153,7 @@
} }
formData.append('keep_files', $('#current_file_label').val()) formData.append('keep_files', $('#current_file_label').val())
formData.append('barcode', $('#barcode').val() || '')
formData.append('cost_price', $('#cost_price').val() || 0) formData.append('cost_price', $('#cost_price').val() || 0)
formData.append('uom', $('#uom').val() || 'pcs') formData.append('uom', $('#uom').val() || 'pcs')