From 83d26de6f9318977aeccd197c6a3893b4e22b44c Mon Sep 17 00:00:00 2001 From: sky121113 Date: Tue, 10 Feb 2026 11:15:08 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E8=AA=BF=E6=95=B4=E7=B5=B1?= =?UTF-8?q?=E8=A8=88=E5=9F=BA=E6=BA=96=E7=82=BA=E6=98=8E=E7=B4=B0=E7=AD=86?= =?UTF-8?q?=E6=95=B8=EF=BC=8C=E4=B8=A6=E6=81=A2=E5=BE=A9=E5=BA=AB=E5=AD=98?= =?UTF-8?q?=E6=9F=A5=E8=A9=A2=E7=82=BA=E5=96=AE=E4=B8=80=E7=B4=B0=E7=9B=AE?= =?UTF-8?q?=E9=A1=AF=E7=A4=BA=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Inventory/Services/InventoryService.php | 108 +++++++----------- .../js/Pages/Inventory/StockQuery/Index.tsx | 5 +- 2 files changed, 43 insertions(+), 70 deletions(-) diff --git a/app/Modules/Inventory/Services/InventoryService.php b/app/Modules/Inventory/Services/InventoryService.php index 93bd89a..29b952b 100644 --- a/app/Modules/Inventory/Services/InventoryService.php +++ b/app/Modules/Inventory/Services/InventoryService.php @@ -246,37 +246,8 @@ class InventoryService implements InventoryServiceInterface $join->on('inventories.warehouse_id', '=', 'ss.warehouse_id') ->on('inventories.product_id', '=', 'ss.product_id'); }) - ->whereNull('inventories.deleted_at'); - - // 決定是否啟用聚合顯示 (Group By Warehouse + Product) - // 當使用者在儀表板點選「狀態篩選」時,為了讓清單筆數與統計數字精確一致 (Dimension Alignment),必須啟用 GROUP BY - $isGrouped = !empty($filters['status']) || ($filters['view_mode'] ?? '') === 'product'; - - if ($isGrouped) { - $query->select([ - DB::raw('MIN(inventories.id) as id'), // 聚合後的代表 ID - 'inventories.warehouse_id', - 'inventories.product_id', - DB::raw('SUM(inventories.quantity) as quantity'), - DB::raw('GROUP_CONCAT(DISTINCT inventories.batch_number SEPARATOR ", ") as batch_number'), - DB::raw('MAX(inventories.expiry_date) as expiry_date'), - DB::raw('GROUP_CONCAT(DISTINCT inventories.location SEPARATOR ", ") as location'), - 'products.code as product_code', - 'products.name as product_name', - 'categories.name as category_name', - 'warehouses.name as warehouse_name', - 'ss.safety_stock', - ])->groupBy( - 'inventories.warehouse_id', - 'inventories.product_id', - 'products.code', - 'products.name', - 'categories.name', - 'warehouses.name', - 'ss.safety_stock' - ); - } else { - $query->select([ + ->whereNull('inventories.deleted_at') + ->select([ 'inventories.id', 'inventories.warehouse_id', 'inventories.product_id', @@ -291,7 +262,6 @@ class InventoryService implements InventoryServiceInterface 'warehouses.name as warehouse_name', 'ss.safety_stock', ]); - } // 篩選:倉庫 if (!empty($filters['warehouse_id'])) { @@ -381,56 +351,61 @@ class InventoryService implements InventoryServiceInterface $query->orderBy('products.code', 'asc'); } - // 統計卡片(無篩選條件下的全域統計) - // 1. 庫存品項數:在庫的「倉庫-商品」對總數 + // 統計卡片(預設無篩選條件下的全域統計,改為明細筆數計數以對齊顯示) + // 1. 庫存明細總數 $totalItems = DB::table('inventories') ->whereNull('deleted_at') - ->distinct() - ->count(DB::raw('CONCAT(warehouse_id, "-", product_id)')); - - // 2. 低庫存:以「倉庫-商品」聚合後的總量與安全庫存比較 (品項計數) - $lowStockCount = DB::table('warehouse_product_safety_stocks as ss') - ->join(DB::raw('(SELECT warehouse_id, product_id, SUM(quantity) as total_qty FROM inventories WHERE deleted_at IS NULL GROUP BY warehouse_id, product_id) as inv'), - function ($join) { - $join->on('ss.warehouse_id', '=', 'inv.warehouse_id') - ->on('ss.product_id', '=', 'inv.product_id'); - }) - ->whereRaw('inv.total_qty <= ss.safety_stock') ->count(); - // 3. 負庫存:只要該「倉庫-商品」的總庫存為負數 (品項計數) - $negativeCount = DB::table(DB::raw('(SELECT warehouse_id, product_id, SUM(quantity) as total_qty FROM inventories WHERE deleted_at IS NULL GROUP BY warehouse_id, product_id) as inv')) - ->where('total_qty', '<', 0) + // 2. 低庫存明細數:只要該明細所屬的「倉庫+商品」總量低於安全庫存,則所有相關明細都計入 + $lowStockCount = DB::table('inventories as i') + ->join('warehouse_product_safety_stocks as ss', function ($join) { + $join->on('i.warehouse_id', '=', 'ss.warehouse_id') + ->on('i.product_id', '=', 'ss.product_id'); + }) + ->whereNull('i.deleted_at') + ->whereIn(DB::raw('(i.warehouse_id, i.product_id)'), function ($sub) { + $sub->select('i2.warehouse_id', 'i2.product_id') + ->from('inventories as i2') + ->whereNull('i2.deleted_at') + ->groupBy('i2.warehouse_id', 'i2.product_id') + ->havingRaw('SUM(i2.quantity) <= (SELECT safety_stock FROM warehouse_product_safety_stocks WHERE warehouse_id = i2.warehouse_id AND product_id = i2.product_id LIMIT 1)'); + }) ->count(); - // 4. 即將過期:有任一批次效期符合的「品項」總數 + // 3. 負庫存明細數 + $negativeCount = DB::table('inventories as i') + ->whereNull('i.deleted_at') + ->whereIn(DB::raw('(i.warehouse_id, i.product_id)'), function ($sub) { + $sub->select('i2.warehouse_id', 'i2.product_id') + ->from('inventories as i2') + ->whereNull('i2.deleted_at') + ->groupBy('i2.warehouse_id', 'i2.product_id') + ->havingRaw('SUM(i2.quantity) < 0'); + }) + ->count(); + + // 4. 即將過期明細數 $expiringCount = DB::table('inventories') ->whereNull('deleted_at') ->whereNotNull('expiry_date') ->where('expiry_date', '<=', $expiryThreshold) - ->distinct() - ->count(DB::raw('CONCAT(warehouse_id, "-", product_id)')); + ->count(); // 分頁 $paginated = $query->paginate($perPage)->withQueryString(); // 為每筆紀錄附加最後入庫/出庫時間 + 狀態 - $items = collect($paginated->items())->map(function ($item) use ($today, $expiryThreshold, $isGrouped) { - // 如果是聚合模式,Transaction 查詢也需要調整或略過單一批次查詢 - $lastIn = null; - $lastOut = null; + $items = collect($paginated->items())->map(function ($item) use ($today, $expiryThreshold) { + $lastIn = \App\Modules\Inventory\Models\InventoryTransaction::where('inventory_id', $item->id) + ->where('type', '入庫') + ->orderByDesc('actual_time') + ->value('actual_time'); - if (!$isGrouped) { - $lastIn = \App\Modules\Inventory\Models\InventoryTransaction::where('inventory_id', $item->id) - ->where('type', '入庫') - ->orderByDesc('actual_time') - ->value('actual_time'); - - $lastOut = \App\Modules\Inventory\Models\InventoryTransaction::where('inventory_id', $item->id) - ->where('type', '出庫') - ->orderByDesc('actual_time') - ->value('actual_time'); - } + $lastOut = \App\Modules\Inventory\Models\InventoryTransaction::where('inventory_id', $item->id) + ->where('type', '出庫') + ->orderByDesc('actual_time') + ->value('actual_time'); // 計算狀態 $statuses = []; @@ -466,7 +441,6 @@ class InventoryService implements InventoryServiceInterface 'last_inbound' => $lastIn ? \Carbon\Carbon::parse($lastIn)->toDateString() : null, 'last_outbound' => $lastOut ? \Carbon\Carbon::parse($lastOut)->toDateString() : null, 'statuses' => $statuses, - 'is_grouped' => $isGrouped, ]; }); diff --git a/resources/js/Pages/Inventory/StockQuery/Index.tsx b/resources/js/Pages/Inventory/StockQuery/Index.tsx index 0eeb948..79e4892 100644 --- a/resources/js/Pages/Inventory/StockQuery/Index.tsx +++ b/resources/js/Pages/Inventory/StockQuery/Index.tsx @@ -40,7 +40,6 @@ interface InventoryItem { last_inbound: string | null; last_outbound: string | null; statuses: string[]; - is_grouped?: boolean; location: string | null; } @@ -493,10 +492,10 @@ export default function StockQueryIndex({ {item.warehouse_name} - + {item.batch_number || "—"} - + {item.location || "—"}