Change 'Rack' to 'Bin'

This commit is contained in:
Thanakorn S
2026-05-27 17:14:53 +07:00
parent ea94b6a6ae
commit 987cf7ded9
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) {
var s = res.output;
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_aisle = s.location_label_aisle || 'Aisle';
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>',
'1':'<span class="badge bg-success">Approved</span>'}[String(r.status)]
|| '—';
var location = [r.zone, r.aisle, r.rack].filter(function(v) {
return v && v !== r.rack;
}).concat([r.rack]).filter(Boolean).join(' / ');
var location = [r.zone, r.aisle, r.bin].filter(function(v) {
return v && v !== r.bin;
}).concat([r.bin]).filter(Boolean).join(' / ');
html += `<tr>
<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>
+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.
*/
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(
"SELECT 1 FROM md_rack
"SELECT 1 FROM md_bin
WHERE company_id = :company_id
AND warehouse = :warehouse
AND COALESCE(zone, '') = :zone
AND COALESCE(aisle,'') = :aisle
AND rack = :rack
AND bin = :bin
LIMIT 1"
);
$sth->execute([
@@ -139,7 +139,7 @@ class BarcodeManager {
':warehouse' => $warehouse_id,
':zone' => $zone,
':aisle' => $aisle,
':rack' => $rack,
':bin' => $bin,
]);
if (!$sth->fetchColumn()) throw new Exception('Location not found.');
}
@@ -343,7 +343,7 @@ class BarcodeManager {
*/
public function listLocationLabels(): array {
$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
WHERE company_id = :company_id
AND barcode_type = 'loc'"
@@ -355,30 +355,30 @@ class BarcodeManager {
/**
* 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 int $warehouse_id Warehouse ID.
* @param string $zone Zone code.
* @param string $aisle Aisle code.
* @param string $rack Rack code.
* @return array Keys: barcode, warehouse_id, zone, aisle, rack.
* @throws Exception If the location is not found in md_rack.
* @param string $bin Bin code.
* @return array Keys: barcode, warehouse_id, zone, aisle, bin.
* @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 {
$this->validateLocation($warehouse_id, $zone, $aisle, $rack);
public function createLocationLabel(string $barcode, int $warehouse_id, string $zone, string $aisle, string $bin): array {
$this->validateLocation($warehouse_id, $zone, $aisle, $bin);
$this->pdo->prepare(
"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
(:company_id, 'loc', :barcode, :warehouse_id, :zone, :aisle, :rack)
(:company_id, 'loc', :barcode, :warehouse_id, :zone, :aisle, :bin)
ON DUPLICATE KEY UPDATE
barcode_type = VALUES(barcode_type),
warehouse_id = VALUES(warehouse_id),
zone = VALUES(zone),
aisle = VALUES(aisle),
rack = VALUES(rack),
bin = VALUES(bin),
status = 1"
)->execute([
':company_id' => $this->company_id,
@@ -386,7 +386,7 @@ class BarcodeManager {
':warehouse_id' => $warehouse_id,
':zone' => $zone,
':aisle' => $aisle,
':rack' => $rack,
':bin' => $bin,
]);
return [
@@ -394,7 +394,7 @@ class BarcodeManager {
'warehouse_id' => $warehouse_id,
'zone' => $zone,
'aisle' => $aisle,
'rack' => $rack,
'bin' => $bin,
];
}
@@ -465,7 +465,7 @@ class BarcodeManager {
* resolve it from the caller via CompanySettingManager::get('advanced_location').
*
* @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
* @throws Exception On parse or validation failure.
*/
@@ -515,13 +515,13 @@ class BarcodeManager {
if ($advancedLocation) {
$zone = trim($parts[2]);
$aisle = trim($parts[3]);
$rack = trim($parts[4]);
$bin = trim($parts[4]);
} else {
$rack = trim($parts[2]);
$zone = $rack;
$aisle = $rack;
$bin = trim($parts[2]);
$zone = $bin;
$aisle = $bin;
}
if (!$warehouse_id || $zone === '' || $aisle === '' || $rack === '') {
if (!$warehouse_id || $zone === '' || $aisle === '' || $bin === '') {
throw new Exception('Location label is incomplete.');
}
@@ -531,19 +531,19 @@ class BarcodeManager {
(int)$label['warehouse_id'] !== $warehouse_id ||
$label['zone'] !== $zone ||
$label['aisle'] !== $aisle ||
$label['rack'] !== $rack
$label['bin'] !== $bin
) {
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 [
'type' => 'loc',
'warehouse_id' => $warehouse_id,
'zone' => $zone,
'aisle' => $aisle,
'rack' => $rack,
'bin' => $bin,
];
}
@@ -8,8 +8,8 @@
* default_stock_status int 0 = draft, 1 = auto-approve on save
* auto_invoice_and_credit_note int 0 = manual, 1 = auto-generate
* auto_complete_on_ship int 0 = manual, 1 = auto-complete on shipped
* advanced_location int 0 = simple (single location), 1 = zone/aisle/rack
* location_label_rack string label for rack / single location field
* advanced_location int 0 = simple (single location), 1 = zone/aisle/bin
* location_label_bin string label for bin / single location field
* location_label_zone string label for zone (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_complete_on_ship' => 1, // auto-complete 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_aisle' => 'Aisle', // advanced level 2 default
'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.
* No separate child table exists. SKU-level reporting is served by
* td_stock_* rows (source='order') rather than querying items JSON.
* - confirmOrder() picks racks via FIFO — oldest approved stock-in row
* still rack-occupied, ordered by td_stock_<warehouse_id>.date ASC.
* - confirmOrder() picks bins via FIFO — oldest approved stock-in row
* still bin-occupied, ordered by td_stock_<warehouse_id>.date ASC.
* - confirmOrder() creates stock-out rows with status=0 (draft).
* 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
* 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.
* Cancellation is blocked if any active invoice (status != 4 / void) or
* active return (status != -1 / cancelled) is linked to the order.
* - 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.
* 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
* in a given warehouse that is still rack-occupied.
* FIFO bin pick — find the oldest approved stock-in row for a SKU
* in a given warehouse that is still bin-occupied.
*
* "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 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.
*/
private function pickFifoRack(int $warehouse_id, string $product_sku): ?array
@@ -94,7 +94,7 @@ class OrderManager {
$table = $this->stockTableNameFromWarehouseId($warehouse_id);
$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((
SELECT SUM(o.`out`)
FROM `{$table}` o
@@ -104,7 +104,7 @@ class OrderManager {
AND o.status != -1
), 0)) AS available_qty
FROM `{$table}` s
INNER JOIN md_rack r
INNER JOIN md_bin r
ON r.company_id = s.company_id
AND r.warehouse = :warehouse_id
AND r.td_stock_id = s.id
@@ -358,7 +358,7 @@ class OrderManager {
if ($stock_out_warehouse_id > 0 && $stock_out_id > 0) {
$table = $this->stockTableNameFromWarehouseId($stock_out_warehouse_id);
$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}`
WHERE company_id = :company_id
AND id = :id
@@ -374,7 +374,7 @@ class OrderManager {
if ($stock_out) {
$item['zone'] = $stock_out['zone'] ?? ($item['zone'] ?? '');
$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['serial_number'] = $stock_out['serial_number'] ?? ($item['serial_number'] ?? '');
$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
* 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:
* 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,
* source='order', source_id=order_id.
* 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.
* 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
* 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 array $logging Audit entry appended to td_order.log.
* @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
{
@@ -668,22 +668,22 @@ class OrderManager {
continue;
}
$rack_stock = $this->pickFifoRack($warehouse_id, $product_sku);
if (!$rack_stock) {
$bin_stock = $this->pickFifoRack($warehouse_id, $product_sku);
if (!$bin_stock) {
$preflight_errors[] = "No available stock for \"{$name}\" in the selected warehouse.";
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) {
$preflight_errors[] =
"Insufficient stock for \"{$name}\": " .
"{$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'))) {
$lot = $rack_stock['lot_number'] ?? '';
$lot = $bin_stock['lot_number'] ?? '';
$preflight_errors[] =
"Lot {$lot} for \"{$name}\" expired on {$expiry} and cannot be picked.";
}
@@ -699,8 +699,8 @@ class OrderManager {
$product_sku = $item['product_sku'] ?? '';
$quantity = (float)($item['quantity'] ?? 0);
$rack_stock = $this->pickFifoRack($warehouse_id, $product_sku);
$available_qty = (float)($rack_stock['available_qty'] ?? $rack_stock['in'] ?? 0);
$bin_stock = $this->pickFifoRack($warehouse_id, $product_sku);
$available_qty = (float)($bin_stock['available_qty'] ?? $bin_stock['in'] ?? 0);
$table = $this->stockTableNameFromWarehouseId($warehouse_id);
$item_uuid = $uuid . '_' . $i;
@@ -709,13 +709,13 @@ class OrderManager {
$this->pdo->prepare(
"INSERT INTO `{$table}`
(uuid, company_id, `date`, product_sku, `out`,
zone, aisle, rack,
zone, aisle, bin,
contact_id, `description`, `log`, `type`,
ref_id, lot_number, serial_number,
source, source_id, price, status)
VALUES
(:uuid, :company_id, :date, :product_sku, :quantity,
:zone, :aisle, :rack,
:zone, :aisle, :bin,
:contact_id, :description, :log, 'out',
:ref_id, :lot_number, :serial_number,
'order', :source_id, :price, 0)"
@@ -725,28 +725,28 @@ class OrderManager {
':date' => date('Y-m-d H:i:s'),
':product_sku' => $product_sku,
':quantity' => $quantity,
':zone' => $rack_stock['zone'],
':aisle' => $rack_stock['aisle'],
':rack' => $rack_stock['rack'],
':zone' => $bin_stock['zone'],
':aisle' => $bin_stock['aisle'],
':bin' => $bin_stock['bin'],
':contact_id' => (int)$order['contact_id'],
':description' => $order['order_number'],
':log' => json_encode($item_log),
':ref_id' => (int)$rack_stock['id'],
':lot_number' => $rack_stock['lot_number'] ?? '',
':serial_number' => $rack_stock['serial_number'] ?? '',
':ref_id' => (int)$bin_stock['id'],
':lot_number' => $bin_stock['lot_number'] ?? '',
':serial_number' => $bin_stock['serial_number'] ?? '',
':source_id' => $order_id,
':price' => (float)($item['unit_price'] ?? 0),
]);
// Write rack context + stock_out_id back into the item object
$item['zone'] = $rack_stock['zone'];
$item['aisle'] = $rack_stock['aisle'];
$item['rack'] = $rack_stock['rack'];
$item['lot_number'] = $rack_stock['lot_number'] ?? '';
$item['serial_number'] = $rack_stock['serial_number'] ?? '';
// Write bin context + stock_out_id back into the item object
$item['zone'] = $bin_stock['zone'];
$item['aisle'] = $bin_stock['aisle'];
$item['bin'] = $bin_stock['bin'];
$item['lot_number'] = $bin_stock['lot_number'] ?? '';
$item['serial_number'] = $bin_stock['serial_number'] ?? '';
$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) {
$stock = new StockManager($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
*
* 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
* order (any status except already -1) are soft-deleted (status → -1)
* across all td_stock_* tables.
@@ -882,7 +882,7 @@ class OrderManager {
$warehouse_id = (int)$matches[1];
$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}`
WHERE company_id = :company_id
AND source = 'order'
@@ -903,11 +903,11 @@ class OrderManager {
$whMgmt->assertStockMovementWindow($row['date'] ?? null, 'Order stock cancellation');
$whMgmt->occupyRack(
$whMgmt->occupyBin(
$warehouse_id,
(string)$row['zone'],
(string)$row['aisle'],
(string)$row['rack'],
(string)$row['bin'],
(string)$row['product_sku'],
(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.
*
* 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.
* 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".
*
* Security fix: was previously using $this->companyId (undefined property),
* 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
{
@@ -554,14 +554,14 @@ class ProductManager {
r.id,
r.zone,
r.aisle,
r.rack,
r.bin,
r.product_sku,
r.td_stock_id,
mw.warehouse_name,
mw.id AS warehouse_id,
p.product_name,
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
ON mw.company_id = r.company_id
AND mw.id = r.warehouse
@@ -571,7 +571,7 @@ class ProductManager {
WHERE r.company_id = :company_id
ORDER BY mw.warehouse_name, r.zone,
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]);
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(),
* 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),
* 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.
* Callers must wrap multi-step operations inside dbTransaction().
*
@@ -274,7 +274,7 @@ class PurchaseOrderManager {
'warehouse_id' => (int)($item['receive_wh'] ?? 0),
'zone' => $item['receive_zone'] ?? '',
'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 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 }
* @param string $uuid UUID prefix for stock-in rows.
* @param array $logging Audit entry.
@@ -573,16 +573,16 @@ class PurchaseOrderManager {
$item_uuid = $uuid . '_' . $j;
$item_log = array_merge($logging, ['action' => 'receive_item']);
$rack = $recv['rack'] ?? '';
$bin = $recv['bin'] ?? '';
$zone = $recv['zone'] ?? '';
$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).
// occupyRack() looks up md_rack WHERE zone=:zone AND aisle=:aisle AND rack=:rack,
// so all three must match — a blank zone/aisle produces "Rack --b does not exist".
if ($zone === '' && $rack !== '') $zone = $rack;
if ($aisle === '' && $rack !== '') $aisle = $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 "Bin --b does not exist".
if ($zone === '' && $bin !== '') $zone = $bin;
if ($aisle === '' && $bin !== '') $aisle = $bin;
// Resolve unit_price: prefer the receive-time override, fall back to PO item price
$po_item_price = 0;
@@ -599,7 +599,7 @@ class PurchaseOrderManager {
'price' => $unit_price,
'zone' => $zone,
'aisle' => $aisle,
'rack' => $rack,
'bin' => $bin,
'lot_number' => $recv['lot_number'] ?? '',
'expiry_date' => $recv['expiry_date'] ?? '',
'serial_number' => $recv['serial_number'] ?? '',
@@ -626,7 +626,7 @@ class PurchaseOrderManager {
]);
// 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) {
$stock->approveStock($new_stock_in_id, $warehouse_id, 'in', $whMgmt);
}
@@ -708,7 +708,7 @@ class PurchaseOrderManager {
* Cancel a PO.
*
* 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).
*
@@ -756,7 +756,7 @@ class PurchaseOrderManager {
$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) {
if (!preg_match('/^td_stock_(\d+)$/', $table, $m)) {
continue;
@@ -764,7 +764,7 @@ class PurchaseOrderManager {
$warehouse_id = (int)$m[1];
$sth = $this->pdo->prepare(
"SELECT id, zone, aisle, rack
"SELECT id, zone, aisle, bin
FROM `{$table}`
WHERE company_id = :company_id
AND source = 'po'
@@ -783,11 +783,11 @@ class PurchaseOrderManager {
':company_id' => $this->company_id,
]);
$whMgmt->releaseRack(
$whMgmt->releaseBin(
$warehouse_id,
$row['zone'],
$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);
foreach ($tables as $table) {
if (!preg_match('/^td_stock_(\d+)$/', (string)$table, $matches)) continue;
$warehouse_id = (int)$matches[1];
$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"
);
$row_sth->execute([':cid' => $this->company_id, ':id' => $po_id]);
$draft_rows = $row_sth->fetchAll(PDO::FETCH_ASSOC);
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(
+35 -35
View File
@@ -12,7 +12,7 @@
* Dashboard reports → stats, movement charts, most moved, recent activity
* Warehouse detail → capacity, space used, balance, movement, trend, activity
* 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
*
* 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.
*/
public function getTotalCapacity(): int
{
$sql = "SELECT COUNT(*)
FROM md_rack
FROM md_bin
WHERE company_id = :company_id";
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.
*/
public function getSpaceUsed(): int
{
$sql = "SELECT COUNT(*)
FROM md_rack
FROM md_bin
WHERE company_id = :company_id
AND product_sku IS NOT NULL";
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.
* @return int Total rack count.
* @return int Total bin count.
*/
public function getWarehouseCapacity(int $warehouse_id): int
{
$sth = $this->pdo->prepare(
"SELECT COUNT(*) FROM md_rack
"SELECT COUNT(*) FROM md_bin
WHERE company_id = :company_id AND warehouse = :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.
* @return int Occupied rack count.
* @return int Occupied bin count.
*/
public function getWarehouseSpaceUsed(int $warehouse_id): int
{
$sth = $this->pdo->prepare(
"SELECT COUNT(*) FROM md_rack
"SELECT COUNT(*) FROM md_bin
WHERE company_id = :company_id AND warehouse = :warehouse_id
AND product_sku IS NOT NULL"
);
@@ -1008,14 +1008,14 @@ class ReportManager
s.lot_number,
s.zone,
s.aisle,
s.rack,
s.bin,
ROUND(SUM(s.`in`) - SUM(s.`out`), 2) AS quantity,
{$wh['id']} AS warehouse_id
FROM `{$table}` s
WHERE s.company_id = {$cid}
AND s.lot_number IS NOT NULL
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";
},
$warehouses
@@ -1026,7 +1026,7 @@ class ReportManager
stock.lot_number,
stock.zone,
stock.aisle,
stock.rack,
stock.bin,
stock.quantity,
l.expiry_date,
DATEDIFF(l.expiry_date, CURDATE()) AS days_remaining,
@@ -1073,7 +1073,7 @@ class ReportManager
'lot_number' => $row['lot_number'],
'zone' => $row['zone'],
'aisle' => $row['aisle'],
'rack' => $row['rack'],
'bin' => $row['bin'],
'quantity' => (float) $row['quantity'],
'expiry_date' => $row['expiry_date'],
'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 $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
{
@@ -1123,7 +1123,7 @@ class ReportManager
s.type,
s.zone,
s.aisle,
s.rack,
s.bin,
ROUND(COALESCE(s.`in`, 0), 2) AS stock_in,
ROUND(COALESCE(s.`out`, 0), 2) AS stock_out,
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.
*
* 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
* 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.
*/
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;
if ($main_db === '' || !ctype_alnum(str_replace('_', '', $main_db))) {
@@ -1277,30 +1277,30 @@ class ReportManager
rl.td_stock_id,
rl.login,
u.name AS user_name
FROM td_rack_log rl
FROM td_bin_log rl
LEFT JOIN `{$main_db}`.user u
ON u.user_id = rl.user_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"
);
$sth->execute([
':company_id' => $this->companyId,
':rack_id' => $rack_id,
':bin_id' => $bin_id,
]);
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.
*
* Used by the rack occupancy dashboard report page to visualise which
* racks are empty vs occupied and which product is in each slot.
* Results are ordered by warehouse → zone → aisle → rack with
* Used by the bin occupancy dashboard report page to visualise which
* bins are empty vs occupied and which product is in each slot.
* Results are ordered by warehouse → zone → aisle → bin with
* 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
{
@@ -1309,14 +1309,14 @@ class ReportManager
r.id,
r.zone,
r.aisle,
r.rack,
r.bin,
r.product_sku,
r.td_stock_id,
mw.warehouse_name,
mw.id AS warehouse_id,
p.product_name,
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
ON mw.company_id = r.company_id
AND mw.id = r.warehouse
@@ -1326,7 +1326,7 @@ class ReportManager
WHERE r.company_id = :company_id
ORDER BY mw.warehouse_name, r.zone,
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]);
return $sth->fetchAll(PDO::FETCH_ASSOC);
@@ -1510,7 +1510,7 @@ class ReportManager
s.serial_number,
s.zone,
s.aisle,
s.rack,
s.bin,
ROUND(COALESCE(s.`in`, 0), 2) AS stock_in,
ROUND(COALESCE(s.`out`, 0), 2) AS stock_out,
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,
* status=1 ALWAYS — confirming a return is a final business decision,
* 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()
* auto-generates a CN linked to the original invoice (status=1 issued).
* 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).
* - 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.
* 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
* 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.
*
@@ -389,7 +389,7 @@ class ReturnManager {
* Flow per item:
* 1. INSERT td_stock_<warehouse_id> in row with type='in', status=1 (force confirmed),
* 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.
*
* After all items:
@@ -400,7 +400,7 @@ class ReturnManager {
*
* Stock-in rows respect $auto_approve (mirrors default_stock_status config).
* 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.
*
@@ -495,13 +495,13 @@ class ReturnManager {
$this->pdo->prepare(
"INSERT INTO `{$table}`
(uuid, company_id, `date`, product_sku, `in`,
zone, aisle, rack,
zone, aisle, bin,
contact_id, `description`, `log`, `type`,
lot_number, serial_number,
source, source_id, ref_id, price, status)
VALUES
(:uuid, :company_id, :date, :product_sku, :quantity,
:zone, :aisle, :rack,
:zone, :aisle, :bin,
:contact_id, :description, :log, 'in',
:lot_number, :serial_number,
'return', :source_id, :ref_id, :price, :status)"
@@ -513,7 +513,7 @@ class ReturnManager {
':quantity' => $quantity,
':zone' => $item['zone'] ?? '',
':aisle' => $item['aisle'] ?? '',
':rack' => $item['rack'] ?? '',
':bin' => $item['bin'] ?? '',
':contact_id' => (int)$return['contact_id'],
':description' => $return['return_number'],
':log' => json_encode($item_log),
@@ -528,11 +528,11 @@ class ReturnManager {
$stock_in_id = (int)$this->pdo->lastInsertId();
if ($auto_approve) {
$whMgmt->occupyRack(
$whMgmt->occupyBin(
$warehouse_id,
$item['zone'] ?? '',
$item['aisle'] ?? '',
$item['rack'] ?? '',
$item['bin'] ?? '',
$product_sku,
$stock_in_id,
true
@@ -583,9 +583,9 @@ class ReturnManager {
* and doc_type.
*
* 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
* 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.
*
@@ -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) {
$sth2 = $this->pdo->prepare(
"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)
$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
AND source = 'return'
AND source_id = :return_id
@@ -679,11 +679,11 @@ class ReturnManager {
if ((int)$row['status'] === 1) {
$whMgmt->assertStockMovementWindow($row['date'] ?? null, 'Return stock cancellation');
// Approved rows: reverse rack occupation + balance
$whMgmt->releaseRack($warehouse_id, $row['zone'], $row['aisle'], $row['rack']);
// Approved rows: reverse bin occupation + balance
$whMgmt->releaseBin($warehouse_id, $row['zone'], $row['aisle'], $row['bin']);
$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
* 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.).
*
* Note: Write methods do NOT manage their own DB transactions.
@@ -228,16 +228,16 @@ class StockManager {
* Insert flow (id = 0):
* 1. Upserts md_lot if lot_number + expiry_date are provided.
* 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().
*
* Update flow (id > 0):
* - 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.
*
* @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.
* @param array $logging Audit entry to append to the log column.
* @param string $uuid UUID for this transaction (shared across transfer pairs).
@@ -303,10 +303,10 @@ class StockManager {
$this->pdo->prepare(
"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)
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())"
)->execute([
':uuid' => $uuid,
@@ -317,7 +317,7 @@ class StockManager {
':price' => (float)($data['price'] ?? 0),
':zone' => $data['zone'],
':aisle' => $data['aisle'],
':rack' => $data['rack'],
':bin' => $data['bin'],
':contact_id' => (int)($data['contact_id'] ?? 0),
':description' => $data['description'] ?? '',
':log' => json_encode($table_log),
@@ -328,13 +328,13 @@ class StockManager {
$td_stock_id = (int)$this->pdo->lastInsertId();
// 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.
$whMgmt->occupyRack(
$whMgmt->occupyBin(
$warehouse_id,
$data['zone'],
$data['aisle'],
$data['rack'],
$data['bin'],
$data['product_sku'],
$td_stock_id
);
@@ -347,9 +347,9 @@ class StockManager {
* Insert a new stock_out record or update metadata on an existing one.
*
* Insert flow (id = 0):
* 1. Validates the rack is occupied with the correct SKU / lot / serial.
* 2. Inserts the td_stock_<warehouse_id> row, copying quantity and lot info from the rack.
* 3. Calls WarehouseManager::releaseRack() to free the rack slot.
* 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 bin.
* 3. Calls WarehouseManager::releaseBin() to free the bin slot.
* 4. Calls WarehouseManager::adjustBalance() to update etl_stock_summary.
*
* Update flow (id > 0):
@@ -357,11 +357,11 @@ class StockManager {
*
* 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.
* @param array $logging Audit entry to append to the log column.
* @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
{
@@ -400,33 +400,33 @@ class StockManager {
} else {
// Validate rack holds the expected product / lot / serial
$source_stock = $whMgmt->getRackStock(
// Validate bin holds the expected product / lot / serial
$source_stock = $whMgmt->getBinStock(
$warehouse_id,
$data['zone'], $data['aisle'], $data['rack']
$data['zone'], $data['aisle'], $data['bin']
);
if (!$source_stock) {
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']) {
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']) {
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']) {
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(
"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)
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())"
)->execute([
':uuid' => $uuid,
@@ -451,7 +451,7 @@ class StockManager {
':quantity' => $quantity,
':zone' => $data['zone'],
':aisle' => $data['aisle'],
':rack' => $data['rack'],
':bin' => $data['bin'],
':contact_id' => (int)($data['contact_id'] ?? 0),
':description' => $data['description'] ?? '',
':log' => json_encode($table_log),
@@ -462,7 +462,7 @@ class StockManager {
$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;
}
}
@@ -475,11 +475,11 @@ class StockManager {
* sharing the same UUID and cross-referencing each other via ref_id.
*
* 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>.
* 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.
* 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).
*
* Update flow (id > 0):
@@ -488,11 +488,11 @@ class StockManager {
* Must be called inside dbTransaction() by the caller.
*
* @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.
* @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.
* @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
{
@@ -566,39 +566,39 @@ class StockManager {
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_zone = $data['zone_from'];
$from_aisle = $data['aisle_from'];
$from_rack = $data['rack_from'];
$from_bin = $data['bin_from'];
$to_warehouse = (int)$data['warehouse_to'];
$to_zone = $data['zone_to'];
$to_aisle = $data['aisle_to'];
$to_rack = $data['rack_to'];
$to_bin = $data['bin_to'];
$source_stock = $whMgmt->getRackStock(
$from_warehouse, $from_zone, $from_aisle, $from_rack
$source_stock = $whMgmt->getBinStock(
$from_warehouse, $from_zone, $from_aisle, $from_bin
);
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']) {
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']) {
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']) {
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(
"INSERT INTO `$from_table`
(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)
VALUES
(: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())"
)->execute([
':uuid' => $uuid,
@@ -630,7 +630,7 @@ class StockManager {
':ref_warehouse' => $to_warehouse,
':zone' => $from_zone,
':aisle' => $from_aisle,
':rack' => $from_rack,
':bin' => $from_bin,
':contact_id' => $contact_id,
':description' => $description,
':log' => json_encode($table_log),
@@ -643,11 +643,11 @@ class StockManager {
$this->pdo->prepare(
"INSERT INTO `$to_table`
(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)
VALUES
(: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())"
)->execute([
':uuid' => $uuid,
@@ -659,7 +659,7 @@ class StockManager {
':ref_id' => $from_stock_id,
':zone' => $to_zone,
':aisle' => $to_aisle,
':rack' => $to_rack,
':bin' => $to_bin,
':contact_id' => $contact_id,
':description' => $description,
':log' => json_encode($table_log),
@@ -678,7 +678,7 @@ class StockManager {
':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;
}
@@ -730,13 +730,13 @@ class StockManager {
"UPDATE `{$table}` SET status = 1, updated_at = NOW() WHERE id = :id AND company_id = :company_id"
)->execute([':id' => $id, ':company_id' => $this->company_id]);
// ── Rack state + balance ──────────────────────────────────────────
// ── Bin state + balance ──────────────────────────────────────────
if ($type === 'in') {
// Occupy rack now that stock is approved
$whMgmt->occupyRack(
// Occupy bin now that stock is approved
$whMgmt->occupyBin(
$warehouse_id,
$row['zone'], $row['aisle'], $row['rack'],
$row['zone'], $row['aisle'], $row['bin'],
$row['product_sku'],
$id
);
@@ -766,9 +766,9 @@ class StockManager {
$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) {
$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'],
$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]);
}
// Rack state: release source, occupy destination
// Bin state: release source, occupy destination
if ($from_row && $from_wh_id) {
$whMgmt->releaseRack(
$whMgmt->releaseBin(
$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'],
(int)$from_row['id'], $from_row['source'] ?? '', (int)($from_row['source_id'] ?? 0), $from_row['date'] ?? '');
}
if ($inbound_row && $paired_wh_id) {
$whMgmt->occupyRack(
$whMgmt->occupyBin(
$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['id']
);
@@ -26,7 +26,7 @@ class StockSourceManager
$warehouse_id = (int)$match[1];
$sth = $this->pdo->prepare(
"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,
p.product_name
FROM `{$table}` s
@@ -104,10 +104,10 @@ class SupplierReturnManager {
$sth = $this->pdo->prepare(
"INSERT INTO td_supplier_return_item
(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
(: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) {
$sth->execute([
@@ -125,7 +125,7 @@ class SupplierReturnManager {
':stock_in_id' => (int)($item['stock_in_id'] ?? 0),
':zone' => $item['zone'] ?? '',
':aisle' => $item['aisle'] ?? '',
':rack' => $item['rack'] ?? '',
':bin' => $item['bin'] ?? '',
]);
}
}
@@ -348,7 +348,7 @@ class SupplierReturnManager {
* Flow per item:
* 1. Validate stock-in row (from PO receive) is approved.
* 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:
* 4. UPDATE td_supplier_return.status = 1.
*
@@ -433,9 +433,9 @@ class SupplierReturnManager {
$zone = $item['zone'] ?? '';
$aisle = $item['aisle'] ?? '';
$rack = $item['rack'] ?? '';
if ($zone === '' && $rack !== '') $zone = $rack;
if ($aisle === '' && $rack !== '') $aisle = $rack;
$bin = $item['bin'] ?? '';
if ($zone === '' && $bin !== '') $zone = $bin;
if ($aisle === '' && $bin !== '') $aisle = $bin;
$item_uuid = $uuid . '_srn_' . $i;
$item_log = [array_merge($logging, ['action' => 'confirm_return_item'])];
@@ -443,13 +443,13 @@ class SupplierReturnManager {
$this->pdo->prepare(
"INSERT INTO `{$table}`
(uuid, company_id, `date`, product_sku, `out`,
zone, aisle, rack,
zone, aisle, bin,
contact_id, `description`, `log`, `type`,
lot_number, serial_number,
source, source_id, ref_id, price, status)
VALUES
(:uuid, :company_id, :date, :product_sku, :quantity,
:zone, :aisle, :rack,
:zone, :aisle, :bin,
:contact_id, :description, :log, 'out',
:lot_number, :serial_number,
'supplier_return', :source_id, :ref_id, :price, :status)"
@@ -461,7 +461,7 @@ class SupplierReturnManager {
':quantity' => $quantity,
':zone' => $zone,
':aisle' => $aisle,
':rack' => $rack,
':bin' => $bin,
':contact_id' => (int)$return['contact_id'],
':description' => $return['return_number'],
':log' => json_encode($item_log),
@@ -475,7 +475,7 @@ class SupplierReturnManager {
if ($auto_approve) {
$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);
}
}
@@ -560,7 +560,7 @@ class SupplierReturnManager {
// Fetch ALL non-cancelled stock-out rows (status=1 approved or status=0 draft)
$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
AND source = 'supplier_return'
AND source_id = :return_id
@@ -582,14 +582,14 @@ class SupplierReturnManager {
if ((int)$row['status'] === 1) {
$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'];
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);
}
// 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);
$zone = trim($data['zone'] ?? '');
$aisle = trim($data['aisle'] ?? '');
$rack = trim($data['rack'] ?? '');
$bin = trim($data['bin'] ?? '');
if ($label_action !== 'list') {
require_role($user_role, ['owner', 'admin', 'staff']);
@@ -56,14 +56,14 @@
}
// Default: create
if ($barcode === '' || !$warehouse_id || $rack === '') {
if ($barcode === '' || !$warehouse_id || $bin === '') {
$answer['message'] = 'Location barcode data is incomplete.';
http_response_code(400);
exit(json_encode($answer));
}
try {
$output = $bm->createLocationLabel($barcode, $warehouse_id, $zone, $aisle, $rack);
$output = $bm->createLocationLabel($barcode, $warehouse_id, $zone, $aisle, $bin);
$answer['success'] = 1;
$answer['output'] = $output;
} 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 (!empty($data['id'])) {
$output = $whMgmt->getRacksAll($warehouse, $zone, $aisle);
$output = $whMgmt->getBinsAll($warehouse, $zone, $aisle);
} elseif ($type === 'to') {
$output = $whMgmt->getRacksIn($warehouse, $zone, $aisle);
$output = $whMgmt->getBinsIn($warehouse, $zone, $aisle);
} elseif ($type === 'from') {
$output = $whMgmt->getRacksOut(
$output = $whMgmt->getBinsOut(
$warehouse,
$zone,
$aisle,
@@ -26,7 +26,7 @@
$data['serial_number'] ?? null
);
} else {
$output = $whMgmt->getRacksAll($warehouse, $zone, $aisle);
$output = $whMgmt->getBinsAll($warehouse, $zone, $aisle);
}
$answer['output'] = $output;
+1 -1
View File
@@ -10,7 +10,7 @@
$id = (int)($data['id'] ?? 0);
// 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) {
$answer['output'] = $wh->getWarehousesByLot($sku, $lot);
} else {
@@ -11,7 +11,7 @@
$warehouse_id = (int)($data['warehouse_id'] ?? 0);
$zone = trim($data['zone'] ?? '');
$aisle = trim($data['aisle'] ?? '');
$rack = trim($data['rack'] ?? '');
$bin = trim($data['bin'] ?? '');
$product_sku = trim($data['product_sku'] ?? '');
$lot_number = trim($data['lot_number'] ?? '');
$serial = trim($data['serial_number'] ?? '');
@@ -22,7 +22,7 @@
$warehouse_id,
$zone,
$aisle,
$rack,
$bin,
$product_sku,
$lot_number,
$serial
+43 -43
View File
@@ -18,7 +18,7 @@
<div class="col-12 d-flex justify-content-between align-items-center">
<div>
<h1 class="fs-3 mb-1">Location Barcode Labels</h1>
<p class="mb-0 text-muted">Print or reprint labels from the rack master list.</p>
<p class="mb-0 text-muted">Print or reprint labels from the bin master list.</p>
</div>
<div class="d-flex gap-2">
<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="position-relative mb-4">
<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">
<i class="ti ti-search text-muted"></i>
</span>
@@ -51,13 +51,13 @@
<th>Warehouse</th>
<th class="th-zone">Zone</th>
<th class="th-aisle">Aisle</th>
<th id="th-rack">Rack</th>
<th id="th-bin">Bin</th>
<th>Status</th>
<th>Label Status</th>
<th class="text-end">Action</th>
</tr>
</thead>
<tbody id="rack_tbody">
<tbody id="bin_tbody">
<tr><td colspan="7" class="text-center text-muted py-4">Loading locations...</td></tr>
</tbody>
</table>
@@ -79,13 +79,13 @@
<script src="https://cdn.jsdelivr.net/npm/jsbarcode@3.11.6/dist/JsBarcode.all.min.js"></script>
<script>
var advanced = false;
var label_rack = 'Location';
var label_bin = 'Location';
var label_zone = 'Zone';
var label_aisle = 'Aisle';
var racks = [];
var bins = [];
var barcode_status = {};
var selected_payload = '';
var selected_rack_index = -1;
var selected_bin_index = -1;
function escape_html(value) {
return String(value ?? '').replace(/[&<>"']/g, function(char) {
@@ -103,9 +103,9 @@
}
function highlight_selected_row() {
$('#rack_tbody tr').removeClass('table-active');
if (selected_rack_index >= 0) {
$('#rack_tbody tr[data-rack-index="' + selected_rack_index + '"]').addClass('table-active');
$('#bin_tbody tr').removeClass('table-active');
if (selected_bin_index >= 0) {
$('#bin_tbody tr[data-bin-index="' + selected_bin_index + '"]').addClass('table-active');
}
}
@@ -119,26 +119,26 @@
onSuccess: function(res) {
var s = res.output || {};
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_aisle = s.location_label_aisle || 'Aisle';
$('.th-zone').text(label_zone).toggleClass('d-none', !advanced);
$('.th-aisle').text(label_aisle).toggleClass('d-none', !advanced);
$('#th-rack').text(label_rack);
$('#th-bin').text(label_bin);
}
});
}
function load_racks() {
function load_bins() {
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,
checkRequired: 0,
action: 'read',
queueLock: false,
onSuccess: function(res) {
racks = res.output || [];
render_racks();
bins = res.output || [];
render_bins();
highlight_selected_row();
}
});
@@ -157,26 +157,26 @@
(res.output || []).forEach(function(item) {
barcode_status[item.barcode] = parseInt(item.status) === 1 ? 'active' : 'disabled';
});
render_racks();
render_bins();
}
});
}
function location_payload(item) {
return advanced
? `LOC|${item.warehouse_id}|${item.zone}|${item.aisle}|${item.rack}`
: `LOC|${item.warehouse_id}|${item.rack}`;
? `LOC|${item.warehouse_id}|${item.zone}|${item.aisle}|${item.bin}`
: `LOC|${item.warehouse_id}|${item.bin}`;
}
function render_racks() {
function render_bins() {
var keyword = $('#search_location').val().toLowerCase();
var rows = racks.filter(function(item) {
var rows = bins.filter(function(item) {
if (!keyword) return true;
return [
item.warehouse_name,
item.zone,
item.aisle,
item.rack,
item.bin,
item.product_sku,
item.product_name,
item.status
@@ -184,13 +184,13 @@
});
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;
}
var body = '';
rows.forEach(function(item) {
var idx = racks.indexOf(item);
var idx = bins.indexOf(item);
var status = item.status === 'occupied'
? '<span class="badge bg-warning text-dark">Occupied</span>'
: '<span class="badge bg-success">Empty</span>';
@@ -202,11 +202,11 @@
if (barcode_status[payload] === 'disabled') {
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 class="th-zone ${advanced ? '' : 'd-none'}">${escape_html(item.zone)}</td>
<td class="th-aisle ${advanced ? '' : 'd-none'}">${escape_html(item.aisle)}</td>
<td>${escape_html(item.rack)}</td>
<td>${escape_html(item.bin)}</td>
<td>${status}</td>
<td>${labelStatus}</td>
<td class="text-end">
@@ -216,7 +216,7 @@
</td>
</tr>`;
});
$('#rack_tbody').html(body);
$('#bin_tbody').html(body);
}
function render_preview(item, payload) {
@@ -224,8 +224,8 @@
<div class="barcode-label mx-auto">
<div class="fw-bold fs-5">${escape_html(item.warehouse_name)}</div>
<div class="small">${advanced
? `${escape_html(item.zone)} / ${escape_html(item.aisle)} / ${escape_html(item.rack)}`
: escape_html(item.rack)}</div>
? `${escape_html(item.zone)} / ${escape_html(item.aisle)} / ${escape_html(item.bin)}`
: escape_html(item.bin)}</div>
<svg id="barcode_svg"></svg>
<div class="small text-muted">${escape_html(payload)}</div>
</div>`);
@@ -233,7 +233,7 @@
}
function preview_label(index) {
var item = racks[index];
var item = bins[index];
if (!item) return;
var payload = location_payload(item);
@@ -249,13 +249,13 @@
warehouse_id: item.warehouse_id,
zone: item.zone || '',
aisle: item.aisle || '',
rack: item.rack
bin: item.bin
},
onSuccess: function() {
selected_payload = payload;
selected_rack_index = index;
selected_bin_index = index;
if (!barcode_status[payload]) barcode_status[payload] = 'active';
render_racks();
render_bins();
refresh_header_buttons();
highlight_selected_row();
render_preview(item, payload);
@@ -303,9 +303,9 @@
onSuccess: function() {
barcode_status[selected_payload] = 'disabled';
selected_payload = '';
selected_rack_index = -1;
selected_bin_index = -1;
$('#label_preview').html('Select a location to preview its label.');
render_racks();
render_bins();
refresh_header_buttons();
highlight_selected_row();
}
@@ -316,8 +316,8 @@
function reenable_selected_label() {
if (!selected_payload) return;
var index = selected_rack_index;
var item = racks[index];
var index = selected_bin_index;
var item = bins[index];
if (!item) return;
ajax_request({
@@ -329,7 +329,7 @@
data: { label_action: 'enable', barcode: selected_payload },
onSuccess: function() {
barcode_status[selected_payload] = 'active';
render_racks();
render_bins();
refresh_header_buttons();
highlight_selected_row();
render_preview(item, selected_payload);
@@ -338,7 +338,7 @@
}
function enable_label_for_row(index) {
var item = racks[index];
var item = bins[index];
if (!item) return;
var payload = location_payload(item);
@@ -352,8 +352,8 @@
onSuccess: function() {
barcode_status[payload] = 'active';
selected_payload = payload;
selected_rack_index = index;
render_racks();
selected_bin_index = index;
render_bins();
refresh_header_buttons();
highlight_selected_row();
render_preview(item, payload);
@@ -361,11 +361,11 @@
});
}
$('#search_location').on('input', render_racks);
$('#search_location').on('input', render_bins);
$(async function() {
await load_config();
await load_racks();
await load_bins();
await load_barcode_statuses();
});
</script>
+33 -33
View File
@@ -97,10 +97,10 @@
</select>
</div>
<div class="mb-3 col-lg-6" id="group_rack">
<label for="rack" class="form-label" id="label_rack">Rack</label>
<select name="rack" id="rack" class="form-select" required>
<option value="">Please select rack</option>
<div class="mb-3 col-lg-6" id="group_bin">
<label for="bin" class="form-label" id="label_bin">Bin</label>
<select name="bin" id="bin" class="form-select" required>
<option value="">Please select bin</option>
</select>
</div>
@@ -137,7 +137,7 @@
// custom.js globals: set_select_value(), load_location_config(), init_lot_search_inputs()
var advanced = false;
var label_rack = 'Location';
var label_bin = 'Location';
var label_zone = 'Zone';
var label_aisle = 'Aisle';
var scan_sku_ready = false;
@@ -150,15 +150,15 @@
$('#group_aisle').show();
$('#label_zone').text(label_zone);
$('#label_aisle').text(label_aisle);
$('#label_rack').text(label_rack);
$('#label_bin').text(label_bin);
$('#zone').attr('placeholder', 'Please select ' + label_zone.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 {
$('#group_zone').hide();
$('#group_aisle').hide();
$('#label_rack').text(label_rack);
$('#rack').attr('placeholder', 'Please select ' + label_rack.toLowerCase());
$('#label_bin').text(label_bin);
$('#bin').attr('placeholder', 'Please select ' + label_bin.toLowerCase());
}
}
@@ -174,7 +174,7 @@
$('#warehouse').val('');
$('#zone').val('');
$('#aisle').val('');
$('#rack').val('');
$('#bin').val('');
$('#product_sku').trigger('blur');
}
@@ -189,7 +189,7 @@
set_select_value('#zone', data.zone, data.zone);
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;
});
}
@@ -207,7 +207,7 @@
warehouse_id: data.warehouse_id,
zone: data.zone,
aisle: data.aisle,
rack: data.rack,
bin: data.bin,
product_sku: $('#product_sku').attr('secondary') || $('#product_sku').val(),
lot_number: $('#lot_number').val(),
serial_number: $('#serial_number').val()
@@ -230,7 +230,7 @@
$('#warehouse').val('');
$('#zone').val('');
$('#aisle').val('');
$('#rack').val('');
$('#bin').val('');
scan_sku_ready = false;
scan_location_ready = false;
$('#product_sku').focus();
@@ -241,7 +241,7 @@
bootbox.alert('Scan the SKU label first.');
return;
}
if (!scan_location_ready && !$('#rack').val()) {
if (!scan_location_ready && !$('#bin').val()) {
bootbox.alert('Scan the location label before saving.');
return;
}
@@ -250,17 +250,17 @@
function manage_stock_in() {
var rack_val = $('#rack').val();
var zone_val = advanced ? $('#zone').val() : rack_val;
var aisle_val = advanced ? $('#aisle').val() : rack_val;
var bin_val = $('#bin').val();
var zone_val = advanced ? $('#zone').val() : bin_val;
var aisle_val = advanced ? $('#aisle').val() : bin_val;
var warehouse_val = $('#warehouse').val();
var sku_val = $('#product_sku').attr('secondary') || $('#product_sku').val();
var quantity_val = $('#quantity').val();
// Mirror to hidden inputs for autoPrepare consistency (simple mode)
if (!advanced) {
$('#zone').val(rack_val);
$('#aisle').val(rack_val);
$('#zone').val(bin_val);
$('#aisle').val(bin_val);
}
return ajax_request({
@@ -275,7 +275,7 @@
'serial_number': $('#serial_number').val(),
'zone': zone_val,
'aisle': aisle_val,
'rack': rack_val,
'bin': bin_val,
'product_sku': sku_val,
'quantity': quantity_val,
'warehouse': warehouse_val,
@@ -398,10 +398,10 @@
function retrieve_rack(type, callback) {
function retrieve_bin(type, callback) {
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,
checkRequired: 0,
noLoading: true,
@@ -411,11 +411,11 @@
},
action: 'read',
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) {
option += '<option value="' + item + '">' + item + '</option>';
});
$('select#rack').html(option);
$('select#bin').html(option);
if (callback) callback(); // ← add this
}
@@ -445,7 +445,7 @@
function populate_fields() {
$.each(data, function(key, item) {
if (!['warehouse', 'zone', 'aisle', 'rack'].includes(key)) {
if (!['warehouse', 'zone', 'aisle', 'bin'].includes(key)) {
$('#' + key).val(item);
}
});
@@ -478,16 +478,16 @@
$('select#zone').val(data.zone).attr('disabled', true);
retrieve_aisle('to', function() {
$('#aisle').val(data.aisle).attr('disabled', true);
retrieve_rack('to', function() {
$('#rack').val(data.rack).attr('disabled', true);
retrieve_bin('to', function() {
$('#bin').val(data.bin).attr('disabled', true);
populate_fields();
});
});
});
} else {
// Simple mode — skip zone/aisle cascade
retrieve_rack('to', function() {
$('#rack').val(data.rack).attr('disabled', true);
retrieve_bin('to', function() {
$('#bin').val(data.bin).attr('disabled', true);
populate_fields();
});
}
@@ -536,11 +536,11 @@
if (advanced) {
retrieve_zone('to', function() {
retrieve_aisle('to', function() {
retrieve_rack('to');
retrieve_bin('to');
});
});
} else {
retrieve_rack('to');
retrieve_bin('to');
}
}
@@ -615,7 +615,7 @@
if (advanced) {
retrieve_zone('to');
} else {
retrieve_rack('to');
retrieve_bin('to');
}
});
@@ -624,7 +624,7 @@
});
$("select#aisle").change(function() {
if (advanced) retrieve_rack('to');
if (advanced) retrieve_bin('to');
});
<?php } ?>
+32 -32
View File
@@ -85,11 +85,11 @@
</select>
</div>
<!-- Step 7: Rack -->
<div class="mb-3 col-lg-6" id="group_rack">
<label for="rack" class="form-label" id="label_rack">Rack</label>
<select name="rack" id="rack" class="form-select" required disabled>
<option value="">Please select rack</option>
<!-- Step 7: Bin -->
<div class="mb-3 col-lg-6" id="group_bin">
<label for="bin" class="form-label" id="label_bin">Bin</label>
<select name="bin" id="bin" class="form-select" required disabled>
<option value="">Please select bin</option>
</select>
</div>
@@ -124,7 +124,7 @@
// custom.js globals: set_select_value(), load_location_config()
var advanced = false;
var label_rack = 'Location';
var label_bin = 'Location';
var label_zone = 'Zone';
var label_aisle = 'Aisle';
var scan_sku_ready = false;
@@ -157,7 +157,7 @@
set_select_value('#zone', data.zone, data.zone);
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;
});
}
@@ -175,7 +175,7 @@
warehouse_id: data.warehouse_id,
zone: data.zone,
aisle: data.aisle,
rack: data.rack,
bin: data.bin,
product_sku: $('#product_sku').attr('secondary') || $('#product_sku').val(),
lot_number: $('#lot_number').val(),
serial_number: $('#serial_number').val()
@@ -204,7 +204,7 @@
bootbox.alert('Scan the SKU label first.');
return;
}
if (!scan_location_ready && !$('#rack').val()) {
if (!scan_location_ready && !$('#bin').val()) {
bootbox.alert('Scan the location label before saving.');
return;
}
@@ -218,22 +218,22 @@
$('#group_aisle').show();
$('#label_zone').text(label_zone);
$('#label_aisle').text(label_aisle);
$('#label_rack').text(label_rack);
$('#label_bin').text(label_bin);
} else {
$('#group_zone').hide();
$('#group_aisle').hide();
$('#label_rack').text(label_rack);
$('#label_bin').text(label_bin);
}
}
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 wh_val = $('#warehouse').val();
if (!advanced) {
$('#zone').val(rack_val);
$('#aisle').val(rack_val);
$('#zone').val(bin_val);
$('#aisle').val(bin_val);
}
return ajax_request({
@@ -243,9 +243,9 @@
debugMode: 0,
data: {
'contact_id': $('#contact').attr('data-id'),
'zone': advanced ? $('#zone').val() : rack_val,
'aisle': advanced ? $('#aisle').val() : rack_val,
'rack': rack_val,
'zone': advanced ? $('#zone').val() : bin_val,
'aisle': advanced ? $('#aisle').val() : bin_val,
'bin': bin_val,
'product_sku': sku_val,
'warehouse': wh_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) {
return ajax_request({
@@ -447,10 +447,10 @@
});
}
function retrieve_rack(callback) {
function retrieve_bin(callback) {
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,
checkRequired: 0,
noLoading: true,
@@ -466,11 +466,11 @@
},
action: 'read',
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) {
option += `<option value='${item}'>${item}</option>`;
});
$('#rack').html(option).prop('disabled', false);
$('#bin').html(option).prop('disabled', false);
if (callback) callback();
}
});
@@ -482,7 +482,7 @@
$('#warehouse').html('<option value="">Select warehouse...</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);
$('#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() {
$.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);
}
});
@@ -534,16 +534,16 @@
$('#zone').val(data.zone).prop('disabled', true);
retrieve_aisle(function() {
$('#aisle').val(data.aisle).prop('disabled', true);
retrieve_rack(function() {
$('#rack').val(data.rack).prop('disabled', true);
retrieve_bin(function() {
$('#bin').val(data.bin).prop('disabled', true);
populate_fields();
});
});
});
} else {
// Simple mode — skip zone/aisle cascade
retrieve_rack(function() {
$('#rack').val(data.rack).prop('disabled', true);
retrieve_bin(function() {
$('#bin').val(data.bin).prop('disabled', true);
populate_fields();
});
}
@@ -600,12 +600,12 @@
retrieve_warehouse();
});
// Warehouse selected → load zone (advanced) or rack directly (simple)
// Warehouse selected → load zone (advanced) or bin directly (simple)
$('#warehouse').on('change', function() {
if (advanced) {
retrieve_zone();
} else {
retrieve_rack();
retrieve_bin();
}
});
@@ -614,9 +614,9 @@
if (advanced) retrieve_aisle();
});
// Aisle selected → load rack (advanced only)
// Aisle selected → load bin (advanced only)
$('#aisle').on('change', function() {
if (advanced) retrieve_rack();
if (advanced) retrieve_bin();
});
<?php } ?>
+56 -56
View File
@@ -87,9 +87,9 @@
</select>
</div>
<div class="mb-3 col-lg-3">
<label class="form-label" id="label_rack_from">From Rack</label>
<select name="rack" id="rack_from" role="from" class="form-select" required disabled>
<option value="">Please select rack</option>
<label class="form-label" id="label_bin_from">From Bin</label>
<select name="bin" id="bin_from" role="from" class="form-select" required disabled>
<option value="">Please select bin</option>
</select>
</div>
@@ -119,9 +119,9 @@
</select>
</div>
<div class="mb-3 col-lg-3">
<label class="form-label" id="label_rack_to">To Rack</label>
<select name="rack" id="rack_to" role="to" class="form-select" required>
<option value="">Please select rack</option>
<label class="form-label" id="label_bin_to">To Bin</label>
<select name="bin" id="bin_to" role="to" class="form-select" required>
<option value="">Please select bin</option>
</select>
</div>
@@ -157,7 +157,7 @@
// ── Location config ───────────────────────────────────────────────────────
var advanced = false;
var label_rack = 'Location';
var label_bin = 'Location';
var label_zone = 'Zone';
var label_aisle = 'Aisle';
var scan_sku_ready = false;
@@ -200,7 +200,7 @@
set_select_value('#zone_' + role, data.zone, data.zone);
set_select_value('#aisle_' + role, data.aisle, data.aisle);
}
set_select_value('#rack_' + role, data.rack, data.rack);
set_select_value('#bin_' + role, data.bin, data.bin);
if (role === 'from') {
scan_from_ready = true;
@@ -214,7 +214,7 @@
return String($('#warehouse_from').val()) === String(data.warehouse_id)
&& String($('#zone_from').val()) === String(data.zone)
&& String($('#aisle_from').val()) === String(data.aisle)
&& String($('#rack_from').val()) === String(data.rack);
&& String($('#bin_from').val()) === String(data.bin);
}
function validate_scanned_location(role, data, onSuccess) {
@@ -230,7 +230,7 @@
warehouse_id: data.warehouse_id,
zone: data.zone,
aisle: data.aisle,
rack: data.rack,
bin: data.bin,
product_sku: $('#product_sku').attr('secondary') || $('#product_sku').val(),
lot_number: $('#lot_number').val(),
serial_number: $('#serial_number').val()
@@ -243,7 +243,7 @@
$('#warehouse_to').val('');
$('#zone_to').html('<option value="">Please select ' + label_zone.toLowerCase() + '</option>').val('');
$('#aisle_to').html('<option value="">Please select ' + label_aisle.toLowerCase() + '</option>').val('');
$('#rack_to').html('<option value="">Please select ' + label_rack.toLowerCase() + '</option>').val('');
$('#bin_to').html('<option value="">Please select ' + label_bin.toLowerCase() + '</option>').val('');
}
function reset_stock_transfer_inputs() {
@@ -269,11 +269,11 @@
bootbox.alert('Scan the SKU label first.');
return;
}
if (!scan_from_ready && !$('#rack_from').val()) {
if (!scan_from_ready && !$('#bin_from').val()) {
bootbox.alert('Scan the from-location label before saving.');
return;
}
if (!scan_to_ready && !$('#rack_to').val()) {
if (!scan_to_ready && !$('#bin_to').val()) {
bootbox.alert('Scan the to-location label before saving.');
return;
}
@@ -290,19 +290,19 @@
$('#label_zone_to').text('To ' + label_zone);
$('#label_aisle_from').text('From ' + label_aisle);
$('#label_aisle_to').text('To ' + label_aisle);
$('#label_rack_from').text('From ' + label_rack);
$('#label_rack_to').text('To ' + label_rack);
$('#label_bin_from').text('From ' + label_bin);
$('#label_bin_to').text('To ' + label_bin);
// Update select placeholders
$('#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());
$('#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 {
$('#group_zone_from, #group_zone_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');
$('#label_rack_from').text('From ' + label_rack);
$('#label_rack_to').text('To ' + label_rack);
$('#rack_from, #rack_to').find('option:first').text('Please select ' + label_rack.toLowerCase());
$('#label_bin_from').text('From ' + label_bin);
$('#label_bin_to').text('To ' + label_bin);
$('#bin_from, #bin_to').find('option:first').text('Please select ' + label_bin.toLowerCase());
}
}
@@ -310,16 +310,16 @@
// ── Submit ────────────────────────────────────────────────────────────────
function manage_stock_transfer() {
var rack_from_val = $('#rack_from').val();
var rack_to_val = $('#rack_to').val();
var bin_from_val = $('#bin_from').val();
var bin_to_val = $('#bin_to').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) {
$('#zone_from').val(rack_from_val);
$('#aisle_from').val(rack_from_val);
$('#zone_to').val(rack_to_val);
$('#aisle_to').val(rack_to_val);
$('#zone_from').val(bin_from_val);
$('#aisle_from').val(bin_from_val);
$('#zone_to').val(bin_to_val);
$('#aisle_to').val(bin_to_val);
}
return ajax_request({
@@ -330,12 +330,12 @@
'contact_id': $('#contact').attr('data-id'),
'warehouse_from': $('#warehouse_from').val(),
'warehouse_to': $('#warehouse_to').val(),
'zone_from': advanced ? $('#zone_from').val() : rack_from_val,
'aisle_from': advanced ? $('#aisle_from').val() : rack_from_val,
'rack_from': rack_from_val,
'zone_to': advanced ? $('#zone_to').val() : rack_to_val,
'aisle_to': advanced ? $('#aisle_to').val() : rack_to_val,
'rack_to': rack_to_val,
'zone_from': advanced ? $('#zone_from').val() : bin_from_val,
'aisle_from': advanced ? $('#aisle_from').val() : bin_from_val,
'bin_from': bin_from_val,
'zone_to': advanced ? $('#zone_to').val() : bin_to_val,
'aisle_to': advanced ? $('#aisle_to').val() : bin_to_val,
'bin_to': bin_to_val,
'product_sku': sku_val,
'lot_number': $('#lot_number').val(),
'serial_number': $('#serial_number').val(),
@@ -467,7 +467,7 @@
}
// ── Zone / Aisle / Rack loaders ───────────────────────────────────────────
// ── Zone / Aisle / Bin loaders ───────────────────────────────────────────
function retrieve_zone(type, callback) {
var extra = {
'warehouse_from': $('#warehouse_from').val(),
@@ -532,7 +532,7 @@
});
}
function retrieve_rack(type, callback) {
function retrieve_bin(type, callback) {
var extra = {
'warehouse_from': $('#warehouse_from').val(),
'warehouse_to': $('#warehouse_to').val(),
@@ -549,7 +549,7 @@
});
}
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,
checkRequired: 0,
noLoading: true,
@@ -557,11 +557,11 @@
data: Object.assign({ 'type': type }, extra),
action: 'read',
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) {
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();
}
});
@@ -573,7 +573,7 @@
$('#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);
$('#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);
retrieve_aisle('from', function() {
$('select[name="aisle"][role="from"]').val(data.aisle).prop('disabled', true);
retrieve_rack('from', function() {
$('select[name="rack"][role="from"]').val(data.rack).prop('disabled', true);
retrieve_bin('from', function() {
$('select[name="bin"][role="from"]').val(data.bin).prop('disabled', true);
populate_to_side(data, to_wh);
});
});
});
} else {
// Simple mode — skip zone/aisle cascade, load rack directly
retrieve_rack('from', function() {
$('select[name="rack"][role="from"]').val(data.rack).prop('disabled', true);
// Simple mode — skip zone/aisle cascade, load bin directly
retrieve_bin('from', function() {
$('select[name="bin"][role="from"]').val(data.bin).prop('disabled', true);
populate_to_side_simple(data, to_wh);
});
}
// Common fields
$.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);
}
});
@@ -655,16 +655,16 @@
$('select[name="zone"][role="to"]').val(data.ref.zone).prop('disabled', true);
retrieve_aisle('to', function() {
$('select[name="aisle"][role="to"]').val(data.ref.aisle).prop('disabled', true);
retrieve_rack('to', function() {
$('select[name="rack"][role="to"]').val(data.ref.rack).prop('disabled', true);
retrieve_bin('to', function() {
$('select[name="bin"][role="to"]').val(data.ref.bin).prop('disabled', true);
});
});
});
}
function populate_to_side_simple(data, to_wh) {
retrieve_rack('to', function() {
$('select[name="rack"][role="to"]').val(data.ref.rack).prop('disabled', true);
retrieve_bin('to', function() {
$('select[name="bin"][role="to"]').val(data.ref.bin).prop('disabled', true);
});
}
@@ -717,9 +717,9 @@
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() {
if (advanced) { retrieve_zone('from'); } else { retrieve_rack('from'); }
if (advanced) { retrieve_zone('from'); } else { retrieve_bin('from'); }
});
// From zone → aisle (advanced only)
@@ -727,14 +727,14 @@
if (advanced) retrieve_aisle('from');
});
// From aisle → rack (advanced only)
// From aisle → bin (advanced only)
$('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() {
if (advanced) { retrieve_zone('to'); } else { retrieve_rack('to'); }
if (advanced) { retrieve_zone('to'); } else { retrieve_bin('to'); }
});
// To zone → aisle (advanced only)
@@ -742,9 +742,9 @@
if (advanced) retrieve_aisle('to');
});
// To aisle → rack (advanced only)
// To aisle → bin (advanced only)
$('select[name="aisle"][role="to"]').on('change', function() {
if (advanced) retrieve_rack('to');
if (advanced) retrieve_bin('to');
});
<?php } ?>
+7 -7
View File
@@ -64,7 +64,7 @@
<th>Source</th>
<th class="th_loc_zone">Zone</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>Action</th>
</tr>
@@ -90,7 +90,7 @@
<script>
var advanced = false;
var label_rack = 'Location';
var label_bin = 'Location';
var label_zone = 'Zone';
var label_aisle = 'Aisle';
@@ -98,10 +98,10 @@
if (advanced) {
$('.th_loc_zone').text(label_zone).show();
$('.th_loc_aisle').text(label_aisle).show();
$('.th_loc_rack').text(label_rack);
$('.th_loc_bin').text(label_bin);
} else {
$('.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) {
return `<td class="py-3">${item['zone'] || ''}</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) {
@@ -309,7 +309,7 @@
var s = res.output || {};
auto_approve = s.default_stock_status == 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_aisle = s.location_label_aisle || 'Aisle';
apply_location_labels();
+7 -7
View File
@@ -63,7 +63,7 @@
<th>Source</th>
<th class="th_loc_zone">Zone</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>Action</th>
</tr>
@@ -89,7 +89,7 @@
<script>
var advanced = false;
var label_rack = 'Location';
var label_bin = 'Location';
var label_zone = 'Zone';
var label_aisle = 'Aisle';
@@ -97,10 +97,10 @@
if (advanced) {
$('.th_loc_zone').text(label_zone).show();
$('.th_loc_aisle').text(label_aisle).show();
$('.th_loc_rack').text(label_rack);
$('.th_loc_bin').text(label_bin);
} else {
$('.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) {
return `<td class="py-3">${item['zone'] || ''}</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) {
@@ -329,7 +329,7 @@
var s = res.output || {};
auto_approve = s.default_stock_status == 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_aisle = s.location_label_aisle || 'Aisle';
apply_location_labels();
+1 -1
View File
@@ -42,7 +42,7 @@
<div>
<span class="mb-4 d-block">Total Capacity</span>
<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>
<i class="ti ti-box-seam fs-3 text-muted"></i>
</div>
+7 -7
View File
@@ -52,7 +52,7 @@
<th style="text-align: right;">Quantity</th>
<th class="th_loc_zone">Zone</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>Action</th>
</tr>
@@ -78,7 +78,7 @@
<script>
var advanced = false;
var label_rack = 'Location';
var label_bin = 'Location';
var label_zone = 'Zone';
var label_aisle = 'Aisle';
@@ -86,10 +86,10 @@
if (advanced) {
$('.th_loc_zone').text(label_zone).show();
$('.th_loc_aisle').text(label_aisle).show();
$('.th_loc_rack').text(label_rack);
$('.th_loc_bin').text(label_bin);
} else {
$('.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) {
return `<td class="py-3">${item['zone'] || ''}</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() {
@@ -240,7 +240,7 @@
var s = res.output || {};
auto_approve = s.default_stock_status == 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_aisle = s.location_label_aisle || 'Aisle';
apply_location_labels();
+1 -1
View File
@@ -165,7 +165,7 @@
<li>
<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>
<span class="nav-text">Rack Occupancy</span>
<span class="nav-text">Bin Occupancy</span>
</a>
</li>
+35 -35
View File
@@ -59,7 +59,7 @@
</div>
</div>
<!-- Advanced mode — zone + aisle range + rack range -->
<!-- Advanced mode — zone + aisle range + bin range -->
<div id="form_advanced" class="d-none">
<div class="row">
<div class="col-md-6 mb-3">
@@ -81,16 +81,16 @@
<div class="form-text">Must be same type as Aisle From</div>
</div>
<div class="col-md-3 mb-3">
<label for="rack_from" class="form-label" id="label_rack_from">Rack From</label>
<input type="text" class="form-control" id="rack_from" placeholder="e.g. 1 or A"
<label for="bin_from" class="form-label" id="label_bin_from">Bin From</label>
<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">
<div class="form-text">Number (1–99) or letter (A–Z)</div>
</div>
<div class="col-md-3 mb-3">
<label for="rack_to" class="form-label" id="label_rack_to">Rack To</label>
<input type="text" class="form-control" id="rack_to" placeholder="e.g. 10 or J"
<label for="bin_to" class="form-label" id="label_bin_to">Bin To</label>
<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">
<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>
@@ -122,7 +122,7 @@
<script>
// ── Location config ───────────────────────────────────────────────────────
var advanced = false;
var label_rack = 'Location';
var label_bin = 'Location';
var label_zone = 'Zone';
var label_aisle = 'Aisle';
@@ -135,7 +135,7 @@
onSuccess: function(res) {
var s = res.output;
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_aisle = s.location_label_aisle || 'Aisle';
apply_location_labels();
@@ -150,13 +150,13 @@
$('#label_zone').text(label_zone);
$('#label_aisle_from').text(label_aisle + ' From');
$('#label_aisle_to').text(label_aisle + ' To');
$('#label_rack_from').text(label_rack + ' From');
$('#label_rack_to').text(label_rack + ' To');
$('#label_bin_from').text(label_bin + ' From');
$('#label_bin_to').text(label_bin + ' To');
} else {
$('#form_advanced').addClass('d-none');
$('#form_simple').removeClass('d-none');
$('#label_location_from').text(label_rack + ' From');
$('#label_location_to').text(label_rack + ' To');
$('#label_location_from').text(label_bin + ' From');
$('#label_location_to').text(label_bin + ' To');
$('#location_from').attr('placeholder', 'e.g. Shelf-A or 1');
$('#location_to').attr('placeholder', 'e.g. Shelf-E or 5');
}
@@ -164,14 +164,14 @@
// ── Save 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) {
zone = $('#zone').val().trim();
aisle_from = $('#aisle_from').val().trim().toUpperCase();
aisle_to = $('#aisle_to').val().trim().toUpperCase();
rack_from = $('#rack_from').val().trim().toUpperCase();
rack_to = $('#rack_to').val().trim().toUpperCase();
bin_from = $('#bin_from').val().trim().toUpperCase();
bin_to = $('#bin_to').val().trim().toUpperCase();
var isNumeric = function(v) { return /^[0-9]+$/.test(v); };
var isAlpha = function(v) { return /^[A-Z]$/.test(v); };
@@ -182,8 +182,8 @@
if (!aisle_from || !aisle_to) {
alert('Please enter the full ' + label_aisle + ' range.'); return;
}
if (!rack_from || !rack_to) {
alert('Please enter the full ' + label_rack + ' range.'); return;
if (!bin_from || !bin_to) {
alert('Please enter the full ' + label_bin + ' range.'); return;
}
if (aisle_from && aisle_to) {
@@ -197,25 +197,25 @@
alert(label_aisle + ' From must be less than or equal to To.'); return;
}
}
if (rack_from && rack_to) {
if (isNumeric(rack_from) !== isNumeric(rack_to)) {
alert(label_rack + ' From and To must both be numbers or both be letters.'); return;
if (bin_from && bin_to) {
if (isNumeric(bin_from) !== isNumeric(bin_to)) {
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)) {
alert(label_rack + ' From must be less than or equal to To.'); return;
if (isAlpha(bin_from) && bin_from.charCodeAt(0) > bin_to.charCodeAt(0)) {
alert(label_bin + ' From must be less than or equal to To.'); return;
}
if (isNumeric(rack_from) && parseInt(rack_from) > parseInt(rack_to)) {
alert(label_rack + ' From must be less than or equal to To.'); return;
if (isNumeric(bin_from) && parseInt(bin_from) > parseInt(bin_to)) {
alert(label_bin + ' From must be less than or equal to To.'); return;
}
}
} 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_to = $('#location_to').val().trim();
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 (!loc_to) loc_to = loc_from;
@@ -225,14 +225,14 @@
var mf = loc_from.match(prefix_pattern);
var mt = loc_to.match(prefix_pattern);
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;
aisle_from = loc_from;
aisle_to = loc_to;
rack_from = loc_from;
rack_to = loc_to;
bin_from = loc_from;
bin_to = loc_to;
}
return ajax_request({
@@ -245,8 +245,8 @@
zone: zone,
aisle_from: aisle_from,
aisle_to: aisle_to,
rack_from: rack_from,
rack_to: rack_to,
bin_from: bin_from,
bin_to: bin_to,
description: $('#description').val(),
},
onSuccess: function() {
@@ -287,11 +287,11 @@
$('#zone').val(d.zone || '');
$('#aisle_from').val(d.aisle_from || '');
$('#aisle_to').val(d.aisle_to || '');
$('#rack_from').val(d.rack_from || '');
$('#rack_to').val(d.rack_to || '');
$('#bin_from').val(d.bin_from || '');
$('#bin_to').val(d.bin_to || '');
} else {
$('#location_from').val(d.rack_from || '');
$('#location_to').val(d.rack_to || '');
$('#location_from').val(d.bin_from || '');
$('#location_to').val(d.bin_to || '');
}
$('button[type=submit]').text('Update Storage');
$('button[type=reset]').hide();
+16 -16
View File
@@ -60,7 +60,7 @@
<div class="d-flex flex-column">
<span class="fs-2 fw-bold mb-3 d-block">—</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>
<span class="text-warning">
@@ -87,7 +87,7 @@
<div class="d-flex flex-column">
<span class="fs-2 fw-bold mb-3 d-block">—</span>
<span class="fs-5">Space Used</span>
<small class="text-success">Racks occupied</small>
<small class="text-success">Bins occupied</small>
</div>
<div>
<span class="text-success">
@@ -206,7 +206,7 @@
<div class="mb-6 d-flex align-items-center justify-content-between">
<div>
<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>
<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" id="th_level1">Zone</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>Action</th>
</tr>
@@ -257,7 +257,7 @@
<script>
// ── Location config ───────────────────────────────────────────────────────
var advanced = false;
var label_rack = 'Location';
var label_bin = 'Location';
var label_zone = 'Zone';
var label_aisle = 'Aisle';
@@ -270,7 +270,7 @@
onSuccess: function(res) {
var s = res.output;
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_aisle = s.location_label_aisle || 'Aisle';
apply_location_labels();
@@ -280,26 +280,26 @@
function apply_location_labels() {
// Stats card subtitles
$('#total_capacity small').text(label_rack + ' positions available');
$('#total_space_used small').text(label_rack + 's occupied');
$('#total_capacity small').text(label_bin + ' positions available');
$('#total_space_used small').text(label_bin + 's occupied');
// Storage tab subtitle + search
$('#storage_subtitle').text(advanced
? label_zone + 's, ' + label_aisle + 's, and ' + label_rack + ' assignments'
: label_rack + ' assignments');
? label_zone + 's, ' + label_aisle + 's, and ' + label_bin + ' assignments'
: label_bin + ' assignments');
$('#search_storage').attr('placeholder', advanced
? 'Search by warehouse or ' + label_zone.toLowerCase() + '…'
: 'Search by warehouse or ' + label_rack.toLowerCase() + '…');
: 'Search by warehouse or ' + label_bin.toLowerCase() + '…');
// Table headers
if (advanced) {
$('#th_level1').text(label_zone).show();
$('#th_level2').text(label_aisle + ' Range').show();
$('#th_level3').text(label_rack + ' Range');
$('#th_level3').text(label_bin + ' Range');
} else {
$('#th_level1').hide();
$('#th_level2').hide();
$('#th_level3').text(label_rack + ' Range');
$('#th_level3').text(label_bin + ' Range');
}
}
@@ -432,7 +432,7 @@
alasql(`SELECT * FROM storage
WHERE LOWER(warehouse_name) LIKE '%${kw}%'
OR LOWER(zone) LIKE '%${kw}%'
OR LOWER(rack) LIKE '%${kw}%'`) :
OR LOWER(bin) LIKE '%${kw}%'`) :
alasql(`SELECT * FROM storage`);
var page = generate_pagination('storage', filtered.length);
@@ -452,8 +452,8 @@
var location_cells = advanced
? `<td class="py-3">${item['zone']}</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['rack_from']} - ${item['rack_to']}</td>`;
<td class="py-3">${item['bin_from']} - ${item['bin_to']}</td>`
: `<td class="py-3">${item['bin_from']} - ${item['bin_to']}</td>`;
body += `<tr>
<td class="py-3">${item['warehouse_name']}</td>
+16 -18
View File
@@ -23,8 +23,9 @@
* b. The elapsed time since otpTime is ≤ 5 minutes.
* Fail either → return "Wrong OTP! Please try again."
* 5. On success:
* a. Check session_token in DB — if non-NULL, another session is active;
* reject login with "account already logged in" message.
* a. Force-replace session_token in DB — writing a new token invalidates
* 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
* issuing a new session ID and deleting the old one.
* c. Generate a fresh CSRF token and store in session.
@@ -105,31 +106,28 @@ if (empty($_SESSION['skip_otp'])) {
}
}
// ── Step 4b: Claim session atomically ─────────────────────────────────────────
// The WHERE clause is the concurrency gate: only a free or stale token can be
// replaced. session_last_seen is set immediately so a fresh login is live before
// the first authenticated heartbeat in db_auth.php.
// ── Step 4b: Claim session — always replace existing token ────────────────────
// Password + OTP is full 2FA proof of identity, so we always grant the login.
// Writing a new token here also invalidates any prior session: 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));
$sth_claim = $pdo1->prepare(
"UPDATE user
SET session_token = :token,
session_token_at = NOW(),
SET session_token = :token,
session_token_at = NOW(),
session_last_seen = NOW()
WHERE user_id = :uid
AND (
session_token IS NULL
OR session_last_seen IS NULL
OR TIMESTAMPDIFF(SECOND, session_last_seen, NOW()) >= :gc_maxlifetime
)"
WHERE user_id = :uid"
);
$sth_claim->execute([
':token' => $session_token,
':uid' => $user_id,
':gc_maxlifetime' => (int)ini_get('session.gc_maxlifetime'),
':token' => $session_token,
':uid' => $user_id,
]);
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));
}
+2 -2
View File
@@ -382,7 +382,7 @@
warehouse_id: wh,
zone: '',
aisle: '',
rack: '',
bin: '',
lot_number: '',
serial_number: '',
quantity: qty,
@@ -566,7 +566,7 @@
// ── Confirm order ─────────────────────────────────────────────────────────
function confirm_order() {
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: {
confirm: { label: 'Confirm Order', className: 'btn-success' },
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_zone" style="width:120px;">Zone</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:100px;">Unit Price</th>
<th style="width:70px;">Tax%</th>
@@ -215,7 +215,7 @@
var returnable = []; // returnable_items from order
var warehouses = [];
var advanced = false;
var label_rack = 'Location';
var label_bin = 'Location';
var label_zone = 'Zone';
var label_aisle = 'Aisle';
@@ -276,16 +276,16 @@
$('.th_return_wh').text('Warehouse');
$('.th_return_zone').text(label_zone).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);
}
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 wh = $row.find('.item_return_warehouse').val();
$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_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();
return ajax_request({
@@ -307,7 +307,7 @@
opts += `<option value="${escape_html(selected_zone)}">${escape_html(selected_zone)}</option>`;
}
$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}`);
$row.find('.item_return_zone').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 (advanced) {
retrieve_return_zones(item_id);
} else {
retrieve_return_racks(item_id);
retrieve_return_bins(item_id);
}
}
function change_return_zone(item_id) {
var $row = $(`#ret_item_${item_id}`);
$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);
}
function change_return_aisle(item_id) {
var $row = $(`#ret_item_${item_id}`);
$row.find('.item_return_rack').val('');
if ($row.find('.item_check').is(':checked')) retrieve_return_racks(item_id);
$row.find('.item_return_bin').val('');
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 wh = $row.find('.item_return_warehouse').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_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();
return ajax_request({
@@ -366,21 +366,21 @@
opts += `<option value="${escape_html(selected_aisle)}">${escape_html(selected_aisle)}</option>`;
}
$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 wh = $row.find('.item_return_warehouse').val();
var zone = advanced ? $row.find('.item_return_zone').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();
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,
checkRequired: 0,
noLoading: true,
@@ -388,16 +388,16 @@
action: 'read',
data: { type: 'to', warehouse: wh, zone: zone, aisle: aisle, id: 0 },
onSuccess: function(res) {
var opts = `<option value="">Select ${label_rack.toLowerCase()}...</option>`;
var has_selected = !selected_rack;
var opts = `<option value="">Select ${label_bin.toLowerCase()}...</option>`;
var has_selected = !selected_bin;
$.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>`;
});
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 zone = selected.zone || '';
var aisle = selected.aisle || '';
var rack = selected.rack || '';
var bin = selected.bin || '';
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>
<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_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>
<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><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) {
@@ -427,15 +427,15 @@
var $row = $(`#ret_item_${item.item_id}`);
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);
return;
}
if (advanced) {
retrieve_return_zones(item.item_id, selected.zone || '', selected.aisle || '', selected.rack || '');
} else if (selected.rack) {
retrieve_return_racks(item.item_id, selected.rack || '');
retrieve_return_zones(item.item_id, selected.zone || '', selected.aisle || '', selected.bin || '');
} else if (selected.bin) {
retrieve_return_bins(item.item_id, selected.bin || '');
}
});
}
@@ -498,7 +498,7 @@
warehouse_id: si ? (si.warehouse_id || item.warehouse_id || '') : (item.warehouse_id || ''),
zone: si ? (si.zone || '') : (item.zone || ''),
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}">
@@ -514,7 +514,7 @@
<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_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_serial" value="${item.serial_number || ''}">
<input type="hidden" class="item_stock_out_id" value="${item.stock_out_id || 0}">
@@ -561,7 +561,7 @@
function toggle_item_row(item_id) {
var $row = $(`#ret_item_${item_id}`);
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) {
$row.find('.item_total').text('—');
return;
@@ -631,7 +631,7 @@
onSuccess: function(res) {
var s = res.output || {};
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_aisle = s.location_label_aisle || 'Aisle';
apply_location_headers();
@@ -664,12 +664,12 @@
var tax_rate = parseFloat($row.find('.item_tax_rate').val()) || 0;
if (qty <= 0) return;
var return_wh = parseInt($row.find('.item_return_warehouse').val()) || 0;
var return_rack = $row.find('.item_return_rack').val() || '';
var return_zone = advanced ? ($row.find('.item_return_zone').val() || '') : return_rack;
var return_aisle = advanced ? ($row.find('.item_return_aisle').val() || '') : return_rack;
var return_bin = $row.find('.item_return_bin').val() || '';
var return_zone = advanced ? ($row.find('.item_return_zone').val() || '') : return_bin;
var return_aisle = advanced ? ($row.find('.item_return_aisle').val() || '') : return_bin;
if (!return_wh || !return_rack || (advanced && (!return_zone || !return_aisle))) {
throw new Error('Please select return warehouse and ' + label_rack.toLowerCase() + ' for every checked item.');
if (!return_wh || !return_bin || (advanced && (!return_zone || !return_aisle))) {
throw new Error('Please select return warehouse and ' + label_bin.toLowerCase() + ' for every checked item.');
}
items.push({
@@ -679,7 +679,7 @@
warehouse_id: return_wh,
zone: return_zone,
aisle: return_aisle,
rack: return_rack,
bin: return_bin,
lot_number: $row.find('.item_lot').val(),
serial_number: $row.find('.item_serial').val(),
stock_out_id: parseInt($row.find('.item_stock_out_id').val()) || 0,
@@ -750,7 +750,7 @@
$('.item_check').prop('disabled', !is_draft);
$('#item_tbody tr.return-item-row').each(function() {
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);
});
+21 -21
View File
@@ -124,7 +124,7 @@
<div class="d-flex justify-content-between align-items-start mb-4">
<div>
<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>
<button class="btn btn-success" id="btn_receive" onclick="receive_goods()">
<i class="ti ti-package-import me-1"></i>Confirm Receipt
@@ -255,7 +255,7 @@
// ── Location config (loaded from company settings) ────────────────────────
var advanced = false;
var label_rack = 'Location';
var label_bin = 'Location';
var label_zone = 'Zone';
var label_aisle = 'Aisle';
@@ -393,7 +393,7 @@
});
$(`#recv_zone_${rowId}`).html(opts);
$(`#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();
}
});
@@ -414,28 +414,28 @@
opts += `<option value="${escape_html(a)}">${escape_html(a)}</option>`;
});
$(`#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();
}
});
}
function recv_load_racks(rowId, callback) {
function recv_load_bins(rowId, callback) {
var wh_id = $(`#recv_wh_${rowId}`).val();
var zone = advanced ? ($(`#recv_zone_${rowId}`).val() || '') : '';
var aisle = advanced ? ($(`#recv_aisle_${rowId}`).val() || '') : '';
if (!wh_id) 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, checkRequired: 0, noLoading: true, queueLock: false,
action: 'read',
data: { type: 'to', warehouse_to: wh_id, zone_to: zone, aisle_to: aisle, id: 0 },
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) {
opts += `<option value="${escape_html(r)}">${escape_html(r)}</option>`;
});
$(`#recv_rack_${rowId}`).html(opts);
$(`#recv_bin_${rowId}`).html(opts);
if (callback) callback();
}
});
@@ -476,7 +476,7 @@
if (advanced) {
recv_load_zones(rowId);
} else {
recv_load_racks(rowId);
recv_load_bins(rowId);
}
}
@@ -507,13 +507,13 @@
<div class="col-lg-4">
<label class="form-label small text-muted">${label_aisle}</label>
<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>
</select>
</div>`;
}
var rack_col_class = advanced ? 'col-lg-4' : 'col-lg-6';
var bin_col_class = advanced ? 'col-lg-4' : 'col-lg-6';
html += `
<div class="border rounded p-4 mb-4"
@@ -569,10 +569,10 @@
${zone_aisle_html}
<div class="${rack_col_class}">
<label class="form-label small text-muted">${label_rack} <span class="text-danger">*</span></label>
<select id="recv_rack_${rowId}" class="form-select form-select-sm">
<option value="">Select ${label_rack.toLowerCase()}...</option>
<div class="${bin_col_class}">
<label class="form-label small text-muted">${label_bin} <span class="text-danger">*</span></label>
<select id="recv_bin_${rowId}" class="form-select form-select-sm">
<option value="">Select ${label_bin.toLowerCase()}...</option>
</select>
</div>
@@ -771,22 +771,22 @@
if (qty <= 0) return;
var wh_id = parseInt($(`#recv_wh_${rowId}`).val()) || 0;
var rack = $(`#recv_rack_${rowId}`).val() || '';
var bin = $(`#recv_bin_${rowId}`).val() || '';
if (!wh_id) {
errors.push(`${$card.data('sku')}: please select a warehouse.`);
return;
}
if (!rack) {
errors.push(`${$card.data('sku')}: please select a ${label_rack.toLowerCase()}.`);
if (!bin) {
errors.push(`${$card.data('sku')}: please select a ${label_bin.toLowerCase()}.`);
return;
}
var zone = advanced ? ($(`#recv_zone_${rowId}`).val() || '') : '';
var aisle = advanced ? ($(`#recv_aisle_${rowId}`).val() || '') : '';
if (!advanced) {
zone = rack;
aisle = rack;
zone = bin;
aisle = bin;
}
// flatpickr with altInput: the real (hidden) input holds the ISO value
@@ -802,7 +802,7 @@
serial_number: $(`#recv_serial_${rowId}`).val().trim(),
zone: zone,
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_zone" value="${item.zone || ''}">
<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_lot" value="${item.lot_number || ''}">
<input type="hidden" class="item_serial" value="${item.serial_number || ''}">
@@ -465,7 +465,7 @@
warehouse_id: parseInt($row.find('.item_warehouse').val()) || 0,
zone: $row.find('.item_zone').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,
lot_number: $row.find('.item_lot').val(),
serial_number: $row.find('.item_serial').val(),
+4 -4
View File
@@ -6,16 +6,16 @@
(new UsageGuard($pdo1, $company_id, $packages))->assertFeatureAccessible('reports');
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['success'] = 1;
exit(json_encode($answer));
}
$report = new ReportManager($pdo2, $company_id, $db_database);
$answer['output'] = $report->getRackLog($rack_id);
$answer['output'] = $report->getBinLog($bin_id);
$answer['success'] = 1;
exit(json_encode($answer));
?>
?>
+6 -6
View File
@@ -194,22 +194,22 @@
var search_term = '';
var filter_status = '';
var advanced = false;
var label_rack = 'Location';
var label_bin = 'Location';
var label_zone = 'Zone';
var label_aisle = 'Aisle';
function apply_location_labels() {
$('.th_location').text(advanced
? label_zone + ' / ' + label_aisle + ' / ' + label_rack
: label_rack
? label_zone + ' / ' + label_aisle + ' / ' + label_bin
: label_bin
);
}
function format_location(item) {
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() {
@@ -221,7 +221,7 @@
onSuccess: function(res) {
var s = res.output || {};
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_aisle = s.location_label_aisle || 'Aisle';
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>
<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>
@@ -112,13 +112,13 @@
<div class="mb-6 d-flex align-items-center justify-content-between">
<div>
<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 class="col-lg-12 mb-5">
<div class="position-relative">
<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">
<i class="ti ti-search text-muted"></i>
</span>
@@ -131,7 +131,7 @@
<th>Warehouse</th>
<th class="th_loc_zone">Zone</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>Product</th>
<th>Status</th>
@@ -161,7 +161,7 @@
<div class="col-lg-12 mb-5">
<div class="position-relative">
<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">
<i class="ti ti-search text-muted"></i>
</span>
@@ -174,7 +174,7 @@
<th>Warehouse</th>
<th class="th_loc_zone">Zone</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>Product</th>
<th>Action</th>
@@ -203,7 +203,7 @@
<div class="col-lg-12 mb-5">
<div class="position-relative">
<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">
<i class="ti ti-search text-muted"></i>
</span>
@@ -216,7 +216,7 @@
<th>Warehouse</th>
<th class="th_loc_zone">Zone</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>
</tr>
</thead>
@@ -231,12 +231,12 @@
</div>
<!-- Rack Log Modal -->
<div class="modal fade" id="rackLogModal" tabindex="-1" aria-labelledby="rackLogModalLabel" aria-hidden="true">
<!-- Bin Log Modal -->
<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-content">
<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>
</div>
<div class="modal-body p-0">
@@ -249,7 +249,7 @@
<th>User</th>
</tr>
</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>
</tbody>
</table>
@@ -267,7 +267,7 @@
// ── Location config ───────────────────────────────────────────────────────
var advanced = false;
var label_rack = 'Location';
var label_bin = 'Location';
var label_zone = 'Zone';
var label_aisle = 'Aisle';
@@ -276,21 +276,21 @@
if (advanced) {
$('.th_loc_zone').text(label_zone).show();
$('.th_loc_aisle').text(label_aisle).show();
$('.th_loc_rack').text(label_rack);
$('.th_loc_bin').text(label_bin);
} else {
$('.th_loc_zone').hide();
$('.th_loc_aisle').hide();
$('.th_loc_rack').text(label_rack);
$('.th_loc_bin').text(label_bin);
}
var loc_hint = advanced
? label_zone.toLowerCase() + ', ' + label_aisle.toLowerCase() + ', ' + label_rack.toLowerCase()
: label_rack.toLowerCase();
? label_zone.toLowerCase() + ', ' + label_aisle.toLowerCase() + ', ' + label_bin.toLowerCase()
: label_bin.toLowerCase();
$('#search_all, #search_occupied').attr('placeholder',
'Search by warehouse, ' + loc_hint + ', or SKU…');
$('#search_empty').attr('placeholder',
'Search by warehouse or ' + loc_hint + '…');
$('#rackLogModalLabel').text(label_rack + ' Log');
$('#stat_label_racks').text('Total ' + label_rack + 's');
$('#binLogModalLabel').text(label_bin + ' Log');
$('#stat_label_bins').text('Total ' + label_bin + 's');
}
// Returns 1 or 3 <td> cells depending on mode
@@ -298,16 +298,16 @@
if (advanced) {
return '<td class="py-3">' + (item.zone || '—') + '</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
function rack_label(item) {
function bin_label(item) {
return advanced
? item.warehouse_name + ' ' + item.zone + '-' + item.aisle + '-' + item.rack
: item.warehouse_name + ' ' + item.rack;
? item.warehouse_name + ' ' + item.zone + '-' + item.aisle + '-' + item.bin
: item.warehouse_name + ' ' + item.bin;
}
var search_all = '';
@@ -316,10 +316,10 @@
// ── Fetch ─────────────────────────────────────────────────────────────
function retrieve_rack_occupancy() {
function retrieve_bin_occupancy() {
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,
checkRequired: 0,
action: 'read',
@@ -338,8 +338,8 @@
$('#stat_rate').text(rate + '%');
// Store in alasql
alasql('CREATE TABLE IF NOT EXISTS rack_all');
alasql.tables.rack_all.data = res.output || [];
alasql('CREATE TABLE IF NOT EXISTS bin_all');
alasql.tables.bin_all.data = res.output || [];
change_page_all(1);
change_page_occupied(1);
@@ -356,14 +356,14 @@
var kw = search_all.toLowerCase();
var filtered = kw ? alasql(
`SELECT * FROM rack_all
`SELECT * FROM bin_all
WHERE LOWER(warehouse_name) LIKE '%${kw}%'
OR LOWER(zone) 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_name) LIKE '%${kw}%'`
) : alasql('SELECT * FROM rack_all');
) : alasql('SELECT * FROM bin_all');
$('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_name || '<span class="text-muted">—</span>'}</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>`;
});
$('table#tbl_all > tbody').html(body);
@@ -398,13 +398,13 @@
var offset = (page_num - 1) * prop_limit;
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) {
return (item.warehouse_name || '').toLowerCase().includes(kw)
|| (item.zone || '').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_name || '').toLowerCase().includes(kw);
}) : base;
@@ -425,7 +425,7 @@
` + location_cells(item) + `
<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"><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>`;
});
$('table#tbl_occupied > tbody').html(body);
@@ -438,13 +438,13 @@
var offset = (page_num - 1) * prop_limit;
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) {
return (item.warehouse_name || '').toLowerCase().includes(kw)
|| (item.zone || '').toLowerCase().includes(kw)
|| (item.aisle || '').toLowerCase().includes(kw)
|| (item.rack || '').toLowerCase().includes(kw);
|| (item.bin || '').toLowerCase().includes(kw);
}) : base;
$('table#tbl_empty tfoot').html(generate_pagination('tbl_empty', filtered.length));
@@ -461,7 +461,7 @@
body += `<tr>
<td class="py-3">${item.warehouse_name}</td>
` + 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>`;
});
$('table#tbl_empty > tbody').html(body);
@@ -501,30 +501,30 @@
// Remember last active tab
$(document).ready(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');
});
// ── Rack Log ─────────────────────────────────────────────────────────
function view_rack_log(rack_id, label) {
// ── Bin Log ──────────────────────────────────────────────────────────
function view_bin_log(bin_id, label) {
$('#rackLogModalLabel').text('Rack Log — ' + label);
$('#rack_log_body').html('<tr><td colspan="4" class="text-center py-4 text-muted">Loading...</td></tr>');
$('#rackLogModal').modal('show');
$('#binLogModalLabel').text('Bin Log — ' + label);
$('#bin_log_body').html('<tr><td colspan="4" class="text-center py-4 text-muted">Loading...</td></tr>');
$('#binLogModal').modal('show');
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,
checkRequired: 0,
action: 'read',
data: { "rack_id": rack_id },
data: { "bin_id": bin_id },
onSuccess: function(res) {
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;
}
@@ -544,7 +544,7 @@
<td class="py-3">${row.user_name || '<span class="text-muted">—</span>'}</td>
</tr>`;
});
$('#rack_log_body').html(body);
$('#bin_log_body').html(body);
}
});
}
@@ -554,7 +554,7 @@
$(async function() {
try {
await load_location_config(apply_location_labels);
await retrieve_rack_occupancy();
await retrieve_bin_occupancy();
} catch(e) {
console.log(e);
}
+6 -6
View File
@@ -183,7 +183,7 @@
var lot_log_page = 1;
var lot_log_limit = 10;
var advanced = false;
var label_rack = 'Location';
var label_bin = 'Location';
var label_zone = 'Zone';
var label_aisle = 'Aisle';
@@ -202,16 +202,16 @@
function apply_location_labels() {
$('#th_location').text(advanced
? label_zone + ' / ' + label_aisle + ' / ' + label_rack
: label_rack
? label_zone + ' / ' + label_aisle + ' / ' + label_bin
: label_bin
);
}
function format_location(row) {
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() {
@@ -223,7 +223,7 @@
onSuccess: function(res) {
var s = res.output || {};
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_aisle = s.location_label_aisle || 'Aisle';
apply_location_labels();
+6 -6
View File
@@ -256,7 +256,7 @@
// 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 label_rack = 'Location';
var label_bin = 'Location';
var label_zone = 'Zone';
var label_aisle = 'Aisle';
@@ -268,16 +268,16 @@
function apply_location_labels() {
$('#th_location').text(advanced
? label_zone + ' / ' + label_aisle + ' / ' + label_rack
: label_rack
? label_zone + ' / ' + label_aisle + ' / ' + label_bin
: label_bin
);
}
function format_location(row) {
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) {
@@ -293,7 +293,7 @@
onSuccess: function(res) {
var s = res.output || {};
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_aisle = s.location_label_aisle || 'Aisle';
apply_location_labels();
+1 -1
View File
@@ -273,7 +273,7 @@
warehouse_id: 0,
zone: '',
aisle: '',
rack: '',
bin: '',
lot_number: '',
serial_number: '',
quantity: qty,
+13 -13
View File
@@ -37,7 +37,7 @@
<p class="text-muted small mb-2">
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
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.
</p>
<div class="form-check form-switch mb-3">
@@ -54,7 +54,7 @@
<label class="col-form-label text-muted small">Location Label</label>
</div>
<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">
</div>
<div class="col-auto">
@@ -94,11 +94,11 @@
<label class="col-form-label text-muted small">Level 3 Label</label>
</div>
<div class="col-lg-4">
<input type="text" id="location_label_rack_advanced" class="form-control form-control-sm"
placeholder="Rack" maxlength="30">
<input type="text" id="location_label_bin_advanced" class="form-control form-control-sm"
placeholder="Bin" maxlength="30">
</div>
<div class="col-auto">
<span class="text-muted small">default: "Rack"</span>
<span class="text-muted small">default: "Bin"</span>
</div>
</div>
</div>
@@ -264,10 +264,10 @@
var advanced = (s.advanced_location ?? 0) == 1;
$('#advanced_location_toggle').prop('checked', 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_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
$('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
var label_rack, label_zone, label_aisle;
var label_bin, label_zone, label_aisle;
if (advanced) {
label_zone = $('#location_label_zone').val().trim() || 'Zone';
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 {
label_rack = $('#location_label_rack_simple').val().trim() || 'Location';
label_zone = label_rack;
label_aisle = label_rack;
label_bin = $('#location_label_bin_simple').val().trim() || 'Location';
label_zone = label_bin;
label_aisle = label_bin;
}
ajax_request({
@@ -370,7 +370,7 @@
advanced_location: advanced,
location_label_zone: label_zone,
location_label_aisle: label_aisle,
location_label_rack: label_rack,
location_label_bin: label_bin,
default_stock_status: stock_status,
auto_invoice_and_credit_note: auto_invoice_and_cn,
auto_complete_on_ship: auto_complete_on_ship,