Change 'Rack' to 'Bin'

This commit is contained in:
Thanakorn S
2026-05-27 17:14:53 +07:00
parent b8798bc02d
commit 8f57ab5570
41 changed files with 931 additions and 933 deletions
+4 -4
View File
@@ -543,7 +543,7 @@ function load_location_config(callback) {
onSuccess: function(res) { onSuccess: function(res) {
var s = res.output; var s = res.output;
advanced = s.advanced_location == 1; advanced = s.advanced_location == 1;
label_rack = s.location_label_rack || (advanced ? 'Rack' : 'Location'); label_bin = s.location_label_bin || (advanced ? 'Bin' : 'Location');
label_zone = s.location_label_zone || 'Zone'; label_zone = s.location_label_zone || 'Zone';
label_aisle = s.location_label_aisle || 'Aisle'; label_aisle = s.location_label_aisle || 'Aisle';
if (callback) callback(); if (callback) callback();
@@ -1156,9 +1156,9 @@ function show_stock_rows(source, source_id, label) {
'0':'<span class="badge bg-warning text-dark">Draft</span>', '0':'<span class="badge bg-warning text-dark">Draft</span>',
'1':'<span class="badge bg-success">Approved</span>'}[String(r.status)] '1':'<span class="badge bg-success">Approved</span>'}[String(r.status)]
|| '—'; || '—';
var location = [r.zone, r.aisle, r.rack].filter(function(v) { var location = [r.zone, r.aisle, r.bin].filter(function(v) {
return v && v !== r.rack; return v && v !== r.bin;
}).concat([r.rack]).filter(Boolean).join(' / '); }).concat([r.bin]).filter(Boolean).join(' / ');
html += `<tr> html += `<tr>
<td class="ps-4">${type_badge}</td> <td class="ps-4">${type_badge}</td>
<td>${escape_html(r.product_name || r.product_sku)}<br><small class="text-muted">${escape_html(r.product_sku)}</small></td> <td>${escape_html(r.product_name || r.product_sku)}<br><small class="text-muted">${escape_html(r.product_sku)}</small></td>
+26 -26
View File
@@ -121,17 +121,17 @@ class BarcodeManager {
} }
/** /**
* Confirm a rack row exists for the given warehouse + zone + aisle + rack. * Confirm a bin row exists for the given warehouse + zone + aisle + bin.
* Throws if not found. * Throws if not found.
*/ */
private function validateLocation(int $warehouse_id, string $zone, string $aisle, string $rack): void { private function validateLocation(int $warehouse_id, string $zone, string $aisle, string $bin): void {
$sth = $this->pdo->prepare( $sth = $this->pdo->prepare(
"SELECT 1 FROM md_rack "SELECT 1 FROM md_bin
WHERE company_id = :company_id WHERE company_id = :company_id
AND warehouse = :warehouse AND warehouse = :warehouse
AND COALESCE(zone, '') = :zone AND COALESCE(zone, '') = :zone
AND COALESCE(aisle,'') = :aisle AND COALESCE(aisle,'') = :aisle
AND rack = :rack AND bin = :bin
LIMIT 1" LIMIT 1"
); );
$sth->execute([ $sth->execute([
@@ -139,7 +139,7 @@ class BarcodeManager {
':warehouse' => $warehouse_id, ':warehouse' => $warehouse_id,
':zone' => $zone, ':zone' => $zone,
':aisle' => $aisle, ':aisle' => $aisle,
':rack' => $rack, ':bin' => $bin,
]); ]);
if (!$sth->fetchColumn()) throw new Exception('Location not found.'); if (!$sth->fetchColumn()) throw new Exception('Location not found.');
} }
@@ -343,7 +343,7 @@ class BarcodeManager {
*/ */
public function listLocationLabels(): array { public function listLocationLabels(): array {
$sth = $this->pdo->prepare( $sth = $this->pdo->prepare(
"SELECT barcode, warehouse_id, zone, aisle, rack, status, print_count, last_printed_dt "SELECT barcode, warehouse_id, zone, aisle, bin, status, print_count, last_printed_dt
FROM md_barcode FROM md_barcode
WHERE company_id = :company_id WHERE company_id = :company_id
AND barcode_type = 'loc'" AND barcode_type = 'loc'"
@@ -355,30 +355,30 @@ class BarcodeManager {
/** /**
* Create a new location barcode label (or re-activate a previously disabled one). * Create a new location barcode label (or re-activate a previously disabled one).
* *
* Validates that the rack exists in md_rack before inserting. * Validates that the bin exists in md_bin before inserting.
* *
* @param string $barcode Full barcode string (e.g. "LOC|1|A|1|R1"). * @param string $barcode Full barcode string (e.g. "LOC|1|A|1|R1").
* @param int $warehouse_id Warehouse ID. * @param int $warehouse_id Warehouse ID.
* @param string $zone Zone code. * @param string $zone Zone code.
* @param string $aisle Aisle code. * @param string $aisle Aisle code.
* @param string $rack Rack code. * @param string $bin Bin code.
* @return array Keys: barcode, warehouse_id, zone, aisle, rack. * @return array Keys: barcode, warehouse_id, zone, aisle, bin.
* @throws Exception If the location is not found in md_rack. * @throws Exception If the location is not found in md_bin.
*/ */
public function createLocationLabel(string $barcode, int $warehouse_id, string $zone, string $aisle, string $rack): array { public function createLocationLabel(string $barcode, int $warehouse_id, string $zone, string $aisle, string $bin): array {
$this->validateLocation($warehouse_id, $zone, $aisle, $rack); $this->validateLocation($warehouse_id, $zone, $aisle, $bin);
$this->pdo->prepare( $this->pdo->prepare(
"INSERT INTO md_barcode "INSERT INTO md_barcode
(company_id, barcode_type, barcode, warehouse_id, zone, aisle, rack) (company_id, barcode_type, barcode, warehouse_id, zone, aisle, bin)
VALUES VALUES
(:company_id, 'loc', :barcode, :warehouse_id, :zone, :aisle, :rack) (:company_id, 'loc', :barcode, :warehouse_id, :zone, :aisle, :bin)
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
barcode_type = VALUES(barcode_type), barcode_type = VALUES(barcode_type),
warehouse_id = VALUES(warehouse_id), warehouse_id = VALUES(warehouse_id),
zone = VALUES(zone), zone = VALUES(zone),
aisle = VALUES(aisle), aisle = VALUES(aisle),
rack = VALUES(rack), bin = VALUES(bin),
status = 1" status = 1"
)->execute([ )->execute([
':company_id' => $this->company_id, ':company_id' => $this->company_id,
@@ -386,7 +386,7 @@ class BarcodeManager {
':warehouse_id' => $warehouse_id, ':warehouse_id' => $warehouse_id,
':zone' => $zone, ':zone' => $zone,
':aisle' => $aisle, ':aisle' => $aisle,
':rack' => $rack, ':bin' => $bin,
]); ]);
return [ return [
@@ -394,7 +394,7 @@ class BarcodeManager {
'warehouse_id' => $warehouse_id, 'warehouse_id' => $warehouse_id,
'zone' => $zone, 'zone' => $zone,
'aisle' => $aisle, 'aisle' => $aisle,
'rack' => $rack, 'bin' => $bin,
]; ];
} }
@@ -465,7 +465,7 @@ class BarcodeManager {
* resolve it from the caller via CompanySettingManager::get('advanced_location'). * resolve it from the caller via CompanySettingManager::get('advanced_location').
* *
* @param string $barcode Full barcode string to resolve. * @param string $barcode Full barcode string to resolve.
* @param bool $advancedLocation True = zone/aisle/rack parts; false = rack only. * @param bool $advancedLocation True = zone/aisle/bin parts; false = bin only.
* @return array * @return array
* @throws Exception On parse or validation failure. * @throws Exception On parse or validation failure.
*/ */
@@ -515,13 +515,13 @@ class BarcodeManager {
if ($advancedLocation) { if ($advancedLocation) {
$zone = trim($parts[2]); $zone = trim($parts[2]);
$aisle = trim($parts[3]); $aisle = trim($parts[3]);
$rack = trim($parts[4]); $bin = trim($parts[4]);
} else { } else {
$rack = trim($parts[2]); $bin = trim($parts[2]);
$zone = $rack; $zone = $bin;
$aisle = $rack; $aisle = $bin;
} }
if (!$warehouse_id || $zone === '' || $aisle === '' || $rack === '') { if (!$warehouse_id || $zone === '' || $aisle === '' || $bin === '') {
throw new Exception('Location label is incomplete.'); throw new Exception('Location label is incomplete.');
} }
@@ -531,19 +531,19 @@ class BarcodeManager {
(int)$label['warehouse_id'] !== $warehouse_id || (int)$label['warehouse_id'] !== $warehouse_id ||
$label['zone'] !== $zone || $label['zone'] !== $zone ||
$label['aisle'] !== $aisle || $label['aisle'] !== $aisle ||
$label['rack'] !== $rack $label['bin'] !== $bin
) { ) {
throw new Exception('Location barcode label does not match its registry.'); throw new Exception('Location barcode label does not match its registry.');
} }
$this->validateLocation($warehouse_id, $zone, $aisle, $rack); $this->validateLocation($warehouse_id, $zone, $aisle, $bin);
return [ return [
'type' => 'loc', 'type' => 'loc',
'warehouse_id' => $warehouse_id, 'warehouse_id' => $warehouse_id,
'zone' => $zone, 'zone' => $zone,
'aisle' => $aisle, 'aisle' => $aisle,
'rack' => $rack, 'bin' => $bin,
]; ];
} }
@@ -8,8 +8,8 @@
* default_stock_status int 0 = draft, 1 = auto-approve on save * default_stock_status int 0 = draft, 1 = auto-approve on save
* auto_invoice_and_credit_note int 0 = manual, 1 = auto-generate * auto_invoice_and_credit_note int 0 = manual, 1 = auto-generate
* auto_complete_on_ship int 0 = manual, 1 = auto-complete on shipped * auto_complete_on_ship int 0 = manual, 1 = auto-complete on shipped
* advanced_location int 0 = simple (single location), 1 = zone/aisle/rack * advanced_location int 0 = simple (single location), 1 = zone/aisle/bin
* location_label_rack string label for rack / single location field * location_label_bin string label for bin / single location field
* location_label_zone string label for zone (advanced only) * location_label_zone string label for zone (advanced only)
* location_label_aisle string label for aisle (advanced only) * location_label_aisle string label for aisle (advanced only)
* *
@@ -27,7 +27,7 @@ class CompanySettingManager
'auto_invoice_and_credit_note' => 0, // manual by default 'auto_invoice_and_credit_note' => 0, // manual by default
'auto_complete_on_ship' => 1, // auto-complete by default 'auto_complete_on_ship' => 1, // auto-complete by default
'advanced_location' => 0, // simple mode by default 'advanced_location' => 0, // simple mode by default
'location_label_rack' => 'Location', // single label default 'location_label_bin' => 'Location', // single label default
'location_label_zone' => 'Zone', // advanced level 1 default 'location_label_zone' => 'Zone', // advanced level 1 default
'location_label_aisle' => 'Aisle', // advanced level 2 default 'location_label_aisle' => 'Aisle', // advanced level 2 default
'posting_open_from' => '', // YYYY-MM-DD; empty = no lower bound 'posting_open_from' => '', // YYYY-MM-DD; empty = no lower bound
+43 -43
View File
@@ -17,19 +17,19 @@ require_once __DIR__ . '/../classes_ac/PostingWindowGuard.php';
* - Order items are stored as a JSON array in td_order.items. * - Order items are stored as a JSON array in td_order.items.
* No separate child table exists. SKU-level reporting is served by * No separate child table exists. SKU-level reporting is served by
* td_stock_* rows (source='order') rather than querying items JSON. * td_stock_* rows (source='order') rather than querying items JSON.
* - confirmOrder() picks racks via FIFO — oldest approved stock-in row * - confirmOrder() picks bins via FIFO — oldest approved stock-in row
* still rack-occupied, ordered by td_stock_<warehouse_id>.date ASC. * still bin-occupied, ordered by td_stock_<warehouse_id>.date ASC.
* - confirmOrder() creates stock-out rows with status=0 (draft). * - confirmOrder() creates stock-out rows with status=0 (draft).
* Warehouse staff approve them via the existing approve_stock.php * Warehouse staff approve them via the existing approve_stock.php
* engine, which handles rack release and balance adjustment. * engine, which handles bin release and balance adjustment.
* - cancelOrder() sets td_order.status = -1 and soft-deletes ALL linked * - cancelOrder() sets td_order.status = -1 and soft-deletes ALL linked
* stock-out rows (status → -1) across all td_stock_* tables. If linked * stock-out rows (status → -1) across all td_stock_* tables. If linked
* stock-out rows were already approved, their rack and balance effects are * stock-out rows were already approved, their bin and balance effects are
* reversed before the rows are cancelled. * reversed before the rows are cancelled.
* Cancellation is blocked if any active invoice (status != 4 / void) or * Cancellation is blocked if any active invoice (status != 4 / void) or
* active return (status != -1 / cancelled) is linked to the order. * active return (status != -1 / cancelled) is linked to the order.
* - Cancel logic stays in the order domain, but uses WarehouseManager for * - Cancel logic stays in the order domain, but uses WarehouseManager for
* the same rack and balance reversal rules as manual stock-out deletion. * the same bin and balance reversal rules as manual stock-out deletion.
* *
* Note: Write methods do NOT manage their own DB transactions. * Note: Write methods do NOT manage their own DB transactions.
* Callers must wrap multi-step operations inside dbTransaction(). * Callers must wrap multi-step operations inside dbTransaction().
@@ -78,15 +78,15 @@ class OrderManager {
} }
/** /**
* FIFO rack pick — find the oldest approved stock-in row for a SKU * FIFO bin pick — find the oldest approved stock-in row for a SKU
* in a given warehouse that is still rack-occupied. * in a given warehouse that is still bin-occupied.
* *
* "Oldest" = smallest date value among status=1 stock-in rows * "Oldest" = smallest date value among status=1 stock-in rows
* that have an md_rack row pointing back to them (td_stock_id IS set). * that have an md_bin row pointing back to them (td_stock_id IS set).
* *
* @param int $warehouse_id * @param int $warehouse_id
* @param string $product_sku * @param string $product_sku
* @return array|null Full td_stock row + zone/aisle/rack from md_rack, * @return array|null Full td_stock row + zone/aisle/bin from md_bin,
* or null if no available stock in this warehouse. * or null if no available stock in this warehouse.
*/ */
private function pickFifoRack(int $warehouse_id, string $product_sku): ?array private function pickFifoRack(int $warehouse_id, string $product_sku): ?array
@@ -94,7 +94,7 @@ class OrderManager {
$table = $this->stockTableNameFromWarehouseId($warehouse_id); $table = $this->stockTableNameFromWarehouseId($warehouse_id);
$sth = $this->pdo->prepare( $sth = $this->pdo->prepare(
"SELECT s.*, r.zone, r.aisle, r.rack, l.expiry_date, "SELECT s.*, r.zone, r.aisle, r.bin, l.expiry_date,
(s.`in` - COALESCE(( (s.`in` - COALESCE((
SELECT SUM(o.`out`) SELECT SUM(o.`out`)
FROM `{$table}` o FROM `{$table}` o
@@ -104,7 +104,7 @@ class OrderManager {
AND o.status != -1 AND o.status != -1
), 0)) AS available_qty ), 0)) AS available_qty
FROM `{$table}` s FROM `{$table}` s
INNER JOIN md_rack r INNER JOIN md_bin r
ON r.company_id = s.company_id ON r.company_id = s.company_id
AND r.warehouse = :warehouse_id AND r.warehouse = :warehouse_id
AND r.td_stock_id = s.id AND r.td_stock_id = s.id
@@ -358,7 +358,7 @@ class OrderManager {
if ($stock_out_warehouse_id > 0 && $stock_out_id > 0) { if ($stock_out_warehouse_id > 0 && $stock_out_id > 0) {
$table = $this->stockTableNameFromWarehouseId($stock_out_warehouse_id); $table = $this->stockTableNameFromWarehouseId($stock_out_warehouse_id);
$sth = $this->pdo->prepare( $sth = $this->pdo->prepare(
"SELECT id, status, zone, aisle, rack, lot_number, serial_number "SELECT id, status, zone, aisle, bin, lot_number, serial_number
FROM `{$table}` FROM `{$table}`
WHERE company_id = :company_id WHERE company_id = :company_id
AND id = :id AND id = :id
@@ -374,7 +374,7 @@ class OrderManager {
if ($stock_out) { if ($stock_out) {
$item['zone'] = $stock_out['zone'] ?? ($item['zone'] ?? ''); $item['zone'] = $stock_out['zone'] ?? ($item['zone'] ?? '');
$item['aisle'] = $stock_out['aisle'] ?? ($item['aisle'] ?? ''); $item['aisle'] = $stock_out['aisle'] ?? ($item['aisle'] ?? '');
$item['rack'] = $stock_out['rack'] ?? ($item['rack'] ?? ''); $item['bin'] = $stock_out['bin'] ?? ($item['bin'] ?? '');
$item['lot_number'] = $stock_out['lot_number'] ?? ($item['lot_number'] ?? ''); $item['lot_number'] = $stock_out['lot_number'] ?? ($item['lot_number'] ?? '');
$item['serial_number'] = $stock_out['serial_number'] ?? ($item['serial_number'] ?? ''); $item['serial_number'] = $stock_out['serial_number'] ?? ($item['serial_number'] ?? '');
$item['stock_out_status'] = (int)$stock_out['status']; $item['stock_out_status'] = (int)$stock_out['status'];
@@ -599,10 +599,10 @@ class OrderManager {
/** /**
* Confirm a draft order — create stock-out rows (status=0) for each item * Confirm a draft order — create stock-out rows (status=0) for each item
* using FIFO rack selection, then advance the order to status=1. * using FIFO bin selection, then advance the order to status=1.
* *
* Flow per item: * Flow per item:
* 1. pickFifoRack() — find oldest rack-occupied stock-in row for the SKU. * 1. pickFifoRack() — find oldest bin-occupied stock-in row for the SKU.
* 2. INSERT into td_stock_<warehouse_id> with type='out', status=0, * 2. INSERT into td_stock_<warehouse_id> with type='out', status=0,
* source='order', source_id=order_id. * source='order', source_id=order_id.
* 3. Write back stock_out_id into the item object in td_order.items. * 3. Write back stock_out_id into the item object in td_order.items.
@@ -611,7 +611,7 @@ class OrderManager {
* 4. UPDATE td_order.items with stock_out_id values written back. * 4. UPDATE td_order.items with stock_out_id values written back.
* 5. UPDATE td_order.status = 1 (confirmed). * 5. UPDATE td_order.status = 1 (confirmed).
* *
* Rack release and balance adjustment are intentionally deferred — * Bin release and balance adjustment are intentionally deferred —
* they happen when warehouse staff approve the stock-out rows via * they happen when warehouse staff approve the stock-out rows via
* the existing approve_stock.php engine (StockManager::approveStock). * the existing approve_stock.php engine (StockManager::approveStock).
* *
@@ -621,7 +621,7 @@ class OrderManager {
* @param string $uuid UUID prefix — each stock-out row gets uuid_{$i}. * @param string $uuid UUID prefix — each stock-out row gets uuid_{$i}.
* @param array $logging Audit entry appended to td_order.log. * @param array $logging Audit entry appended to td_order.log.
* @throws Exception If order not found, not in draft status, or any item * @throws Exception If order not found, not in draft status, or any item
* has no available FIFO rack in its assigned warehouse. * has no available FIFO bin in its assigned warehouse.
*/ */
public function confirmOrder(int $order_id, string $uuid, array $logging, bool $auto_approve = false): void public function confirmOrder(int $order_id, string $uuid, array $logging, bool $auto_approve = false): void
{ {
@@ -668,22 +668,22 @@ class OrderManager {
continue; continue;
} }
$rack_stock = $this->pickFifoRack($warehouse_id, $product_sku); $bin_stock = $this->pickFifoRack($warehouse_id, $product_sku);
if (!$rack_stock) { if (!$bin_stock) {
$preflight_errors[] = "No available stock for \"{$name}\" in the selected warehouse."; $preflight_errors[] = "No available stock for \"{$name}\" in the selected warehouse.";
continue; continue;
} }
$available_qty = (float)($rack_stock['available_qty'] ?? $rack_stock['in'] ?? 0); $available_qty = (float)($bin_stock['available_qty'] ?? $bin_stock['in'] ?? 0);
if ($quantity - $available_qty > 0.000001) { if ($quantity - $available_qty > 0.000001) {
$preflight_errors[] = $preflight_errors[] =
"Insufficient stock for \"{$name}\": " . "Insufficient stock for \"{$name}\": " .
"{$available_qty} available, {$quantity} requested."; "{$available_qty} available, {$quantity} requested.";
} }
$expiry = $rack_stock['expiry_date'] ?? null; $expiry = $bin_stock['expiry_date'] ?? null;
if ($expiry && strtotime($expiry) < strtotime(date('Y-m-d'))) { if ($expiry && strtotime($expiry) < strtotime(date('Y-m-d'))) {
$lot = $rack_stock['lot_number'] ?? ''; $lot = $bin_stock['lot_number'] ?? '';
$preflight_errors[] = $preflight_errors[] =
"Lot {$lot} for \"{$name}\" expired on {$expiry} and cannot be picked."; "Lot {$lot} for \"{$name}\" expired on {$expiry} and cannot be picked.";
} }
@@ -699,8 +699,8 @@ class OrderManager {
$product_sku = $item['product_sku'] ?? ''; $product_sku = $item['product_sku'] ?? '';
$quantity = (float)($item['quantity'] ?? 0); $quantity = (float)($item['quantity'] ?? 0);
$rack_stock = $this->pickFifoRack($warehouse_id, $product_sku); $bin_stock = $this->pickFifoRack($warehouse_id, $product_sku);
$available_qty = (float)($rack_stock['available_qty'] ?? $rack_stock['in'] ?? 0); $available_qty = (float)($bin_stock['available_qty'] ?? $bin_stock['in'] ?? 0);
$table = $this->stockTableNameFromWarehouseId($warehouse_id); $table = $this->stockTableNameFromWarehouseId($warehouse_id);
$item_uuid = $uuid . '_' . $i; $item_uuid = $uuid . '_' . $i;
@@ -709,13 +709,13 @@ class OrderManager {
$this->pdo->prepare( $this->pdo->prepare(
"INSERT INTO `{$table}` "INSERT INTO `{$table}`
(uuid, company_id, `date`, product_sku, `out`, (uuid, company_id, `date`, product_sku, `out`,
zone, aisle, rack, zone, aisle, bin,
contact_id, `description`, `log`, `type`, contact_id, `description`, `log`, `type`,
ref_id, lot_number, serial_number, ref_id, lot_number, serial_number,
source, source_id, price, status) source, source_id, price, status)
VALUES VALUES
(:uuid, :company_id, :date, :product_sku, :quantity, (:uuid, :company_id, :date, :product_sku, :quantity,
:zone, :aisle, :rack, :zone, :aisle, :bin,
:contact_id, :description, :log, 'out', :contact_id, :description, :log, 'out',
:ref_id, :lot_number, :serial_number, :ref_id, :lot_number, :serial_number,
'order', :source_id, :price, 0)" 'order', :source_id, :price, 0)"
@@ -725,28 +725,28 @@ class OrderManager {
':date' => date('Y-m-d H:i:s'), ':date' => date('Y-m-d H:i:s'),
':product_sku' => $product_sku, ':product_sku' => $product_sku,
':quantity' => $quantity, ':quantity' => $quantity,
':zone' => $rack_stock['zone'], ':zone' => $bin_stock['zone'],
':aisle' => $rack_stock['aisle'], ':aisle' => $bin_stock['aisle'],
':rack' => $rack_stock['rack'], ':bin' => $bin_stock['bin'],
':contact_id' => (int)$order['contact_id'], ':contact_id' => (int)$order['contact_id'],
':description' => $order['order_number'], ':description' => $order['order_number'],
':log' => json_encode($item_log), ':log' => json_encode($item_log),
':ref_id' => (int)$rack_stock['id'], ':ref_id' => (int)$bin_stock['id'],
':lot_number' => $rack_stock['lot_number'] ?? '', ':lot_number' => $bin_stock['lot_number'] ?? '',
':serial_number' => $rack_stock['serial_number'] ?? '', ':serial_number' => $bin_stock['serial_number'] ?? '',
':source_id' => $order_id, ':source_id' => $order_id,
':price' => (float)($item['unit_price'] ?? 0), ':price' => (float)($item['unit_price'] ?? 0),
]); ]);
// Write rack context + stock_out_id back into the item object // Write bin context + stock_out_id back into the item object
$item['zone'] = $rack_stock['zone']; $item['zone'] = $bin_stock['zone'];
$item['aisle'] = $rack_stock['aisle']; $item['aisle'] = $bin_stock['aisle'];
$item['rack'] = $rack_stock['rack']; $item['bin'] = $bin_stock['bin'];
$item['lot_number'] = $rack_stock['lot_number'] ?? ''; $item['lot_number'] = $bin_stock['lot_number'] ?? '';
$item['serial_number'] = $rack_stock['serial_number'] ?? ''; $item['serial_number'] = $bin_stock['serial_number'] ?? '';
$item['stock_out_id'] = (int)$this->pdo->lastInsertId(); $item['stock_out_id'] = (int)$this->pdo->lastInsertId();
// Auto-approve: immediately release rack + adjust balance // Auto-approve: immediately release bin + adjust balance
if ($auto_approve) { if ($auto_approve) {
$stock = new StockManager($this->pdo, $this->company_id); $stock = new StockManager($this->pdo, $this->company_id);
$whMgmt = new WarehouseManager($this->pdo, $this->company_id); $whMgmt = new WarehouseManager($this->pdo, $this->company_id);
@@ -802,7 +802,7 @@ class OrderManager {
* - td_return where order_id = $order_id AND status != -1 * - td_return where order_id = $order_id AND status != -1
* *
* Stock-out rows are children of the order — they follow the parent. * Stock-out rows are children of the order — they follow the parent.
* On cancel, approved rows first re-occupy the original source rack and * On cancel, approved rows first re-occupy the original source bin and
* reverse the stock-out balance. Then ALL stock-out rows linked to this * reverse the stock-out balance. Then ALL stock-out rows linked to this
* order (any status except already -1) are soft-deleted (status → -1) * order (any status except already -1) are soft-deleted (status → -1)
* across all td_stock_* tables. * across all td_stock_* tables.
@@ -882,7 +882,7 @@ class OrderManager {
$warehouse_id = (int)$matches[1]; $warehouse_id = (int)$matches[1];
$row_sth = $this->pdo->prepare( $row_sth = $this->pdo->prepare(
"SELECT id, product_sku, `out`, zone, aisle, rack, ref_id, status, `date` "SELECT id, product_sku, `out`, zone, aisle, bin, ref_id, status, `date`
FROM `{$table}` FROM `{$table}`
WHERE company_id = :company_id WHERE company_id = :company_id
AND source = 'order' AND source = 'order'
@@ -903,11 +903,11 @@ class OrderManager {
$whMgmt->assertStockMovementWindow($row['date'] ?? null, 'Order stock cancellation'); $whMgmt->assertStockMovementWindow($row['date'] ?? null, 'Order stock cancellation');
$whMgmt->occupyRack( $whMgmt->occupyBin(
$warehouse_id, $warehouse_id,
(string)$row['zone'], (string)$row['zone'],
(string)$row['aisle'], (string)$row['aisle'],
(string)$row['rack'], (string)$row['bin'],
(string)$row['product_sku'], (string)$row['product_sku'],
(int)$row['ref_id'] (int)$row['ref_id']
); );
+7 -7
View File
@@ -534,18 +534,18 @@ class ProductManager {
// ───────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────
/** /**
* Return all rack slots across all warehouses with occupancy status, * Return all bin slots across all warehouses with occupancy status,
* warehouse name, and product name. * warehouse name, and product name.
* *
* Used by the rack occupancy dashboard to visualise which racks are * Used by the bin occupancy dashboard to visualise which bins are
* empty vs. occupied, and which product is in each slot. * empty vs. occupied, and which product is in each slot.
* Results are ordered by warehouse → zone → aisle → rack, with * Results are ordered by warehouse → zone → aisle → bin, with
* numeric-first sorting via CAST so e.g. "2" sorts before "10". * numeric-first sorting via CAST so e.g. "2" sorts before "10".
* *
* Security fix: was previously using $this->companyId (undefined property), * Security fix: was previously using $this->companyId (undefined property),
* corrected to $this->company_id. * corrected to $this->company_id.
* *
* @return array All md_rack rows joined to warehouse and product, with 'status' field. * @return array All md_bin rows joined to warehouse and product, with 'status' field.
*/ */
public function getRackOccupancy(): array public function getRackOccupancy(): array
{ {
@@ -554,14 +554,14 @@ class ProductManager {
r.id, r.id,
r.zone, r.zone,
r.aisle, r.aisle,
r.rack, r.bin,
r.product_sku, r.product_sku,
r.td_stock_id, r.td_stock_id,
mw.warehouse_name, mw.warehouse_name,
mw.id AS warehouse_id, mw.id AS warehouse_id,
p.product_name, p.product_name,
CASE WHEN r.product_sku IS NOT NULL THEN 'occupied' ELSE 'empty' END AS status CASE WHEN r.product_sku IS NOT NULL THEN 'occupied' ELSE 'empty' END AS status
FROM md_rack r FROM md_bin r
INNER JOIN md_warehouse mw INNER JOIN md_warehouse mw
ON mw.company_id = r.company_id ON mw.company_id = r.company_id
AND mw.id = r.warehouse AND mw.id = r.warehouse
@@ -571,7 +571,7 @@ class ProductManager {
WHERE r.company_id = :company_id WHERE r.company_id = :company_id
ORDER BY mw.warehouse_name, r.zone, ORDER BY mw.warehouse_name, r.zone,
CAST(r.aisle AS UNSIGNED), r.aisle, CAST(r.aisle AS UNSIGNED), r.aisle,
CAST(r.rack AS UNSIGNED), r.rack" CAST(r.bin AS UNSIGNED), r.bin"
); );
$sth->execute([':company_id' => $this->company_id]); $sth->execute([':company_id' => $this->company_id]);
return $sth->fetchAll(PDO::FETCH_ASSOC); return $sth->fetchAll(PDO::FETCH_ASSOC);
@@ -25,7 +25,7 @@ require_once __DIR__ . '/../classes_ac/PostingWindowGuard.php';
* Receipt progress (pending/partial/received) is derived via deriveReceiptStatus(), * Receipt progress (pending/partial/received) is derived via deriveReceiptStatus(),
* same pattern as fulfillment_status on SO. Never stored in td_purchase_order. * same pattern as fulfillment_status on SO. Never stored in td_purchase_order.
* - cancelPo() is blocked if any linked stock-in rows have been approved (status=1), * - cancelPo() is blocked if any linked stock-in rows have been approved (status=1),
* because those have already modified rack and balance. * because those have already modified bin and balance.
* - Write methods do NOT manage their own DB transactions. * - Write methods do NOT manage their own DB transactions.
* Callers must wrap multi-step operations inside dbTransaction(). * Callers must wrap multi-step operations inside dbTransaction().
* *
@@ -274,7 +274,7 @@ class PurchaseOrderManager {
'warehouse_id' => (int)($item['receive_wh'] ?? 0), 'warehouse_id' => (int)($item['receive_wh'] ?? 0),
'zone' => $item['receive_zone'] ?? '', 'zone' => $item['receive_zone'] ?? '',
'aisle' => $item['receive_aisle'] ?? '', 'aisle' => $item['receive_aisle'] ?? '',
'rack' => $item['receive_rack'] ?? '', 'bin' => $item['receive_bin'] ?? '',
]); ]);
} }
} }
@@ -492,7 +492,7 @@ class PurchaseOrderManager {
* *
* @param int $po_id td_purchase_order.id * @param int $po_id td_purchase_order.id
* @param array $receive_items Each item: { item_id, product_sku, warehouse_id, * @param array $receive_items Each item: { item_id, product_sku, warehouse_id,
* quantity, zone, aisle, rack, lot_number, * quantity, zone, aisle, bin, lot_number,
* expiry_date, serial_number, contact_id } * expiry_date, serial_number, contact_id }
* @param string $uuid UUID prefix for stock-in rows. * @param string $uuid UUID prefix for stock-in rows.
* @param array $logging Audit entry. * @param array $logging Audit entry.
@@ -573,16 +573,16 @@ class PurchaseOrderManager {
$item_uuid = $uuid . '_' . $j; $item_uuid = $uuid . '_' . $j;
$item_log = array_merge($logging, ['action' => 'receive_item']); $item_log = array_merge($logging, ['action' => 'receive_item']);
$rack = $recv['rack'] ?? ''; $bin = $recv['bin'] ?? '';
$zone = $recv['zone'] ?? ''; $zone = $recv['zone'] ?? '';
$aisle = $recv['aisle'] ?? ''; $aisle = $recv['aisle'] ?? '';
// Simple location mode: zone and aisle must mirror the rack value // Simple location mode: zone and aisle must mirror the bin value
// (same convention as manage_stock_in.php). // (same convention as manage_stock_in.php).
// occupyRack() looks up md_rack WHERE zone=:zone AND aisle=:aisle AND rack=:rack, // occupyBin() looks up md_bin WHERE zone=:zone AND aisle=:aisle AND bin=:bin,
// so all three must match — a blank zone/aisle produces "Rack --b does not exist". // so all three must match — a blank zone/aisle produces "Bin --b does not exist".
if ($zone === '' && $rack !== '') $zone = $rack; if ($zone === '' && $bin !== '') $zone = $bin;
if ($aisle === '' && $rack !== '') $aisle = $rack; if ($aisle === '' && $bin !== '') $aisle = $bin;
// Resolve unit_price: prefer the receive-time override, fall back to PO item price // Resolve unit_price: prefer the receive-time override, fall back to PO item price
$po_item_price = 0; $po_item_price = 0;
@@ -599,7 +599,7 @@ class PurchaseOrderManager {
'price' => $unit_price, 'price' => $unit_price,
'zone' => $zone, 'zone' => $zone,
'aisle' => $aisle, 'aisle' => $aisle,
'rack' => $rack, 'bin' => $bin,
'lot_number' => $recv['lot_number'] ?? '', 'lot_number' => $recv['lot_number'] ?? '',
'expiry_date' => $recv['expiry_date'] ?? '', 'expiry_date' => $recv['expiry_date'] ?? '',
'serial_number' => $recv['serial_number'] ?? '', 'serial_number' => $recv['serial_number'] ?? '',
@@ -626,7 +626,7 @@ class PurchaseOrderManager {
]); ]);
// approveStock() handles balance updates. saveStockIn() has // approveStock() handles balance updates. saveStockIn() has
// already reserved the rack so draft receipts cannot be reused. // already reserved the bin so draft receipts cannot be reused.
if ($auto_approve) { if ($auto_approve) {
$stock->approveStock($new_stock_in_id, $warehouse_id, 'in', $whMgmt); $stock->approveStock($new_stock_in_id, $warehouse_id, 'in', $whMgmt);
} }
@@ -708,7 +708,7 @@ class PurchaseOrderManager {
* Cancel a PO. * Cancel a PO.
* *
* Blocked if any linked stock-in rows have already been approved (status=1) * Blocked if any linked stock-in rows have already been approved (status=1)
* because those have modified rack occupancy and balance. * because those have modified bin occupancy and balance.
* *
* For draft stock-in rows (status=0), they are soft-deleted (status=-1). * For draft stock-in rows (status=0), they are soft-deleted (status=-1).
* *
@@ -756,7 +756,7 @@ class PurchaseOrderManager {
$whMgmt = new WarehouseManager($this->pdo, $this->company_id); $whMgmt = new WarehouseManager($this->pdo, $this->company_id);
// Soft-delete draft stock-in rows and release their rack reservations. // Soft-delete draft stock-in rows and release their bin reservations.
foreach ($tables as $table) { foreach ($tables as $table) {
if (!preg_match('/^td_stock_(\d+)$/', $table, $m)) { if (!preg_match('/^td_stock_(\d+)$/', $table, $m)) {
continue; continue;
@@ -764,7 +764,7 @@ class PurchaseOrderManager {
$warehouse_id = (int)$m[1]; $warehouse_id = (int)$m[1];
$sth = $this->pdo->prepare( $sth = $this->pdo->prepare(
"SELECT id, zone, aisle, rack "SELECT id, zone, aisle, bin
FROM `{$table}` FROM `{$table}`
WHERE company_id = :company_id WHERE company_id = :company_id
AND source = 'po' AND source = 'po'
@@ -783,11 +783,11 @@ class PurchaseOrderManager {
':company_id' => $this->company_id, ':company_id' => $this->company_id,
]); ]);
$whMgmt->releaseRack( $whMgmt->releaseBin(
$warehouse_id, $warehouse_id,
$row['zone'], $row['zone'],
$row['aisle'], $row['aisle'],
$row['rack'] $row['bin']
); );
} }
} }
@@ -894,21 +894,21 @@ class PurchaseOrderManager {
} }
} }
// Release racks from draft stock-in rows and negate them // Release bins from draft stock-in rows and negate them
$whMgmt = new WarehouseManager($this->pdo, $this->company_id); $whMgmt = new WarehouseManager($this->pdo, $this->company_id);
foreach ($tables as $table) { foreach ($tables as $table) {
if (!preg_match('/^td_stock_(\d+)$/', (string)$table, $matches)) continue; if (!preg_match('/^td_stock_(\d+)$/', (string)$table, $matches)) continue;
$warehouse_id = (int)$matches[1]; $warehouse_id = (int)$matches[1];
$row_sth = $this->pdo->prepare( $row_sth = $this->pdo->prepare(
"SELECT id, zone, aisle, rack FROM `{$table}` "SELECT id, zone, aisle, bin FROM `{$table}`
WHERE company_id = :cid AND source = 'po' AND source_id = :id AND status = 0" WHERE company_id = :cid AND source = 'po' AND source_id = :id AND status = 0"
); );
$row_sth->execute([':cid' => $this->company_id, ':id' => $po_id]); $row_sth->execute([':cid' => $this->company_id, ':id' => $po_id]);
$draft_rows = $row_sth->fetchAll(PDO::FETCH_ASSOC); $draft_rows = $row_sth->fetchAll(PDO::FETCH_ASSOC);
foreach ($draft_rows as $row) { foreach ($draft_rows as $row) {
$whMgmt->releaseRack($warehouse_id, $row['zone'], $row['aisle'], $row['rack']); $whMgmt->releaseBin($warehouse_id, $row['zone'], $row['aisle'], $row['bin']);
} }
$this->pdo->prepare( $this->pdo->prepare(
+35 -35
View File
@@ -12,7 +12,7 @@
* Dashboard reports → stats, movement charts, most moved, recent activity * Dashboard reports → stats, movement charts, most moved, recent activity
* Warehouse detail → capacity, space used, balance, movement, trend, activity * Warehouse detail → capacity, space used, balance, movement, trend, activity
* Expiry reports → expired / near-expiry stock * Expiry reports → expired / near-expiry stock
* Lot / Rack reports → lot stock log, product lots, rack log, rack occupancy * Lot / Bin reports → lot stock log, product lots, bin log, bin occupancy
* Balance summary → getWarehouseBalanceSummary * Balance summary → getWarehouseBalanceSummary
* *
* Security: All SQL uses PDO prepared statements with bound parameters. * Security: All SQL uses PDO prepared statements with bound parameters.
@@ -159,25 +159,25 @@ class ReportManager
} }
/** /**
* Total rack capacity (all racks across all warehouses) for this company. * Total bin capacity (all bins across all warehouses) for this company.
* Used by the reports_stats dashboard tile. * Used by the reports_stats dashboard tile.
*/ */
public function getTotalCapacity(): int public function getTotalCapacity(): int
{ {
$sql = "SELECT COUNT(*) $sql = "SELECT COUNT(*)
FROM md_rack FROM md_bin
WHERE company_id = :company_id"; WHERE company_id = :company_id";
return (int) $this->fetchScalar($sql); return (int) $this->fetchScalar($sql);
} }
/** /**
* Number of occupied racks (product_sku IS NOT NULL) for this company. * Number of occupied bins (product_sku IS NOT NULL) for this company.
* Used by the reports_stats dashboard tile. * Used by the reports_stats dashboard tile.
*/ */
public function getSpaceUsed(): int public function getSpaceUsed(): int
{ {
$sql = "SELECT COUNT(*) $sql = "SELECT COUNT(*)
FROM md_rack FROM md_bin
WHERE company_id = :company_id WHERE company_id = :company_id
AND product_sku IS NOT NULL"; AND product_sku IS NOT NULL";
return (int) $this->fetchScalar($sql); return (int) $this->fetchScalar($sql);
@@ -710,15 +710,15 @@ class ReportManager
// ───────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────
/** /**
* Total rack capacity for a specific warehouse (all racks regardless of occupancy). * Total bin capacity for a specific warehouse (all bins regardless of occupancy).
* *
* @param int $warehouse_id The warehouse to query. * @param int $warehouse_id The warehouse to query.
* @return int Total rack count. * @return int Total bin count.
*/ */
public function getWarehouseCapacity(int $warehouse_id): int public function getWarehouseCapacity(int $warehouse_id): int
{ {
$sth = $this->pdo->prepare( $sth = $this->pdo->prepare(
"SELECT COUNT(*) FROM md_rack "SELECT COUNT(*) FROM md_bin
WHERE company_id = :company_id AND warehouse = :warehouse_id" WHERE company_id = :company_id AND warehouse = :warehouse_id"
); );
$sth->execute([':company_id' => $this->companyId, ':warehouse_id' => $warehouse_id]); $sth->execute([':company_id' => $this->companyId, ':warehouse_id' => $warehouse_id]);
@@ -726,15 +726,15 @@ class ReportManager
} }
/** /**
* Number of occupied racks (product_sku IS NOT NULL) for a specific warehouse. * Number of occupied bins (product_sku IS NOT NULL) for a specific warehouse.
* *
* @param int $warehouse_id The warehouse to query. * @param int $warehouse_id The warehouse to query.
* @return int Occupied rack count. * @return int Occupied bin count.
*/ */
public function getWarehouseSpaceUsed(int $warehouse_id): int public function getWarehouseSpaceUsed(int $warehouse_id): int
{ {
$sth = $this->pdo->prepare( $sth = $this->pdo->prepare(
"SELECT COUNT(*) FROM md_rack "SELECT COUNT(*) FROM md_bin
WHERE company_id = :company_id AND warehouse = :warehouse_id WHERE company_id = :company_id AND warehouse = :warehouse_id
AND product_sku IS NOT NULL" AND product_sku IS NOT NULL"
); );
@@ -1008,14 +1008,14 @@ class ReportManager
s.lot_number, s.lot_number,
s.zone, s.zone,
s.aisle, s.aisle,
s.rack, s.bin,
ROUND(SUM(s.`in`) - SUM(s.`out`), 2) AS quantity, ROUND(SUM(s.`in`) - SUM(s.`out`), 2) AS quantity,
{$wh['id']} AS warehouse_id {$wh['id']} AS warehouse_id
FROM `{$table}` s FROM `{$table}` s
WHERE s.company_id = {$cid} WHERE s.company_id = {$cid}
AND s.lot_number IS NOT NULL AND s.lot_number IS NOT NULL
AND s.status = 1 AND s.status = 1
GROUP BY s.product_sku, s.lot_number, s.zone, s.aisle, s.rack GROUP BY s.product_sku, s.lot_number, s.zone, s.aisle, s.bin
HAVING quantity > 0"; HAVING quantity > 0";
}, },
$warehouses $warehouses
@@ -1026,7 +1026,7 @@ class ReportManager
stock.lot_number, stock.lot_number,
stock.zone, stock.zone,
stock.aisle, stock.aisle,
stock.rack, stock.bin,
stock.quantity, stock.quantity,
l.expiry_date, l.expiry_date,
DATEDIFF(l.expiry_date, CURDATE()) AS days_remaining, DATEDIFF(l.expiry_date, CURDATE()) AS days_remaining,
@@ -1073,7 +1073,7 @@ class ReportManager
'lot_number' => $row['lot_number'], 'lot_number' => $row['lot_number'],
'zone' => $row['zone'], 'zone' => $row['zone'],
'aisle' => $row['aisle'], 'aisle' => $row['aisle'],
'rack' => $row['rack'], 'bin' => $row['bin'],
'quantity' => (float) $row['quantity'], 'quantity' => (float) $row['quantity'],
'expiry_date' => $row['expiry_date'], 'expiry_date' => $row['expiry_date'],
'days_remaining' => $days, 'days_remaining' => $days,
@@ -1085,7 +1085,7 @@ class ReportManager
} }
// ───────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────
// REPORT BASIS — Lot / Rack reports // REPORT BASIS — Lot / Bin reports
// ───────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────
/** /**
@@ -1097,7 +1097,7 @@ class ReportManager
* *
* @param string $product_sku SKU to filter by. * @param string $product_sku SKU to filter by.
* @param string $lot_number Lot number to filter by. * @param string $lot_number Lot number to filter by.
* @return array Stock rows with date, type, zone/aisle/rack, in/out amounts, warehouse_name. * @return array Stock rows with date, type, zone/aisle/bin, in/out amounts, warehouse_name.
*/ */
public function getLotStockLog(string $product_sku, string $lot_number): array public function getLotStockLog(string $product_sku, string $lot_number): array
{ {
@@ -1123,7 +1123,7 @@ class ReportManager
s.type, s.type,
s.zone, s.zone,
s.aisle, s.aisle,
s.rack, s.bin,
ROUND(COALESCE(s.`in`, 0), 2) AS stock_in, ROUND(COALESCE(s.`in`, 0), 2) AS stock_in,
ROUND(COALESCE(s.`out`, 0), 2) AS stock_out, ROUND(COALESCE(s.`out`, 0), 2) AS stock_out,
s.serial_number, s.serial_number,
@@ -1247,21 +1247,21 @@ class ReportManager
} }
/** /**
* Return the action log for a specific rack, with user name. * Return the action log for a specific bin, with user name.
* *
* Used by the rack log dashboard report page. * Used by the bin log dashboard report page.
* Joins the wms.user table for the acting user's name. * Joins the wms.user table for the acting user's name.
* *
* Security fix: the database name is now fetched once via a bound query * Security fix: the database name is now fetched once via a bound query
* and stored as $db_name (string), then used as a backtick-quoted identifier * and stored as $db_name (string), then used as a backtick-quoted identifier
* rather than being embedded in the JOIN without escaping. * rather than being embedded in the JOIN without escaping.
* *
* @param int $rack_id The md_rack.id to fetch logs for (0 returns empty array). * @param int $bin_id The md_bin.id to fetch logs for (0 returns empty array).
* @return array Log rows with dt, action, product_sku, td_stock_id, login, user_name. * @return array Log rows with dt, action, product_sku, td_stock_id, login, user_name.
*/ */
public function getRackLog(int $rack_id): array public function getBinLog(int $bin_id): array
{ {
if (!$rack_id) return []; if (!$bin_id) return [];
$main_db = $this->mainDb; $main_db = $this->mainDb;
if ($main_db === '' || !ctype_alnum(str_replace('_', '', $main_db))) { if ($main_db === '' || !ctype_alnum(str_replace('_', '', $main_db))) {
@@ -1277,30 +1277,30 @@ class ReportManager
rl.td_stock_id, rl.td_stock_id,
rl.login, rl.login,
u.name AS user_name u.name AS user_name
FROM td_rack_log rl FROM td_bin_log rl
LEFT JOIN `{$main_db}`.user u LEFT JOIN `{$main_db}`.user u
ON u.user_id = rl.user_id ON u.user_id = rl.user_id
WHERE rl.company_id = :company_id WHERE rl.company_id = :company_id
AND rl.md_rack_id = :rack_id AND rl.md_bin_id = :bin_id
ORDER BY rl.dt DESC" ORDER BY rl.dt DESC"
); );
$sth->execute([ $sth->execute([
':company_id' => $this->companyId, ':company_id' => $this->companyId,
':rack_id' => $rack_id, ':bin_id' => $bin_id,
]); ]);
return $sth->fetchAll(PDO::FETCH_ASSOC); return $sth->fetchAll(PDO::FETCH_ASSOC);
} }
/** /**
* Return all rack slots across all warehouses with occupancy status, * Return all bin slots across all warehouses with occupancy status,
* warehouse name, and product name. * warehouse name, and product name.
* *
* Used by the rack occupancy dashboard report page to visualise which * Used by the bin occupancy dashboard report page to visualise which
* racks are empty vs occupied and which product is in each slot. * bins are empty vs occupied and which product is in each slot.
* Results are ordered by warehouse → zone → aisle → rack with * Results are ordered by warehouse → zone → aisle → bin with
* numeric-first sorting via CAST. * numeric-first sorting via CAST.
* *
* @return array All md_rack rows joined to warehouse and product with 'status' field. * @return array All md_bin rows joined to warehouse and product with 'status' field.
*/ */
public function getRackOccupancy(): array public function getRackOccupancy(): array
{ {
@@ -1309,14 +1309,14 @@ class ReportManager
r.id, r.id,
r.zone, r.zone,
r.aisle, r.aisle,
r.rack, r.bin,
r.product_sku, r.product_sku,
r.td_stock_id, r.td_stock_id,
mw.warehouse_name, mw.warehouse_name,
mw.id AS warehouse_id, mw.id AS warehouse_id,
p.product_name, p.product_name,
CASE WHEN r.product_sku IS NOT NULL THEN 'occupied' ELSE 'empty' END AS status CASE WHEN r.product_sku IS NOT NULL THEN 'occupied' ELSE 'empty' END AS status
FROM md_rack r FROM md_bin r
INNER JOIN md_warehouse mw INNER JOIN md_warehouse mw
ON mw.company_id = r.company_id ON mw.company_id = r.company_id
AND mw.id = r.warehouse AND mw.id = r.warehouse
@@ -1326,7 +1326,7 @@ class ReportManager
WHERE r.company_id = :company_id WHERE r.company_id = :company_id
ORDER BY mw.warehouse_name, r.zone, ORDER BY mw.warehouse_name, r.zone,
CAST(r.aisle AS UNSIGNED), r.aisle, CAST(r.aisle AS UNSIGNED), r.aisle,
CAST(r.rack AS UNSIGNED), r.rack" CAST(r.bin AS UNSIGNED), r.bin"
); );
$sth->execute([':company_id' => $this->companyId]); $sth->execute([':company_id' => $this->companyId]);
return $sth->fetchAll(PDO::FETCH_ASSOC); return $sth->fetchAll(PDO::FETCH_ASSOC);
@@ -1510,7 +1510,7 @@ class ReportManager
s.serial_number, s.serial_number,
s.zone, s.zone,
s.aisle, s.aisle,
s.rack, s.bin,
ROUND(COALESCE(s.`in`, 0), 2) AS stock_in, ROUND(COALESCE(s.`in`, 0), 2) AS stock_in,
ROUND(COALESCE(s.`out`, 0), 2) AS stock_out, ROUND(COALESCE(s.`out`, 0), 2) AS stock_out,
CASE CASE
+17 -17
View File
@@ -17,13 +17,13 @@ require_once __DIR__ . '/../classes_ac/PostingWindowGuard.php';
* 1. INSERT td_stock_<warehouse_id> in rows (source='return', source_id=return_id, * 1. INSERT td_stock_<warehouse_id> in rows (source='return', source_id=return_id,
* status=1 ALWAYS — confirming a return is a final business decision, * status=1 ALWAYS — confirming a return is a final business decision,
* no separate warehouse approval step needed). * no separate warehouse approval step needed).
* 2. occupyRack() + adjustBalance() via WarehouseManager. * 2. occupyBin() + adjustBalance() via WarehouseManager.
* 3. If auto_invoice_and_credit_note=1 → InvoiceManager::createCreditNote() * 3. If auto_invoice_and_credit_note=1 → InvoiceManager::createCreditNote()
* auto-generates a CN linked to the original invoice (status=1 issued). * auto-generates a CN linked to the original invoice (status=1 issued).
* If =0 → user creates CN manually from the return detail page. * If =0 → user creates CN manually from the return detail page.
* - Return items stored as JSON in td_return.items (same pattern as td_order). * - Return items stored as JSON in td_return.items (same pattern as td_order).
* - Each item references stock_out_id from the original order so the correct * - Each item references stock_out_id from the original order so the correct
* warehouse/rack/lot context is known for restock. * warehouse/bin/lot context is known for restock.
* *
* Note: Write methods do NOT manage their own DB transactions. * Note: Write methods do NOT manage their own DB transactions.
* Callers must wrap multi-step operations inside dbTransaction(). * Callers must wrap multi-step operations inside dbTransaction().
@@ -242,7 +242,7 @@ class ReturnManager {
* *
* items JSON array — each element mirrors td_order.items with stock_out_id * items JSON array — each element mirrors td_order.items with stock_out_id
* referencing the original stock-out row so confirmReturn() knows which * referencing the original stock-out row so confirmReturn() knows which
* rack/warehouse/lot to restock. * bin/warehouse/lot to restock.
* *
* Must be called inside dbTransaction() by the caller. * Must be called inside dbTransaction() by the caller.
* *
@@ -389,7 +389,7 @@ class ReturnManager {
* Flow per item: * Flow per item:
* 1. INSERT td_stock_<warehouse_id> in row with type='in', status=1 (force confirmed), * 1. INSERT td_stock_<warehouse_id> in row with type='in', status=1 (force confirmed),
* source='return', source_id=return_id. * source='return', source_id=return_id.
* 2. occupyRack() — place returned stock into the user-selected empty rack. * 2. occupyBin() — place returned stock into the user-selected empty bin.
* 3. adjustBalance() — update etl_stock_summary. * 3. adjustBalance() — update etl_stock_summary.
* *
* After all items: * After all items:
@@ -400,7 +400,7 @@ class ReturnManager {
* *
* Stock-in rows respect $auto_approve (mirrors default_stock_status config). * Stock-in rows respect $auto_approve (mirrors default_stock_status config).
* When false, rows are created as draft (status=0) and warehouse staff approve * When false, rows are created as draft (status=0) and warehouse staff approve
* them via approve_stock.php; rack occupation and balance are deferred. * them via approve_stock.php; bin occupation and balance are deferred.
* *
* Must be called inside dbTransaction() by the caller. * Must be called inside dbTransaction() by the caller.
* *
@@ -495,13 +495,13 @@ class ReturnManager {
$this->pdo->prepare( $this->pdo->prepare(
"INSERT INTO `{$table}` "INSERT INTO `{$table}`
(uuid, company_id, `date`, product_sku, `in`, (uuid, company_id, `date`, product_sku, `in`,
zone, aisle, rack, zone, aisle, bin,
contact_id, `description`, `log`, `type`, contact_id, `description`, `log`, `type`,
lot_number, serial_number, lot_number, serial_number,
source, source_id, ref_id, price, status) source, source_id, ref_id, price, status)
VALUES VALUES
(:uuid, :company_id, :date, :product_sku, :quantity, (:uuid, :company_id, :date, :product_sku, :quantity,
:zone, :aisle, :rack, :zone, :aisle, :bin,
:contact_id, :description, :log, 'in', :contact_id, :description, :log, 'in',
:lot_number, :serial_number, :lot_number, :serial_number,
'return', :source_id, :ref_id, :price, :status)" 'return', :source_id, :ref_id, :price, :status)"
@@ -513,7 +513,7 @@ class ReturnManager {
':quantity' => $quantity, ':quantity' => $quantity,
':zone' => $item['zone'] ?? '', ':zone' => $item['zone'] ?? '',
':aisle' => $item['aisle'] ?? '', ':aisle' => $item['aisle'] ?? '',
':rack' => $item['rack'] ?? '', ':bin' => $item['bin'] ?? '',
':contact_id' => (int)$return['contact_id'], ':contact_id' => (int)$return['contact_id'],
':description' => $return['return_number'], ':description' => $return['return_number'],
':log' => json_encode($item_log), ':log' => json_encode($item_log),
@@ -528,11 +528,11 @@ class ReturnManager {
$stock_in_id = (int)$this->pdo->lastInsertId(); $stock_in_id = (int)$this->pdo->lastInsertId();
if ($auto_approve) { if ($auto_approve) {
$whMgmt->occupyRack( $whMgmt->occupyBin(
$warehouse_id, $warehouse_id,
$item['zone'] ?? '', $item['zone'] ?? '',
$item['aisle'] ?? '', $item['aisle'] ?? '',
$item['rack'] ?? '', $item['bin'] ?? '',
$product_sku, $product_sku,
$stock_in_id, $stock_in_id,
true true
@@ -583,9 +583,9 @@ class ReturnManager {
* and doc_type. * and doc_type.
* *
* If the return was confirmed, stock-in rows (source='return', source_id=$return_id) * If the return was confirmed, stock-in rows (source='return', source_id=$return_id)
* were created with status=1 and rack/balance were already adjusted. * were created with status=1 and bin/balance were already adjusted.
* On cancel we soft-delete those rows (status → -1) and reverse the * On cancel we soft-delete those rows (status → -1) and reverse the
* rack occupation and balance via WarehouseManager. * bin occupation and balance via WarehouseManager.
* *
* If the return was still draft, no stock rows exist — simple status flip. * If the return was still draft, no stock rows exist — simple status flip.
* *
@@ -636,7 +636,7 @@ class ReturnManager {
} }
} }
// ── If confirmed: reverse stock-in rows + rack/balance ──────────── // ── If confirmed: reverse stock-in rows + bin/balance ────────────
if ($status === 1) { if ($status === 1) {
$sth2 = $this->pdo->prepare( $sth2 = $this->pdo->prepare(
"SELECT * FROM td_return_item "SELECT * FROM td_return_item
@@ -657,7 +657,7 @@ class ReturnManager {
// Fetch ALL non-cancelled stock-in rows (status=1 approved or status=0 draft) // Fetch ALL non-cancelled stock-in rows (status=1 approved or status=0 draft)
$sth = $this->pdo->prepare( $sth = $this->pdo->prepare(
"SELECT id, status, zone, aisle, rack, `date` FROM `{$table}` "SELECT id, status, zone, aisle, bin, `date` FROM `{$table}`
WHERE company_id = :company_id WHERE company_id = :company_id
AND source = 'return' AND source = 'return'
AND source_id = :return_id AND source_id = :return_id
@@ -679,11 +679,11 @@ class ReturnManager {
if ((int)$row['status'] === 1) { if ((int)$row['status'] === 1) {
$whMgmt->assertStockMovementWindow($row['date'] ?? null, 'Return stock cancellation'); $whMgmt->assertStockMovementWindow($row['date'] ?? null, 'Return stock cancellation');
// Approved rows: reverse rack occupation + balance // Approved rows: reverse bin occupation + balance
$whMgmt->releaseRack($warehouse_id, $row['zone'], $row['aisle'], $row['rack']); $whMgmt->releaseBin($warehouse_id, $row['zone'], $row['aisle'], $row['bin']);
$whMgmt->adjustBalance('out', $warehouse_id, $product_sku, 0, $quantity); $whMgmt->adjustBalance('out', $warehouse_id, $product_sku, 0, $quantity);
} }
// Draft (status=0) rows: just soft-delete — rack/balance were never applied // Draft (status=0) rows: just soft-delete — bin/balance were never applied
} }
} }
} }
+57 -57
View File
@@ -15,7 +15,7 @@ require_once __DIR__ . '/../notify_node.php';
* getTransferById, saveStockIn, saveStockOut, saveStockTransfer * getTransferById, saveStockIn, saveStockOut, saveStockTransfer
* Report basis → (none — reporting is handled by ReportManager) * Report basis → (none — reporting is handled by ReportManager)
* *
* Write operations delegate rack and balance side-effects to WarehouseManager. * Write operations delegate bin and balance side-effects to WarehouseManager.
* Delete operations are handled directly in WarehouseManager (deleteStockIn, etc.). * Delete operations are handled directly in WarehouseManager (deleteStockIn, etc.).
* *
* Note: Write methods do NOT manage their own DB transactions. * Note: Write methods do NOT manage their own DB transactions.
@@ -228,16 +228,16 @@ class StockManager {
* Insert flow (id = 0): * Insert flow (id = 0):
* 1. Upserts md_lot if lot_number + expiry_date are provided. * 1. Upserts md_lot if lot_number + expiry_date are provided.
* 2. Inserts the td_stock_<warehouse_id> row. * 2. Inserts the td_stock_<warehouse_id> row.
* 3. Calls WarehouseManager::occupyRack() to reserve the rack. * 3. Calls WarehouseManager::occupyBin() to reserve the bin.
* etl_stock_summary is updated later by approveStock(). * etl_stock_summary is updated later by approveStock().
* *
* Update flow (id > 0): * Update flow (id > 0):
* - Updates contact_id, description, and log only. * - Updates contact_id, description, and log only.
* - Quantity, rack, lot, and serial are immutable after creation. * - Quantity, bin, lot, and serial are immutable after creation.
* *
* Must be called inside dbTransaction() by the caller. * Must be called inside dbTransaction() by the caller.
* *
* @param array $data Keys: id, warehouse, product_sku, quantity, zone, aisle, rack, * @param array $data Keys: id, warehouse, product_sku, quantity, zone, aisle, bin,
* contact_id, description, lot_number, expiry_date, serial_number. * contact_id, description, lot_number, expiry_date, serial_number.
* @param array $logging Audit entry to append to the log column. * @param array $logging Audit entry to append to the log column.
* @param string $uuid UUID for this transaction (shared across transfer pairs). * @param string $uuid UUID for this transaction (shared across transfer pairs).
@@ -303,10 +303,10 @@ class StockManager {
$this->pdo->prepare( $this->pdo->prepare(
"INSERT INTO `$table` "INSERT INTO `$table`
(uuid, company_id, `date`, product_sku, `in`, price, zone, aisle, rack, (uuid, company_id, `date`, product_sku, `in`, price, zone, aisle, bin,
contact_id, `description`, `log`, `type`, lot_number, serial_number, status, updated_at) contact_id, `description`, `log`, `type`, lot_number, serial_number, status, updated_at)
VALUES VALUES
(:uuid, :company_id, :date, :product_sku, :quantity, :price, :zone, :aisle, :rack, (:uuid, :company_id, :date, :product_sku, :quantity, :price, :zone, :aisle, :bin,
:contact_id, :description, :log, 'in', :lot_number, :serial_number, 0, NOW())" :contact_id, :description, :log, 'in', :lot_number, :serial_number, 0, NOW())"
)->execute([ )->execute([
':uuid' => $uuid, ':uuid' => $uuid,
@@ -317,7 +317,7 @@ class StockManager {
':price' => (float)($data['price'] ?? 0), ':price' => (float)($data['price'] ?? 0),
':zone' => $data['zone'], ':zone' => $data['zone'],
':aisle' => $data['aisle'], ':aisle' => $data['aisle'],
':rack' => $data['rack'], ':bin' => $data['bin'],
':contact_id' => (int)($data['contact_id'] ?? 0), ':contact_id' => (int)($data['contact_id'] ?? 0),
':description' => $data['description'] ?? '', ':description' => $data['description'] ?? '',
':log' => json_encode($table_log), ':log' => json_encode($table_log),
@@ -328,13 +328,13 @@ class StockManager {
$td_stock_id = (int)$this->pdo->lastInsertId(); $td_stock_id = (int)$this->pdo->lastInsertId();
// Reserve the location immediately so draft stock-in rows cannot // Reserve the location immediately so draft stock-in rows cannot
// leave the same rack available for another receipt. Balance still // leave the same bin available for another receipt. Balance still
// changes only when approveStock() runs. // changes only when approveStock() runs.
$whMgmt->occupyRack( $whMgmt->occupyBin(
$warehouse_id, $warehouse_id,
$data['zone'], $data['zone'],
$data['aisle'], $data['aisle'],
$data['rack'], $data['bin'],
$data['product_sku'], $data['product_sku'],
$td_stock_id $td_stock_id
); );
@@ -347,9 +347,9 @@ class StockManager {
* Insert a new stock_out record or update metadata on an existing one. * Insert a new stock_out record or update metadata on an existing one.
* *
* Insert flow (id = 0): * Insert flow (id = 0):
* 1. Validates the rack is occupied with the correct SKU / lot / serial. * 1. Validates the bin is occupied with the correct SKU / lot / serial.
* 2. Inserts the td_stock_<warehouse_id> row, copying quantity and lot info from the rack. * 2. Inserts the td_stock_<warehouse_id> row, copying quantity and lot info from the bin.
* 3. Calls WarehouseManager::releaseRack() to free the rack slot. * 3. Calls WarehouseManager::releaseBin() to free the bin slot.
* 4. Calls WarehouseManager::adjustBalance() to update etl_stock_summary. * 4. Calls WarehouseManager::adjustBalance() to update etl_stock_summary.
* *
* Update flow (id > 0): * Update flow (id > 0):
@@ -357,11 +357,11 @@ class StockManager {
* *
* Must be called inside dbTransaction() by the caller. * Must be called inside dbTransaction() by the caller.
* *
* @param array $data Keys: id, warehouse, product_sku, zone, aisle, rack, * @param array $data Keys: id, warehouse, product_sku, zone, aisle, bin,
* contact_id, description, lot_number, serial_number. * contact_id, description, lot_number, serial_number.
* @param array $logging Audit entry to append to the log column. * @param array $logging Audit entry to append to the log column.
* @param string $uuid UUID for this transaction. * @param string $uuid UUID for this transaction.
* @throws Exception If the rack is empty, holds a different SKU/lot/serial. * @throws Exception If the bin is empty, holds a different SKU/lot/serial.
*/ */
public function saveStockOut(array $data, array $logging, string $uuid): int public function saveStockOut(array $data, array $logging, string $uuid): int
{ {
@@ -400,33 +400,33 @@ class StockManager {
} else { } else {
// Validate rack holds the expected product / lot / serial // Validate bin holds the expected product / lot / serial
$source_stock = $whMgmt->getRackStock( $source_stock = $whMgmt->getBinStock(
$warehouse_id, $warehouse_id,
$data['zone'], $data['aisle'], $data['rack'] $data['zone'], $data['aisle'], $data['bin']
); );
if (!$source_stock) { if (!$source_stock) {
throw new Exception( throw new Exception(
"Rack {$data['zone']}-{$data['aisle']}-{$data['rack']} is empty — nothing to take out." "Bin {$data['zone']}-{$data['aisle']}-{$data['bin']} is empty — nothing to take out."
); );
} }
if ($source_stock['product_sku'] !== $data['product_sku']) { if ($source_stock['product_sku'] !== $data['product_sku']) {
throw new Exception( throw new Exception(
"Rack holds {$source_stock['product_sku']}, not {$data['product_sku']}." "Bin holds {$source_stock['product_sku']}, not {$data['product_sku']}."
); );
} }
if (!empty($data['lot_number']) && $source_stock['lot_number'] !== $data['lot_number']) { if (!empty($data['lot_number']) && $source_stock['lot_number'] !== $data['lot_number']) {
throw new Exception( throw new Exception(
"Rack holds lot '{$source_stock['lot_number']}', not '{$data['lot_number']}'." "Bin holds lot '{$source_stock['lot_number']}', not '{$data['lot_number']}'."
); );
} }
if (!empty($data['serial_number']) && $source_stock['serial_number'] !== $data['serial_number']) { if (!empty($data['serial_number']) && $source_stock['serial_number'] !== $data['serial_number']) {
throw new Exception( throw new Exception(
"Rack holds serial '{$source_stock['serial_number']}', not '{$data['serial_number']}'." "Bin holds serial '{$source_stock['serial_number']}', not '{$data['serial_number']}'."
); );
} }
@@ -438,10 +438,10 @@ class StockManager {
$this->pdo->prepare( $this->pdo->prepare(
"INSERT INTO `$table` "INSERT INTO `$table`
(uuid, company_id, `date`, product_sku, `out`, zone, aisle, rack, (uuid, company_id, `date`, product_sku, `out`, zone, aisle, bin,
contact_id, `description`, `log`, `type`, ref_id, lot_number, serial_number, status, updated_at) contact_id, `description`, `log`, `type`, ref_id, lot_number, serial_number, status, updated_at)
VALUES VALUES
(:uuid, :company_id, :date, :product_sku, :quantity, :zone, :aisle, :rack, (:uuid, :company_id, :date, :product_sku, :quantity, :zone, :aisle, :bin,
:contact_id, :description, :log, 'out', :ref_id, :lot_number, :serial_number, 0, NOW())" :contact_id, :description, :log, 'out', :ref_id, :lot_number, :serial_number, 0, NOW())"
)->execute([ )->execute([
':uuid' => $uuid, ':uuid' => $uuid,
@@ -451,7 +451,7 @@ class StockManager {
':quantity' => $quantity, ':quantity' => $quantity,
':zone' => $data['zone'], ':zone' => $data['zone'],
':aisle' => $data['aisle'], ':aisle' => $data['aisle'],
':rack' => $data['rack'], ':bin' => $data['bin'],
':contact_id' => (int)($data['contact_id'] ?? 0), ':contact_id' => (int)($data['contact_id'] ?? 0),
':description' => $data['description'] ?? '', ':description' => $data['description'] ?? '',
':log' => json_encode($table_log), ':log' => json_encode($table_log),
@@ -462,7 +462,7 @@ class StockManager {
$out_stock_id = (int)$this->pdo->lastInsertId(); $out_stock_id = (int)$this->pdo->lastInsertId();
// releaseRack and adjustBalance deferred — called from approveStock() only. // releaseBin and adjustBalance deferred — called from approveStock() only.
return $out_stock_id; return $out_stock_id;
} }
} }
@@ -475,11 +475,11 @@ class StockManager {
* sharing the same UUID and cross-referencing each other via ref_id. * sharing the same UUID and cross-referencing each other via ref_id.
* *
* Insert flow (id = 0): * Insert flow (id = 0):
* 1. Validates the source rack holds the correct SKU / lot / serial. * 1. Validates the source bin holds the correct SKU / lot / serial.
* 2. Inserts the outbound row in td_stock_<from>. * 2. Inserts the outbound row in td_stock_<from>.
* 3. Inserts the inbound row in td_stock_<to> with ref_id pointing to from. * 3. Inserts the inbound row in td_stock_<to> with ref_id pointing to from.
* 4. Back-fills ref_id on the from row so both point at each other. * 4. Back-fills ref_id on the from row so both point at each other.
* 5. Releases the source rack, occupies the destination rack. * 5. Releases the source bin, occupies the destination bin.
* 6. Adjusts balance on both warehouses (out from source, in to dest). * 6. Adjusts balance on both warehouses (out from source, in to dest).
* *
* Update flow (id > 0): * Update flow (id > 0):
@@ -488,11 +488,11 @@ class StockManager {
* Must be called inside dbTransaction() by the caller. * Must be called inside dbTransaction() by the caller.
* *
* @param array $data Keys: id, warehouse_from, warehouse_to, product_sku, * @param array $data Keys: id, warehouse_from, warehouse_to, product_sku,
* zone_from, aisle_from, rack_from, zone_to, aisle_to, rack_to, * zone_from, aisle_from, bin_from, zone_to, aisle_to, bin_to,
* contact_id, description, lot_number, serial_number. * contact_id, description, lot_number, serial_number.
* @param array $logging Audit entry to append to the log column on both rows. * @param array $logging Audit entry to append to the log column on both rows.
* @param string $uuid UUID shared by both the from and to rows. * @param string $uuid UUID shared by both the from and to rows.
* @throws Exception If source rack validation fails or paired record is missing on update. * @throws Exception If source bin validation fails or paired record is missing on update.
*/ */
public function saveStockTransfer(array $data, array $logging, string $uuid): int public function saveStockTransfer(array $data, array $logging, string $uuid): int
{ {
@@ -566,39 +566,39 @@ class StockManager {
return 0; // update — no new row return 0; // update — no new row
} }
// Insert: validate source rack, then create paired rows // Insert: validate source bin, then create paired rows
$from_warehouse = (int)$data['warehouse_from']; $from_warehouse = (int)$data['warehouse_from'];
$from_zone = $data['zone_from']; $from_zone = $data['zone_from'];
$from_aisle = $data['aisle_from']; $from_aisle = $data['aisle_from'];
$from_rack = $data['rack_from']; $from_bin = $data['bin_from'];
$to_warehouse = (int)$data['warehouse_to']; $to_warehouse = (int)$data['warehouse_to'];
$to_zone = $data['zone_to']; $to_zone = $data['zone_to'];
$to_aisle = $data['aisle_to']; $to_aisle = $data['aisle_to'];
$to_rack = $data['rack_to']; $to_bin = $data['bin_to'];
$source_stock = $whMgmt->getRackStock( $source_stock = $whMgmt->getBinStock(
$from_warehouse, $from_zone, $from_aisle, $from_rack $from_warehouse, $from_zone, $from_aisle, $from_bin
); );
if (!$source_stock) { if (!$source_stock) {
throw new Exception("Source rack {$from_zone}-{$from_aisle}-{$from_rack} is empty."); throw new Exception("Source bin {$from_zone}-{$from_aisle}-{$from_bin} is empty.");
} }
if ($source_stock['product_sku'] !== $data['product_sku']) { if ($source_stock['product_sku'] !== $data['product_sku']) {
throw new Exception( throw new Exception(
"Source rack holds {$source_stock['product_sku']}, not {$data['product_sku']}." "Source bin holds {$source_stock['product_sku']}, not {$data['product_sku']}."
); );
} }
if (!empty($data['lot_number']) && $source_stock['lot_number'] !== $data['lot_number']) { if (!empty($data['lot_number']) && $source_stock['lot_number'] !== $data['lot_number']) {
throw new Exception( throw new Exception(
"Source rack holds lot '{$source_stock['lot_number']}', not '{$data['lot_number']}'." "Source bin holds lot '{$source_stock['lot_number']}', not '{$data['lot_number']}'."
); );
} }
if (!empty($data['serial_number']) && $source_stock['serial_number'] !== $data['serial_number']) { if (!empty($data['serial_number']) && $source_stock['serial_number'] !== $data['serial_number']) {
throw new Exception( throw new Exception(
"Source rack holds serial '{$source_stock['serial_number']}', not '{$data['serial_number']}'." "Source bin holds serial '{$source_stock['serial_number']}', not '{$data['serial_number']}'."
); );
} }
@@ -615,11 +615,11 @@ class StockManager {
$this->pdo->prepare( $this->pdo->prepare(
"INSERT INTO `$from_table` "INSERT INTO `$from_table`
(uuid, company_id, `date`, product_sku, `out`, (uuid, company_id, `date`, product_sku, `out`,
ref_warehouse, zone, aisle, rack, ref_warehouse, zone, aisle, bin,
contact_id, `description`, `log`, `type`, lot_number, serial_number, status, updated_at) contact_id, `description`, `log`, `type`, lot_number, serial_number, status, updated_at)
VALUES VALUES
(:uuid, :company_id, :date, :product_sku, :quantity, (:uuid, :company_id, :date, :product_sku, :quantity,
:ref_warehouse, :zone, :aisle, :rack, :ref_warehouse, :zone, :aisle, :bin,
:contact_id, :description, :log, 'transfer', :lot_number, :serial_number, 0, NOW())" :contact_id, :description, :log, 'transfer', :lot_number, :serial_number, 0, NOW())"
)->execute([ )->execute([
':uuid' => $uuid, ':uuid' => $uuid,
@@ -630,7 +630,7 @@ class StockManager {
':ref_warehouse' => $to_warehouse, ':ref_warehouse' => $to_warehouse,
':zone' => $from_zone, ':zone' => $from_zone,
':aisle' => $from_aisle, ':aisle' => $from_aisle,
':rack' => $from_rack, ':bin' => $from_bin,
':contact_id' => $contact_id, ':contact_id' => $contact_id,
':description' => $description, ':description' => $description,
':log' => json_encode($table_log), ':log' => json_encode($table_log),
@@ -643,11 +643,11 @@ class StockManager {
$this->pdo->prepare( $this->pdo->prepare(
"INSERT INTO `$to_table` "INSERT INTO `$to_table`
(uuid, company_id, `date`, product_sku, `in`, (uuid, company_id, `date`, product_sku, `in`,
ref_warehouse, ref_id, zone, aisle, rack, ref_warehouse, ref_id, zone, aisle, bin,
contact_id, `description`, `log`, `type`, lot_number, serial_number, status, updated_at) contact_id, `description`, `log`, `type`, lot_number, serial_number, status, updated_at)
VALUES VALUES
(:uuid, :company_id, :date, :product_sku, :quantity, (:uuid, :company_id, :date, :product_sku, :quantity,
:ref_warehouse, :ref_id, :zone, :aisle, :rack, :ref_warehouse, :ref_id, :zone, :aisle, :bin,
:contact_id, :description, :log, 'transfer', :lot_number, :serial_number, 0, NOW())" :contact_id, :description, :log, 'transfer', :lot_number, :serial_number, 0, NOW())"
)->execute([ )->execute([
':uuid' => $uuid, ':uuid' => $uuid,
@@ -659,7 +659,7 @@ class StockManager {
':ref_id' => $from_stock_id, ':ref_id' => $from_stock_id,
':zone' => $to_zone, ':zone' => $to_zone,
':aisle' => $to_aisle, ':aisle' => $to_aisle,
':rack' => $to_rack, ':bin' => $to_bin,
':contact_id' => $contact_id, ':contact_id' => $contact_id,
':description' => $description, ':description' => $description,
':log' => json_encode($table_log), ':log' => json_encode($table_log),
@@ -678,7 +678,7 @@ class StockManager {
':company_id' => $this->company_id, ':company_id' => $this->company_id,
]); ]);
// releaseRack, occupyRack and adjustBalance deferred — called from approveStock() only. // releaseBin, occupyBin and adjustBalance deferred — called from approveStock() only.
return $from_stock_id; return $from_stock_id;
} }
@@ -730,13 +730,13 @@ class StockManager {
"UPDATE `{$table}` SET status = 1, updated_at = NOW() WHERE id = :id AND company_id = :company_id" "UPDATE `{$table}` SET status = 1, updated_at = NOW() WHERE id = :id AND company_id = :company_id"
)->execute([':id' => $id, ':company_id' => $this->company_id]); )->execute([':id' => $id, ':company_id' => $this->company_id]);
// ── Rack state + balance ────────────────────────────────────────── // ── Bin state + balance ──────────────────────────────────────────
if ($type === 'in') { if ($type === 'in') {
// Occupy rack now that stock is approved // Occupy bin now that stock is approved
$whMgmt->occupyRack( $whMgmt->occupyBin(
$warehouse_id, $warehouse_id,
$row['zone'], $row['aisle'], $row['rack'], $row['zone'], $row['aisle'], $row['bin'],
$row['product_sku'], $row['product_sku'],
$id $id
); );
@@ -766,9 +766,9 @@ class StockManager {
$remaining_qty = (float)$remaining_sth->fetchColumn(); $remaining_qty = (float)$remaining_sth->fetchColumn();
} }
// Release the rack only when the source batch is fully consumed. // Release the bin only when the source batch is fully consumed.
if ($remaining_qty <= 0.000001) { if ($remaining_qty <= 0.000001) {
$whMgmt->releaseRack($warehouse_id, $row['zone'], $row['aisle'], $row['rack']); $whMgmt->releaseBin($warehouse_id, $row['zone'], $row['aisle'], $row['bin']);
} }
$whMgmt->adjustBalance('out', $warehouse_id, $row['product_sku'], 0, (float)$row['out'], $whMgmt->adjustBalance('out', $warehouse_id, $row['product_sku'], 0, (float)$row['out'],
$id, $row['source'] ?? '', (int)($row['source_id'] ?? 0), $row['date'] ?? ''); $id, $row['source'] ?? '', (int)($row['source_id'] ?? 0), $row['date'] ?? '');
@@ -848,19 +848,19 @@ class StockManager {
)->execute([':id' => $inbound_row['id'], ':company_id' => $this->company_id]); )->execute([':id' => $inbound_row['id'], ':company_id' => $this->company_id]);
} }
// Rack state: release source, occupy destination // Bin state: release source, occupy destination
if ($from_row && $from_wh_id) { if ($from_row && $from_wh_id) {
$whMgmt->releaseRack( $whMgmt->releaseBin(
$from_wh_id, $from_wh_id,
$from_row['zone'], $from_row['aisle'], $from_row['rack'] $from_row['zone'], $from_row['aisle'], $from_row['bin']
); );
$whMgmt->adjustBalance('out', $from_wh_id, $from_row['product_sku'], 0, (float)$from_row['out'], $whMgmt->adjustBalance('out', $from_wh_id, $from_row['product_sku'], 0, (float)$from_row['out'],
(int)$from_row['id'], $from_row['source'] ?? '', (int)($from_row['source_id'] ?? 0), $from_row['date'] ?? ''); (int)$from_row['id'], $from_row['source'] ?? '', (int)($from_row['source_id'] ?? 0), $from_row['date'] ?? '');
} }
if ($inbound_row && $paired_wh_id) { if ($inbound_row && $paired_wh_id) {
$whMgmt->occupyRack( $whMgmt->occupyBin(
$paired_wh_id, $paired_wh_id,
$inbound_row['zone'], $inbound_row['aisle'], $inbound_row['rack'], $inbound_row['zone'], $inbound_row['aisle'], $inbound_row['bin'],
$inbound_row['product_sku'], $inbound_row['product_sku'],
$inbound_row['id'] $inbound_row['id']
); );
@@ -26,7 +26,7 @@ class StockSourceManager
$warehouse_id = (int)$match[1]; $warehouse_id = (int)$match[1];
$sth = $this->pdo->prepare( $sth = $this->pdo->prepare(
"SELECT s.id, s.type, s.product_sku, s.`in`, s.`out`, "SELECT s.id, s.type, s.product_sku, s.`in`, s.`out`,
s.zone, s.aisle, s.rack, s.lot_number, s.serial_number, s.zone, s.aisle, s.bin, s.lot_number, s.serial_number,
s.status, s.date, s.price, s.status, s.date, s.price,
p.product_name p.product_name
FROM `{$table}` s FROM `{$table}` s
@@ -104,10 +104,10 @@ class SupplierReturnManager {
$sth = $this->pdo->prepare( $sth = $this->pdo->prepare(
"INSERT INTO td_supplier_return_item "INSERT INTO td_supplier_return_item
(company_id, return_id, item_id, product_sku, product_name, (company_id, return_id, item_id, product_sku, product_name,
quantity, unit_price, total_price, tax_amount, tax_rate, warehouse_id, stock_in_id, zone, aisle, rack) quantity, unit_price, total_price, tax_amount, tax_rate, warehouse_id, stock_in_id, zone, aisle, bin)
VALUES VALUES
(:company_id, :return_id, :item_id, :product_sku, :product_name, (:company_id, :return_id, :item_id, :product_sku, :product_name,
:quantity, :unit_price, :total_price, :tax_amount, :tax_rate, :warehouse_id, :stock_in_id, :zone, :aisle, :rack)" :quantity, :unit_price, :total_price, :tax_amount, :tax_rate, :warehouse_id, :stock_in_id, :zone, :aisle, :bin)"
); );
foreach ($items as $pos => $item) { foreach ($items as $pos => $item) {
$sth->execute([ $sth->execute([
@@ -125,7 +125,7 @@ class SupplierReturnManager {
':stock_in_id' => (int)($item['stock_in_id'] ?? 0), ':stock_in_id' => (int)($item['stock_in_id'] ?? 0),
':zone' => $item['zone'] ?? '', ':zone' => $item['zone'] ?? '',
':aisle' => $item['aisle'] ?? '', ':aisle' => $item['aisle'] ?? '',
':rack' => $item['rack'] ?? '', ':bin' => $item['bin'] ?? '',
]); ]);
} }
} }
@@ -348,7 +348,7 @@ class SupplierReturnManager {
* Flow per item: * Flow per item:
* 1. Validate stock-in row (from PO receive) is approved. * 1. Validate stock-in row (from PO receive) is approved.
* 2. INSERT td_stock_<warehouse_id> out row, status respects $auto_approve. * 2. INSERT td_stock_<warehouse_id> out row, status respects $auto_approve.
* 3. If $auto_approve: releaseRack() + adjustBalance('out'). * 3. If $auto_approve: releaseBin() + adjustBalance('out').
* After all items: * After all items:
* 4. UPDATE td_supplier_return.status = 1. * 4. UPDATE td_supplier_return.status = 1.
* *
@@ -433,9 +433,9 @@ class SupplierReturnManager {
$zone = $item['zone'] ?? ''; $zone = $item['zone'] ?? '';
$aisle = $item['aisle'] ?? ''; $aisle = $item['aisle'] ?? '';
$rack = $item['rack'] ?? ''; $bin = $item['bin'] ?? '';
if ($zone === '' && $rack !== '') $zone = $rack; if ($zone === '' && $bin !== '') $zone = $bin;
if ($aisle === '' && $rack !== '') $aisle = $rack; if ($aisle === '' && $bin !== '') $aisle = $bin;
$item_uuid = $uuid . '_srn_' . $i; $item_uuid = $uuid . '_srn_' . $i;
$item_log = [array_merge($logging, ['action' => 'confirm_return_item'])]; $item_log = [array_merge($logging, ['action' => 'confirm_return_item'])];
@@ -443,13 +443,13 @@ class SupplierReturnManager {
$this->pdo->prepare( $this->pdo->prepare(
"INSERT INTO `{$table}` "INSERT INTO `{$table}`
(uuid, company_id, `date`, product_sku, `out`, (uuid, company_id, `date`, product_sku, `out`,
zone, aisle, rack, zone, aisle, bin,
contact_id, `description`, `log`, `type`, contact_id, `description`, `log`, `type`,
lot_number, serial_number, lot_number, serial_number,
source, source_id, ref_id, price, status) source, source_id, ref_id, price, status)
VALUES VALUES
(:uuid, :company_id, :date, :product_sku, :quantity, (:uuid, :company_id, :date, :product_sku, :quantity,
:zone, :aisle, :rack, :zone, :aisle, :bin,
:contact_id, :description, :log, 'out', :contact_id, :description, :log, 'out',
:lot_number, :serial_number, :lot_number, :serial_number,
'supplier_return', :source_id, :ref_id, :price, :status)" 'supplier_return', :source_id, :ref_id, :price, :status)"
@@ -461,7 +461,7 @@ class SupplierReturnManager {
':quantity' => $quantity, ':quantity' => $quantity,
':zone' => $zone, ':zone' => $zone,
':aisle' => $aisle, ':aisle' => $aisle,
':rack' => $rack, ':bin' => $bin,
':contact_id' => (int)$return['contact_id'], ':contact_id' => (int)$return['contact_id'],
':description' => $return['return_number'], ':description' => $return['return_number'],
':log' => json_encode($item_log), ':log' => json_encode($item_log),
@@ -475,7 +475,7 @@ class SupplierReturnManager {
if ($auto_approve) { if ($auto_approve) {
$stock_out_id = (int)$this->pdo->lastInsertId(); $stock_out_id = (int)$this->pdo->lastInsertId();
$whMgmt->releaseRack($warehouse_id, $zone, $aisle, $rack); $whMgmt->releaseBin($warehouse_id, $zone, $aisle, $bin);
$whMgmt->adjustBalance('out', $warehouse_id, $product_sku, 0, $quantity); $whMgmt->adjustBalance('out', $warehouse_id, $product_sku, 0, $quantity);
} }
} }
@@ -560,7 +560,7 @@ class SupplierReturnManager {
// Fetch ALL non-cancelled stock-out rows (status=1 approved or status=0 draft) // Fetch ALL non-cancelled stock-out rows (status=1 approved or status=0 draft)
$sth = $this->pdo->prepare( $sth = $this->pdo->prepare(
"SELECT id, status, zone, aisle, rack, ref_id, `date` FROM `{$table}` "SELECT id, status, zone, aisle, bin, ref_id, `date` FROM `{$table}`
WHERE company_id = :company_id WHERE company_id = :company_id
AND source = 'supplier_return' AND source = 'supplier_return'
AND source_id = :return_id AND source_id = :return_id
@@ -582,14 +582,14 @@ class SupplierReturnManager {
if ((int)$row['status'] === 1) { if ((int)$row['status'] === 1) {
$whMgmt->assertStockMovementWindow($row['date'] ?? null, 'Supplier return stock cancellation'); $whMgmt->assertStockMovementWindow($row['date'] ?? null, 'Supplier return stock cancellation');
// Approved rows: re-occupy rack (reverse the release) + reverse balance // Approved rows: re-occupy bin (reverse the release) + reverse balance
$ref_id = (int)$row['ref_id']; $ref_id = (int)$row['ref_id'];
if ($ref_id > 0) { if ($ref_id > 0) {
$whMgmt->occupyRack($warehouse_id, $row['zone'], $row['aisle'], $row['rack'], $product_sku, $ref_id, false); $whMgmt->occupyBin($warehouse_id, $row['zone'], $row['aisle'], $row['bin'], $product_sku, $ref_id, false);
} }
$whMgmt->adjustBalance('in', $warehouse_id, $product_sku, 0, $quantity); $whMgmt->adjustBalance('in', $warehouse_id, $product_sku, 0, $quantity);
} }
// Draft (status=0) rows: just soft-delete — rack/balance were never applied // Draft (status=0) rows: just soft-delete — bin/balance were never applied
} }
} }
} }
File diff suppressed because it is too large Load Diff
@@ -8,7 +8,7 @@
$warehouse_id = (int)($data['warehouse_id'] ?? 0); $warehouse_id = (int)($data['warehouse_id'] ?? 0);
$zone = trim($data['zone'] ?? ''); $zone = trim($data['zone'] ?? '');
$aisle = trim($data['aisle'] ?? ''); $aisle = trim($data['aisle'] ?? '');
$rack = trim($data['rack'] ?? ''); $bin = trim($data['bin'] ?? '');
if ($label_action !== 'list') { if ($label_action !== 'list') {
require_role($user_role, ['owner', 'admin', 'staff']); require_role($user_role, ['owner', 'admin', 'staff']);
@@ -56,14 +56,14 @@
} }
// Default: create // Default: create
if ($barcode === '' || !$warehouse_id || $rack === '') { if ($barcode === '' || !$warehouse_id || $bin === '') {
$answer['message'] = 'Location barcode data is incomplete.'; $answer['message'] = 'Location barcode data is incomplete.';
http_response_code(400); http_response_code(400);
exit(json_encode($answer)); exit(json_encode($answer));
} }
try { try {
$output = $bm->createLocationLabel($barcode, $warehouse_id, $zone, $aisle, $rack); $output = $bm->createLocationLabel($barcode, $warehouse_id, $zone, $aisle, $bin);
$answer['success'] = 1; $answer['success'] = 1;
$answer['output'] = $output; $answer['output'] = $output;
} catch (Exception $e) { } catch (Exception $e) {
+4 -4
View File
@@ -13,11 +13,11 @@
if (isset($data['warehouse_to']) && $type === 'to') { $warehouse = (int)$data['warehouse_to']; $zone = $data['zone_to'] ?? $zone; $aisle = $data['aisle_to'] ?? $aisle; } if (isset($data['warehouse_to']) && $type === 'to') { $warehouse = (int)$data['warehouse_to']; $zone = $data['zone_to'] ?? $zone; $aisle = $data['aisle_to'] ?? $aisle; }
if (!empty($data['id'])) { if (!empty($data['id'])) {
$output = $whMgmt->getRacksAll($warehouse, $zone, $aisle); $output = $whMgmt->getBinsAll($warehouse, $zone, $aisle);
} elseif ($type === 'to') { } elseif ($type === 'to') {
$output = $whMgmt->getRacksIn($warehouse, $zone, $aisle); $output = $whMgmt->getBinsIn($warehouse, $zone, $aisle);
} elseif ($type === 'from') { } elseif ($type === 'from') {
$output = $whMgmt->getRacksOut( $output = $whMgmt->getBinsOut(
$warehouse, $warehouse,
$zone, $zone,
$aisle, $aisle,
@@ -26,7 +26,7 @@
$data['serial_number'] ?? null $data['serial_number'] ?? null
); );
} else { } else {
$output = $whMgmt->getRacksAll($warehouse, $zone, $aisle); $output = $whMgmt->getBinsAll($warehouse, $zone, $aisle);
} }
$answer['output'] = $output; $answer['output'] = $output;
+1 -1
View File
@@ -10,7 +10,7 @@
$id = (int)($data['id'] ?? 0); $id = (int)($data['id'] ?? 0);
// When type=from and a lot is selected, filter warehouses to only those // When type=from and a lot is selected, filter warehouses to only those
// holding that specific lot in an approved, rack-occupied stock-in row. // holding that specific lot in an approved, bin-occupied stock-in row.
if ($type === 'from' && $sku && $lot) { if ($type === 'from' && $sku && $lot) {
$answer['output'] = $wh->getWarehousesByLot($sku, $lot); $answer['output'] = $wh->getWarehousesByLot($sku, $lot);
} else { } else {
@@ -11,7 +11,7 @@
$warehouse_id = (int)($data['warehouse_id'] ?? 0); $warehouse_id = (int)($data['warehouse_id'] ?? 0);
$zone = trim($data['zone'] ?? ''); $zone = trim($data['zone'] ?? '');
$aisle = trim($data['aisle'] ?? ''); $aisle = trim($data['aisle'] ?? '');
$rack = trim($data['rack'] ?? ''); $bin = trim($data['bin'] ?? '');
$product_sku = trim($data['product_sku'] ?? ''); $product_sku = trim($data['product_sku'] ?? '');
$lot_number = trim($data['lot_number'] ?? ''); $lot_number = trim($data['lot_number'] ?? '');
$serial = trim($data['serial_number'] ?? ''); $serial = trim($data['serial_number'] ?? '');
@@ -22,7 +22,7 @@
$warehouse_id, $warehouse_id,
$zone, $zone,
$aisle, $aisle,
$rack, $bin,
$product_sku, $product_sku,
$lot_number, $lot_number,
$serial $serial
+43 -43
View File
@@ -18,7 +18,7 @@
<div class="col-12 d-flex justify-content-between align-items-center"> <div class="col-12 d-flex justify-content-between align-items-center">
<div> <div>
<h1 class="fs-3 mb-1">Location Barcode Labels</h1> <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> <p class="mb-0 text-muted">Print or reprint labels from the bin master list.</p>
</div> </div>
<div class="d-flex gap-2"> <div class="d-flex gap-2">
<button id="btn_print" class="btn btn-outline-success" data-min-role="staff" onclick="print_selected_label()" disabled> <button id="btn_print" class="btn btn-outline-success" data-min-role="staff" onclick="print_selected_label()" disabled>
@@ -39,7 +39,7 @@
<div class="card"><div class="card-body p-5"> <div class="card"><div class="card-body p-5">
<div class="position-relative mb-4"> <div class="position-relative mb-4">
<input type="search" id="search_location" class="form-control ps-9" <input type="search" id="search_location" class="form-control ps-9"
placeholder="Search warehouse, zone, aisle, rack, or SKU..."> placeholder="Search warehouse, zone, aisle, bin, or SKU...">
<span class="position-absolute top-25 ms-4"> <span class="position-absolute top-25 ms-4">
<i class="ti ti-search text-muted"></i> <i class="ti ti-search text-muted"></i>
</span> </span>
@@ -51,13 +51,13 @@
<th>Warehouse</th> <th>Warehouse</th>
<th class="th-zone">Zone</th> <th class="th-zone">Zone</th>
<th class="th-aisle">Aisle</th> <th class="th-aisle">Aisle</th>
<th id="th-rack">Rack</th> <th id="th-bin">Bin</th>
<th>Status</th> <th>Status</th>
<th>Label Status</th> <th>Label Status</th>
<th class="text-end">Action</th> <th class="text-end">Action</th>
</tr> </tr>
</thead> </thead>
<tbody id="rack_tbody"> <tbody id="bin_tbody">
<tr><td colspan="7" class="text-center text-muted py-4">Loading locations...</td></tr> <tr><td colspan="7" class="text-center text-muted py-4">Loading locations...</td></tr>
</tbody> </tbody>
</table> </table>
@@ -79,13 +79,13 @@
<script src="https://cdn.jsdelivr.net/npm/jsbarcode@3.11.6/dist/JsBarcode.all.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/jsbarcode@3.11.6/dist/JsBarcode.all.min.js"></script>
<script> <script>
var advanced = false; var advanced = false;
var label_rack = 'Location'; var label_bin = 'Location';
var label_zone = 'Zone'; var label_zone = 'Zone';
var label_aisle = 'Aisle'; var label_aisle = 'Aisle';
var racks = []; var bins = [];
var barcode_status = {}; var barcode_status = {};
var selected_payload = ''; var selected_payload = '';
var selected_rack_index = -1; var selected_bin_index = -1;
function escape_html(value) { function escape_html(value) {
return String(value ?? '').replace(/[&<>"']/g, function(char) { return String(value ?? '').replace(/[&<>"']/g, function(char) {
@@ -103,9 +103,9 @@
} }
function highlight_selected_row() { function highlight_selected_row() {
$('#rack_tbody tr').removeClass('table-active'); $('#bin_tbody tr').removeClass('table-active');
if (selected_rack_index >= 0) { if (selected_bin_index >= 0) {
$('#rack_tbody tr[data-rack-index="' + selected_rack_index + '"]').addClass('table-active'); $('#bin_tbody tr[data-bin-index="' + selected_bin_index + '"]').addClass('table-active');
} }
} }
@@ -119,26 +119,26 @@
onSuccess: function(res) { onSuccess: function(res) {
var s = res.output || {}; var s = res.output || {};
advanced = s.advanced_location == 1; advanced = s.advanced_location == 1;
label_rack = s.location_label_rack || (advanced ? 'Rack' : 'Location'); label_bin = s.location_label_bin || (advanced ? 'Bin' : 'Location');
label_zone = s.location_label_zone || 'Zone'; label_zone = s.location_label_zone || 'Zone';
label_aisle = s.location_label_aisle || 'Aisle'; label_aisle = s.location_label_aisle || 'Aisle';
$('.th-zone').text(label_zone).toggleClass('d-none', !advanced); $('.th-zone').text(label_zone).toggleClass('d-none', !advanced);
$('.th-aisle').text(label_aisle).toggleClass('d-none', !advanced); $('.th-aisle').text(label_aisle).toggleClass('d-none', !advanced);
$('#th-rack').text(label_rack); $('#th-bin').text(label_bin);
} }
}); });
} }
function load_racks() { function load_bins() {
return ajax_request({ return ajax_request({
url: server_url + 'reports/api/engine_report/rack_occupancy.php', url: server_url + 'reports/api/engine_report/bin_occupancy.php',
autoPrepare: true, autoPrepare: true,
checkRequired: 0, checkRequired: 0,
action: 'read', action: 'read',
queueLock: false, queueLock: false,
onSuccess: function(res) { onSuccess: function(res) {
racks = res.output || []; bins = res.output || [];
render_racks(); render_bins();
highlight_selected_row(); highlight_selected_row();
} }
}); });
@@ -157,26 +157,26 @@
(res.output || []).forEach(function(item) { (res.output || []).forEach(function(item) {
barcode_status[item.barcode] = parseInt(item.status) === 1 ? 'active' : 'disabled'; barcode_status[item.barcode] = parseInt(item.status) === 1 ? 'active' : 'disabled';
}); });
render_racks(); render_bins();
} }
}); });
} }
function location_payload(item) { function location_payload(item) {
return advanced return advanced
? `LOC|${item.warehouse_id}|${item.zone}|${item.aisle}|${item.rack}` ? `LOC|${item.warehouse_id}|${item.zone}|${item.aisle}|${item.bin}`
: `LOC|${item.warehouse_id}|${item.rack}`; : `LOC|${item.warehouse_id}|${item.bin}`;
} }
function render_racks() { function render_bins() {
var keyword = $('#search_location').val().toLowerCase(); var keyword = $('#search_location').val().toLowerCase();
var rows = racks.filter(function(item) { var rows = bins.filter(function(item) {
if (!keyword) return true; if (!keyword) return true;
return [ return [
item.warehouse_name, item.warehouse_name,
item.zone, item.zone,
item.aisle, item.aisle,
item.rack, item.bin,
item.product_sku, item.product_sku,
item.product_name, item.product_name,
item.status item.status
@@ -184,13 +184,13 @@
}); });
if (!rows.length) { if (!rows.length) {
$('#rack_tbody').html('<tr><td colspan="7" class="text-center text-muted py-4">No locations found.</td></tr>'); $('#bin_tbody').html('<tr><td colspan="7" class="text-center text-muted py-4">No locations found.</td></tr>');
return; return;
} }
var body = ''; var body = '';
rows.forEach(function(item) { rows.forEach(function(item) {
var idx = racks.indexOf(item); var idx = bins.indexOf(item);
var status = item.status === 'occupied' var status = item.status === 'occupied'
? '<span class="badge bg-warning text-dark">Occupied</span>' ? '<span class="badge bg-warning text-dark">Occupied</span>'
: '<span class="badge bg-success">Empty</span>'; : '<span class="badge bg-success">Empty</span>';
@@ -202,11 +202,11 @@
if (barcode_status[payload] === 'disabled') { if (barcode_status[payload] === 'disabled') {
labelStatus = '<span class="badge bg-danger">Disabled</span>'; labelStatus = '<span class="badge bg-danger">Disabled</span>';
} }
body += `<tr data-rack-index="${idx}"> body += `<tr data-bin-index="${idx}">
<td>${escape_html(item.warehouse_name)}</td> <td>${escape_html(item.warehouse_name)}</td>
<td class="th-zone ${advanced ? '' : 'd-none'}">${escape_html(item.zone)}</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 class="th-aisle ${advanced ? '' : 'd-none'}">${escape_html(item.aisle)}</td>
<td>${escape_html(item.rack)}</td> <td>${escape_html(item.bin)}</td>
<td>${status}</td> <td>${status}</td>
<td>${labelStatus}</td> <td>${labelStatus}</td>
<td class="text-end"> <td class="text-end">
@@ -216,7 +216,7 @@
</td> </td>
</tr>`; </tr>`;
}); });
$('#rack_tbody').html(body); $('#bin_tbody').html(body);
} }
function render_preview(item, payload) { function render_preview(item, payload) {
@@ -224,8 +224,8 @@
<div class="barcode-label mx-auto"> <div class="barcode-label mx-auto">
<div class="fw-bold fs-5">${escape_html(item.warehouse_name)}</div> <div class="fw-bold fs-5">${escape_html(item.warehouse_name)}</div>
<div class="small">${advanced <div class="small">${advanced
? `${escape_html(item.zone)} / ${escape_html(item.aisle)} / ${escape_html(item.rack)}` ? `${escape_html(item.zone)} / ${escape_html(item.aisle)} / ${escape_html(item.bin)}`
: escape_html(item.rack)}</div> : escape_html(item.bin)}</div>
<svg id="barcode_svg"></svg> <svg id="barcode_svg"></svg>
<div class="small text-muted">${escape_html(payload)}</div> <div class="small text-muted">${escape_html(payload)}</div>
</div>`); </div>`);
@@ -233,7 +233,7 @@
} }
function preview_label(index) { function preview_label(index) {
var item = racks[index]; var item = bins[index];
if (!item) return; if (!item) return;
var payload = location_payload(item); var payload = location_payload(item);
@@ -249,13 +249,13 @@
warehouse_id: item.warehouse_id, warehouse_id: item.warehouse_id,
zone: item.zone || '', zone: item.zone || '',
aisle: item.aisle || '', aisle: item.aisle || '',
rack: item.rack bin: item.bin
}, },
onSuccess: function() { onSuccess: function() {
selected_payload = payload; selected_payload = payload;
selected_rack_index = index; selected_bin_index = index;
if (!barcode_status[payload]) barcode_status[payload] = 'active'; if (!barcode_status[payload]) barcode_status[payload] = 'active';
render_racks(); render_bins();
refresh_header_buttons(); refresh_header_buttons();
highlight_selected_row(); highlight_selected_row();
render_preview(item, payload); render_preview(item, payload);
@@ -303,9 +303,9 @@
onSuccess: function() { onSuccess: function() {
barcode_status[selected_payload] = 'disabled'; barcode_status[selected_payload] = 'disabled';
selected_payload = ''; selected_payload = '';
selected_rack_index = -1; selected_bin_index = -1;
$('#label_preview').html('Select a location to preview its label.'); $('#label_preview').html('Select a location to preview its label.');
render_racks(); render_bins();
refresh_header_buttons(); refresh_header_buttons();
highlight_selected_row(); highlight_selected_row();
} }
@@ -316,8 +316,8 @@
function reenable_selected_label() { function reenable_selected_label() {
if (!selected_payload) return; if (!selected_payload) return;
var index = selected_rack_index; var index = selected_bin_index;
var item = racks[index]; var item = bins[index];
if (!item) return; if (!item) return;
ajax_request({ ajax_request({
@@ -329,7 +329,7 @@
data: { label_action: 'enable', barcode: selected_payload }, data: { label_action: 'enable', barcode: selected_payload },
onSuccess: function() { onSuccess: function() {
barcode_status[selected_payload] = 'active'; barcode_status[selected_payload] = 'active';
render_racks(); render_bins();
refresh_header_buttons(); refresh_header_buttons();
highlight_selected_row(); highlight_selected_row();
render_preview(item, selected_payload); render_preview(item, selected_payload);
@@ -338,7 +338,7 @@
} }
function enable_label_for_row(index) { function enable_label_for_row(index) {
var item = racks[index]; var item = bins[index];
if (!item) return; if (!item) return;
var payload = location_payload(item); var payload = location_payload(item);
@@ -352,8 +352,8 @@
onSuccess: function() { onSuccess: function() {
barcode_status[payload] = 'active'; barcode_status[payload] = 'active';
selected_payload = payload; selected_payload = payload;
selected_rack_index = index; selected_bin_index = index;
render_racks(); render_bins();
refresh_header_buttons(); refresh_header_buttons();
highlight_selected_row(); highlight_selected_row();
render_preview(item, payload); render_preview(item, payload);
@@ -361,11 +361,11 @@
}); });
} }
$('#search_location').on('input', render_racks); $('#search_location').on('input', render_bins);
$(async function() { $(async function() {
await load_config(); await load_config();
await load_racks(); await load_bins();
await load_barcode_statuses(); await load_barcode_statuses();
}); });
</script> </script>
+33 -33
View File
@@ -97,10 +97,10 @@
</select> </select>
</div> </div>
<div class="mb-3 col-lg-6" id="group_rack"> <div class="mb-3 col-lg-6" id="group_bin">
<label for="rack" class="form-label" id="label_rack">Rack</label> <label for="bin" class="form-label" id="label_bin">Bin</label>
<select name="rack" id="rack" class="form-select" required> <select name="bin" id="bin" class="form-select" required>
<option value="">Please select rack</option> <option value="">Please select bin</option>
</select> </select>
</div> </div>
@@ -137,7 +137,7 @@
// custom.js globals: set_select_value(), load_location_config(), init_lot_search_inputs() // custom.js globals: set_select_value(), load_location_config(), init_lot_search_inputs()
var advanced = false; var advanced = false;
var label_rack = 'Location'; var label_bin = 'Location';
var label_zone = 'Zone'; var label_zone = 'Zone';
var label_aisle = 'Aisle'; var label_aisle = 'Aisle';
var scan_sku_ready = false; var scan_sku_ready = false;
@@ -150,15 +150,15 @@
$('#group_aisle').show(); $('#group_aisle').show();
$('#label_zone').text(label_zone); $('#label_zone').text(label_zone);
$('#label_aisle').text(label_aisle); $('#label_aisle').text(label_aisle);
$('#label_rack').text(label_rack); $('#label_bin').text(label_bin);
$('#zone').attr('placeholder', 'Please select ' + label_zone.toLowerCase()); $('#zone').attr('placeholder', 'Please select ' + label_zone.toLowerCase());
$('#aisle').attr('placeholder', 'Please select ' + label_aisle.toLowerCase()); $('#aisle').attr('placeholder', 'Please select ' + label_aisle.toLowerCase());
$('#rack').attr('placeholder', 'Please select ' + label_rack.toLowerCase()); $('#bin').attr('placeholder', 'Please select ' + label_bin.toLowerCase());
} else { } else {
$('#group_zone').hide(); $('#group_zone').hide();
$('#group_aisle').hide(); $('#group_aisle').hide();
$('#label_rack').text(label_rack); $('#label_bin').text(label_bin);
$('#rack').attr('placeholder', 'Please select ' + label_rack.toLowerCase()); $('#bin').attr('placeholder', 'Please select ' + label_bin.toLowerCase());
} }
} }
@@ -174,7 +174,7 @@
$('#warehouse').val(''); $('#warehouse').val('');
$('#zone').val(''); $('#zone').val('');
$('#aisle').val(''); $('#aisle').val('');
$('#rack').val(''); $('#bin').val('');
$('#product_sku').trigger('blur'); $('#product_sku').trigger('blur');
} }
@@ -189,7 +189,7 @@
set_select_value('#zone', data.zone, data.zone); set_select_value('#zone', data.zone, data.zone);
set_select_value('#aisle', data.aisle, data.aisle); set_select_value('#aisle', data.aisle, data.aisle);
} }
set_select_value('#rack', data.rack, data.rack); set_select_value('#bin', data.bin, data.bin);
scan_location_ready = true; scan_location_ready = true;
}); });
} }
@@ -207,7 +207,7 @@
warehouse_id: data.warehouse_id, warehouse_id: data.warehouse_id,
zone: data.zone, zone: data.zone,
aisle: data.aisle, aisle: data.aisle,
rack: data.rack, bin: data.bin,
product_sku: $('#product_sku').attr('secondary') || $('#product_sku').val(), product_sku: $('#product_sku').attr('secondary') || $('#product_sku').val(),
lot_number: $('#lot_number').val(), lot_number: $('#lot_number').val(),
serial_number: $('#serial_number').val() serial_number: $('#serial_number').val()
@@ -230,7 +230,7 @@
$('#warehouse').val(''); $('#warehouse').val('');
$('#zone').val(''); $('#zone').val('');
$('#aisle').val(''); $('#aisle').val('');
$('#rack').val(''); $('#bin').val('');
scan_sku_ready = false; scan_sku_ready = false;
scan_location_ready = false; scan_location_ready = false;
$('#product_sku').focus(); $('#product_sku').focus();
@@ -241,7 +241,7 @@
bootbox.alert('Scan the SKU label first.'); bootbox.alert('Scan the SKU label first.');
return; return;
} }
if (!scan_location_ready && !$('#rack').val()) { if (!scan_location_ready && !$('#bin').val()) {
bootbox.alert('Scan the location label before saving.'); bootbox.alert('Scan the location label before saving.');
return; return;
} }
@@ -250,17 +250,17 @@
function manage_stock_in() { function manage_stock_in() {
var rack_val = $('#rack').val(); var bin_val = $('#bin').val();
var zone_val = advanced ? $('#zone').val() : rack_val; var zone_val = advanced ? $('#zone').val() : bin_val;
var aisle_val = advanced ? $('#aisle').val() : rack_val; var aisle_val = advanced ? $('#aisle').val() : bin_val;
var warehouse_val = $('#warehouse').val(); var warehouse_val = $('#warehouse').val();
var sku_val = $('#product_sku').attr('secondary') || $('#product_sku').val(); var sku_val = $('#product_sku').attr('secondary') || $('#product_sku').val();
var quantity_val = $('#quantity').val(); var quantity_val = $('#quantity').val();
// Mirror to hidden inputs for autoPrepare consistency (simple mode) // Mirror to hidden inputs for autoPrepare consistency (simple mode)
if (!advanced) { if (!advanced) {
$('#zone').val(rack_val); $('#zone').val(bin_val);
$('#aisle').val(rack_val); $('#aisle').val(bin_val);
} }
return ajax_request({ return ajax_request({
@@ -275,7 +275,7 @@
'serial_number': $('#serial_number').val(), 'serial_number': $('#serial_number').val(),
'zone': zone_val, 'zone': zone_val,
'aisle': aisle_val, 'aisle': aisle_val,
'rack': rack_val, 'bin': bin_val,
'product_sku': sku_val, 'product_sku': sku_val,
'quantity': quantity_val, 'quantity': quantity_val,
'warehouse': warehouse_val, 'warehouse': warehouse_val,
@@ -398,10 +398,10 @@
function retrieve_rack(type, callback) { function retrieve_bin(type, callback) {
return ajax_request({ return ajax_request({
url: "<?php echo $server_url?>ics/api/engine/retrieve_rack.php", url: "<?php echo $server_url?>ics/api/engine/retrieve_bin.php",
autoPrepare: true, autoPrepare: true,
checkRequired: 0, checkRequired: 0,
noLoading: true, noLoading: true,
@@ -411,11 +411,11 @@
}, },
action: 'read', action: 'read',
onSuccess: function(res) { onSuccess: function(res) {
var option = '<option value="">Please select ' + label_rack.toLowerCase() + '</option>'; var option = '<option value="">Please select ' + label_bin.toLowerCase() + '</option>';
$.each(res.output, function(key, item) { $.each(res.output, function(key, item) {
option += '<option value="' + item + '">' + item + '</option>'; option += '<option value="' + item + '">' + item + '</option>';
}); });
$('select#rack').html(option); $('select#bin').html(option);
if (callback) callback(); // ← add this if (callback) callback(); // ← add this
} }
@@ -445,7 +445,7 @@
function populate_fields() { function populate_fields() {
$.each(data, function(key, item) { $.each(data, function(key, item) {
if (!['warehouse', 'zone', 'aisle', 'rack'].includes(key)) { if (!['warehouse', 'zone', 'aisle', 'bin'].includes(key)) {
$('#' + key).val(item); $('#' + key).val(item);
} }
}); });
@@ -478,16 +478,16 @@
$('select#zone').val(data.zone).attr('disabled', true); $('select#zone').val(data.zone).attr('disabled', true);
retrieve_aisle('to', function() { retrieve_aisle('to', function() {
$('#aisle').val(data.aisle).attr('disabled', true); $('#aisle').val(data.aisle).attr('disabled', true);
retrieve_rack('to', function() { retrieve_bin('to', function() {
$('#rack').val(data.rack).attr('disabled', true); $('#bin').val(data.bin).attr('disabled', true);
populate_fields(); populate_fields();
}); });
}); });
}); });
} else { } else {
// Simple mode — skip zone/aisle cascade // Simple mode — skip zone/aisle cascade
retrieve_rack('to', function() { retrieve_bin('to', function() {
$('#rack').val(data.rack).attr('disabled', true); $('#bin').val(data.bin).attr('disabled', true);
populate_fields(); populate_fields();
}); });
} }
@@ -536,11 +536,11 @@
if (advanced) { if (advanced) {
retrieve_zone('to', function() { retrieve_zone('to', function() {
retrieve_aisle('to', function() { retrieve_aisle('to', function() {
retrieve_rack('to'); retrieve_bin('to');
}); });
}); });
} else { } else {
retrieve_rack('to'); retrieve_bin('to');
} }
} }
@@ -615,7 +615,7 @@
if (advanced) { if (advanced) {
retrieve_zone('to'); retrieve_zone('to');
} else { } else {
retrieve_rack('to'); retrieve_bin('to');
} }
}); });
@@ -624,7 +624,7 @@
}); });
$("select#aisle").change(function() { $("select#aisle").change(function() {
if (advanced) retrieve_rack('to'); if (advanced) retrieve_bin('to');
}); });
<?php } ?> <?php } ?>
+32 -32
View File
@@ -85,11 +85,11 @@
</select> </select>
</div> </div>
<!-- Step 7: Rack --> <!-- Step 7: Bin -->
<div class="mb-3 col-lg-6" id="group_rack"> <div class="mb-3 col-lg-6" id="group_bin">
<label for="rack" class="form-label" id="label_rack">Rack</label> <label for="bin" class="form-label" id="label_bin">Bin</label>
<select name="rack" id="rack" class="form-select" required disabled> <select name="bin" id="bin" class="form-select" required disabled>
<option value="">Please select rack</option> <option value="">Please select bin</option>
</select> </select>
</div> </div>
@@ -124,7 +124,7 @@
// custom.js globals: set_select_value(), load_location_config() // custom.js globals: set_select_value(), load_location_config()
var advanced = false; var advanced = false;
var label_rack = 'Location'; var label_bin = 'Location';
var label_zone = 'Zone'; var label_zone = 'Zone';
var label_aisle = 'Aisle'; var label_aisle = 'Aisle';
var scan_sku_ready = false; var scan_sku_ready = false;
@@ -157,7 +157,7 @@
set_select_value('#zone', data.zone, data.zone); set_select_value('#zone', data.zone, data.zone);
set_select_value('#aisle', data.aisle, data.aisle); set_select_value('#aisle', data.aisle, data.aisle);
} }
set_select_value('#rack', data.rack, data.rack); set_select_value('#bin', data.bin, data.bin);
scan_location_ready = true; scan_location_ready = true;
}); });
} }
@@ -175,7 +175,7 @@
warehouse_id: data.warehouse_id, warehouse_id: data.warehouse_id,
zone: data.zone, zone: data.zone,
aisle: data.aisle, aisle: data.aisle,
rack: data.rack, bin: data.bin,
product_sku: $('#product_sku').attr('secondary') || $('#product_sku').val(), product_sku: $('#product_sku').attr('secondary') || $('#product_sku').val(),
lot_number: $('#lot_number').val(), lot_number: $('#lot_number').val(),
serial_number: $('#serial_number').val() serial_number: $('#serial_number').val()
@@ -204,7 +204,7 @@
bootbox.alert('Scan the SKU label first.'); bootbox.alert('Scan the SKU label first.');
return; return;
} }
if (!scan_location_ready && !$('#rack').val()) { if (!scan_location_ready && !$('#bin').val()) {
bootbox.alert('Scan the location label before saving.'); bootbox.alert('Scan the location label before saving.');
return; return;
} }
@@ -218,22 +218,22 @@
$('#group_aisle').show(); $('#group_aisle').show();
$('#label_zone').text(label_zone); $('#label_zone').text(label_zone);
$('#label_aisle').text(label_aisle); $('#label_aisle').text(label_aisle);
$('#label_rack').text(label_rack); $('#label_bin').text(label_bin);
} else { } else {
$('#group_zone').hide(); $('#group_zone').hide();
$('#group_aisle').hide(); $('#group_aisle').hide();
$('#label_rack').text(label_rack); $('#label_bin').text(label_bin);
} }
} }
function manage_stock_out() { function manage_stock_out() {
var rack_val = $('#rack').val(); var bin_val = $('#bin').val();
var sku_val = $('#product_sku').attr('secondary') || $('#product_sku').val(); var sku_val = $('#product_sku').attr('secondary') || $('#product_sku').val();
var wh_val = $('#warehouse').val(); var wh_val = $('#warehouse').val();
if (!advanced) { if (!advanced) {
$('#zone').val(rack_val); $('#zone').val(bin_val);
$('#aisle').val(rack_val); $('#aisle').val(bin_val);
} }
return ajax_request({ return ajax_request({
@@ -243,9 +243,9 @@
debugMode: 0, debugMode: 0,
data: { data: {
'contact_id': $('#contact').attr('data-id'), 'contact_id': $('#contact').attr('data-id'),
'zone': advanced ? $('#zone').val() : rack_val, 'zone': advanced ? $('#zone').val() : bin_val,
'aisle': advanced ? $('#aisle').val() : rack_val, 'aisle': advanced ? $('#aisle').val() : bin_val,
'rack': rack_val, 'bin': bin_val,
'product_sku': sku_val, 'product_sku': sku_val,
'warehouse': wh_val, 'warehouse': wh_val,
'lot_number': $('#lot_number').val(), 'lot_number': $('#lot_number').val(),
@@ -391,7 +391,7 @@
} }
// ── Steps 5-7: Zone → Aisle → Rack ─────────────────────────────────── // ── Steps 5-7: Zone → Aisle → Bin ───────────────────────────────────
function retrieve_zone(callback) { function retrieve_zone(callback) {
return ajax_request({ return ajax_request({
@@ -447,10 +447,10 @@
}); });
} }
function retrieve_rack(callback) { function retrieve_bin(callback) {
return ajax_request({ return ajax_request({
url: "<?php echo $server_url?>ics/api/engine/retrieve_rack.php", url: "<?php echo $server_url?>ics/api/engine/retrieve_bin.php",
autoPrepare: true, autoPrepare: true,
checkRequired: 0, checkRequired: 0,
noLoading: true, noLoading: true,
@@ -466,11 +466,11 @@
}, },
action: 'read', action: 'read',
onSuccess: function(res) { onSuccess: function(res) {
var option = '<option value="">Please select ' + label_rack.toLowerCase() + '</option>'; var option = '<option value="">Please select ' + label_bin.toLowerCase() + '</option>';
$.each(res.output, function(key, item) { $.each(res.output, function(key, item) {
option += `<option value='${item}'>${item}</option>`; option += `<option value='${item}'>${item}</option>`;
}); });
$('#rack').html(option).prop('disabled', false); $('#bin').html(option).prop('disabled', false);
if (callback) callback(); if (callback) callback();
} }
}); });
@@ -482,7 +482,7 @@
$('#warehouse').html('<option value="">Select warehouse...</option>').prop('disabled', true); $('#warehouse').html('<option value="">Select warehouse...</option>').prop('disabled', true);
$('#zone').html('<option value="">Please select ' + label_zone.toLowerCase() + '</option>').prop('disabled', true); $('#zone').html('<option value="">Please select ' + label_zone.toLowerCase() + '</option>').prop('disabled', true);
$('#aisle').html('<option value="">Please select ' + label_aisle.toLowerCase() + '</option>').prop('disabled', true); $('#aisle').html('<option value="">Please select ' + label_aisle.toLowerCase() + '</option>').prop('disabled', true);
$('#rack').html('<option value="">Please select ' + label_rack.toLowerCase() + '</option>').prop('disabled', true); $('#bin').html('<option value="">Please select ' + label_bin.toLowerCase() + '</option>').prop('disabled', true);
} }
@@ -513,7 +513,7 @@
function populate_fields() { function populate_fields() {
$.each(data, function(key, item) { $.each(data, function(key, item) {
if (!['warehouse', 'zone', 'aisle', 'rack', 'lot_number', 'serial_number'].includes(key)) { if (!['warehouse', 'zone', 'aisle', 'bin', 'lot_number', 'serial_number'].includes(key)) {
$('#' + key).val(item); $('#' + key).val(item);
} }
}); });
@@ -534,16 +534,16 @@
$('#zone').val(data.zone).prop('disabled', true); $('#zone').val(data.zone).prop('disabled', true);
retrieve_aisle(function() { retrieve_aisle(function() {
$('#aisle').val(data.aisle).prop('disabled', true); $('#aisle').val(data.aisle).prop('disabled', true);
retrieve_rack(function() { retrieve_bin(function() {
$('#rack').val(data.rack).prop('disabled', true); $('#bin').val(data.bin).prop('disabled', true);
populate_fields(); populate_fields();
}); });
}); });
}); });
} else { } else {
// Simple mode — skip zone/aisle cascade // Simple mode — skip zone/aisle cascade
retrieve_rack(function() { retrieve_bin(function() {
$('#rack').val(data.rack).prop('disabled', true); $('#bin').val(data.bin).prop('disabled', true);
populate_fields(); populate_fields();
}); });
} }
@@ -600,12 +600,12 @@
retrieve_warehouse(); retrieve_warehouse();
}); });
// Warehouse selected → load zone (advanced) or rack directly (simple) // Warehouse selected → load zone (advanced) or bin directly (simple)
$('#warehouse').on('change', function() { $('#warehouse').on('change', function() {
if (advanced) { if (advanced) {
retrieve_zone(); retrieve_zone();
} else { } else {
retrieve_rack(); retrieve_bin();
} }
}); });
@@ -614,9 +614,9 @@
if (advanced) retrieve_aisle(); if (advanced) retrieve_aisle();
}); });
// Aisle selected → load rack (advanced only) // Aisle selected → load bin (advanced only)
$('#aisle').on('change', function() { $('#aisle').on('change', function() {
if (advanced) retrieve_rack(); if (advanced) retrieve_bin();
}); });
<?php } ?> <?php } ?>
+56 -56
View File
@@ -87,9 +87,9 @@
</select> </select>
</div> </div>
<div class="mb-3 col-lg-3"> <div class="mb-3 col-lg-3">
<label class="form-label" id="label_rack_from">From Rack</label> <label class="form-label" id="label_bin_from">From Bin</label>
<select name="rack" id="rack_from" role="from" class="form-select" required disabled> <select name="bin" id="bin_from" role="from" class="form-select" required disabled>
<option value="">Please select rack</option> <option value="">Please select bin</option>
</select> </select>
</div> </div>
@@ -119,9 +119,9 @@
</select> </select>
</div> </div>
<div class="mb-3 col-lg-3"> <div class="mb-3 col-lg-3">
<label class="form-label" id="label_rack_to">To Rack</label> <label class="form-label" id="label_bin_to">To Bin</label>
<select name="rack" id="rack_to" role="to" class="form-select" required> <select name="bin" id="bin_to" role="to" class="form-select" required>
<option value="">Please select rack</option> <option value="">Please select bin</option>
</select> </select>
</div> </div>
@@ -157,7 +157,7 @@
// ── Location config ─────────────────────────────────────────────────────── // ── Location config ───────────────────────────────────────────────────────
var advanced = false; var advanced = false;
var label_rack = 'Location'; var label_bin = 'Location';
var label_zone = 'Zone'; var label_zone = 'Zone';
var label_aisle = 'Aisle'; var label_aisle = 'Aisle';
var scan_sku_ready = false; var scan_sku_ready = false;
@@ -200,7 +200,7 @@
set_select_value('#zone_' + role, data.zone, data.zone); set_select_value('#zone_' + role, data.zone, data.zone);
set_select_value('#aisle_' + role, data.aisle, data.aisle); set_select_value('#aisle_' + role, data.aisle, data.aisle);
} }
set_select_value('#rack_' + role, data.rack, data.rack); set_select_value('#bin_' + role, data.bin, data.bin);
if (role === 'from') { if (role === 'from') {
scan_from_ready = true; scan_from_ready = true;
@@ -214,7 +214,7 @@
return String($('#warehouse_from').val()) === String(data.warehouse_id) return String($('#warehouse_from').val()) === String(data.warehouse_id)
&& String($('#zone_from').val()) === String(data.zone) && String($('#zone_from').val()) === String(data.zone)
&& String($('#aisle_from').val()) === String(data.aisle) && String($('#aisle_from').val()) === String(data.aisle)
&& String($('#rack_from').val()) === String(data.rack); && String($('#bin_from').val()) === String(data.bin);
} }
function validate_scanned_location(role, data, onSuccess) { function validate_scanned_location(role, data, onSuccess) {
@@ -230,7 +230,7 @@
warehouse_id: data.warehouse_id, warehouse_id: data.warehouse_id,
zone: data.zone, zone: data.zone,
aisle: data.aisle, aisle: data.aisle,
rack: data.rack, bin: data.bin,
product_sku: $('#product_sku').attr('secondary') || $('#product_sku').val(), product_sku: $('#product_sku').attr('secondary') || $('#product_sku').val(),
lot_number: $('#lot_number').val(), lot_number: $('#lot_number').val(),
serial_number: $('#serial_number').val() serial_number: $('#serial_number').val()
@@ -243,7 +243,7 @@
$('#warehouse_to').val(''); $('#warehouse_to').val('');
$('#zone_to').html('<option value="">Please select ' + label_zone.toLowerCase() + '</option>').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(''); $('#aisle_to').html('<option value="">Please select ' + label_aisle.toLowerCase() + '</option>').val('');
$('#rack_to').html('<option value="">Please select ' + label_rack.toLowerCase() + '</option>').val(''); $('#bin_to').html('<option value="">Please select ' + label_bin.toLowerCase() + '</option>').val('');
} }
function reset_stock_transfer_inputs() { function reset_stock_transfer_inputs() {
@@ -269,11 +269,11 @@
bootbox.alert('Scan the SKU label first.'); bootbox.alert('Scan the SKU label first.');
return; return;
} }
if (!scan_from_ready && !$('#rack_from').val()) { if (!scan_from_ready && !$('#bin_from').val()) {
bootbox.alert('Scan the from-location label before saving.'); bootbox.alert('Scan the from-location label before saving.');
return; return;
} }
if (!scan_to_ready && !$('#rack_to').val()) { if (!scan_to_ready && !$('#bin_to').val()) {
bootbox.alert('Scan the to-location label before saving.'); bootbox.alert('Scan the to-location label before saving.');
return; return;
} }
@@ -290,19 +290,19 @@
$('#label_zone_to').text('To ' + label_zone); $('#label_zone_to').text('To ' + label_zone);
$('#label_aisle_from').text('From ' + label_aisle); $('#label_aisle_from').text('From ' + label_aisle);
$('#label_aisle_to').text('To ' + label_aisle); $('#label_aisle_to').text('To ' + label_aisle);
$('#label_rack_from').text('From ' + label_rack); $('#label_bin_from').text('From ' + label_bin);
$('#label_rack_to').text('To ' + label_rack); $('#label_bin_to').text('To ' + label_bin);
// Update select placeholders // Update select placeholders
$('#zone_from, #zone_to').find('option:first').text('Please select ' + label_zone.toLowerCase()); $('#zone_from, #zone_to').find('option:first').text('Please select ' + label_zone.toLowerCase());
$('#aisle_from, #aisle_to').find('option:first').text('Please select ' + label_aisle.toLowerCase()); $('#aisle_from, #aisle_to').find('option:first').text('Please select ' + label_aisle.toLowerCase());
$('#rack_from, #rack_to').find('option:first').text('Please select ' + label_rack.toLowerCase()); $('#bin_from, #bin_to').find('option:first').text('Please select ' + label_bin.toLowerCase());
} else { } else {
$('#group_zone_from, #group_zone_to').hide(); $('#group_zone_from, #group_zone_to').hide();
$('#group_aisle_from, #group_aisle_to').hide(); $('#group_aisle_from, #group_aisle_to').hide();
$('#zone_from, #zone_to, #aisle_from, #aisle_to').prop('required', false).removeClass('is-invalid is-valid'); $('#zone_from, #zone_to, #aisle_from, #aisle_to').prop('required', false).removeClass('is-invalid is-valid');
$('#label_rack_from').text('From ' + label_rack); $('#label_bin_from').text('From ' + label_bin);
$('#label_rack_to').text('To ' + label_rack); $('#label_bin_to').text('To ' + label_bin);
$('#rack_from, #rack_to').find('option:first').text('Please select ' + label_rack.toLowerCase()); $('#bin_from, #bin_to').find('option:first').text('Please select ' + label_bin.toLowerCase());
} }
} }
@@ -310,16 +310,16 @@
// ── Submit ──────────────────────────────────────────────────────────────── // ── Submit ────────────────────────────────────────────────────────────────
function manage_stock_transfer() { function manage_stock_transfer() {
var rack_from_val = $('#rack_from').val(); var bin_from_val = $('#bin_from').val();
var rack_to_val = $('#rack_to').val(); var bin_to_val = $('#bin_to').val();
var sku_val = $('#product_sku').attr('secondary') || $('#product_sku').val(); var sku_val = $('#product_sku').attr('secondary') || $('#product_sku').val();
// Simple mode — mirror rack value to zone and aisle before submit // Simple mode — mirror bin value to zone and aisle before submit
if (!advanced) { if (!advanced) {
$('#zone_from').val(rack_from_val); $('#zone_from').val(bin_from_val);
$('#aisle_from').val(rack_from_val); $('#aisle_from').val(bin_from_val);
$('#zone_to').val(rack_to_val); $('#zone_to').val(bin_to_val);
$('#aisle_to').val(rack_to_val); $('#aisle_to').val(bin_to_val);
} }
return ajax_request({ return ajax_request({
@@ -330,12 +330,12 @@
'contact_id': $('#contact').attr('data-id'), 'contact_id': $('#contact').attr('data-id'),
'warehouse_from': $('#warehouse_from').val(), 'warehouse_from': $('#warehouse_from').val(),
'warehouse_to': $('#warehouse_to').val(), 'warehouse_to': $('#warehouse_to').val(),
'zone_from': advanced ? $('#zone_from').val() : rack_from_val, 'zone_from': advanced ? $('#zone_from').val() : bin_from_val,
'aisle_from': advanced ? $('#aisle_from').val() : rack_from_val, 'aisle_from': advanced ? $('#aisle_from').val() : bin_from_val,
'rack_from': rack_from_val, 'bin_from': bin_from_val,
'zone_to': advanced ? $('#zone_to').val() : rack_to_val, 'zone_to': advanced ? $('#zone_to').val() : bin_to_val,
'aisle_to': advanced ? $('#aisle_to').val() : rack_to_val, 'aisle_to': advanced ? $('#aisle_to').val() : bin_to_val,
'rack_to': rack_to_val, 'bin_to': bin_to_val,
'product_sku': sku_val, 'product_sku': sku_val,
'lot_number': $('#lot_number').val(), 'lot_number': $('#lot_number').val(),
'serial_number': $('#serial_number').val(), 'serial_number': $('#serial_number').val(),
@@ -467,7 +467,7 @@
} }
// ── Zone / Aisle / Rack loaders ─────────────────────────────────────────── // ── Zone / Aisle / Bin loaders ───────────────────────────────────────────
function retrieve_zone(type, callback) { function retrieve_zone(type, callback) {
var extra = { var extra = {
'warehouse_from': $('#warehouse_from').val(), 'warehouse_from': $('#warehouse_from').val(),
@@ -532,7 +532,7 @@
}); });
} }
function retrieve_rack(type, callback) { function retrieve_bin(type, callback) {
var extra = { var extra = {
'warehouse_from': $('#warehouse_from').val(), 'warehouse_from': $('#warehouse_from').val(),
'warehouse_to': $('#warehouse_to').val(), 'warehouse_to': $('#warehouse_to').val(),
@@ -549,7 +549,7 @@
}); });
} }
return ajax_request({ return ajax_request({
url: '<?php echo $server_url?>ics/api/engine/retrieve_rack.php', url: '<?php echo $server_url?>ics/api/engine/retrieve_bin.php',
autoPrepare: true, autoPrepare: true,
checkRequired: 0, checkRequired: 0,
noLoading: true, noLoading: true,
@@ -557,11 +557,11 @@
data: Object.assign({ 'type': type }, extra), data: Object.assign({ 'type': type }, extra),
action: 'read', action: 'read',
onSuccess: function(res) { onSuccess: function(res) {
var option = '<option value="">Please select ' + label_rack.toLowerCase() + '</option>'; var option = '<option value="">Please select ' + label_bin.toLowerCase() + '</option>';
$.each(res.output, function(key, item) { $.each(res.output, function(key, item) {
option += '<option value="' + item + '">' + item + '</option>'; option += '<option value="' + item + '">' + item + '</option>';
}); });
$('select[name="rack"][role="' + type + '"]').html(option).prop('disabled', false); $('select[name="bin"][role="' + type + '"]').html(option).prop('disabled', false);
if (callback) callback(); if (callback) callback();
} }
}); });
@@ -573,7 +573,7 @@
$('#warehouse_from').html('<option value="">Select warehouse...</option>').prop('disabled', true); $('#warehouse_from').html('<option value="">Select warehouse...</option>').prop('disabled', true);
$('#zone_from').html('<option value="">Please select ' + label_zone.toLowerCase() + '</option>').prop('disabled', true); $('#zone_from').html('<option value="">Please select ' + label_zone.toLowerCase() + '</option>').prop('disabled', true);
$('#aisle_from').html('<option value="">Please select ' + label_aisle.toLowerCase() + '</option>').prop('disabled', true); $('#aisle_from').html('<option value="">Please select ' + label_aisle.toLowerCase() + '</option>').prop('disabled', true);
$('#rack_from').html('<option value="">Please select ' + label_rack.toLowerCase() + '</option>').prop('disabled', true); $('#bin_from').html('<option value="">Please select ' + label_bin.toLowerCase() + '</option>').prop('disabled', true);
} }
@@ -616,23 +616,23 @@
$('select[name="zone"][role="from"]').val(data.zone).prop('disabled', true); $('select[name="zone"][role="from"]').val(data.zone).prop('disabled', true);
retrieve_aisle('from', function() { retrieve_aisle('from', function() {
$('select[name="aisle"][role="from"]').val(data.aisle).prop('disabled', true); $('select[name="aisle"][role="from"]').val(data.aisle).prop('disabled', true);
retrieve_rack('from', function() { retrieve_bin('from', function() {
$('select[name="rack"][role="from"]').val(data.rack).prop('disabled', true); $('select[name="bin"][role="from"]').val(data.bin).prop('disabled', true);
populate_to_side(data, to_wh); populate_to_side(data, to_wh);
}); });
}); });
}); });
} else { } else {
// Simple mode — skip zone/aisle cascade, load rack directly // Simple mode — skip zone/aisle cascade, load bin directly
retrieve_rack('from', function() { retrieve_bin('from', function() {
$('select[name="rack"][role="from"]').val(data.rack).prop('disabled', true); $('select[name="bin"][role="from"]').val(data.bin).prop('disabled', true);
populate_to_side_simple(data, to_wh); populate_to_side_simple(data, to_wh);
}); });
} }
// Common fields // Common fields
$.each(data, function(key, item) { $.each(data, function(key, item) {
if (!['warehouse', 'zone', 'aisle', 'rack', 'lot_number', 'serial_number', 'ref'].includes(key)) { if (!['warehouse', 'zone', 'aisle', 'bin', 'lot_number', 'serial_number', 'ref'].includes(key)) {
$('#' + key).val(item); $('#' + key).val(item);
} }
}); });
@@ -655,16 +655,16 @@
$('select[name="zone"][role="to"]').val(data.ref.zone).prop('disabled', true); $('select[name="zone"][role="to"]').val(data.ref.zone).prop('disabled', true);
retrieve_aisle('to', function() { retrieve_aisle('to', function() {
$('select[name="aisle"][role="to"]').val(data.ref.aisle).prop('disabled', true); $('select[name="aisle"][role="to"]').val(data.ref.aisle).prop('disabled', true);
retrieve_rack('to', function() { retrieve_bin('to', function() {
$('select[name="rack"][role="to"]').val(data.ref.rack).prop('disabled', true); $('select[name="bin"][role="to"]').val(data.ref.bin).prop('disabled', true);
}); });
}); });
}); });
} }
function populate_to_side_simple(data, to_wh) { function populate_to_side_simple(data, to_wh) {
retrieve_rack('to', function() { retrieve_bin('to', function() {
$('select[name="rack"][role="to"]').val(data.ref.rack).prop('disabled', true); $('select[name="bin"][role="to"]').val(data.ref.bin).prop('disabled', true);
}); });
} }
@@ -717,9 +717,9 @@
retrieve_warehouse('from'); retrieve_warehouse('from');
}); });
// From warehouse → zone (advanced) or rack directly (simple) // From warehouse → zone (advanced) or bin directly (simple)
$('select[name="warehouse"][role="from"]').on('change', function() { $('select[name="warehouse"][role="from"]').on('change', function() {
if (advanced) { retrieve_zone('from'); } else { retrieve_rack('from'); } if (advanced) { retrieve_zone('from'); } else { retrieve_bin('from'); }
}); });
// From zone → aisle (advanced only) // From zone → aisle (advanced only)
@@ -727,14 +727,14 @@
if (advanced) retrieve_aisle('from'); if (advanced) retrieve_aisle('from');
}); });
// From aisle → rack (advanced only) // From aisle → bin (advanced only)
$('select[name="aisle"][role="from"]').on('change', function() { $('select[name="aisle"][role="from"]').on('change', function() {
if (advanced) retrieve_rack('from'); if (advanced) retrieve_bin('from');
}); });
// To warehouse → zone (advanced) or rack directly (simple) // To warehouse → zone (advanced) or bin directly (simple)
$('select[name="warehouse"][role="to"]').on('change', function() { $('select[name="warehouse"][role="to"]').on('change', function() {
if (advanced) { retrieve_zone('to'); } else { retrieve_rack('to'); } if (advanced) { retrieve_zone('to'); } else { retrieve_bin('to'); }
}); });
// To zone → aisle (advanced only) // To zone → aisle (advanced only)
@@ -742,9 +742,9 @@
if (advanced) retrieve_aisle('to'); if (advanced) retrieve_aisle('to');
}); });
// To aisle → rack (advanced only) // To aisle → bin (advanced only)
$('select[name="aisle"][role="to"]').on('change', function() { $('select[name="aisle"][role="to"]').on('change', function() {
if (advanced) retrieve_rack('to'); if (advanced) retrieve_bin('to');
}); });
<?php } ?> <?php } ?>
+7 -7
View File
@@ -64,7 +64,7 @@
<th>Source</th> <th>Source</th>
<th class="th_loc_zone">Zone</th> <th class="th_loc_zone">Zone</th>
<th class="th_loc_aisle">Aisle</th> <th class="th_loc_aisle">Aisle</th>
<th class="th_loc_rack">Rack</th> <th class="th_loc_bin">Bin</th>
<th>Status</th> <th>Status</th>
<th>Action</th> <th>Action</th>
</tr> </tr>
@@ -90,7 +90,7 @@
<script> <script>
var advanced = false; var advanced = false;
var label_rack = 'Location'; var label_bin = 'Location';
var label_zone = 'Zone'; var label_zone = 'Zone';
var label_aisle = 'Aisle'; var label_aisle = 'Aisle';
@@ -98,10 +98,10 @@
if (advanced) { if (advanced) {
$('.th_loc_zone').text(label_zone).show(); $('.th_loc_zone').text(label_zone).show();
$('.th_loc_aisle').text(label_aisle).show(); $('.th_loc_aisle').text(label_aisle).show();
$('.th_loc_rack').text(label_rack); $('.th_loc_bin').text(label_bin);
} else { } else {
$('.th_loc_zone, .th_loc_aisle').hide(); $('.th_loc_zone, .th_loc_aisle').hide();
$('.th_loc_rack').text(label_rack); $('.th_loc_bin').text(label_bin);
} }
} }
@@ -109,9 +109,9 @@
if (advanced) { if (advanced) {
return `<td class="py-3">${item['zone'] || ''}</td> return `<td class="py-3">${item['zone'] || ''}</td>
<td class="py-3">${item['aisle'] || ''}</td> <td class="py-3">${item['aisle'] || ''}</td>
<td class="py-3">${item['rack'] || ''}</td>`; <td class="py-3">${item['bin'] || ''}</td>`;
} }
return `<td class="py-3">${item['rack'] || ''}</td>`; return `<td class="py-3">${item['bin'] || ''}</td>`;
} }
function escape_html(value) { function escape_html(value) {
@@ -309,7 +309,7 @@
var s = res.output || {}; var s = res.output || {};
auto_approve = s.default_stock_status == 1; auto_approve = s.default_stock_status == 1;
advanced = s.advanced_location == 1; advanced = s.advanced_location == 1;
label_rack = s.location_label_rack || (advanced ? 'Rack' : 'Location'); label_bin = s.location_label_bin || (advanced ? 'Bin' : 'Location');
label_zone = s.location_label_zone || 'Zone'; label_zone = s.location_label_zone || 'Zone';
label_aisle = s.location_label_aisle || 'Aisle'; label_aisle = s.location_label_aisle || 'Aisle';
apply_location_labels(); apply_location_labels();
+7 -7
View File
@@ -63,7 +63,7 @@
<th>Source</th> <th>Source</th>
<th class="th_loc_zone">Zone</th> <th class="th_loc_zone">Zone</th>
<th class="th_loc_aisle">Aisle</th> <th class="th_loc_aisle">Aisle</th>
<th class="th_loc_rack">Rack</th> <th class="th_loc_bin">Bin</th>
<th>Status</th> <th>Status</th>
<th>Action</th> <th>Action</th>
</tr> </tr>
@@ -89,7 +89,7 @@
<script> <script>
var advanced = false; var advanced = false;
var label_rack = 'Location'; var label_bin = 'Location';
var label_zone = 'Zone'; var label_zone = 'Zone';
var label_aisle = 'Aisle'; var label_aisle = 'Aisle';
@@ -97,10 +97,10 @@
if (advanced) { if (advanced) {
$('.th_loc_zone').text(label_zone).show(); $('.th_loc_zone').text(label_zone).show();
$('.th_loc_aisle').text(label_aisle).show(); $('.th_loc_aisle').text(label_aisle).show();
$('.th_loc_rack').text(label_rack); $('.th_loc_bin').text(label_bin);
} else { } else {
$('.th_loc_zone, .th_loc_aisle').hide(); $('.th_loc_zone, .th_loc_aisle').hide();
$('.th_loc_rack').text(label_rack); $('.th_loc_bin').text(label_bin);
} }
} }
@@ -108,9 +108,9 @@
if (advanced) { if (advanced) {
return `<td class="py-3">${item['zone'] || ''}</td> return `<td class="py-3">${item['zone'] || ''}</td>
<td class="py-3">${item['aisle'] || ''}</td> <td class="py-3">${item['aisle'] || ''}</td>
<td class="py-3">${item['rack'] || ''}</td>`; <td class="py-3">${item['bin'] || ''}</td>`;
} }
return `<td class="py-3">${item['rack'] || ''}</td>`; return `<td class="py-3">${item['bin'] || ''}</td>`;
} }
function escape_html(value) { function escape_html(value) {
@@ -329,7 +329,7 @@
var s = res.output || {}; var s = res.output || {};
auto_approve = s.default_stock_status == 1; auto_approve = s.default_stock_status == 1;
advanced = s.advanced_location == 1; advanced = s.advanced_location == 1;
label_rack = s.location_label_rack || (advanced ? 'Rack' : 'Location'); label_bin = s.location_label_bin || (advanced ? 'Bin' : 'Location');
label_zone = s.location_label_zone || 'Zone'; label_zone = s.location_label_zone || 'Zone';
label_aisle = s.location_label_aisle || 'Aisle'; label_aisle = s.location_label_aisle || 'Aisle';
apply_location_labels(); apply_location_labels();
+1 -1
View File
@@ -42,7 +42,7 @@
<div> <div>
<span class="mb-4 d-block">Total Capacity</span> <span class="mb-4 d-block">Total Capacity</span>
<h3 class="fw-bold mb-0 text-info" id="stat_capacity">—</h3> <h3 class="fw-bold mb-0 text-info" id="stat_capacity">—</h3>
<p class="mb-0 small text-secondary" id="stat_capacity_sub">Racks in warehouse</p> <p class="mb-0 small text-secondary" id="stat_capacity_sub">Bins in warehouse</p>
</div> </div>
<i class="ti ti-box-seam fs-3 text-muted"></i> <i class="ti ti-box-seam fs-3 text-muted"></i>
</div> </div>
+7 -7
View File
@@ -52,7 +52,7 @@
<th style="text-align: right;">Quantity</th> <th style="text-align: right;">Quantity</th>
<th class="th_loc_zone">Zone</th> <th class="th_loc_zone">Zone</th>
<th class="th_loc_aisle">Aisle</th> <th class="th_loc_aisle">Aisle</th>
<th class="th_loc_rack">Rack</th> <th class="th_loc_bin">Bin</th>
<th>Status</th> <th>Status</th>
<th>Action</th> <th>Action</th>
</tr> </tr>
@@ -78,7 +78,7 @@
<script> <script>
var advanced = false; var advanced = false;
var label_rack = 'Location'; var label_bin = 'Location';
var label_zone = 'Zone'; var label_zone = 'Zone';
var label_aisle = 'Aisle'; var label_aisle = 'Aisle';
@@ -86,10 +86,10 @@
if (advanced) { if (advanced) {
$('.th_loc_zone').text(label_zone).show(); $('.th_loc_zone').text(label_zone).show();
$('.th_loc_aisle').text(label_aisle).show(); $('.th_loc_aisle').text(label_aisle).show();
$('.th_loc_rack').text(label_rack); $('.th_loc_bin').text(label_bin);
} else { } else {
$('.th_loc_zone, .th_loc_aisle').hide(); $('.th_loc_zone, .th_loc_aisle').hide();
$('.th_loc_rack').text(label_rack); $('.th_loc_bin').text(label_bin);
} }
} }
@@ -97,9 +97,9 @@
if (advanced) { if (advanced) {
return `<td class="py-3">${item['zone'] || ''}</td> return `<td class="py-3">${item['zone'] || ''}</td>
<td class="py-3">${item['aisle'] || ''}</td> <td class="py-3">${item['aisle'] || ''}</td>
<td class="py-3">${item['rack'] || ''}</td>`; <td class="py-3">${item['bin'] || ''}</td>`;
} }
return `<td class="py-3">${item['rack'] || ''}</td>`; return `<td class="py-3">${item['bin'] || ''}</td>`;
} }
function retrieve_stock_transfer() { function retrieve_stock_transfer() {
@@ -240,7 +240,7 @@
var s = res.output || {}; var s = res.output || {};
auto_approve = s.default_stock_status == 1; auto_approve = s.default_stock_status == 1;
advanced = s.advanced_location == 1; advanced = s.advanced_location == 1;
label_rack = s.location_label_rack || (advanced ? 'Rack' : 'Location'); label_bin = s.location_label_bin || (advanced ? 'Bin' : 'Location');
label_zone = s.location_label_zone || 'Zone'; label_zone = s.location_label_zone || 'Zone';
label_aisle = s.location_label_aisle || 'Aisle'; label_aisle = s.location_label_aisle || 'Aisle';
apply_location_labels(); apply_location_labels();
+1 -1
View File
@@ -165,7 +165,7 @@
<li> <li>
<a class="nav-link <?php echo wms_active('occupy_rack'); ?>" href="<?php echo $server_url?>reports/occupy_rack.php"> <a class="nav-link <?php echo wms_active('occupy_rack'); ?>" href="<?php echo $server_url?>reports/occupy_rack.php">
<i class="ti ti-layout-grid"></i> <i class="ti ti-layout-grid"></i>
<span class="nav-text">Rack Occupancy</span> <span class="nav-text">Bin Occupancy</span>
</a> </a>
</li> </li>
+35 -35
View File
@@ -59,7 +59,7 @@
</div> </div>
</div> </div>
<!-- Advanced mode — zone + aisle range + rack range --> <!-- Advanced mode — zone + aisle range + bin range -->
<div id="form_advanced" class="d-none"> <div id="form_advanced" class="d-none">
<div class="row"> <div class="row">
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
@@ -81,16 +81,16 @@
<div class="form-text">Must be same type as Aisle From</div> <div class="form-text">Must be same type as Aisle From</div>
</div> </div>
<div class="col-md-3 mb-3"> <div class="col-md-3 mb-3">
<label for="rack_from" class="form-label" id="label_rack_from">Rack From</label> <label for="bin_from" class="form-label" id="label_bin_from">Bin From</label>
<input type="text" class="form-control" id="rack_from" placeholder="e.g. 1 or A" <input type="text" class="form-control" id="bin_from" placeholder="e.g. 1 or A"
pattern="^([0-9]+|[A-Za-z])$" title="Number or single letter"> pattern="^([0-9]+|[A-Za-z])$" title="Number or single letter">
<div class="form-text">Number (1–99) or letter (A–Z)</div> <div class="form-text">Number (1–99) or letter (A–Z)</div>
</div> </div>
<div class="col-md-3 mb-3"> <div class="col-md-3 mb-3">
<label for="rack_to" class="form-label" id="label_rack_to">Rack To</label> <label for="bin_to" class="form-label" id="label_bin_to">Bin To</label>
<input type="text" class="form-control" id="rack_to" placeholder="e.g. 10 or J" <input type="text" class="form-control" id="bin_to" placeholder="e.g. 10 or J"
pattern="^([0-9]+|[A-Za-z])$" title="Number or single letter"> pattern="^([0-9]+|[A-Za-z])$" title="Number or single letter">
<div class="form-text">Must be same type as Rack From</div> <div class="form-text">Must be same type as Bin From</div>
</div> </div>
</div> </div>
</div> </div>
@@ -122,7 +122,7 @@
<script> <script>
// ── Location config ─────────────────────────────────────────────────────── // ── Location config ───────────────────────────────────────────────────────
var advanced = false; var advanced = false;
var label_rack = 'Location'; var label_bin = 'Location';
var label_zone = 'Zone'; var label_zone = 'Zone';
var label_aisle = 'Aisle'; var label_aisle = 'Aisle';
@@ -135,7 +135,7 @@
onSuccess: function(res) { onSuccess: function(res) {
var s = res.output; var s = res.output;
advanced = s.advanced_location == 1; advanced = s.advanced_location == 1;
label_rack = s.location_label_rack || (advanced ? 'Rack' : 'Location'); label_bin = s.location_label_bin || (advanced ? 'Bin' : 'Location');
label_zone = s.location_label_zone || 'Zone'; label_zone = s.location_label_zone || 'Zone';
label_aisle = s.location_label_aisle || 'Aisle'; label_aisle = s.location_label_aisle || 'Aisle';
apply_location_labels(); apply_location_labels();
@@ -150,13 +150,13 @@
$('#label_zone').text(label_zone); $('#label_zone').text(label_zone);
$('#label_aisle_from').text(label_aisle + ' From'); $('#label_aisle_from').text(label_aisle + ' From');
$('#label_aisle_to').text(label_aisle + ' To'); $('#label_aisle_to').text(label_aisle + ' To');
$('#label_rack_from').text(label_rack + ' From'); $('#label_bin_from').text(label_bin + ' From');
$('#label_rack_to').text(label_rack + ' To'); $('#label_bin_to').text(label_bin + ' To');
} else { } else {
$('#form_advanced').addClass('d-none'); $('#form_advanced').addClass('d-none');
$('#form_simple').removeClass('d-none'); $('#form_simple').removeClass('d-none');
$('#label_location_from').text(label_rack + ' From'); $('#label_location_from').text(label_bin + ' From');
$('#label_location_to').text(label_rack + ' To'); $('#label_location_to').text(label_bin + ' To');
$('#location_from').attr('placeholder', 'e.g. Shelf-A or 1'); $('#location_from').attr('placeholder', 'e.g. Shelf-A or 1');
$('#location_to').attr('placeholder', 'e.g. Shelf-E or 5'); $('#location_to').attr('placeholder', 'e.g. Shelf-E or 5');
} }
@@ -164,14 +164,14 @@
// ── Save storage ────────────────────────────────────────────────────────── // ── Save storage ──────────────────────────────────────────────────────────
function manage_storage() { function manage_storage() {
var zone, aisle_from, aisle_to, rack_from, rack_to; var zone, aisle_from, aisle_to, bin_from, bin_to;
if (advanced) { if (advanced) {
zone = $('#zone').val().trim(); zone = $('#zone').val().trim();
aisle_from = $('#aisle_from').val().trim().toUpperCase(); aisle_from = $('#aisle_from').val().trim().toUpperCase();
aisle_to = $('#aisle_to').val().trim().toUpperCase(); aisle_to = $('#aisle_to').val().trim().toUpperCase();
rack_from = $('#rack_from').val().trim().toUpperCase(); bin_from = $('#bin_from').val().trim().toUpperCase();
rack_to = $('#rack_to').val().trim().toUpperCase(); bin_to = $('#bin_to').val().trim().toUpperCase();
var isNumeric = function(v) { return /^[0-9]+$/.test(v); }; var isNumeric = function(v) { return /^[0-9]+$/.test(v); };
var isAlpha = function(v) { return /^[A-Z]$/.test(v); }; var isAlpha = function(v) { return /^[A-Z]$/.test(v); };
@@ -182,8 +182,8 @@
if (!aisle_from || !aisle_to) { if (!aisle_from || !aisle_to) {
alert('Please enter the full ' + label_aisle + ' range.'); return; alert('Please enter the full ' + label_aisle + ' range.'); return;
} }
if (!rack_from || !rack_to) { if (!bin_from || !bin_to) {
alert('Please enter the full ' + label_rack + ' range.'); return; alert('Please enter the full ' + label_bin + ' range.'); return;
} }
if (aisle_from && aisle_to) { if (aisle_from && aisle_to) {
@@ -197,25 +197,25 @@
alert(label_aisle + ' From must be less than or equal to To.'); return; alert(label_aisle + ' From must be less than or equal to To.'); return;
} }
} }
if (rack_from && rack_to) { if (bin_from && bin_to) {
if (isNumeric(rack_from) !== isNumeric(rack_to)) { if (isNumeric(bin_from) !== isNumeric(bin_to)) {
alert(label_rack + ' From and To must both be numbers or both be letters.'); return; alert(label_bin + ' From and To must both be numbers or both be letters.'); return;
} }
if (isAlpha(rack_from) && rack_from.charCodeAt(0) > rack_to.charCodeAt(0)) { if (isAlpha(bin_from) && bin_from.charCodeAt(0) > bin_to.charCodeAt(0)) {
alert(label_rack + ' From must be less than or equal to To.'); return; alert(label_bin + ' From must be less than or equal to To.'); return;
} }
if (isNumeric(rack_from) && parseInt(rack_from) > parseInt(rack_to)) { if (isNumeric(bin_from) && parseInt(bin_from) > parseInt(bin_to)) {
alert(label_rack + ' From must be less than or equal to To.'); return; alert(label_bin + ' From must be less than or equal to To.'); return;
} }
} }
} else { } else {
// Simple mode — from/to, same value written to zone/aisle/rack // Simple mode — from/to, same value written to zone/aisle/bin
var loc_from = $('#location_from').val().trim(); var loc_from = $('#location_from').val().trim();
var loc_to = $('#location_to').val().trim(); var loc_to = $('#location_to').val().trim();
if (!loc_from) { if (!loc_from) {
alert('Please enter a ' + label_rack + ' From value.'); return; alert('Please enter a ' + label_bin + ' From value.'); return;
} }
// If To is blank, default to same as From (single location) // If To is blank, default to same as From (single location)
if (!loc_to) loc_to = loc_from; if (!loc_to) loc_to = loc_from;
@@ -225,14 +225,14 @@
var mf = loc_from.match(prefix_pattern); var mf = loc_from.match(prefix_pattern);
var mt = loc_to.match(prefix_pattern); var mt = loc_to.match(prefix_pattern);
if (mf && mt && mf[1] !== mt[1]) { if (mf && mt && mf[1] !== mt[1]) {
alert(label_rack + ' From and To must share the same prefix (e.g. Shelf-A → Shelf-E).'); return; alert(label_bin + ' From and To must share the same prefix (e.g. Shelf-A → Shelf-E).'); return;
} }
zone = loc_from; zone = loc_from;
aisle_from = loc_from; aisle_from = loc_from;
aisle_to = loc_to; aisle_to = loc_to;
rack_from = loc_from; bin_from = loc_from;
rack_to = loc_to; bin_to = loc_to;
} }
return ajax_request({ return ajax_request({
@@ -245,8 +245,8 @@
zone: zone, zone: zone,
aisle_from: aisle_from, aisle_from: aisle_from,
aisle_to: aisle_to, aisle_to: aisle_to,
rack_from: rack_from, bin_from: bin_from,
rack_to: rack_to, bin_to: bin_to,
description: $('#description').val(), description: $('#description').val(),
}, },
onSuccess: function() { onSuccess: function() {
@@ -287,11 +287,11 @@
$('#zone').val(d.zone || ''); $('#zone').val(d.zone || '');
$('#aisle_from').val(d.aisle_from || ''); $('#aisle_from').val(d.aisle_from || '');
$('#aisle_to').val(d.aisle_to || ''); $('#aisle_to').val(d.aisle_to || '');
$('#rack_from').val(d.rack_from || ''); $('#bin_from').val(d.bin_from || '');
$('#rack_to').val(d.rack_to || ''); $('#bin_to').val(d.bin_to || '');
} else { } else {
$('#location_from').val(d.rack_from || ''); $('#location_from').val(d.bin_from || '');
$('#location_to').val(d.rack_to || ''); $('#location_to').val(d.bin_to || '');
} }
$('button[type=submit]').text('Update Storage'); $('button[type=submit]').text('Update Storage');
$('button[type=reset]').hide(); $('button[type=reset]').hide();
+16 -16
View File
@@ -60,7 +60,7 @@
<div class="d-flex flex-column"> <div class="d-flex flex-column">
<span class="fs-2 fw-bold mb-3 d-block">—</span> <span class="fs-2 fw-bold mb-3 d-block">—</span>
<span class="fs-5">Total Capacity</span> <span class="fs-5">Total Capacity</span>
<small class="text-warning">Rack positions available</small> <small class="text-warning">Bin positions available</small>
</div> </div>
<div> <div>
<span class="text-warning"> <span class="text-warning">
@@ -87,7 +87,7 @@
<div class="d-flex flex-column"> <div class="d-flex flex-column">
<span class="fs-2 fw-bold mb-3 d-block">—</span> <span class="fs-2 fw-bold mb-3 d-block">—</span>
<span class="fs-5">Space Used</span> <span class="fs-5">Space Used</span>
<small class="text-success">Racks occupied</small> <small class="text-success">Bins occupied</small>
</div> </div>
<div> <div>
<span class="text-success"> <span class="text-success">
@@ -206,7 +206,7 @@
<div class="mb-6 d-flex align-items-center justify-content-between"> <div class="mb-6 d-flex align-items-center justify-content-between">
<div> <div>
<h2 class="fs-4 mb-2">Storage Locations</h2> <h2 class="fs-4 mb-2">Storage Locations</h2>
<p id="storage_subtitle">Zones, aisles, and rack assignments</p> <p id="storage_subtitle">Zones, aisles, and bin assignments</p>
</div> </div>
<div> <div>
<a href="<?php echo $server_url?>inventory/manage_storage.php" class="btn btn-secondary" data-min-role="admin"> <a href="<?php echo $server_url?>inventory/manage_storage.php" class="btn btn-secondary" data-min-role="admin">
@@ -234,7 +234,7 @@
<th class="py-3">Warehouse</th> <th class="py-3">Warehouse</th>
<th class="py-3" id="th_level1">Zone</th> <th class="py-3" id="th_level1">Zone</th>
<th class="py-3" id="th_level2">Aisle Range</th> <th class="py-3" id="th_level2">Aisle Range</th>
<th class="py-3" id="th_level3">Rack Range</th> <th class="py-3" id="th_level3">Bin Range</th>
<th class="py-3">Items Stored</th> <th class="py-3">Items Stored</th>
<th>Action</th> <th>Action</th>
</tr> </tr>
@@ -257,7 +257,7 @@
<script> <script>
// ── Location config ─────────────────────────────────────────────────────── // ── Location config ───────────────────────────────────────────────────────
var advanced = false; var advanced = false;
var label_rack = 'Location'; var label_bin = 'Location';
var label_zone = 'Zone'; var label_zone = 'Zone';
var label_aisle = 'Aisle'; var label_aisle = 'Aisle';
@@ -270,7 +270,7 @@
onSuccess: function(res) { onSuccess: function(res) {
var s = res.output; var s = res.output;
advanced = s.advanced_location == 1; advanced = s.advanced_location == 1;
label_rack = s.location_label_rack || (advanced ? 'Rack' : 'Location'); label_bin = s.location_label_bin || (advanced ? 'Bin' : 'Location');
label_zone = s.location_label_zone || 'Zone'; label_zone = s.location_label_zone || 'Zone';
label_aisle = s.location_label_aisle || 'Aisle'; label_aisle = s.location_label_aisle || 'Aisle';
apply_location_labels(); apply_location_labels();
@@ -280,26 +280,26 @@
function apply_location_labels() { function apply_location_labels() {
// Stats card subtitles // Stats card subtitles
$('#total_capacity small').text(label_rack + ' positions available'); $('#total_capacity small').text(label_bin + ' positions available');
$('#total_space_used small').text(label_rack + 's occupied'); $('#total_space_used small').text(label_bin + 's occupied');
// Storage tab subtitle + search // Storage tab subtitle + search
$('#storage_subtitle').text(advanced $('#storage_subtitle').text(advanced
? label_zone + 's, ' + label_aisle + 's, and ' + label_rack + ' assignments' ? label_zone + 's, ' + label_aisle + 's, and ' + label_bin + ' assignments'
: label_rack + ' assignments'); : label_bin + ' assignments');
$('#search_storage').attr('placeholder', advanced $('#search_storage').attr('placeholder', advanced
? 'Search by warehouse or ' + label_zone.toLowerCase() + '…' ? 'Search by warehouse or ' + label_zone.toLowerCase() + '…'
: 'Search by warehouse or ' + label_rack.toLowerCase() + '…'); : 'Search by warehouse or ' + label_bin.toLowerCase() + '…');
// Table headers // Table headers
if (advanced) { if (advanced) {
$('#th_level1').text(label_zone).show(); $('#th_level1').text(label_zone).show();
$('#th_level2').text(label_aisle + ' Range').show(); $('#th_level2').text(label_aisle + ' Range').show();
$('#th_level3').text(label_rack + ' Range'); $('#th_level3').text(label_bin + ' Range');
} else { } else {
$('#th_level1').hide(); $('#th_level1').hide();
$('#th_level2').hide(); $('#th_level2').hide();
$('#th_level3').text(label_rack + ' Range'); $('#th_level3').text(label_bin + ' Range');
} }
} }
@@ -432,7 +432,7 @@
alasql(`SELECT * FROM storage alasql(`SELECT * FROM storage
WHERE LOWER(warehouse_name) LIKE '%${kw}%' WHERE LOWER(warehouse_name) LIKE '%${kw}%'
OR LOWER(zone) LIKE '%${kw}%' OR LOWER(zone) LIKE '%${kw}%'
OR LOWER(rack) LIKE '%${kw}%'`) : OR LOWER(bin) LIKE '%${kw}%'`) :
alasql(`SELECT * FROM storage`); alasql(`SELECT * FROM storage`);
var page = generate_pagination('storage', filtered.length); var page = generate_pagination('storage', filtered.length);
@@ -452,8 +452,8 @@
var location_cells = advanced var location_cells = advanced
? `<td class="py-3">${item['zone']}</td> ? `<td class="py-3">${item['zone']}</td>
<td class="py-3">${item['aisle_from']} - ${item['aisle_to']}</td> <td class="py-3">${item['aisle_from']} - ${item['aisle_to']}</td>
<td class="py-3">${item['rack_from']} - ${item['rack_to']}</td>` <td class="py-3">${item['bin_from']} - ${item['bin_to']}</td>`
: `<td class="py-3">${item['rack_from']} - ${item['rack_to']}</td>`; : `<td class="py-3">${item['bin_from']} - ${item['bin_to']}</td>`;
body += `<tr> body += `<tr>
<td class="py-3">${item['warehouse_name']}</td> <td class="py-3">${item['warehouse_name']}</td>
+12 -14
View File
@@ -23,8 +23,9 @@
* b. The elapsed time since otpTime is ≤ 5 minutes. * b. The elapsed time since otpTime is ≤ 5 minutes.
* Fail either → return "Wrong OTP! Please try again." * Fail either → return "Wrong OTP! Please try again."
* 5. On success: * 5. On success:
* a. Check session_token in DB — if non-NULL, another session is active; * a. Force-replace session_token in DB — writing a new token invalidates
* reject login with "account already logged in" message. * any prior session (old device gets kicked out by db_auth.php on its
* next request). No block: password + OTP is full 2FA proof of identity.
* b. session_regenerate_id(true) — prevents session fixation attack by * b. session_regenerate_id(true) — prevents session fixation attack by
* issuing a new session ID and deleting the old one. * issuing a new session ID and deleting the old one.
* c. Generate a fresh CSRF token and store in session. * c. Generate a fresh CSRF token and store in session.
@@ -105,31 +106,28 @@ if (empty($_SESSION['skip_otp'])) {
} }
} }
// ── Step 4b: Claim session atomically ───────────────────────────────────────── // ── Step 4b: Claim session — always replace existing token ────────────────────
// The WHERE clause is the concurrency gate: only a free or stale token can be // Password + OTP is full 2FA proof of identity, so we always grant the login.
// replaced. session_last_seen is set immediately so a fresh login is live before // Writing a new token here also invalidates any prior session: db_auth.php
// the first authenticated heartbeat in db_auth.php. // compares session_token in the DB to the one stored in the PHP session, so the
// old device is kicked out on its next request. This also fixes the case where
// a PHP session expired without clearing the DB token, which would otherwise
// permanently block re-login.
$session_token = bin2hex(random_bytes(32)); $session_token = bin2hex(random_bytes(32));
$sth_claim = $pdo1->prepare( $sth_claim = $pdo1->prepare(
"UPDATE user "UPDATE user
SET session_token = :token, SET session_token = :token,
session_token_at = NOW(), session_token_at = NOW(),
session_last_seen = NOW() session_last_seen = NOW()
WHERE user_id = :uid WHERE user_id = :uid"
AND (
session_token IS NULL
OR session_last_seen IS NULL
OR TIMESTAMPDIFF(SECOND, session_last_seen, NOW()) >= :gc_maxlifetime
)"
); );
$sth_claim->execute([ $sth_claim->execute([
':token' => $session_token, ':token' => $session_token,
':uid' => $user_id, ':uid' => $user_id,
':gc_maxlifetime' => (int)ini_get('session.gc_maxlifetime'),
]); ]);
if ($sth_claim->rowCount() !== 1) { if ($sth_claim->rowCount() !== 1) {
$answer["message"] = "This account is currently logged in on another device. Please wait for the other session to end."; $answer["message"] = "Login failed: user record not found.";
exit(json_encode($answer)); exit(json_encode($answer));
} }
+2 -2
View File
@@ -382,7 +382,7 @@
warehouse_id: wh, warehouse_id: wh,
zone: '', zone: '',
aisle: '', aisle: '',
rack: '', bin: '',
lot_number: '', lot_number: '',
serial_number: '', serial_number: '',
quantity: qty, quantity: qty,
@@ -566,7 +566,7 @@
// ── Confirm order ───────────────────────────────────────────────────────── // ── Confirm order ─────────────────────────────────────────────────────────
function confirm_order() { function confirm_order() {
bootbox.confirm({ bootbox.confirm({
message: 'Confirm this order? Stock-out rows will be created for all items using FIFO rack selection.', message: 'Confirm this order? Stock-out rows will be created for all items using FIFO bin selection.',
buttons: { buttons: {
confirm: { label: 'Confirm Order', className: 'btn-success' }, confirm: { label: 'Confirm Order', className: 'btn-success' },
cancel: { label: 'Back', className: 'btn-secondary' } cancel: { label: 'Back', className: 'btn-secondary' }
+40 -40
View File
@@ -102,7 +102,7 @@
<th class="th_return_wh" style="width:150px;">Warehouse</th> <th class="th_return_wh" style="width:150px;">Warehouse</th>
<th class="th_return_zone" style="width:120px;">Zone</th> <th class="th_return_zone" style="width:120px;">Zone</th>
<th class="th_return_aisle" style="width:120px;">Aisle</th> <th class="th_return_aisle" style="width:120px;">Aisle</th>
<th class="th_return_rack" style="width:120px;">Location</th> <th class="th_return_bin" style="width:120px;">Location</th>
<th class="text-end" style="width:110px;">Return Qty</th> <th class="text-end" style="width:110px;">Return Qty</th>
<th class="text-end" style="width:100px;">Unit Price</th> <th class="text-end" style="width:100px;">Unit Price</th>
<th style="width:70px;">Tax%</th> <th style="width:70px;">Tax%</th>
@@ -215,7 +215,7 @@
var returnable = []; // returnable_items from order var returnable = []; // returnable_items from order
var warehouses = []; var warehouses = [];
var advanced = false; var advanced = false;
var label_rack = 'Location'; var label_bin = 'Location';
var label_zone = 'Zone'; var label_zone = 'Zone';
var label_aisle = 'Aisle'; var label_aisle = 'Aisle';
@@ -276,16 +276,16 @@
$('.th_return_wh').text('Warehouse'); $('.th_return_wh').text('Warehouse');
$('.th_return_zone').text(label_zone).toggleClass('d-none', !advanced); $('.th_return_zone').text(label_zone).toggleClass('d-none', !advanced);
$('.th_return_aisle').text(label_aisle).toggleClass('d-none', !advanced); $('.th_return_aisle').text(label_aisle).toggleClass('d-none', !advanced);
$('.th_return_rack').text(label_rack); $('.th_return_bin').text(label_bin);
$('#loading_row td').attr('colspan', 12); $('#loading_row td').attr('colspan', 12);
} }
function retrieve_return_zones(item_id, selected_zone, selected_aisle, selected_rack) { function retrieve_return_zones(item_id, selected_zone, selected_aisle, selected_bin) {
var $row = $(`#ret_item_${item_id}`); var $row = $(`#ret_item_${item_id}`);
var wh = $row.find('.item_return_warehouse').val(); var wh = $row.find('.item_return_warehouse').val();
$row.find('.item_return_zone').html(`<option value="">Select ${label_zone.toLowerCase()}...</option>`); $row.find('.item_return_zone').html(`<option value="">Select ${label_zone.toLowerCase()}...</option>`);
$row.find('.item_return_aisle').html(`<option value="">Select ${label_aisle.toLowerCase()}...</option>`); $row.find('.item_return_aisle').html(`<option value="">Select ${label_aisle.toLowerCase()}...</option>`);
$row.find('.item_return_rack').html(`<option value="">Select ${label_rack.toLowerCase()}...</option>`); $row.find('.item_return_bin').html(`<option value="">Select ${label_bin.toLowerCase()}...</option>`);
if (!wh) return Promise.resolve(); if (!wh) return Promise.resolve();
return ajax_request({ return ajax_request({
@@ -307,7 +307,7 @@
opts += `<option value="${escape_html(selected_zone)}">${escape_html(selected_zone)}</option>`; opts += `<option value="${escape_html(selected_zone)}">${escape_html(selected_zone)}</option>`;
} }
$row.find('.item_return_zone').html(opts).val(selected_zone || ''); $row.find('.item_return_zone').html(opts).val(selected_zone || '');
if (selected_zone) retrieve_return_aisles(item_id, selected_aisle, selected_rack); if (selected_zone) retrieve_return_aisles(item_id, selected_aisle, selected_bin);
} }
}); });
} }
@@ -316,35 +316,35 @@
var $row = $(`#ret_item_${item_id}`); var $row = $(`#ret_item_${item_id}`);
$row.find('.item_return_zone').val(''); $row.find('.item_return_zone').val('');
$row.find('.item_return_aisle').val(''); $row.find('.item_return_aisle').val('');
$row.find('.item_return_rack').val(''); $row.find('.item_return_bin').val('');
if (!$row.find('.item_check').is(':checked')) return; if (!$row.find('.item_check').is(':checked')) return;
if (advanced) { if (advanced) {
retrieve_return_zones(item_id); retrieve_return_zones(item_id);
} else { } else {
retrieve_return_racks(item_id); retrieve_return_bins(item_id);
} }
} }
function change_return_zone(item_id) { function change_return_zone(item_id) {
var $row = $(`#ret_item_${item_id}`); var $row = $(`#ret_item_${item_id}`);
$row.find('.item_return_aisle').val(''); $row.find('.item_return_aisle').val('');
$row.find('.item_return_rack').val(''); $row.find('.item_return_bin').val('');
if ($row.find('.item_check').is(':checked')) retrieve_return_aisles(item_id); if ($row.find('.item_check').is(':checked')) retrieve_return_aisles(item_id);
} }
function change_return_aisle(item_id) { function change_return_aisle(item_id) {
var $row = $(`#ret_item_${item_id}`); var $row = $(`#ret_item_${item_id}`);
$row.find('.item_return_rack').val(''); $row.find('.item_return_bin').val('');
if ($row.find('.item_check').is(':checked')) retrieve_return_racks(item_id); if ($row.find('.item_check').is(':checked')) retrieve_return_bins(item_id);
} }
function retrieve_return_aisles(item_id, selected_aisle, selected_rack) { function retrieve_return_aisles(item_id, selected_aisle, selected_bin) {
var $row = $(`#ret_item_${item_id}`); var $row = $(`#ret_item_${item_id}`);
var wh = $row.find('.item_return_warehouse').val(); var wh = $row.find('.item_return_warehouse').val();
var zone = $row.find('.item_return_zone').val(); var zone = $row.find('.item_return_zone').val();
$row.find('.item_return_aisle').html(`<option value="">Select ${label_aisle.toLowerCase()}...</option>`); $row.find('.item_return_aisle').html(`<option value="">Select ${label_aisle.toLowerCase()}...</option>`);
$row.find('.item_return_rack').html(`<option value="">Select ${label_rack.toLowerCase()}...</option>`); $row.find('.item_return_bin').html(`<option value="">Select ${label_bin.toLowerCase()}...</option>`);
if (!wh || !zone) return Promise.resolve(); if (!wh || !zone) return Promise.resolve();
return ajax_request({ return ajax_request({
@@ -366,21 +366,21 @@
opts += `<option value="${escape_html(selected_aisle)}">${escape_html(selected_aisle)}</option>`; opts += `<option value="${escape_html(selected_aisle)}">${escape_html(selected_aisle)}</option>`;
} }
$row.find('.item_return_aisle').html(opts).val(selected_aisle || ''); $row.find('.item_return_aisle').html(opts).val(selected_aisle || '');
if (selected_aisle) retrieve_return_racks(item_id, selected_rack); if (selected_aisle) retrieve_return_bins(item_id, selected_bin);
} }
}); });
} }
function retrieve_return_racks(item_id, selected_rack) { function retrieve_return_bins(item_id, selected_bin) {
var $row = $(`#ret_item_${item_id}`); var $row = $(`#ret_item_${item_id}`);
var wh = $row.find('.item_return_warehouse').val(); var wh = $row.find('.item_return_warehouse').val();
var zone = advanced ? $row.find('.item_return_zone').val() : ''; var zone = advanced ? $row.find('.item_return_zone').val() : '';
var aisle = advanced ? $row.find('.item_return_aisle').val() : ''; var aisle = advanced ? $row.find('.item_return_aisle').val() : '';
$row.find('.item_return_rack').html(`<option value="">Select ${label_rack.toLowerCase()}...</option>`); $row.find('.item_return_bin').html(`<option value="">Select ${label_bin.toLowerCase()}...</option>`);
if (!wh || (advanced && (!zone || !aisle))) return Promise.resolve(); if (!wh || (advanced && (!zone || !aisle))) return Promise.resolve();
return ajax_request({ return ajax_request({
url: '<?php echo $server_url?>ics/api/engine/retrieve_rack.php', url: '<?php echo $server_url?>ics/api/engine/retrieve_bin.php',
autoPrepare: true, autoPrepare: true,
checkRequired: 0, checkRequired: 0,
noLoading: true, noLoading: true,
@@ -388,16 +388,16 @@
action: 'read', action: 'read',
data: { type: 'to', warehouse: wh, zone: zone, aisle: aisle, id: 0 }, data: { type: 'to', warehouse: wh, zone: zone, aisle: aisle, id: 0 },
onSuccess: function(res) { onSuccess: function(res) {
var opts = `<option value="">Select ${label_rack.toLowerCase()}...</option>`; var opts = `<option value="">Select ${label_bin.toLowerCase()}...</option>`;
var has_selected = !selected_rack; var has_selected = !selected_bin;
$.each(res.output || [], function(_, value) { $.each(res.output || [], function(_, value) {
if (String(value) === String(selected_rack)) has_selected = true; if (String(value) === String(selected_bin)) has_selected = true;
opts += `<option value="${escape_html(value)}">${escape_html(value)}</option>`; opts += `<option value="${escape_html(value)}">${escape_html(value)}</option>`;
}); });
if (!has_selected) { if (!has_selected) {
opts += `<option value="${escape_html(selected_rack)}">${escape_html(selected_rack)}</option>`; opts += `<option value="${escape_html(selected_bin)}">${escape_html(selected_bin)}</option>`;
} }
$row.find('.item_return_rack').html(opts).val(selected_rack || ''); $row.find('.item_return_bin').html(opts).val(selected_bin || '');
} }
}); });
} }
@@ -406,19 +406,19 @@
var wh = selected.warehouse_id || ''; var wh = selected.warehouse_id || '';
var zone = selected.zone || ''; var zone = selected.zone || '';
var aisle = selected.aisle || ''; var aisle = selected.aisle || '';
var rack = selected.rack || ''; var bin = selected.bin || '';
if (advanced) { if (advanced) {
return `<td><select class="form-select form-select-sm item_return_warehouse" onchange="change_return_warehouse(${item_id})">${warehouse_options(wh)}</select></td> return `<td><select class="form-select form-select-sm item_return_warehouse" onchange="change_return_warehouse(${item_id})">${warehouse_options(wh)}</select></td>
<td><select class="form-select form-select-sm item_return_zone" onchange="change_return_zone(${item_id})"><option value="">Select ${label_zone.toLowerCase()}...</option></select></td> <td><select class="form-select form-select-sm item_return_zone" onchange="change_return_zone(${item_id})"><option value="">Select ${label_zone.toLowerCase()}...</option></select></td>
<td><select class="form-select form-select-sm item_return_aisle" onchange="change_return_aisle(${item_id})"><option value="">Select ${label_aisle.toLowerCase()}...</option></select></td> <td><select class="form-select form-select-sm item_return_aisle" onchange="change_return_aisle(${item_id})"><option value="">Select ${label_aisle.toLowerCase()}...</option></select></td>
<td><select class="form-select form-select-sm item_return_rack"><option value="">Select ${label_rack.toLowerCase()}...</option></select></td>`; <td><select class="form-select form-select-sm item_return_bin"><option value="">Select ${label_bin.toLowerCase()}...</option></select></td>`;
} }
return `<td><select class="form-select form-select-sm item_return_warehouse" onchange="change_return_warehouse(${item_id})">${warehouse_options(wh)}</select></td> return `<td><select class="form-select form-select-sm item_return_warehouse" onchange="change_return_warehouse(${item_id})">${warehouse_options(wh)}</select></td>
<td class="d-none"><select class="form-select form-select-sm item_return_zone"><option value=""></option></select></td> <td class="d-none"><select class="form-select form-select-sm item_return_zone"><option value=""></option></select></td>
<td class="d-none"><select class="form-select form-select-sm item_return_aisle"><option value=""></option></select></td> <td class="d-none"><select class="form-select form-select-sm item_return_aisle"><option value=""></option></select></td>
<td><select class="form-select form-select-sm item_return_rack"><option value="">Select ${label_rack.toLowerCase()}...</option></select></td>`; <td><select class="form-select form-select-sm item_return_bin"><option value="">Select ${label_bin.toLowerCase()}...</option></select></td>`;
} }
function hydrate_location_selectors(items, selected_map) { function hydrate_location_selectors(items, selected_map) {
@@ -427,15 +427,15 @@
var $row = $(`#ret_item_${item.item_id}`); var $row = $(`#ret_item_${item.item_id}`);
if (!$row.find('.item_check').is(':checked')) return; if (!$row.find('.item_check').is(':checked')) return;
if (!selected.warehouse_id && !selected.zone && !selected.aisle && !selected.rack) { if (!selected.warehouse_id && !selected.zone && !selected.aisle && !selected.bin) {
change_return_warehouse(item.item_id); change_return_warehouse(item.item_id);
return; return;
} }
if (advanced) { if (advanced) {
retrieve_return_zones(item.item_id, selected.zone || '', selected.aisle || '', selected.rack || ''); retrieve_return_zones(item.item_id, selected.zone || '', selected.aisle || '', selected.bin || '');
} else if (selected.rack) { } else if (selected.bin) {
retrieve_return_racks(item.item_id, selected.rack || ''); retrieve_return_bins(item.item_id, selected.bin || '');
} }
}); });
} }
@@ -498,7 +498,7 @@
warehouse_id: si ? (si.warehouse_id || item.warehouse_id || '') : (item.warehouse_id || ''), warehouse_id: si ? (si.warehouse_id || item.warehouse_id || '') : (item.warehouse_id || ''),
zone: si ? (si.zone || '') : (item.zone || ''), zone: si ? (si.zone || '') : (item.zone || ''),
aisle: si ? (si.aisle || '') : (item.aisle || ''), aisle: si ? (si.aisle || '') : (item.aisle || ''),
rack: si ? (si.rack || '') : (item.rack || ''), bin: si ? (si.bin || '') : (item.bin || ''),
}; };
rows += `<tr class="return-item-row" id="ret_item_${item.item_id}"> rows += `<tr class="return-item-row" id="ret_item_${item.item_id}">
@@ -514,7 +514,7 @@
<input type="hidden" class="item_warehouse_id" value="${item.warehouse_id || 0}"> <input type="hidden" class="item_warehouse_id" value="${item.warehouse_id || 0}">
<input type="hidden" class="item_zone" value="${item.zone || ''}"> <input type="hidden" class="item_zone" value="${item.zone || ''}">
<input type="hidden" class="item_aisle" value="${item.aisle || ''}"> <input type="hidden" class="item_aisle" value="${item.aisle || ''}">
<input type="hidden" class="item_rack" value="${item.rack || ''}"> <input type="hidden" class="item_bin" value="${item.bin || ''}">
<input type="hidden" class="item_lot" value="${item.lot_number || ''}"> <input type="hidden" class="item_lot" value="${item.lot_number || ''}">
<input type="hidden" class="item_serial" value="${item.serial_number || ''}"> <input type="hidden" class="item_serial" value="${item.serial_number || ''}">
<input type="hidden" class="item_stock_out_id" value="${item.stock_out_id || 0}"> <input type="hidden" class="item_stock_out_id" value="${item.stock_out_id || 0}">
@@ -561,7 +561,7 @@
function toggle_item_row(item_id) { function toggle_item_row(item_id) {
var $row = $(`#ret_item_${item_id}`); var $row = $(`#ret_item_${item_id}`);
var checked = $row.find('.item_check').is(':checked'); var checked = $row.find('.item_check').is(':checked');
$row.find('.item_qty, .item_price, .item_tax_rate, .item_return_warehouse, .item_return_zone, .item_return_aisle, .item_return_rack').prop('disabled', !checked); $row.find('.item_qty, .item_price, .item_tax_rate, .item_return_warehouse, .item_return_zone, .item_return_aisle, .item_return_bin').prop('disabled', !checked);
if (!checked) { if (!checked) {
$row.find('.item_total').text('—'); $row.find('.item_total').text('—');
return; return;
@@ -631,7 +631,7 @@
onSuccess: function(res) { onSuccess: function(res) {
var s = res.output || {}; var s = res.output || {};
advanced = s.advanced_location == 1; advanced = s.advanced_location == 1;
label_rack = s.location_label_rack || (advanced ? 'Rack' : 'Location'); label_bin = s.location_label_bin || (advanced ? 'Bin' : 'Location');
label_zone = s.location_label_zone || 'Zone'; label_zone = s.location_label_zone || 'Zone';
label_aisle = s.location_label_aisle || 'Aisle'; label_aisle = s.location_label_aisle || 'Aisle';
apply_location_headers(); apply_location_headers();
@@ -664,12 +664,12 @@
var tax_rate = parseFloat($row.find('.item_tax_rate').val()) || 0; var tax_rate = parseFloat($row.find('.item_tax_rate').val()) || 0;
if (qty <= 0) return; if (qty <= 0) return;
var return_wh = parseInt($row.find('.item_return_warehouse').val()) || 0; var return_wh = parseInt($row.find('.item_return_warehouse').val()) || 0;
var return_rack = $row.find('.item_return_rack').val() || ''; var return_bin = $row.find('.item_return_bin').val() || '';
var return_zone = advanced ? ($row.find('.item_return_zone').val() || '') : return_rack; var return_zone = advanced ? ($row.find('.item_return_zone').val() || '') : return_bin;
var return_aisle = advanced ? ($row.find('.item_return_aisle').val() || '') : return_rack; var return_aisle = advanced ? ($row.find('.item_return_aisle').val() || '') : return_bin;
if (!return_wh || !return_rack || (advanced && (!return_zone || !return_aisle))) { if (!return_wh || !return_bin || (advanced && (!return_zone || !return_aisle))) {
throw new Error('Please select return warehouse and ' + label_rack.toLowerCase() + ' for every checked item.'); throw new Error('Please select return warehouse and ' + label_bin.toLowerCase() + ' for every checked item.');
} }
items.push({ items.push({
@@ -679,7 +679,7 @@
warehouse_id: return_wh, warehouse_id: return_wh,
zone: return_zone, zone: return_zone,
aisle: return_aisle, aisle: return_aisle,
rack: return_rack, bin: return_bin,
lot_number: $row.find('.item_lot').val(), lot_number: $row.find('.item_lot').val(),
serial_number: $row.find('.item_serial').val(), serial_number: $row.find('.item_serial').val(),
stock_out_id: parseInt($row.find('.item_stock_out_id').val()) || 0, stock_out_id: parseInt($row.find('.item_stock_out_id').val()) || 0,
@@ -750,7 +750,7 @@
$('.item_check').prop('disabled', !is_draft); $('.item_check').prop('disabled', !is_draft);
$('#item_tbody tr.return-item-row').each(function() { $('#item_tbody tr.return-item-row').each(function() {
var checked = $(this).find('.item_check').is(':checked'); var checked = $(this).find('.item_check').is(':checked');
$(this).find('.item_qty, .item_price, .item_tax_rate, .item_return_warehouse, .item_return_zone, .item_return_aisle, .item_return_rack') $(this).find('.item_qty, .item_price, .item_tax_rate, .item_return_warehouse, .item_return_zone, .item_return_aisle, .item_return_bin')
.prop('disabled', !is_draft || !checked); .prop('disabled', !is_draft || !checked);
}); });
+21 -21
View File
@@ -124,7 +124,7 @@
<div class="d-flex justify-content-between align-items-start mb-4"> <div class="d-flex justify-content-between align-items-start mb-4">
<div> <div>
<h2 class="fs-5 mb-1">Receive Goods</h2> <h2 class="fs-5 mb-1">Receive Goods</h2>
<p class="mb-0 text-muted small">Enter quantities received and assign rack locations for each line</p> <p class="mb-0 text-muted small">Enter quantities received and assign bin locations for each line</p>
</div> </div>
<button class="btn btn-success" id="btn_receive" onclick="receive_goods()"> <button class="btn btn-success" id="btn_receive" onclick="receive_goods()">
<i class="ti ti-package-import me-1"></i>Confirm Receipt <i class="ti ti-package-import me-1"></i>Confirm Receipt
@@ -255,7 +255,7 @@
// ── Location config (loaded from company settings) ──────────────────────── // ── Location config (loaded from company settings) ────────────────────────
var advanced = false; var advanced = false;
var label_rack = 'Location'; var label_bin = 'Location';
var label_zone = 'Zone'; var label_zone = 'Zone';
var label_aisle = 'Aisle'; var label_aisle = 'Aisle';
@@ -393,7 +393,7 @@
}); });
$(`#recv_zone_${rowId}`).html(opts); $(`#recv_zone_${rowId}`).html(opts);
$(`#recv_aisle_${rowId}`).html(`<option value="">Select ${label_aisle.toLowerCase()}...</option>`); $(`#recv_aisle_${rowId}`).html(`<option value="">Select ${label_aisle.toLowerCase()}...</option>`);
$(`#recv_rack_${rowId}`).html(`<option value="">Select ${label_rack.toLowerCase()}...</option>`); $(`#recv_bin_${rowId}`).html(`<option value="">Select ${label_bin.toLowerCase()}...</option>`);
if (callback) callback(); if (callback) callback();
} }
}); });
@@ -414,28 +414,28 @@
opts += `<option value="${escape_html(a)}">${escape_html(a)}</option>`; opts += `<option value="${escape_html(a)}">${escape_html(a)}</option>`;
}); });
$(`#recv_aisle_${rowId}`).html(opts); $(`#recv_aisle_${rowId}`).html(opts);
$(`#recv_rack_${rowId}`).html(`<option value="">Select ${label_rack.toLowerCase()}...</option>`); $(`#recv_bin_${rowId}`).html(`<option value="">Select ${label_bin.toLowerCase()}...</option>`);
if (callback) callback(); if (callback) callback();
} }
}); });
} }
function recv_load_racks(rowId, callback) { function recv_load_bins(rowId, callback) {
var wh_id = $(`#recv_wh_${rowId}`).val(); var wh_id = $(`#recv_wh_${rowId}`).val();
var zone = advanced ? ($(`#recv_zone_${rowId}`).val() || '') : ''; var zone = advanced ? ($(`#recv_zone_${rowId}`).val() || '') : '';
var aisle = advanced ? ($(`#recv_aisle_${rowId}`).val() || '') : ''; var aisle = advanced ? ($(`#recv_aisle_${rowId}`).val() || '') : '';
if (!wh_id) return; if (!wh_id) return;
ajax_request({ ajax_request({
url: '<?php echo $server_url?>ics/api/engine/retrieve_rack.php', url: '<?php echo $server_url?>ics/api/engine/retrieve_bin.php',
autoPrepare: true, checkRequired: 0, noLoading: true, queueLock: false, autoPrepare: true, checkRequired: 0, noLoading: true, queueLock: false,
action: 'read', action: 'read',
data: { type: 'to', warehouse_to: wh_id, zone_to: zone, aisle_to: aisle, id: 0 }, data: { type: 'to', warehouse_to: wh_id, zone_to: zone, aisle_to: aisle, id: 0 },
onSuccess: function(res) { onSuccess: function(res) {
var opts = `<option value="">Select ${label_rack.toLowerCase()}...</option>`; var opts = `<option value="">Select ${label_bin.toLowerCase()}...</option>`;
(res.output || []).forEach(function(r) { (res.output || []).forEach(function(r) {
opts += `<option value="${escape_html(r)}">${escape_html(r)}</option>`; opts += `<option value="${escape_html(r)}">${escape_html(r)}</option>`;
}); });
$(`#recv_rack_${rowId}`).html(opts); $(`#recv_bin_${rowId}`).html(opts);
if (callback) callback(); if (callback) callback();
} }
}); });
@@ -476,7 +476,7 @@
if (advanced) { if (advanced) {
recv_load_zones(rowId); recv_load_zones(rowId);
} else { } else {
recv_load_racks(rowId); recv_load_bins(rowId);
} }
} }
@@ -507,13 +507,13 @@
<div class="col-lg-4"> <div class="col-lg-4">
<label class="form-label small text-muted">${label_aisle}</label> <label class="form-label small text-muted">${label_aisle}</label>
<select id="recv_aisle_${rowId}" class="form-select form-select-sm" <select id="recv_aisle_${rowId}" class="form-select form-select-sm"
onchange="recv_load_racks('${rowId}')"> onchange="recv_load_bins('${rowId}')">
<option value="">Select ${label_aisle.toLowerCase()}...</option> <option value="">Select ${label_aisle.toLowerCase()}...</option>
</select> </select>
</div>`; </div>`;
} }
var rack_col_class = advanced ? 'col-lg-4' : 'col-lg-6'; var bin_col_class = advanced ? 'col-lg-4' : 'col-lg-6';
html += ` html += `
<div class="border rounded p-4 mb-4" <div class="border rounded p-4 mb-4"
@@ -569,10 +569,10 @@
${zone_aisle_html} ${zone_aisle_html}
<div class="${rack_col_class}"> <div class="${bin_col_class}">
<label class="form-label small text-muted">${label_rack} <span class="text-danger">*</span></label> <label class="form-label small text-muted">${label_bin} <span class="text-danger">*</span></label>
<select id="recv_rack_${rowId}" class="form-select form-select-sm"> <select id="recv_bin_${rowId}" class="form-select form-select-sm">
<option value="">Select ${label_rack.toLowerCase()}...</option> <option value="">Select ${label_bin.toLowerCase()}...</option>
</select> </select>
</div> </div>
@@ -771,22 +771,22 @@
if (qty <= 0) return; if (qty <= 0) return;
var wh_id = parseInt($(`#recv_wh_${rowId}`).val()) || 0; var wh_id = parseInt($(`#recv_wh_${rowId}`).val()) || 0;
var rack = $(`#recv_rack_${rowId}`).val() || ''; var bin = $(`#recv_bin_${rowId}`).val() || '';
if (!wh_id) { if (!wh_id) {
errors.push(`${$card.data('sku')}: please select a warehouse.`); errors.push(`${$card.data('sku')}: please select a warehouse.`);
return; return;
} }
if (!rack) { if (!bin) {
errors.push(`${$card.data('sku')}: please select a ${label_rack.toLowerCase()}.`); errors.push(`${$card.data('sku')}: please select a ${label_bin.toLowerCase()}.`);
return; return;
} }
var zone = advanced ? ($(`#recv_zone_${rowId}`).val() || '') : ''; var zone = advanced ? ($(`#recv_zone_${rowId}`).val() || '') : '';
var aisle = advanced ? ($(`#recv_aisle_${rowId}`).val() || '') : ''; var aisle = advanced ? ($(`#recv_aisle_${rowId}`).val() || '') : '';
if (!advanced) { if (!advanced) {
zone = rack; zone = bin;
aisle = rack; aisle = bin;
} }
// flatpickr with altInput: the real (hidden) input holds the ISO value // flatpickr with altInput: the real (hidden) input holds the ISO value
@@ -802,7 +802,7 @@
serial_number: $(`#recv_serial_${rowId}`).val().trim(), serial_number: $(`#recv_serial_${rowId}`).val().trim(),
zone: zone, zone: zone,
aisle: aisle, aisle: aisle,
rack: rack, bin: bin,
}); });
}); });
+2 -2
View File
@@ -276,7 +276,7 @@
<input type="hidden" class="item_warehouse" value="${item.warehouse_id || 0}"> <input type="hidden" class="item_warehouse" value="${item.warehouse_id || 0}">
<input type="hidden" class="item_zone" value="${item.zone || ''}"> <input type="hidden" class="item_zone" value="${item.zone || ''}">
<input type="hidden" class="item_aisle" value="${item.aisle || ''}"> <input type="hidden" class="item_aisle" value="${item.aisle || ''}">
<input type="hidden" class="item_rack" value="${item.rack || ''}"> <input type="hidden" class="item_bin" value="${item.bin || ''}">
<input type="hidden" class="item_stock_in_id" value="${item.stock_in_id || 0}"> <input type="hidden" class="item_stock_in_id" value="${item.stock_in_id || 0}">
<input type="hidden" class="item_lot" value="${item.lot_number || ''}"> <input type="hidden" class="item_lot" value="${item.lot_number || ''}">
<input type="hidden" class="item_serial" value="${item.serial_number || ''}"> <input type="hidden" class="item_serial" value="${item.serial_number || ''}">
@@ -465,7 +465,7 @@
warehouse_id: parseInt($row.find('.item_warehouse').val()) || 0, warehouse_id: parseInt($row.find('.item_warehouse').val()) || 0,
zone: $row.find('.item_zone').val(), zone: $row.find('.item_zone').val(),
aisle: $row.find('.item_aisle').val(), aisle: $row.find('.item_aisle').val(),
rack: $row.find('.item_rack').val(), bin: $row.find('.item_bin').val(),
stock_in_id: parseInt($row.find('.item_stock_in_id').val()) || 0, stock_in_id: parseInt($row.find('.item_stock_in_id').val()) || 0,
lot_number: $row.find('.item_lot').val(), lot_number: $row.find('.item_lot').val(),
serial_number: $row.find('.item_serial').val(), serial_number: $row.find('.item_serial').val(),
+3 -3
View File
@@ -6,16 +6,16 @@
(new UsageGuard($pdo1, $company_id, $packages))->assertFeatureAccessible('reports'); (new UsageGuard($pdo1, $company_id, $packages))->assertFeatureAccessible('reports');
require_once '../../../assets/utils/classes/ReportManager.php'; require_once '../../../assets/utils/classes/ReportManager.php';
$rack_id = (int)($data['rack_id'] ?? 0); $bin_id = (int)($data['bin_id'] ?? 0);
if (!$rack_id) { if (!$bin_id) {
$answer['output'] = []; $answer['output'] = [];
$answer['success'] = 1; $answer['success'] = 1;
exit(json_encode($answer)); exit(json_encode($answer));
} }
$report = new ReportManager($pdo2, $company_id, $db_database); $report = new ReportManager($pdo2, $company_id, $db_database);
$answer['output'] = $report->getRackLog($rack_id); $answer['output'] = $report->getBinLog($bin_id);
$answer['success'] = 1; $answer['success'] = 1;
exit(json_encode($answer)); exit(json_encode($answer));
?> ?>
+6 -6
View File
@@ -194,22 +194,22 @@
var search_term = ''; var search_term = '';
var filter_status = ''; var filter_status = '';
var advanced = false; var advanced = false;
var label_rack = 'Location'; var label_bin = 'Location';
var label_zone = 'Zone'; var label_zone = 'Zone';
var label_aisle = 'Aisle'; var label_aisle = 'Aisle';
function apply_location_labels() { function apply_location_labels() {
$('.th_location').text(advanced $('.th_location').text(advanced
? label_zone + ' / ' + label_aisle + ' / ' + label_rack ? label_zone + ' / ' + label_aisle + ' / ' + label_bin
: label_rack : label_bin
); );
} }
function format_location(item) { function format_location(item) {
if (advanced) { if (advanced) {
return [item.zone, item.aisle, item.rack].filter(Boolean).join('-') || '<span class="text-muted">—</span>'; return [item.zone, item.aisle, item.bin].filter(Boolean).join('-') || '<span class="text-muted">—</span>';
} }
return item.rack || '<span class="text-muted">—</span>'; return item.bin || '<span class="text-muted">—</span>';
} }
function load_company_setting() { function load_company_setting() {
@@ -221,7 +221,7 @@
onSuccess: function(res) { onSuccess: function(res) {
var s = res.output || {}; var s = res.output || {};
advanced = s.advanced_location == 1; advanced = s.advanced_location == 1;
label_rack = s.location_label_rack || (advanced ? 'Rack' : 'Location'); label_bin = s.location_label_bin || (advanced ? 'Bin' : 'Location');
label_zone = s.location_label_zone || 'Zone'; label_zone = s.location_label_zone || 'Zone';
label_aisle = s.location_label_aisle || 'Aisle'; label_aisle = s.location_label_aisle || 'Aisle';
apply_location_labels(); apply_location_labels();
+50 -50
View File
@@ -18,7 +18,7 @@
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-5 gap-3"> <div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-5 gap-3">
<div> <div>
<h1 class="fs-3 mb-1">Rack Occupancy</h1> <h1 class="fs-3 mb-1">Rack Occupancy</h1>
<p class="mb-0">View occupied and available rack positions across all warehouses</p> <p class="mb-0">View occupied and available bin positions across all warehouses</p>
</div> </div>
</div> </div>
</div> </div>
@@ -112,13 +112,13 @@
<div class="mb-6 d-flex align-items-center justify-content-between"> <div class="mb-6 d-flex align-items-center justify-content-between">
<div> <div>
<h3 class="mb-1 fs-4">All Racks</h3> <h3 class="mb-1 fs-4">All Racks</h3>
<p class="mb-0">Complete rack list across all warehouses</p> <p class="mb-0">Complete bin list across all warehouses</p>
</div> </div>
</div> </div>
<div class="col-lg-12 mb-5"> <div class="col-lg-12 mb-5">
<div class="position-relative"> <div class="position-relative">
<input type="search" id="search_all" class="form-control ps-9" <input type="search" id="search_all" class="form-control ps-9"
placeholder="Search by warehouse, zone, aisle, rack, or SKU…"> placeholder="Search by warehouse, zone, aisle, bin, or SKU…">
<span class="position-absolute top-25 ms-4"> <span class="position-absolute top-25 ms-4">
<i class="ti ti-search text-muted"></i> <i class="ti ti-search text-muted"></i>
</span> </span>
@@ -131,7 +131,7 @@
<th>Warehouse</th> <th>Warehouse</th>
<th class="th_loc_zone">Zone</th> <th class="th_loc_zone">Zone</th>
<th class="th_loc_aisle">Aisle</th> <th class="th_loc_aisle">Aisle</th>
<th class="th_loc_rack">Rack</th> <th class="th_loc_bin">Rack</th>
<th>SKU</th> <th>SKU</th>
<th>Product</th> <th>Product</th>
<th>Status</th> <th>Status</th>
@@ -161,7 +161,7 @@
<div class="col-lg-12 mb-5"> <div class="col-lg-12 mb-5">
<div class="position-relative"> <div class="position-relative">
<input type="search" id="search_occupied" class="form-control ps-9" <input type="search" id="search_occupied" class="form-control ps-9"
placeholder="Search by warehouse, zone, aisle, rack, or SKU…"> placeholder="Search by warehouse, zone, aisle, bin, or SKU…">
<span class="position-absolute top-25 ms-4"> <span class="position-absolute top-25 ms-4">
<i class="ti ti-search text-muted"></i> <i class="ti ti-search text-muted"></i>
</span> </span>
@@ -174,7 +174,7 @@
<th>Warehouse</th> <th>Warehouse</th>
<th class="th_loc_zone">Zone</th> <th class="th_loc_zone">Zone</th>
<th class="th_loc_aisle">Aisle</th> <th class="th_loc_aisle">Aisle</th>
<th class="th_loc_rack">Rack</th> <th class="th_loc_bin">Rack</th>
<th>SKU</th> <th>SKU</th>
<th>Product</th> <th>Product</th>
<th>Action</th> <th>Action</th>
@@ -203,7 +203,7 @@
<div class="col-lg-12 mb-5"> <div class="col-lg-12 mb-5">
<div class="position-relative"> <div class="position-relative">
<input type="search" id="search_empty" class="form-control ps-9" <input type="search" id="search_empty" class="form-control ps-9"
placeholder="Search by warehouse, zone, aisle, or rack…"> placeholder="Search by warehouse, zone, aisle, or bin…">
<span class="position-absolute top-25 ms-4"> <span class="position-absolute top-25 ms-4">
<i class="ti ti-search text-muted"></i> <i class="ti ti-search text-muted"></i>
</span> </span>
@@ -216,7 +216,7 @@
<th>Warehouse</th> <th>Warehouse</th>
<th class="th_loc_zone">Zone</th> <th class="th_loc_zone">Zone</th>
<th class="th_loc_aisle">Aisle</th> <th class="th_loc_aisle">Aisle</th>
<th class="th_loc_rack">Rack</th> <th class="th_loc_bin">Rack</th>
<th>Action</th> <th>Action</th>
</tr> </tr>
</thead> </thead>
@@ -231,12 +231,12 @@
</div> </div>
<!-- Rack Log Modal --> <!-- Bin Log Modal -->
<div class="modal fade" id="rackLogModal" tabindex="-1" aria-labelledby="rackLogModalLabel" aria-hidden="true"> <div class="modal fade" id="binLogModal" tabindex="-1" aria-labelledby="binLogModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-scrollable"> <div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="rackLogModalLabel">Rack Log</h5> <h5 class="modal-title" id="binLogModalLabel">Bin Log</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div> </div>
<div class="modal-body p-0"> <div class="modal-body p-0">
@@ -249,7 +249,7 @@
<th>User</th> <th>User</th>
</tr> </tr>
</thead> </thead>
<tbody id="rack_log_body"> <tbody id="bin_log_body">
<tr><td colspan="4" class="text-center py-4 text-muted">Loading...</td></tr> <tr><td colspan="4" class="text-center py-4 text-muted">Loading...</td></tr>
</tbody> </tbody>
</table> </table>
@@ -267,7 +267,7 @@
// ── Location config ─────────────────────────────────────────────────────── // ── Location config ───────────────────────────────────────────────────────
var advanced = false; var advanced = false;
var label_rack = 'Location'; var label_bin = 'Location';
var label_zone = 'Zone'; var label_zone = 'Zone';
var label_aisle = 'Aisle'; var label_aisle = 'Aisle';
@@ -276,21 +276,21 @@
if (advanced) { if (advanced) {
$('.th_loc_zone').text(label_zone).show(); $('.th_loc_zone').text(label_zone).show();
$('.th_loc_aisle').text(label_aisle).show(); $('.th_loc_aisle').text(label_aisle).show();
$('.th_loc_rack').text(label_rack); $('.th_loc_bin').text(label_bin);
} else { } else {
$('.th_loc_zone').hide(); $('.th_loc_zone').hide();
$('.th_loc_aisle').hide(); $('.th_loc_aisle').hide();
$('.th_loc_rack').text(label_rack); $('.th_loc_bin').text(label_bin);
} }
var loc_hint = advanced var loc_hint = advanced
? label_zone.toLowerCase() + ', ' + label_aisle.toLowerCase() + ', ' + label_rack.toLowerCase() ? label_zone.toLowerCase() + ', ' + label_aisle.toLowerCase() + ', ' + label_bin.toLowerCase()
: label_rack.toLowerCase(); : label_bin.toLowerCase();
$('#search_all, #search_occupied').attr('placeholder', $('#search_all, #search_occupied').attr('placeholder',
'Search by warehouse, ' + loc_hint + ', or SKU…'); 'Search by warehouse, ' + loc_hint + ', or SKU…');
$('#search_empty').attr('placeholder', $('#search_empty').attr('placeholder',
'Search by warehouse or ' + loc_hint + '…'); 'Search by warehouse or ' + loc_hint + '…');
$('#rackLogModalLabel').text(label_rack + ' Log'); $('#binLogModalLabel').text(label_bin + ' Log');
$('#stat_label_racks').text('Total ' + label_rack + 's'); $('#stat_label_bins').text('Total ' + label_bin + 's');
} }
// Returns 1 or 3 <td> cells depending on mode // Returns 1 or 3 <td> cells depending on mode
@@ -298,16 +298,16 @@
if (advanced) { if (advanced) {
return '<td class="py-3">' + (item.zone || '—') + '</td>' return '<td class="py-3">' + (item.zone || '—') + '</td>'
+ '<td class="py-3">' + (item.aisle || '—') + '</td>' + '<td class="py-3">' + (item.aisle || '—') + '</td>'
+ '<td class="py-3">' + (item.rack || '—') + '</td>'; + '<td class="py-3">' + (item.bin || '—') + '</td>';
} }
return '<td class="py-3">' + (item.rack || '—') + '</td>'; return '<td class="py-3">' + (item.bin || '—') + '</td>';
} }
// Rack log modal label // Rack log modal label
function rack_label(item) { function bin_label(item) {
return advanced return advanced
? item.warehouse_name + ' ' + item.zone + '-' + item.aisle + '-' + item.rack ? item.warehouse_name + ' ' + item.zone + '-' + item.aisle + '-' + item.bin
: item.warehouse_name + ' ' + item.rack; : item.warehouse_name + ' ' + item.bin;
} }
var search_all = ''; var search_all = '';
@@ -316,10 +316,10 @@
// ── Fetch ───────────────────────────────────────────────────────────── // ── Fetch ─────────────────────────────────────────────────────────────
function retrieve_rack_occupancy() { function retrieve_bin_occupancy() {
return ajax_request({ return ajax_request({
url: "<?php echo $server_url?>reports/api/engine_report/rack_occupancy.php", url: "<?php echo $server_url?>reports/api/engine_report/bin_occupancy.php",
autoPrepare: true, autoPrepare: true,
checkRequired: 0, checkRequired: 0,
action: 'read', action: 'read',
@@ -338,8 +338,8 @@
$('#stat_rate').text(rate + '%'); $('#stat_rate').text(rate + '%');
// Store in alasql // Store in alasql
alasql('CREATE TABLE IF NOT EXISTS rack_all'); alasql('CREATE TABLE IF NOT EXISTS bin_all');
alasql.tables.rack_all.data = res.output || []; alasql.tables.bin_all.data = res.output || [];
change_page_all(1); change_page_all(1);
change_page_occupied(1); change_page_occupied(1);
@@ -356,14 +356,14 @@
var kw = search_all.toLowerCase(); var kw = search_all.toLowerCase();
var filtered = kw ? alasql( var filtered = kw ? alasql(
`SELECT * FROM rack_all `SELECT * FROM bin_all
WHERE LOWER(warehouse_name) LIKE '%${kw}%' WHERE LOWER(warehouse_name) LIKE '%${kw}%'
OR LOWER(zone) LIKE '%${kw}%' OR LOWER(zone) LIKE '%${kw}%'
OR LOWER(aisle) LIKE '%${kw}%' OR LOWER(aisle) LIKE '%${kw}%'
OR LOWER(rack) LIKE '%${kw}%' OR LOWER(bin) LIKE '%${kw}%'
OR LOWER(product_sku) LIKE '%${kw}%' OR LOWER(product_sku) LIKE '%${kw}%'
OR LOWER(product_name) LIKE '%${kw}%'` OR LOWER(product_name) LIKE '%${kw}%'`
) : alasql('SELECT * FROM rack_all'); ) : alasql('SELECT * FROM bin_all');
$('table#tbl_all tfoot').html(generate_pagination('tbl_all', filtered.length)); $('table#tbl_all tfoot').html(generate_pagination('tbl_all', filtered.length));
@@ -385,7 +385,7 @@
<td class="py-3">${item.product_sku || '<span class="text-muted">—</span>'}</td> <td class="py-3">${item.product_sku || '<span class="text-muted">—</span>'}</td>
<td class="py-3">${item.product_name || '<span class="text-muted">—</span>'}</td> <td class="py-3">${item.product_name || '<span class="text-muted">—</span>'}</td>
<td class="py-3">${badge}</td> <td class="py-3">${badge}</td>
<td class="py-3"><a href="javascript:;" onclick="view_rack_log(${item.id}, '${rack_label(item)}')"><i class="ti ti-history fs-5"></i></a></td> <td class="py-3"><a href="javascript:;" onclick="view_bin_log(${item.id}, '${bin_label(item)}')"><i class="ti ti-history fs-5"></i></a></td>
</tr>`; </tr>`;
}); });
$('table#tbl_all > tbody').html(body); $('table#tbl_all > tbody').html(body);
@@ -398,13 +398,13 @@
var offset = (page_num - 1) * prop_limit; var offset = (page_num - 1) * prop_limit;
var kw = search_occupied.toLowerCase(); var kw = search_occupied.toLowerCase();
var base = alasql("SELECT * FROM rack_all WHERE status = 'occupied'"); var base = alasql("SELECT * FROM bin_all WHERE status = 'occupied'");
var filtered = kw ? base.filter(function(item) { var filtered = kw ? base.filter(function(item) {
return (item.warehouse_name || '').toLowerCase().includes(kw) return (item.warehouse_name || '').toLowerCase().includes(kw)
|| (item.zone || '').toLowerCase().includes(kw) || (item.zone || '').toLowerCase().includes(kw)
|| (item.aisle || '').toLowerCase().includes(kw) || (item.aisle || '').toLowerCase().includes(kw)
|| (item.rack || '').toLowerCase().includes(kw) || (item.bin || '').toLowerCase().includes(kw)
|| (item.product_sku || '').toLowerCase().includes(kw) || (item.product_sku || '').toLowerCase().includes(kw)
|| (item.product_name || '').toLowerCase().includes(kw); || (item.product_name || '').toLowerCase().includes(kw);
}) : base; }) : base;
@@ -425,7 +425,7 @@
` + location_cells(item) + ` ` + location_cells(item) + `
<td class="py-3">${item.product_sku}</td> <td class="py-3">${item.product_sku}</td>
<td class="py-3">${item.product_name || '<span class="text-muted">—</span>'}</td> <td class="py-3">${item.product_name || '<span class="text-muted">—</span>'}</td>
<td class="py-3"><a href="javascript:;" onclick="view_rack_log(${item.id}, '${rack_label(item)}')"><i class="ti ti-history fs-5"></i></a></td> <td class="py-3"><a href="javascript:;" onclick="view_bin_log(${item.id}, '${bin_label(item)}')"><i class="ti ti-history fs-5"></i></a></td>
</tr>`; </tr>`;
}); });
$('table#tbl_occupied > tbody').html(body); $('table#tbl_occupied > tbody').html(body);
@@ -438,13 +438,13 @@
var offset = (page_num - 1) * prop_limit; var offset = (page_num - 1) * prop_limit;
var kw = search_empty.toLowerCase(); var kw = search_empty.toLowerCase();
var base = alasql("SELECT * FROM rack_all WHERE status = 'empty'"); var base = alasql("SELECT * FROM bin_all WHERE status = 'empty'");
var filtered = kw ? base.filter(function(item) { var filtered = kw ? base.filter(function(item) {
return (item.warehouse_name || '').toLowerCase().includes(kw) return (item.warehouse_name || '').toLowerCase().includes(kw)
|| (item.zone || '').toLowerCase().includes(kw) || (item.zone || '').toLowerCase().includes(kw)
|| (item.aisle || '').toLowerCase().includes(kw) || (item.aisle || '').toLowerCase().includes(kw)
|| (item.rack || '').toLowerCase().includes(kw); || (item.bin || '').toLowerCase().includes(kw);
}) : base; }) : base;
$('table#tbl_empty tfoot').html(generate_pagination('tbl_empty', filtered.length)); $('table#tbl_empty tfoot').html(generate_pagination('tbl_empty', filtered.length));
@@ -461,7 +461,7 @@
body += `<tr> body += `<tr>
<td class="py-3">${item.warehouse_name}</td> <td class="py-3">${item.warehouse_name}</td>
` + location_cells(item) + ` ` + location_cells(item) + `
<td class="py-3"><a href="javascript:;" onclick="view_rack_log(${item.id}, '${rack_label(item)}')"><i class="ti ti-history fs-5"></i></a></td> <td class="py-3"><a href="javascript:;" onclick="view_bin_log(${item.id}, '${bin_label(item)}')"><i class="ti ti-history fs-5"></i></a></td>
</tr>`; </tr>`;
}); });
$('table#tbl_empty > tbody').html(body); $('table#tbl_empty > tbody').html(body);
@@ -501,30 +501,30 @@
// Remember last active tab // Remember last active tab
$(document).ready(function() { $(document).ready(function() {
$('button.nav-link').on('click', function() { $('button.nav-link').on('click', function() {
sessionStorage.setItem('rack_nav_id', $(this).attr('id')); sessionStorage.setItem('bin_nav_id', $(this).attr('id'));
}); });
var nav_id = sessionStorage.getItem('rack_nav_id'); var nav_id = sessionStorage.getItem('bin_nav_id');
if (nav_id) $(`button#${nav_id}`).trigger('click'); if (nav_id) $(`button#${nav_id}`).trigger('click');
}); });
// ── Rack Log ───────────────────────────────────────────────────────── // ── Bin Log ──────────────────────────────────────────────────────────
function view_rack_log(rack_id, label) { function view_bin_log(bin_id, label) {
$('#rackLogModalLabel').text('Rack Log — ' + label); $('#binLogModalLabel').text('Bin Log — ' + label);
$('#rack_log_body').html('<tr><td colspan="4" class="text-center py-4 text-muted">Loading...</td></tr>'); $('#bin_log_body').html('<tr><td colspan="4" class="text-center py-4 text-muted">Loading...</td></tr>');
$('#rackLogModal').modal('show'); $('#binLogModal').modal('show');
ajax_request({ ajax_request({
url: "<?php echo $server_url?>reports/api/engine_report/rack_log.php", url: "<?php echo $server_url?>reports/api/engine_report/bin_log.php",
autoPrepare: true, autoPrepare: true,
checkRequired: 0, checkRequired: 0,
action: 'read', action: 'read',
data: { "rack_id": rack_id }, data: { "bin_id": bin_id },
onSuccess: function(res) { onSuccess: function(res) {
if (!res.output || res.output.length === 0) { if (!res.output || res.output.length === 0) {
$('#rack_log_body').html('<tr><td colspan="4" class="text-center py-4 text-muted">No log entries.</td></tr>'); $('#bin_log_body').html('<tr><td colspan="4" class="text-center py-4 text-muted">No log entries.</td></tr>');
return; return;
} }
@@ -544,7 +544,7 @@
<td class="py-3">${row.user_name || '<span class="text-muted">—</span>'}</td> <td class="py-3">${row.user_name || '<span class="text-muted">—</span>'}</td>
</tr>`; </tr>`;
}); });
$('#rack_log_body').html(body); $('#bin_log_body').html(body);
} }
}); });
} }
@@ -554,7 +554,7 @@
$(async function() { $(async function() {
try { try {
await load_location_config(apply_location_labels); await load_location_config(apply_location_labels);
await retrieve_rack_occupancy(); await retrieve_bin_occupancy();
} catch(e) { } catch(e) {
console.log(e); console.log(e);
} }
+6 -6
View File
@@ -183,7 +183,7 @@
var lot_log_page = 1; var lot_log_page = 1;
var lot_log_limit = 10; var lot_log_limit = 10;
var advanced = false; var advanced = false;
var label_rack = 'Location'; var label_bin = 'Location';
var label_zone = 'Zone'; var label_zone = 'Zone';
var label_aisle = 'Aisle'; var label_aisle = 'Aisle';
@@ -202,16 +202,16 @@
function apply_location_labels() { function apply_location_labels() {
$('#th_location').text(advanced $('#th_location').text(advanced
? label_zone + ' / ' + label_aisle + ' / ' + label_rack ? label_zone + ' / ' + label_aisle + ' / ' + label_bin
: label_rack : label_bin
); );
} }
function format_location(row) { function format_location(row) {
if (advanced) { if (advanced) {
return [row.zone, row.aisle, row.rack].filter(Boolean).join('-') || '<span class="text-muted">—</span>'; return [row.zone, row.aisle, row.bin].filter(Boolean).join('-') || '<span class="text-muted">—</span>';
} }
return row.rack || '<span class="text-muted">—</span>'; return row.bin || '<span class="text-muted">—</span>';
} }
function load_company_setting() { function load_company_setting() {
@@ -223,7 +223,7 @@
onSuccess: function(res) { onSuccess: function(res) {
var s = res.output || {}; var s = res.output || {};
advanced = s.advanced_location == 1; advanced = s.advanced_location == 1;
label_rack = s.location_label_rack || (advanced ? 'Rack' : 'Location'); label_bin = s.location_label_bin || (advanced ? 'Bin' : 'Location');
label_zone = s.location_label_zone || 'Zone'; label_zone = s.location_label_zone || 'Zone';
label_aisle = s.location_label_aisle || 'Aisle'; label_aisle = s.location_label_aisle || 'Aisle';
apply_location_labels(); apply_location_labels();
+6 -6
View File
@@ -256,7 +256,7 @@
// Movement Summary running totals (reset on each search) // Movement Summary running totals (reset on each search)
var run_revenue = 0, run_cogs = 0, run_gp = 0, sum_in = 0, sum_out = 0, sum_txn = 0; var run_revenue = 0, run_cogs = 0, run_gp = 0, sum_in = 0, sum_out = 0, sum_txn = 0;
var label_rack = 'Location'; var label_bin = 'Location';
var label_zone = 'Zone'; var label_zone = 'Zone';
var label_aisle = 'Aisle'; var label_aisle = 'Aisle';
@@ -268,16 +268,16 @@
function apply_location_labels() { function apply_location_labels() {
$('#th_location').text(advanced $('#th_location').text(advanced
? label_zone + ' / ' + label_aisle + ' / ' + label_rack ? label_zone + ' / ' + label_aisle + ' / ' + label_bin
: label_rack : label_bin
); );
} }
function format_location(row) { function format_location(row) {
if (advanced) { if (advanced) {
return [row.zone, row.aisle, row.rack].filter(Boolean).join('-'); return [row.zone, row.aisle, row.bin].filter(Boolean).join('-');
} }
return row.rack || ''; return row.bin || '';
} }
function display_location(row) { function display_location(row) {
@@ -293,7 +293,7 @@
onSuccess: function(res) { onSuccess: function(res) {
var s = res.output || {}; var s = res.output || {};
advanced = s.advanced_location == 1; advanced = s.advanced_location == 1;
label_rack = s.location_label_rack || (advanced ? 'Rack' : 'Location'); label_bin = s.location_label_bin || (advanced ? 'Bin' : 'Location');
label_zone = s.location_label_zone || 'Zone'; label_zone = s.location_label_zone || 'Zone';
label_aisle = s.location_label_aisle || 'Aisle'; label_aisle = s.location_label_aisle || 'Aisle';
apply_location_labels(); apply_location_labels();
+1 -1
View File
@@ -273,7 +273,7 @@
warehouse_id: 0, warehouse_id: 0,
zone: '', zone: '',
aisle: '', aisle: '',
rack: '', bin: '',
lot_number: '', lot_number: '',
serial_number: '', serial_number: '',
quantity: qty, quantity: qty,
+13 -13
View File
@@ -37,7 +37,7 @@
<p class="text-muted small mb-2"> <p class="text-muted small mb-2">
By default, each storage location uses a single <strong>Location</strong> field — By default, each storage location uses a single <strong>Location</strong> field —
ideal for small warehouses. Enable <strong>Advanced</strong> to use a three-level ideal for small warehouses. Enable <strong>Advanced</strong> to use a three-level
hierarchy (e.g. Zone → Aisle → Rack) for larger operations. hierarchy (e.g. Zone → Aisle → Bin) for larger operations.
You can customise the label for each level to match your team's terminology. You can customise the label for each level to match your team's terminology.
</p> </p>
<div class="form-check form-switch mb-3"> <div class="form-check form-switch mb-3">
@@ -54,7 +54,7 @@
<label class="col-form-label text-muted small">Location Label</label> <label class="col-form-label text-muted small">Location Label</label>
</div> </div>
<div class="col-lg-4"> <div class="col-lg-4">
<input type="text" id="location_label_rack_simple" class="form-control form-control-sm" <input type="text" id="location_label_bin_simple" class="form-control form-control-sm"
placeholder="Location" maxlength="30"> placeholder="Location" maxlength="30">
</div> </div>
<div class="col-auto"> <div class="col-auto">
@@ -94,11 +94,11 @@
<label class="col-form-label text-muted small">Level 3 Label</label> <label class="col-form-label text-muted small">Level 3 Label</label>
</div> </div>
<div class="col-lg-4"> <div class="col-lg-4">
<input type="text" id="location_label_rack_advanced" class="form-control form-control-sm" <input type="text" id="location_label_bin_advanced" class="form-control form-control-sm"
placeholder="Rack" maxlength="30"> placeholder="Bin" maxlength="30">
</div> </div>
<div class="col-auto"> <div class="col-auto">
<span class="text-muted small">default: "Rack"</span> <span class="text-muted small">default: "Bin"</span>
</div> </div>
</div> </div>
</div> </div>
@@ -264,10 +264,10 @@
var advanced = (s.advanced_location ?? 0) == 1; var advanced = (s.advanced_location ?? 0) == 1;
$('#advanced_location_toggle').prop('checked', advanced); $('#advanced_location_toggle').prop('checked', advanced);
toggle_location_mode(advanced); toggle_location_mode(advanced);
$('#location_label_rack_simple').val(s.location_label_rack || ''); $('#location_label_bin_simple').val(s.location_label_bin || '');
$('#location_label_zone').val(s.location_label_zone || ''); $('#location_label_zone').val(s.location_label_zone || '');
$('#location_label_aisle').val(s.location_label_aisle || ''); $('#location_label_aisle').val(s.location_label_aisle || '');
$('#location_label_rack_advanced').val(s.location_label_rack || ''); $('#location_label_bin_advanced').val(s.location_label_bin || '');
// Other settings // Other settings
$('input[name="default_stock_status"][value="' + (s.default_stock_status ?? 1) + '"]').prop('checked', true); $('input[name="default_stock_status"][value="' + (s.default_stock_status ?? 1) + '"]').prop('checked', true);
@@ -350,15 +350,15 @@
} }
// Resolve label values — fall back to defaults if blank // Resolve label values — fall back to defaults if blank
var label_rack, label_zone, label_aisle; var label_bin, label_zone, label_aisle;
if (advanced) { if (advanced) {
label_zone = $('#location_label_zone').val().trim() || 'Zone'; label_zone = $('#location_label_zone').val().trim() || 'Zone';
label_aisle = $('#location_label_aisle').val().trim() || 'Aisle'; label_aisle = $('#location_label_aisle').val().trim() || 'Aisle';
label_rack = $('#location_label_rack_advanced').val().trim() || 'Rack'; label_bin = $('#location_label_bin_advanced').val().trim() || 'Bin';
} else { } else {
label_rack = $('#location_label_rack_simple').val().trim() || 'Location'; label_bin = $('#location_label_bin_simple').val().trim() || 'Location';
label_zone = label_rack; label_zone = label_bin;
label_aisle = label_rack; label_aisle = label_bin;
} }
ajax_request({ ajax_request({
@@ -370,7 +370,7 @@
advanced_location: advanced, advanced_location: advanced,
location_label_zone: label_zone, location_label_zone: label_zone,
location_label_aisle: label_aisle, location_label_aisle: label_aisle,
location_label_rack: label_rack, location_label_bin: label_bin,
default_stock_status: stock_status, default_stock_status: stock_status,
auto_invoice_and_credit_note: auto_invoice_and_cn, auto_invoice_and_credit_note: auto_invoice_and_cn,
auto_complete_on_ship: auto_complete_on_ship, auto_complete_on_ship: auto_complete_on_ship,
+1 -1
View File
@@ -92,7 +92,7 @@ $active_nav = 'home';
<div class="text-container"> <div class="text-container">
<h2>Track every item, every movement, in real time</h2> <h2>Track every item, every movement, in real time</h2>
<p>MN3 WMS gives you a live stock overview across all your warehouses. Drill into individual lots, view expiry dates, and spot low-stock alerts before they become a problem.</p> <p>MN3 WMS gives you a live stock overview across all your warehouses. Drill into individual lots, view expiry dates, and spot low-stock alerts before they become a problem.</p>
<p>Advanced and simple location modes let you model everything from a single rack to complex zone-aisle-rack layouts.</p> <p>Advanced and simple location modes let you model everything from a single bin to complex zone-aisle-bin layouts.</p>
<a class="btn-solid-reg" href="<?= $app_url ?>/login/index.php">See it in action</a> <a class="btn-solid-reg" href="<?= $app_url ?>/login/index.php">See it in action</a>
</div> </div>
</div> </div>
+14 -14
View File
@@ -256,8 +256,8 @@ CREATE TABLE IF NOT EXISTS `md_storage` (
`zone` text NOT NULL DEFAULT '', `zone` text NOT NULL DEFAULT '',
`aisle_from` varchar(255) NOT NULL DEFAULT '', `aisle_from` varchar(255) NOT NULL DEFAULT '',
`aisle_to` varchar(255) NOT NULL DEFAULT '', `aisle_to` varchar(255) NOT NULL DEFAULT '',
`rack_from` varchar(255) NOT NULL DEFAULT '', `bin_from` varchar(255) NOT NULL DEFAULT '',
`rack_to` varchar(255) NOT NULL DEFAULT '', `bin_to` varchar(255) NOT NULL DEFAULT '',
`description` text NOT NULL DEFAULT '', `description` text NOT NULL DEFAULT '',
`status` int(1) NOT NULL DEFAULT 1, `status` int(1) NOT NULL DEFAULT 1,
`log` mediumtext NOT NULL DEFAULT '{}', `log` mediumtext NOT NULL DEFAULT '{}',
@@ -266,7 +266,7 @@ CREATE TABLE IF NOT EXISTS `md_storage` (
", 'md_storage'); ", 'md_storage');
run($pdo, " run($pdo, "
CREATE TABLE IF NOT EXISTS `md_rack` ( CREATE TABLE IF NOT EXISTS `md_bin` (
`id` int(11) NOT NULL AUTO_INCREMENT, `id` int(11) NOT NULL AUTO_INCREMENT,
`company_id` int(11) NOT NULL, `company_id` int(11) NOT NULL,
`td_stock_id` int(11) NOT NULL DEFAULT 0, `td_stock_id` int(11) NOT NULL DEFAULT 0,
@@ -274,20 +274,20 @@ CREATE TABLE IF NOT EXISTS `md_rack` (
`storage_id` int(11) NOT NULL DEFAULT 0, `storage_id` int(11) NOT NULL DEFAULT 0,
`zone` varchar(10) NOT NULL, `zone` varchar(10) NOT NULL,
`aisle` varchar(10) NOT NULL, `aisle` varchar(10) NOT NULL,
`rack` varchar(10) NOT NULL, `bin` varchar(10) NOT NULL,
`product_sku` varchar(50) DEFAULT NULL, `product_sku` varchar(50) DEFAULT NULL,
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
UNIQUE KEY `uk_rack_location` (`company_id`,`warehouse`,`zone`,`aisle`,`rack`), UNIQUE KEY `uk_bin_location` (`company_id`,`warehouse`,`zone`,`aisle`,`bin`),
KEY `idx_storage` (`storage_id`), KEY `idx_storage` (`storage_id`),
KEY `idx_sku` (`company_id`,`product_sku`) KEY `idx_sku` (`company_id`,`product_sku`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
", 'md_rack'); ", 'md_bin');
run($pdo, " run($pdo, "
CREATE TABLE IF NOT EXISTS `td_rack_log` ( CREATE TABLE IF NOT EXISTS `td_bin_log` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`company_id` int(10) NOT NULL, `company_id` int(10) NOT NULL,
`md_rack_id` int(10) unsigned NOT NULL, `md_bin_id` int(10) unsigned NOT NULL,
`user_id` int(10) unsigned DEFAULT NULL, `user_id` int(10) unsigned DEFAULT NULL,
`dt` datetime NOT NULL DEFAULT current_timestamp(), `dt` datetime NOT NULL DEFAULT current_timestamp(),
`login` datetime DEFAULT NULL, `login` datetime DEFAULT NULL,
@@ -295,10 +295,10 @@ CREATE TABLE IF NOT EXISTS `td_rack_log` (
`product_sku` varchar(100) DEFAULT NULL, `product_sku` varchar(100) DEFAULT NULL,
`td_stock_id` int(10) unsigned DEFAULT NULL, `td_stock_id` int(10) unsigned DEFAULT NULL,
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
KEY `idx_rack` (`company_id`,`md_rack_id`), KEY `idx_bin` (`company_id`,`md_bin_id`),
KEY `idx_user` (`company_id`,`user_id`) KEY `idx_user` (`company_id`,`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
", 'td_rack_log'); ", 'td_bin_log');
run($pdo, " run($pdo, "
CREATE TABLE IF NOT EXISTS `md_product_category` ( CREATE TABLE IF NOT EXISTS `md_product_category` (
@@ -401,7 +401,7 @@ CREATE TABLE IF NOT EXISTS `md_barcode` (
`warehouse_id` int(10) unsigned DEFAULT NULL, `warehouse_id` int(10) unsigned DEFAULT NULL,
`zone` varchar(100) DEFAULT NULL, `zone` varchar(100) DEFAULT NULL,
`aisle` varchar(100) DEFAULT NULL, `aisle` varchar(100) DEFAULT NULL,
`rack` varchar(100) DEFAULT NULL, `bin` varchar(100) DEFAULT NULL,
`print_count` int(10) unsigned NOT NULL DEFAULT 0, `print_count` int(10) unsigned NOT NULL DEFAULT 0,
`last_printed_dt` datetime DEFAULT NULL, `last_printed_dt` datetime DEFAULT NULL,
`created_dt` datetime NOT NULL DEFAULT current_timestamp(), `created_dt` datetime NOT NULL DEFAULT current_timestamp(),
@@ -410,7 +410,7 @@ CREATE TABLE IF NOT EXISTS `md_barcode` (
UNIQUE KEY `company_id_barcode` (`company_id`,`barcode`), UNIQUE KEY `company_id_barcode` (`company_id`,`barcode`),
KEY `company_id_type` (`company_id`,`barcode_type`), KEY `company_id_type` (`company_id`,`barcode_type`),
KEY `company_id_product_lot` (`company_id`,`product_sku`,`lot_number`), KEY `company_id_product_lot` (`company_id`,`product_sku`,`lot_number`),
KEY `company_id_location` (`company_id`,`warehouse_id`,`zone`,`aisle`,`rack`) KEY `company_id_location` (`company_id`,`warehouse_id`,`zone`,`aisle`,`bin`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
", 'md_barcode'); ", 'md_barcode');
@@ -527,7 +527,7 @@ CREATE TABLE IF NOT EXISTS `td_stock` (
`price` decimal(18,4) NOT NULL DEFAULT 0.0000, `price` decimal(18,4) NOT NULL DEFAULT 0.0000,
`zone` varchar(255) NOT NULL DEFAULT '', `zone` varchar(255) NOT NULL DEFAULT '',
`aisle` varchar(255) NOT NULL DEFAULT '', `aisle` varchar(255) NOT NULL DEFAULT '',
`rack` varchar(255) NOT NULL DEFAULT '', `bin` varchar(255) NOT NULL DEFAULT '',
`contact_id` int(11) NOT NULL DEFAULT 0, `contact_id` int(11) NOT NULL DEFAULT 0,
`description` text NOT NULL DEFAULT '', `description` text NOT NULL DEFAULT '',
`status` int(3) NOT NULL DEFAULT 0, `status` int(3) NOT NULL DEFAULT 0,
@@ -972,7 +972,7 @@ CREATE TABLE IF NOT EXISTS `td_supplier_return_item` (
`stock_in_id` int(11) NOT NULL DEFAULT 0, `stock_in_id` int(11) NOT NULL DEFAULT 0,
`zone` varchar(50) NOT NULL DEFAULT '', `zone` varchar(50) NOT NULL DEFAULT '',
`aisle` varchar(50) NOT NULL DEFAULT '', `aisle` varchar(50) NOT NULL DEFAULT '',
`rack` varchar(50) NOT NULL DEFAULT '', `bin` varchar(50) NOT NULL DEFAULT '',
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
UNIQUE KEY `uq_sr_item` (`return_id`,`item_id`), UNIQUE KEY `uq_sr_item` (`return_id`,`item_id`),
KEY `idx_return` (`company_id`,`return_id`) KEY `idx_return` (`company_id`,`return_id`)