From ed23c269eacabc44e13fd821a1764074387346e9 Mon Sep 17 00:00:00 2001 From: Thanakorn S Date: Thu, 23 Apr 2026 08:24:30 +0700 Subject: [PATCH] warehouse capacity and occupancy --- app.zip | Bin 17898927 -> 17901948 bytes app/assets/utils/classes/Reports.php | 40 +- app/assets/utils/classes/WarehouseManager.php | 353 ++++++++++++++++-- .../api/engine/manage_contact_type.php | 2 +- app/ics/api/engine/manage_stock_in.php | 3 +- app/ics/api/engine/manage_stock_out.php | 3 +- app/ics/api/engine/manage_stock_transfer.php | 8 +- app/inventory/api/engine/manage_category.php | 2 +- app/inventory/api/engine/manage_product.php | 2 +- app/inventory/api/engine/manage_storage.php | 32 +- app/inventory/api/engine/manage_warehouse.php | 8 +- app/inventory/api/engine/warehouse.php | 31 +- app/inventory/api/engine/warehouse_stats.php | 1 + app/inventory/manage_warehouse.php | 4 - app/inventory/warehouse.php | 25 +- 15 files changed, 429 insertions(+), 85 deletions(-) diff --git a/app.zip b/app.zip index 5cf4c6e0964331269ac7f77fe5a50e7ee5c86b46..46fb58e6020c99950ef303d386c2a953261ca0ac 100644 GIT binary patch delta 21889 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 Ee^XN8xBvhE delta 18705 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 = $companyId; } + /** + * Master Data Summary: + * - Total Categories: Count of all categories in md_product_category for the company. + * - Active Categories: Count of categories where status = 1. + * - Inactive Categories: Count of categories where status = 0. + */ public function getTotalCategory(): int { $sql = "SELECT COUNT(*) @@ -41,7 +47,8 @@ class Reports { $sql = "SELECT COUNT(*) FROM md_warehouse - WHERE company_id = :company_id"; + WHERE company_id = :company_id + AND `status` = 1"; return (int) $this->fetchScalar($sql); } @@ -49,18 +56,39 @@ class Reports { $sql = "SELECT COUNT(DISTINCT `location`) FROM md_warehouse + WHERE company_id = :company_id + AND `status` = 1"; + return (int) $this->fetchScalar($sql); + } + + /** + * Total capacity = total count of active racks across all warehouses. + * One rack = one storage position (holds one product unit). + */ + public function getTotalCapacity(): int + { + $sql = "SELECT COUNT(*) + FROM md_rack WHERE company_id = :company_id"; return (int) $this->fetchScalar($sql); } - public function getTotalCapacity(): float + /** + * Space Used = count of racks currently occupied (product_sku IS NOT NULL). + */ + public function getSpaceUsed(): int { - $sql = "SELECT SUM(capacity) - FROM md_warehouse - WHERE company_id = :company_id"; - return (float) $this->fetchScalar($sql); + $sql = "SELECT COUNT(*) + FROM md_rack + WHERE company_id = :company_id + AND product_sku IS NOT NULL"; + return (int) $this->fetchScalar($sql); } + /** + * Product Count = total count of distinct products in the md_product table for the company. + * Low Stock Count = count of products where current stock balance (total_in - total_out) is less than the min_stock defined in md_product. + */ public function getTotalProduct(): int { $sql = "SELECT COUNT(*) diff --git a/app/assets/utils/classes/WarehouseManager.php b/app/assets/utils/classes/WarehouseManager.php index 97604f5..e3b278a 100644 --- a/app/assets/utils/classes/WarehouseManager.php +++ b/app/assets/utils/classes/WarehouseManager.php @@ -1,5 +1,16 @@ company_id = $company_id; } + // ───────────────────────────────────────────────────────────── + // Warehouse lookup + // ───────────────────────────────────────────────────────────── + + /** + * Fetch the warehouse name by its ID. + */ public function getWarehouseName($warehouse_id) { $sql = "SELECT warehouse_name FROM md_warehouse WHERE company_id = :company_id AND id = :warehouse_id"; @@ -22,9 +40,16 @@ class WarehouseManager { return $sth->fetchColumn(); } + /** + * Resolve the per-warehouse stock table and (optionally) fetch a specific row. + * Returns: ['table' => 'td_stock_xxx', 'name' => 'xxx', 'row' => [...] | []] + */ public function getStockContext($warehouse_id, $id) { $name = $this->getWarehouseName($warehouse_id); - $table = "td_stock_" . $name; + + // Sanitize table suffix to prevent SQL injection + $safe_name = preg_replace('/[^a-zA-Z0-9_]/', '', $name); + $table = "td_stock_" . $safe_name; if (empty($id)) { return [ @@ -50,56 +75,306 @@ class WarehouseManager { ]; } + // ───────────────────────────────────────────────────────────── + // Warehouse balance (warehouse_balance table) + // ───────────────────────────────────────────────────────────── + + /** + * Adjust the running balance for a (warehouse, SKU) pair. + * + * Used by stock_in / stock_out engines to keep warehouse_balance in sync with movements. + * The delta-based update (-old_qty + new_qty) supports both create and edit flows. + */ public function adjustBalance($type, $warehouse_id, $product_sku, $old_qty, $new_qty) { - // Lock the row — other transactions wait here - $sql = "SELECT id FROM warehouse_balance - WHERE company_id = :company_id - AND warehouse_id = :warehouse_id - AND product_sku = :product_sku - FOR UPDATE"; - - $sth = $this->pdo->prepare($sql); - $sth->execute([ - ":company_id" => $this->company_id, - ":warehouse_id" => $warehouse_id, - ":product_sku" => $product_sku - ]); - - if (!$sth->fetchColumn()) { - $sql = "INSERT INTO warehouse_balance - (company_id, warehouse_id, product_sku) - VALUES (:company_id, :warehouse_id, :product_sku)"; - - $sth = $this->pdo->prepare($sql); - $sth->execute([ - ":company_id" => $this->company_id, - ":warehouse_id" => $warehouse_id, - ":product_sku" => $product_sku - ]); - } - + // Upsert the balance row (atomic, no race condition) $column = $type === 'in' ? 'total_in' : 'total_out'; + $delta = $new_qty - $old_qty; - // This is already atomic since it uses relative update - $sql = "UPDATE warehouse_balance - SET `$column` = `$column` - :old_qty + :new_qty - WHERE company_id = :company_id - AND warehouse_id = :warehouse_id - AND product_sku = :product_sku"; + $sql = "INSERT INTO warehouse_balance + (company_id, warehouse_id, product_sku, `$column`) + VALUES + (:company_id, :warehouse_id, :product_sku, :delta) + ON DUPLICATE KEY UPDATE + `$column` = `$column` + VALUES(`$column`)"; $sth = $this->pdo->prepare($sql); $sth->execute([ ":company_id" => $this->company_id, ":warehouse_id" => $warehouse_id, ":product_sku" => $product_sku, - ":old_qty" => $old_qty, - ":new_qty" => $new_qty + ":delta" => $delta + ]); + } + + // ───────────────────────────────────────────────────────────── + // Rack lifecycle (md_rack table) + // ───────────────────────────────────────────────────────────── + + /** + * Ensure md_rack rows match the md_storage range. + * + * - Validates no occupied racks fall outside the new range + * - Deletes racks outside the new range + * - Inserts new racks inside the range (INSERT IGNORE skips existing) + * + * Must be called inside a DB transaction by the caller. + * + * @param int $storage_id The md_storage.id this range belongs to + * @param array $range Keys: warehouse, zone, aisle_from, aisle_to, rack_from, rack_to + * @throws Exception if occupied racks would be removed + */ + public function syncRacks(int $storage_id, array $range): void { + + $this->validateRangeChange($storage_id, $range); + $this->removeRacksOutsideRange($storage_id, $range); + $this->insertRacksInRange($storage_id, $range); + } + + /** + * Guard: reject the update if any occupied rack would be removed. + */ + private function validateRangeChange(int $storage_id, array $range): void { + + $sql = "SELECT COUNT(*) FROM md_rack + WHERE company_id = :company_id + AND storage_id = :storage_id + AND product_sku IS NOT NULL + AND ( + CAST(aisle AS UNSIGNED) < :aisle_from OR + CAST(aisle AS UNSIGNED) > :aisle_to OR + CAST(rack AS UNSIGNED) < :rack_from OR + CAST(rack AS UNSIGNED) > :rack_to + )"; + + $sth = $this->pdo->prepare($sql); + $sth->execute([ + ":company_id" => $this->company_id, + ":storage_id" => $storage_id, + ":aisle_from" => (int) $range['aisle_from'], + ":aisle_to" => (int) $range['aisle_to'], + ":rack_from" => (int) $range['rack_from'], + ":rack_to" => (int) $range['rack_to'], + ]); + + if ($sth->fetchColumn() > 0) { + throw new Exception("Cannot shrink range — some racks still have stock"); + } + } + + /** + * Delete empty racks that fall outside the new range. + */ + private function removeRacksOutsideRange(int $storage_id, array $range): void { + + $sql = "DELETE FROM md_rack + WHERE company_id = :company_id + AND storage_id = :storage_id + AND ( + CAST(aisle AS UNSIGNED) < :aisle_from OR + CAST(aisle AS UNSIGNED) > :aisle_to OR + CAST(rack AS UNSIGNED) < :rack_from OR + CAST(rack AS UNSIGNED) > :rack_to + )"; + + $sth = $this->pdo->prepare($sql); + $sth->execute([ + ":company_id" => $this->company_id, + ":storage_id" => $storage_id, + ":aisle_from" => (int) $range['aisle_from'], + ":aisle_to" => (int) $range['aisle_to'], + ":rack_from" => (int) $range['rack_from'], + ":rack_to" => (int) $range['rack_to'], + ]); + } + + /** + * Insert rack rows for the full range (IGNORE skips duplicates). + */ + private function insertRacksInRange(int $storage_id, array $range): void { + + $sql = "INSERT IGNORE INTO md_rack + (company_id, warehouse, storage_id, zone, aisle, rack) + VALUES + (:company_id, :warehouse, :storage_id, :zone, :aisle, :rack)"; + $sth = $this->pdo->prepare($sql); + + $aisle_from = (int) $range['aisle_from']; + $aisle_to = (int) $range['aisle_to']; + $rack_from = (int) $range['rack_from']; + $rack_to = (int) $range['rack_to']; + + for ($a = $aisle_from; $a <= $aisle_to; $a++) { + for ($r = $rack_from; $r <= $rack_to; $r++) { + $sth->execute([ + ":company_id" => $this->company_id, + ":warehouse" => $range['warehouse'], + ":storage_id" => $storage_id, + ":zone" => $range['zone'], + ":aisle" => (string) $a, + ":rack" => (string) $r, + ]); + } + } + } + + // ───────────────────────────────────────────────────────────── + // Rack occupancy (md_rack.product_sku state) + // ───────────────────────────────────────────────────────────── + + /** + * Assign a product SKU to an empty rack. + * + * @throws Exception if the rack is already occupied by a different SKU + * or if the rack doesn't exist. + */ + public function occupyRack($warehouse_id, $zone, $aisle, $rack, $product_sku): void { + + // Lock and check the rack's current state + $sql = "SELECT id, product_sku FROM md_rack + WHERE company_id = :company_id + AND warehouse = :warehouse + AND zone = :zone + AND aisle = :aisle + AND rack = :rack + FOR UPDATE"; + + $sth = $this->pdo->prepare($sql); + $sth->execute([ + ":company_id" => $this->company_id, + ":warehouse" => $warehouse_id, + ":zone" => $zone, + ":aisle" => $aisle, + ":rack" => $rack, + ]); + $current = $sth->fetch(PDO::FETCH_ASSOC); + + if (!$current) { + throw new Exception("Rack {$zone}-{$aisle}-{$rack} does not exist"); + } + + if ($current['product_sku'] !== null + && $current['product_sku'] !== $product_sku) { + throw new Exception( + "Rack {$zone}-{$aisle}-{$rack} is already occupied by {$current['product_sku']}" + ); + } + + // Assign the SKU + $sql = "UPDATE md_rack + SET product_sku = :product_sku + WHERE id = :id"; + + $sth = $this->pdo->prepare($sql); + $sth->execute([ + ":product_sku" => $product_sku, + ":id" => $current['id'], + ]); + } + + /** + * Release a rack (set product_sku to NULL). + */ + public function releaseRack($warehouse_id, $zone, $aisle, $rack): void { + + $sql = "UPDATE md_rack + SET product_sku = NULL + WHERE company_id = :company_id + AND warehouse = :warehouse + AND zone = :zone + AND aisle = :aisle + AND rack = :rack"; + + $sth = $this->pdo->prepare($sql); + $sth->execute([ + ":company_id" => $this->company_id, + ":warehouse" => $warehouse_id, + ":zone" => $zone, + ":aisle" => $aisle, + ":rack" => $rack, + ]); + } + + /** + * Move a rack assignment from one location to another. + * + * Source rack must be occupied, destination rack must be empty. + * Locks both racks (lower ID first) to avoid deadlocks. + */ + public function transferRack( + $from_warehouse, $from_zone, $from_aisle, $from_rack, + $to_warehouse, $to_zone, $to_aisle, $to_rack + ): void { + + // Fetch both racks with FOR UPDATE, ordered by id to prevent deadlocks + $sql = "SELECT id, warehouse, zone, aisle, rack, product_sku + FROM md_rack + WHERE company_id = :company_id + AND ( + (warehouse = :from_wh AND zone = :from_zone + AND aisle = :from_aisle AND rack = :from_rack) + OR + (warehouse = :to_wh AND zone = :to_zone + AND aisle = :to_aisle AND rack = :to_rack) + ) + ORDER BY id + FOR UPDATE"; + + $sth = $this->pdo->prepare($sql); + $sth->execute([ + ":company_id" => $this->company_id, + ":from_wh" => $from_warehouse, + ":from_zone" => $from_zone, + ":from_aisle" => $from_aisle, + ":from_rack" => $from_rack, + ":to_wh" => $to_warehouse, + ":to_zone" => $to_zone, + ":to_aisle" => $to_aisle, + ":to_rack" => $to_rack, + ]); + + $racks = $sth->fetchAll(PDO::FETCH_ASSOC); + + // Identify source and destination from the fetched rows + $from = null; + $to = null; + foreach ($racks as $r) { + if ($r['warehouse'] == $from_warehouse && $r['zone'] == $from_zone + && $r['aisle'] == $from_aisle && $r['rack'] == $from_rack) { + $from = $r; + } + if ($r['warehouse'] == $to_warehouse && $r['zone'] == $to_zone + && $r['aisle'] == $to_aisle && $r['rack'] == $to_rack) { + $to = $r; + } + } + + if (!$from) { + throw new Exception("Source rack not found"); + } + if (!$to) { + throw new Exception("Destination rack not found"); + } + if ($from['product_sku'] === null) { + throw new Exception("Source rack is empty"); + } + if ($to['product_sku'] !== null) { + throw new Exception("Destination rack is already occupied"); + } + + $sku = $from['product_sku']; + + // Clear source + $sql = "UPDATE md_rack SET product_sku = NULL WHERE id = :id"; + $this->pdo->prepare($sql)->execute([":id" => $from['id']]); + + // Populate destination + $sql = "UPDATE md_rack SET product_sku = :sku WHERE id = :id"; + $this->pdo->prepare($sql)->execute([ + ":sku" => $sku, + ":id" => $to['id'], ]); } } - - - ?> \ No newline at end of file diff --git a/app/contact/api/engine/manage_contact_type.php b/app/contact/api/engine/manage_contact_type.php index ed3d1d0..21f27ee 100644 --- a/app/contact/api/engine/manage_contact_type.php +++ b/app/contact/api/engine/manage_contact_type.php @@ -59,7 +59,7 @@ } catch (Exception $e) { $answer["success"] = 0; - $answer["message"] = "Something went wrong"; + $answer["message"] = $e->getMessage(); } exit(json_encode($answer)); diff --git a/app/ics/api/engine/manage_stock_in.php b/app/ics/api/engine/manage_stock_in.php index 3e63f15..7185698 100644 --- a/app/ics/api/engine/manage_stock_in.php +++ b/app/ics/api/engine/manage_stock_in.php @@ -69,6 +69,7 @@ ]); } + $whMgmt->occupyRack($warehouse_id,$data["zone"],$data["aisle"],$data["rack"],$data["product_sku"]); $whMgmt->adjustBalance('in', $warehouse_id, $data["product_sku"], $row['in'] ?? 0, $data["quantity"]); }); @@ -81,7 +82,7 @@ } catch (Exception $e) { $answer["success"] = 0; - $answer["message"] = "Something went wrong"; + $answer["message"] = $e->getMessage(); } exit(json_encode($answer)); diff --git a/app/ics/api/engine/manage_stock_out.php b/app/ics/api/engine/manage_stock_out.php index 30406bd..3c77afc 100644 --- a/app/ics/api/engine/manage_stock_out.php +++ b/app/ics/api/engine/manage_stock_out.php @@ -69,6 +69,7 @@ ]); } + $whMgmt->releaseRack($warehouse_id, $data["zone"], $data["aisle"], $data["rack"],$data["product_sku"]); $whMgmt->adjustBalance('out', $warehouse_id, $data["product_sku"], $row['out'] ?? 0, $data["quantity"]); }); @@ -81,7 +82,7 @@ } catch (Exception $e) { $answer["success"] = 0; - $answer["message"] = "Something went wrong"; + $answer["message"] = $e->getMessage(); } exit(json_encode($answer)); diff --git a/app/ics/api/engine/manage_stock_transfer.php b/app/ics/api/engine/manage_stock_transfer.php index 7ab2d09..b58e9e4 100644 --- a/app/ics/api/engine/manage_stock_transfer.php +++ b/app/ics/api/engine/manage_stock_transfer.php @@ -93,6 +93,12 @@ ":log" => json_encode($table_log), ]); } + + if($data['type'] == 'from'){ + $whMgmt->releaseRack($warehouse_id, $data["zone"], $data["aisle"], $data["rack"]); + }elseif($data['type'] == 'to'){ + $whMgmt->occupyRack($warehouse_id, $data["zone"], $data["aisle"], $data["rack"]); + } $whMgmt->adjustBalance($data['col'], $warehouse_id, $data["product_sku"], $row[$data['col']] ?? 0, $data["quantity"]); } @@ -107,7 +113,7 @@ } catch (Exception $e) { $answer["success"] = 0; - $answer["message"] = "Something went wrong"; + $answer["message"] = $e->getMessage(); } exit(json_encode($answer)); diff --git a/app/inventory/api/engine/manage_category.php b/app/inventory/api/engine/manage_category.php index dc37c10..e746fc6 100644 --- a/app/inventory/api/engine/manage_category.php +++ b/app/inventory/api/engine/manage_category.php @@ -67,7 +67,7 @@ } catch (Exception $e) { $answer["success"] = 0; - $answer["message"] = "Something went wrong"; + $answer["message"] = $e->getMessage(); } exit(json_encode($answer)); diff --git a/app/inventory/api/engine/manage_product.php b/app/inventory/api/engine/manage_product.php index e968e37..c24d8bf 100644 --- a/app/inventory/api/engine/manage_product.php +++ b/app/inventory/api/engine/manage_product.php @@ -115,7 +115,7 @@ } catch (Exception $e) { $answer["success"] = 0; - $answer["message"] = "Something went wrong"; + $answer["message"] = $e->getMessage(); $answer["error"] = $e->getMessage(); } diff --git a/app/inventory/api/engine/manage_storage.php b/app/inventory/api/engine/manage_storage.php index 9ff4a0b..593ea0c 100644 --- a/app/inventory/api/engine/manage_storage.php +++ b/app/inventory/api/engine/manage_storage.php @@ -1,6 +1,7 @@ prepare($sql); + $sth->execute($params); + + $storage_id = $id; + } else { $sql = "INSERT INTO md_storage - (company_id, warehouse, zone, aisle_from, aisle_to, rack_from, rack_to, description, status, `log`) + (company_id, warehouse, zone, aisle_from, aisle_to, + rack_from, rack_to, description, status, `log`) VALUES - (:company_id, :warehouse, :zone, :aisle_from, :aisle_to, :rack_from, :rack_to, :description, :status, :log)"; + (:company_id, :warehouse, :zone, :aisle_from, :aisle_to, + :rack_from, :rack_to, :description, :status, :log)"; + + $sth = $pdo->prepare($sql); + $sth->execute($params); + + $storage_id = (int) $pdo->lastInsertId(); } - $sth = $pdo->prepare($sql); - $sth->execute($params); - + // Sync md_rack rows — same $pdo, inside the same transaction + $wm = new WarehouseManager($pdo, $company_id); + $wm->syncRacks($storage_id, [ + 'warehouse' => $data['warehouse'], + 'zone' => $data['zone'], + 'aisle_from' => $data['aisle_from'], + 'aisle_to' => $data['aisle_to'], + 'rack_from' => $data['rack_from'], + 'rack_to' => $data['rack_to'], + ]); }); $answer["success"] = 1; @@ -69,7 +89,7 @@ } catch (Exception $e) { $answer["success"] = 0; - $answer["message"] = "Something went wrong"; + $answer["message"] = $e->getMessage(); } exit(json_encode($answer)); diff --git a/app/inventory/api/engine/manage_warehouse.php b/app/inventory/api/engine/manage_warehouse.php index 9c90d31..73f919f 100644 --- a/app/inventory/api/engine/manage_warehouse.php +++ b/app/inventory/api/engine/manage_warehouse.php @@ -24,7 +24,6 @@ ":warehouse_name" => $data["warehouse_name"] ?? "", ":location" => $data["location"] ?? "", ":manager" => (int)($data["manager"] ?? 0), - ":capacity" => (int)($data["capacity"] ?? 0), ":description" => $data["description"] ?? "", ":status" => (int)($data["status"] ?? 1), ":log" => json_encode($table_log, JSON_UNESCAPED_UNICODE), @@ -36,7 +35,6 @@ `warehouse_name` = :warehouse_name, `location` = :location, `manager` = :manager, - `capacity` = :capacity, `description` = :description, `status` = :status, `log` = :log @@ -47,9 +45,9 @@ } else { $sql = "INSERT INTO md_warehouse - (company_id, warehouse_name, location, manager, capacity, description, status, `log`) + (company_id, warehouse_name, location, manager, description, status, `log`) VALUES - (:company_id, :warehouse_name, :location, :manager, :capacity, :description, :status, :log)"; + (:company_id, :warehouse_name, :location, :manager, :description, :status, :log)"; } $sth = $pdo->prepare($sql); @@ -72,7 +70,7 @@ } catch (Exception $e) { $answer["success"] = 0; - $answer["message"] = "Something went wrong"; + $answer["message"] = $e->getMessage(); } exit(json_encode($answer)); diff --git a/app/inventory/api/engine/warehouse.php b/app/inventory/api/engine/warehouse.php index ad30552..aa04750 100644 --- a/app/inventory/api/engine/warehouse.php +++ b/app/inventory/api/engine/warehouse.php @@ -3,17 +3,26 @@ require '../../../assets/utils/db_auth.php'; - $sql = "SELECT a.*, IFNULL(COUNT(DISTINCT b.product_sku), 0) AS unique_product, - c.name AS manager_name, c.surname AS manager_surname - FROM md_warehouse a - LEFT JOIN warehouse_balance b ON - a.company_id = b.company_id AND - a.id = b.warehouse_id - LEFT JOIN wms.user c ON - a.manager = c.user_id - WHERE - a.company_id = :company_id - GROUP BY a.id"; + $sql = "SELECT + a.*, + COALESCE(r.total_racks, 0) AS capacity, + COALESCE(r.occupied_racks, 0) AS space_used, + COALESCE(r.unique_product, 0) AS unique_product, + c.name AS manager_name, + c.surname AS manager_surname + FROM md_warehouse a + LEFT JOIN ( + SELECT + company_id, warehouse, + COUNT(*) AS total_racks, + COUNT(product_sku) AS occupied_racks, + COUNT(DISTINCT product_sku) AS unique_product + FROM md_rack + GROUP BY company_id, warehouse + ) r ON a.company_id = r.company_id AND a.id = r.warehouse + LEFT JOIN wms.user c ON a.manager = c.user_id + WHERE a.company_id = :company_id"; + $sth = $pdo2->prepare($sql); $sth->execute([ ":company_id" => $company_id diff --git a/app/inventory/api/engine/warehouse_stats.php b/app/inventory/api/engine/warehouse_stats.php index e6c0ae4..b506b6a 100644 --- a/app/inventory/api/engine/warehouse_stats.php +++ b/app/inventory/api/engine/warehouse_stats.php @@ -9,6 +9,7 @@ $answer["total_warehouse"] = $report->getTotalWarehouse(); $answer["total_location"] = $report->getTotalLocation(); $answer["total_capacity"] = $report->getTotalCapacity(); + $answer["space_used"] = $report->getSpaceUsed(); $answer["total_product"] = $report->getTotalProduct(); $answer["success"] = 1; diff --git a/app/inventory/manage_warehouse.php b/app/inventory/manage_warehouse.php index 4d2732a..aa884de 100644 --- a/app/inventory/manage_warehouse.php +++ b/app/inventory/manage_warehouse.php @@ -51,10 +51,6 @@ -
- - -
diff --git a/app/inventory/warehouse.php b/app/inventory/warehouse.php index 2768683..666cdaa 100644 --- a/app/inventory/warehouse.php +++ b/app/inventory/warehouse.php @@ -53,9 +53,10 @@
41,000 + Total Capacity - - Storage units available + Rack positions available +
@@ -81,9 +82,10 @@
25,500 + Space Used - - Storage units available + Racks occupied +
@@ -252,7 +254,6 @@