Location Barcode Labels
+Print or reprint labels from the rack master list.
+| Warehouse | +Zone | +Aisle | +Rack | +Status | +Label Status | +Action | +
|---|---|---|---|---|---|---|
| Loading locations... | ||||||
diff --git a/app/assets/images/apple-touch-icon.png b/app/assets/images/apple-touch-icon.png deleted file mode 100644 index dc37230..0000000 Binary files a/app/assets/images/apple-touch-icon.png and /dev/null differ diff --git a/app/assets/images/favicon-16x16.png b/app/assets/images/favicon-16x16.png deleted file mode 100644 index e01e137..0000000 Binary files a/app/assets/images/favicon-16x16.png and /dev/null differ diff --git a/app/assets/images/favicon-32x32.png b/app/assets/images/favicon-32x32.png deleted file mode 100644 index 6639fab..0000000 Binary files a/app/assets/images/favicon-32x32.png and /dev/null differ diff --git a/app/assets/images/favicon.png b/app/assets/images/favicon.png new file mode 100644 index 0000000..bea5eb5 Binary files /dev/null and b/app/assets/images/favicon.png differ diff --git a/app/assets/js/scanner.js b/app/assets/js/scanner.js new file mode 100644 index 0000000..54a96e7 --- /dev/null +++ b/app/assets/js/scanner.js @@ -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(` +
`); + _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(); + } + }; +})(); diff --git a/app/assets/utils/classes/ProductManager.php b/app/assets/utils/classes/ProductManager.php index a3ba21c..2467eb3 100644 --- a/app/assets/utils/classes/ProductManager.php +++ b/app/assets/utils/classes/ProductManager.php @@ -317,7 +317,8 @@ class ProductManager { $sth = $this->pdo->prepare( "SELECT * FROM md_product WHERE company_id = :company_id - AND sku LIKE :keyword + AND (sku LIKE :keyword OR barcode LIKE :keyword) + AND status > 0 LIMIT 50" ); $sth->execute([ @@ -358,6 +359,7 @@ class ProductManager { ':company_id' => $this->company_id, ':product_name' => $data['product_name'], ':sku' => $data['sku'], + ':barcode' => trim($data['barcode'] ?? '') ?: null, ':uom' => $data['uom'] ?? 'pcs', ':price' => $data['price'], ':cost_price' => (float)($data['cost_price'] ?? 0), @@ -376,6 +378,7 @@ class ProductManager { "UPDATE md_product SET product_name = :product_name, sku = :sku, + barcode = :barcode, uom = :uom, price = :price, cost_price = :cost_price, @@ -391,10 +394,10 @@ class ProductManager { } else { $this->pdo->prepare( "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`) 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)" )->execute($params); } diff --git a/app/ics/api/engine/barcode_lookup.php b/app/ics/api/engine/barcode_lookup.php new file mode 100644 index 0000000..089faae --- /dev/null +++ b/app/ics/api/engine/barcode_lookup.php @@ -0,0 +1,205 @@ +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)); +?> diff --git a/app/ics/api/engine/location_barcode_label.php b/app/ics/api/engine/location_barcode_label.php new file mode 100644 index 0000000..9d4c577 --- /dev/null +++ b/app/ics/api/engine/location_barcode_label.php @@ -0,0 +1,153 @@ +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)); +?> diff --git a/app/ics/api/engine/sku_barcode_label.php b/app/ics/api/engine/sku_barcode_label.php new file mode 100644 index 0000000..a0bc5d1 --- /dev/null +++ b/app/ics/api/engine/sku_barcode_label.php @@ -0,0 +1,245 @@ +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)); +?> diff --git a/app/ics/api/engine/sku_label_lots.php b/app/ics/api/engine/sku_label_lots.php new file mode 100644 index 0000000..ba56836 --- /dev/null +++ b/app/ics/api/engine/sku_label_lots.php @@ -0,0 +1,159 @@ +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)); +?> diff --git a/app/ics/api/engine/sku_label_products.php b/app/ics/api/engine/sku_label_products.php new file mode 100644 index 0000000..4b0b1a0 --- /dev/null +++ b/app/ics/api/engine/sku_label_products.php @@ -0,0 +1,17 @@ +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)); +?> diff --git a/app/ics/api/engine/validate_scan_location.php b/app/ics/api/engine/validate_scan_location.php new file mode 100644 index 0000000..0a710dc --- /dev/null +++ b/app/ics/api/engine/validate_scan_location.php @@ -0,0 +1,86 @@ +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)); + } +?> diff --git a/app/ics/location_barcode_label.php b/app/ics/location_barcode_label.php new file mode 100644 index 0000000..0051a0d --- /dev/null +++ b/app/ics/location_barcode_label.php @@ -0,0 +1,388 @@ + + + + + + +Print or reprint labels from the rack master list.
+| Warehouse | +Zone | +Aisle | +Rack | +Status | +Label Status | +Action | +
|---|---|---|---|---|---|---|
| Loading locations... | ||||||