Barcode Operations
This commit is contained in:
@@ -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));
|
||||
?>
|
||||
@@ -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));
|
||||
?>
|
||||
@@ -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));
|
||||
?>
|
||||
@@ -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));
|
||||
}
|
||||
?>
|
||||
@@ -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 { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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
@@ -30,8 +30,13 @@
|
||||
<!-- Product Name -->
|
||||
<div class="mb-3 col-lg-3">
|
||||
<label for="product_sku" class="form-label">SKU</label>
|
||||
<input type="text" id="product_sku" class="form-control product_search"
|
||||
placeholder="Type product sku..." autocomplete="off" required>
|
||||
<div class="input-group">
|
||||
<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 class="mb-3 col-lg-3">
|
||||
<label for="product_name" class="form-label">Name</label>
|
||||
@@ -130,6 +135,8 @@
|
||||
var label_rack = 'Location';
|
||||
var label_zone = 'Zone';
|
||||
var label_aisle = 'Aisle';
|
||||
var scan_sku_ready = false;
|
||||
var scan_location_ready = false;
|
||||
|
||||
function load_location_config(callback) {
|
||||
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() {
|
||||
|
||||
var rack_val = $('#rack').val();
|
||||
@@ -200,7 +301,11 @@
|
||||
},
|
||||
action: 'manage',
|
||||
onSuccess: function() {
|
||||
<?php if(empty($_GET["id"])){ ?>
|
||||
reset_stock_in_inputs();
|
||||
<?php } else { ?>
|
||||
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",
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
noLoading: true,
|
||||
queueLock: false,
|
||||
data: {
|
||||
"type": type
|
||||
},
|
||||
@@ -262,6 +369,7 @@
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
noLoading: true,
|
||||
queueLock: false,
|
||||
data : {
|
||||
"type": type
|
||||
},
|
||||
@@ -289,6 +397,7 @@
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
noLoading: true,
|
||||
queueLock: false,
|
||||
data: {
|
||||
"type": type
|
||||
},
|
||||
@@ -315,6 +424,7 @@
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
noLoading: true,
|
||||
queueLock: false,
|
||||
data: {
|
||||
"type": type
|
||||
},
|
||||
@@ -462,6 +572,14 @@
|
||||
await load_location_config(apply_location_labels);
|
||||
await retrieve_warehouse('to');
|
||||
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"])){ ?>
|
||||
prefill_from_restock();
|
||||
@@ -488,6 +606,13 @@
|
||||
|
||||
|
||||
<?php if(empty($_GET["id"])){ ?>
|
||||
$(document).on('keydown', function(e) {
|
||||
if (e.key === 'F2') {
|
||||
e.preventDefault();
|
||||
save_scanned_stock_in();
|
||||
}
|
||||
});
|
||||
|
||||
$("select#warehouse").change(function() {
|
||||
if (advanced) {
|
||||
retrieve_zone('to');
|
||||
|
||||
@@ -28,8 +28,13 @@
|
||||
<!-- Step 1: Product -->
|
||||
<div class="mb-3 col-lg-3">
|
||||
<label for="product_sku" class="form-label">SKU</label>
|
||||
<input type="text" id="product_sku" class="form-control product_search"
|
||||
placeholder="Type product sku..." autocomplete="off" required>
|
||||
<div class="input-group">
|
||||
<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 class="mb-3 col-lg-3">
|
||||
<label for="product_name" class="form-label">Name</label>
|
||||
@@ -117,6 +122,97 @@
|
||||
var label_rack = 'Location';
|
||||
var label_zone = 'Zone';
|
||||
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) {
|
||||
return ajax_request({
|
||||
@@ -171,10 +267,16 @@
|
||||
'rack': rack_val,
|
||||
'product_sku': sku_val,
|
||||
'warehouse': wh_val,
|
||||
'lot_number': $('#lot_number').val(),
|
||||
'serial_number': $('#serial_number').val(),
|
||||
},
|
||||
action: 'manage',
|
||||
onSuccess: function() {
|
||||
<?php if(empty($_GET["id"])){ ?>
|
||||
reset_stock_out_inputs();
|
||||
<?php } else { ?>
|
||||
window.location.href = '<?php echo $server_url?>ics/stock_out.php';
|
||||
<?php } ?>
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -215,6 +317,7 @@
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
noLoading: true,
|
||||
queueLock: false,
|
||||
action: 'read',
|
||||
data: { "product_sku": sku },
|
||||
onSuccess: function(res) {
|
||||
@@ -226,10 +329,18 @@
|
||||
option += `<option value="${item.lot_number}">${label}</option>`;
|
||||
});
|
||||
$('#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);
|
||||
|
||||
// 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,
|
||||
checkRequired: 0,
|
||||
noLoading: true,
|
||||
queueLock: false,
|
||||
action: 'read',
|
||||
data: { "product_sku": sku, "lot_number": lot },
|
||||
onSuccess: function(res) {
|
||||
@@ -257,9 +369,14 @@
|
||||
option += `<option value="${serial}">${serial}</option>`;
|
||||
});
|
||||
$('#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_location();
|
||||
if (!scan_location_ready) reset_location();
|
||||
retrieve_warehouse();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -273,6 +390,7 @@
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
noLoading: true,
|
||||
queueLock: false,
|
||||
data: {
|
||||
"type": 'from',
|
||||
"product_sku": $('#product_sku').attr('secondary') || $('#product_sku').val(),
|
||||
@@ -280,11 +398,13 @@
|
||||
},
|
||||
action: 'read',
|
||||
onSuccess: function(res) {
|
||||
var current = $('#warehouse').val();
|
||||
var option = '<option value="">Select warehouse...</option>';
|
||||
$.each(res.output, function(key, item) {
|
||||
option += `<option value='${item.id}'>${item.warehouse_name}</option>`;
|
||||
});
|
||||
$('#warehouse').html(option).prop('disabled', false);
|
||||
if (current) set_select_value('#warehouse', current, current);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -298,6 +418,7 @@
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
noLoading: true,
|
||||
queueLock: false,
|
||||
data: {
|
||||
"warehouse": $('#warehouse').val(),
|
||||
"type": 'from',
|
||||
@@ -324,6 +445,7 @@
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
noLoading: true,
|
||||
queueLock: false,
|
||||
data: {
|
||||
"warehouse": $('#warehouse').val(),
|
||||
"zone": $('#zone').val(),
|
||||
@@ -351,6 +473,7 @@
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
noLoading: true,
|
||||
queueLock: false,
|
||||
data: {
|
||||
"warehouse": $('#warehouse').val(),
|
||||
"zone": $('#zone').val(),
|
||||
@@ -454,6 +577,14 @@
|
||||
await load_location_config(apply_location_labels);
|
||||
await retrieve_warehouse();
|
||||
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) {
|
||||
console.log(e);
|
||||
}
|
||||
@@ -462,6 +593,12 @@
|
||||
|
||||
// ── Change handlers (create mode only) ───────────────────────────────
|
||||
<?php if(empty($_GET["id"])){ ?>
|
||||
$(document).on('keydown', function(e) {
|
||||
if (e.key === 'F2') {
|
||||
e.preventDefault();
|
||||
save_scanned_stock_out();
|
||||
}
|
||||
});
|
||||
|
||||
// SKU selected → load lots
|
||||
$('input#product_sku').on('blur', function() {
|
||||
|
||||
@@ -28,8 +28,13 @@
|
||||
<!-- Step 1: Product -->
|
||||
<div class="mb-3 col-lg-3">
|
||||
<label for="product_sku" class="form-label">SKU</label>
|
||||
<input type="text" id="product_sku" class="form-control product_search"
|
||||
placeholder="Type product sku..." autocomplete="off" required>
|
||||
<div class="input-group">
|
||||
<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 class="mb-3 col-lg-3">
|
||||
<label for="product_name" class="form-label">Name</label>
|
||||
@@ -150,6 +155,133 @@
|
||||
var label_rack = 'Location';
|
||||
var label_zone = 'Zone';
|
||||
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) {
|
||||
return ajax_request({
|
||||
@@ -224,10 +356,16 @@
|
||||
'aisle_to': advanced ? $('#aisle_to').val() : rack_to_val,
|
||||
'rack_to': rack_to_val,
|
||||
'product_sku': sku_val,
|
||||
'lot_number': $('#lot_number').val(),
|
||||
'serial_number': $('#serial_number').val(),
|
||||
},
|
||||
action: 'manage',
|
||||
onSuccess: function() {
|
||||
<?php if(empty($_GET['id'])){ ?>
|
||||
reset_stock_transfer_inputs();
|
||||
<?php } else { ?>
|
||||
window.location.href = '<?php echo $server_url?>ics/stock_transfer.php';
|
||||
<?php } ?>
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -266,6 +404,7 @@
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
noLoading: true,
|
||||
queueLock: false,
|
||||
action: 'read',
|
||||
data: { 'product_sku': sku },
|
||||
onSuccess: function(res) {
|
||||
@@ -276,8 +415,16 @@
|
||||
option += '<option value="' + item.lot_number + '">' + label + '</option>';
|
||||
});
|
||||
$('#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);
|
||||
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,
|
||||
checkRequired: 0,
|
||||
noLoading: true,
|
||||
queueLock: false,
|
||||
action: 'read',
|
||||
data: { 'product_sku': sku, 'lot_number': $('#lot_number').val() },
|
||||
onSuccess: function(res) {
|
||||
@@ -300,7 +448,12 @@
|
||||
option += '<option value="' + serial + '">' + serial + '</option>';
|
||||
});
|
||||
$('#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,
|
||||
checkRequired: 0,
|
||||
noLoading: true,
|
||||
queueLock: false,
|
||||
data: {
|
||||
'type': type,
|
||||
'product_sku': type === 'from' ? ($('#product_sku').attr('secondary') || $('#product_sku').val()) : '',
|
||||
@@ -320,11 +474,13 @@
|
||||
},
|
||||
action: 'read',
|
||||
onSuccess: function(res) {
|
||||
var current = $('#warehouse_' + type).val();
|
||||
var option = '<option value="">Select warehouse...</option>';
|
||||
$.each(res.output, function(key, item) {
|
||||
option += '<option value="' + item.id + '">' + item.warehouse_name + '</option>';
|
||||
});
|
||||
$('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,
|
||||
checkRequired: 0,
|
||||
noLoading: true,
|
||||
queueLock: false,
|
||||
data: Object.assign({ 'type': type }, extra),
|
||||
action: 'read',
|
||||
onSuccess: function(res) {
|
||||
@@ -380,6 +537,7 @@
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
noLoading: true,
|
||||
queueLock: false,
|
||||
data: Object.assign({ 'type': type }, extra),
|
||||
action: 'read',
|
||||
onSuccess: function(res) {
|
||||
@@ -414,6 +572,7 @@
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
noLoading: true,
|
||||
queueLock: false,
|
||||
data: Object.assign({ 'type': type }, extra),
|
||||
action: 'read',
|
||||
onSuccess: function(res) {
|
||||
@@ -535,6 +694,14 @@
|
||||
await retrieve_warehouse('to');
|
||||
<?php } ?>
|
||||
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) {
|
||||
console.log(e);
|
||||
}
|
||||
@@ -544,6 +711,13 @@
|
||||
// ── Change handlers (create mode only) ───────────────────────────────────
|
||||
<?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() {
|
||||
var sku = $(this).attr('secondary');
|
||||
if (!sku) return;
|
||||
|
||||
@@ -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 { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char];
|
||||
});
|
||||
}
|
||||
|
||||
function refresh_header_buttons() {
|
||||
var has = !!selected_payload;
|
||||
$('#btn_print').prop('disabled', !has);
|
||||
$('#btn_disable').prop('disabled', !has);
|
||||
}
|
||||
|
||||
function highlight_selected_row() {
|
||||
$('#lot_tbody tr').removeClass('table-active');
|
||||
if (selected_lot_index >= 0) {
|
||||
$('#lot_tbody tr[data-lot-index="' + selected_lot_index + '"]').addClass('table-active');
|
||||
}
|
||||
}
|
||||
|
||||
function load_lots() {
|
||||
return ajax_request({
|
||||
url: server_url + 'ics/api/engine/sku_label_lots.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'read',
|
||||
queueLock: false,
|
||||
onSuccess: function(res) {
|
||||
lots = res.output || [];
|
||||
render_lots();
|
||||
highlight_selected_row();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function load_products() {
|
||||
return ajax_request({
|
||||
url: server_url + 'ics/api/engine/sku_label_products.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'read',
|
||||
queueLock: false,
|
||||
onSuccess: function(res) {
|
||||
products = res.output || [];
|
||||
var opts = '<option value="">Select SKU...</option>';
|
||||
products.forEach(function(item) {
|
||||
opts += `<option value="${escape_html(item.sku)}">${escape_html(item.sku)} - ${escape_html(item.product_name)}</option>`;
|
||||
});
|
||||
$('#create_sku').html(opts);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function selected_create_product() {
|
||||
var sku = $('#create_sku').val();
|
||||
return products.find(function(item) { return item.sku === sku; }) || {};
|
||||
}
|
||||
|
||||
function sync_create_product_name() {
|
||||
var item = selected_create_product();
|
||||
$('#create_product_name').val(item.product_name || '');
|
||||
}
|
||||
|
||||
function open_create_label_modal() {
|
||||
$('#create_sku').val('');
|
||||
$('#create_product_name').val('');
|
||||
$('#create_lot_number').val('');
|
||||
$('#create_serial_number').val('');
|
||||
create_label_modal.show();
|
||||
}
|
||||
|
||||
function render_lots() {
|
||||
var keyword = $('#search_lot').val().toLowerCase();
|
||||
var rows = lots.filter(function(item) {
|
||||
if (!keyword) return true;
|
||||
return [
|
||||
item.product_sku,
|
||||
item.product_name,
|
||||
item.lot_number,
|
||||
item.expiry_date,
|
||||
item.description
|
||||
].join(' ').toLowerCase().includes(keyword);
|
||||
});
|
||||
|
||||
if (!rows.length) {
|
||||
$('#lot_tbody').html('<tr><td colspan="7" class="text-center text-muted py-4">No lots found.</td></tr>');
|
||||
return;
|
||||
}
|
||||
|
||||
var body = '';
|
||||
rows.forEach(function(item) {
|
||||
var idx = lots.indexOf(item);
|
||||
var source = '<span class="badge bg-success">Lot master</span>';
|
||||
if (item.source === 'active_stock') {
|
||||
source = '<span class="badge bg-warning text-dark">Stock only</span>';
|
||||
}
|
||||
if (item.source === 'md_barcode') {
|
||||
source = '<span class="badge bg-info text-dark">Barcode only</span>';
|
||||
}
|
||||
|
||||
body += `<tr data-lot-index="${idx}">
|
||||
<td class="fw-semibold">${escape_html(item.product_sku)}</td>
|
||||
<td>${escape_html(item.product_name)}</td>
|
||||
<td>${escape_html(item.lot_number)}</td>
|
||||
<td>${item.expiry_date ? escape_html(item.expiry_date) : '<span class="text-muted">—</span>'}</td>
|
||||
<td>${source}</td>
|
||||
<td class="text-end">
|
||||
<button class="btn btn-sm btn-outline-primary" onclick="open_label_modal(${idx})">
|
||||
<i class="ti ti-list me-1"></i>Labels
|
||||
</button>
|
||||
</td>
|
||||
</tr>`;
|
||||
});
|
||||
$('#lot_tbody').html(body);
|
||||
}
|
||||
|
||||
function open_label_modal(index) {
|
||||
var item = lots[index];
|
||||
if (!item) return;
|
||||
|
||||
$('#modal_lot_index').val(index);
|
||||
var info = escape_html(item.product_sku) + ' · ' + escape_html(item.lot_number);
|
||||
if (item.expiry_date) info += ' · Exp ' + escape_html(item.expiry_date);
|
||||
$('#modal_lot_info').html(info);
|
||||
$('#modal_serial_number').val('');
|
||||
$('#existing_label_list').html('Loading...');
|
||||
sku_label_modal.show();
|
||||
load_existing_labels(item);
|
||||
setTimeout(function() { $('#modal_serial_number').trigger('focus'); }, 300);
|
||||
}
|
||||
|
||||
function load_existing_labels(item) {
|
||||
ajax_request({
|
||||
url: server_url + 'ics/api/engine/sku_barcode_label.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'read',
|
||||
queueLock: false,
|
||||
data: {
|
||||
product_sku: item.product_sku,
|
||||
lot_number: item.lot_number
|
||||
},
|
||||
onSuccess: function(res) {
|
||||
active_labels = res.output || [];
|
||||
if (!active_labels.length) {
|
||||
$('#existing_label_list').html('<span class="text-muted">No labels prepared for this lot yet.</span>');
|
||||
return;
|
||||
}
|
||||
|
||||
var html = '<div class="list-group list-group-flush">';
|
||||
active_labels.forEach(function(label, index) {
|
||||
var statusBadge = parseInt(label.status) === 1
|
||||
? '<span class="badge bg-success ms-2">Active</span>'
|
||||
: '<span class="badge bg-danger ms-2">Disabled</span>';
|
||||
var actionButtons = parseInt(label.status) === 1
|
||||
? `<button type="button" class="btn btn-sm btn-outline-primary" onclick="preview_existing_label(${index})">
|
||||
<i class="ti ti-eye me-1"></i>Select
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" onclick="disable_existing_label(${index})">
|
||||
<i class="ti ti-ban me-1"></i>Disable
|
||||
</button>`
|
||||
: `<button type="button" class="btn btn-sm btn-outline-success" onclick="reenable_existing_label(${index})">
|
||||
<i class="ti ti-refresh me-1"></i>Re-enable
|
||||
</button>`;
|
||||
html += `<div class="list-group-item d-flex justify-content-between align-items-center px-0">
|
||||
<span class="small">${escape_html(label.serial_number)} ${statusBadge}</span>
|
||||
<div class="d-flex gap-2">${actionButtons}</div>
|
||||
</div>`;
|
||||
});
|
||||
html += '</div>';
|
||||
$('#existing_label_list').html(html);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function preview_label(data, lot_index) {
|
||||
selected_payload = data.barcode;
|
||||
selected_label_data = data;
|
||||
if (lot_index !== undefined) selected_lot_index = lot_index;
|
||||
$('#label_preview').html(`
|
||||
<div class="barcode-label mx-auto">
|
||||
<div class="fw-bold fs-5">${escape_html(data.product_sku)}</div>
|
||||
<div class="small text-muted">${escape_html(data.product_name)}</div>
|
||||
<svg id="barcode_svg"></svg>
|
||||
<div class="small">Lot: ${escape_html(data.lot_number)} Serial: ${escape_html(data.serial_number)}</div>
|
||||
<div class="small text-muted">${escape_html(data.barcode)}</div>
|
||||
</div>`);
|
||||
JsBarcode('#barcode_svg', data.barcode, { format: 'CODE128', width: 1.6, height: 70, displayValue: false });
|
||||
sku_label_modal.hide();
|
||||
refresh_header_buttons();
|
||||
highlight_selected_row();
|
||||
}
|
||||
|
||||
function preview_existing_label(index) {
|
||||
var label = active_labels[index];
|
||||
if (!label) return;
|
||||
var lot_index = parseInt($('#modal_lot_index').val(), 10);
|
||||
preview_label(label, lot_index);
|
||||
}
|
||||
|
||||
function clear_selected_label() {
|
||||
selected_payload = '';
|
||||
selected_label_data = null;
|
||||
selected_lot_index = -1;
|
||||
$('#label_preview').html('Select a lot to preview its label.');
|
||||
refresh_header_buttons();
|
||||
highlight_selected_row();
|
||||
}
|
||||
|
||||
function disable_barcode_label(barcode, onDone) {
|
||||
bootbox.confirm({
|
||||
message: 'Disable this SKU barcode label? It will no longer be accepted by stock scans.',
|
||||
buttons: {
|
||||
confirm: { label: 'Disable', className: 'btn-danger' },
|
||||
cancel: { label: 'Back', className: 'btn-secondary' }
|
||||
},
|
||||
callback: function(result) {
|
||||
if (!result) return;
|
||||
ajax_request({
|
||||
url: server_url + 'ics/api/engine/sku_barcode_label.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'read',
|
||||
queueLock: false,
|
||||
data: { label_action: 'disable', barcode: barcode },
|
||||
onSuccess: function() {
|
||||
clear_selected_label();
|
||||
load_lots();
|
||||
onDone?.();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function disable_existing_label(index) {
|
||||
var label = active_labels[index];
|
||||
if (!label) return;
|
||||
disable_barcode_label(label.barcode, function() {
|
||||
var item = lots[parseInt($('#modal_lot_index').val(), 10)];
|
||||
if (item) load_existing_labels(item);
|
||||
});
|
||||
}
|
||||
|
||||
function reenable_existing_label(index) {
|
||||
var label = active_labels[index];
|
||||
if (!label) return;
|
||||
ajax_request({
|
||||
url: server_url + 'ics/api/engine/sku_barcode_label.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'read',
|
||||
queueLock: false,
|
||||
data: {
|
||||
label_action: 'create',
|
||||
allow_unlisted: 1,
|
||||
product_sku: label.product_sku,
|
||||
lot_number: label.lot_number,
|
||||
serial_number: label.serial_number
|
||||
},
|
||||
onSuccess: function() {
|
||||
var item = lots[parseInt($('#modal_lot_index').val(), 10)];
|
||||
if (item) load_existing_labels(item);
|
||||
load_lots();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function disable_selected_label() {
|
||||
disable_barcode_label(selected_payload);
|
||||
}
|
||||
|
||||
function generate_label_from_modal() {
|
||||
var item = lots[parseInt($('#modal_lot_index').val(), 10)];
|
||||
var serial = $('#modal_serial_number').val().trim();
|
||||
if (!item || !serial) {
|
||||
bootbox.alert('Serial no is required.');
|
||||
return;
|
||||
}
|
||||
|
||||
ajax_request({
|
||||
url: server_url + 'ics/api/engine/sku_barcode_label.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'read',
|
||||
queueLock: false,
|
||||
data: {
|
||||
label_action: 'create',
|
||||
product_sku: item.product_sku,
|
||||
lot_number: item.lot_number,
|
||||
serial_number: serial
|
||||
},
|
||||
onSuccess: function(res) {
|
||||
var lot_index = parseInt($('#modal_lot_index').val(), 10);
|
||||
preview_label(res.output, lot_index);
|
||||
load_lots();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function create_custom_label() {
|
||||
var sku = $('#create_sku').val();
|
||||
var lot = $('#create_lot_number').val().trim();
|
||||
var serial = $('#create_serial_number').val().trim();
|
||||
|
||||
if (!sku || !lot || !serial) {
|
||||
bootbox.alert('SKU, lot no, and serial no are required.');
|
||||
return;
|
||||
}
|
||||
|
||||
ajax_request({
|
||||
url: server_url + 'ics/api/engine/sku_barcode_label.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'read',
|
||||
queueLock: false,
|
||||
data: {
|
||||
label_action: 'create',
|
||||
allow_unlisted: 1,
|
||||
product_sku: sku,
|
||||
lot_number: lot,
|
||||
serial_number: serial
|
||||
},
|
||||
onSuccess: function(res) {
|
||||
selected_lot_index = -1;
|
||||
create_label_modal.hide();
|
||||
preview_label(res.output);
|
||||
load_lots();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function print_selected_label() {
|
||||
if (!selected_payload) return;
|
||||
ajax_request({
|
||||
url: server_url + 'ics/api/engine/sku_barcode_label.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'read',
|
||||
queueLock: false,
|
||||
data: { label_action: 'print', barcode: selected_payload },
|
||||
onSuccess: function() { window.print(); }
|
||||
});
|
||||
}
|
||||
|
||||
$('#search_lot').on('input', render_lots);
|
||||
|
||||
$(function() {
|
||||
sku_label_modal = new bootstrap.Modal(document.getElementById('skuLabelModal'));
|
||||
create_label_modal = new bootstrap.Modal(document.getElementById('createSkuLabelModal'));
|
||||
load_lots();
|
||||
load_products();
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
.barcode-label { max-width: 420px; background: #fff; color: #111; }
|
||||
.table-scroll {
|
||||
max-height: calc(100vh - 320px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
.table-scroll thead th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
background: #f8f9fa !important;
|
||||
box-shadow: inset 0 -1px 0 #dee2e6;
|
||||
}
|
||||
@media print {
|
||||
body * { visibility: hidden; }
|
||||
#label_preview, #label_preview * { visibility: visible; }
|
||||
#label_preview { position: absolute; left: 0; top: 0; width: 100%; border: 0 !important; }
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user