From 38642cc58bd37500ec9295e3bbde87d3d334ac31 Mon Sep 17 00:00:00 2001 From: sky121113 Date: Tue, 10 Feb 2026 11:09:22 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=B5=B1=E4=B8=80=E5=BA=A6=E9=87=8F?= =?UTF-8?q?=E8=A1=A1=EF=BC=8C=E7=A2=BA=E4=BF=9D=E5=84=80=E8=A1=A8=E6=9D=BF?= =?UTF-8?q?=E7=B5=B1=E8=A8=88=E8=88=87=E5=BA=AB=E5=AD=98=E6=9F=A5=E8=A9=A2?= =?UTF-8?q?=E6=B8=85=E5=96=AE=E6=95=B8=E6=93=9A=E7=B2=BE=E7=A2=BA=E4=B8=80?= =?UTF-8?q?=E8=87=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Inventory/Services/InventoryService.php | 64 +++++++++++++++---- .../js/Pages/Inventory/StockQuery/Index.tsx | 13 ++-- 2 files changed, 55 insertions(+), 22 deletions(-) diff --git a/app/Modules/Inventory/Services/InventoryService.php b/app/Modules/Inventory/Services/InventoryService.php index 451bf60..93bd89a 100644 --- a/app/Modules/Inventory/Services/InventoryService.php +++ b/app/Modules/Inventory/Services/InventoryService.php @@ -246,8 +246,37 @@ class InventoryService implements InventoryServiceInterface $join->on('inventories.warehouse_id', '=', 'ss.warehouse_id') ->on('inventories.product_id', '=', 'ss.product_id'); }) - ->whereNull('inventories.deleted_at') - ->select([ + ->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([ 'inventories.id', 'inventories.warehouse_id', 'inventories.product_id', @@ -256,13 +285,13 @@ class InventoryService implements InventoryServiceInterface 'inventories.expiry_date', 'inventories.location', 'inventories.quality_status', - 'inventories.arrival_date', 'products.code as product_code', 'products.name as product_name', 'categories.name as category_name', 'warehouses.name as warehouse_name', 'ss.safety_stock', ]); + } // 篩選:倉庫 if (!empty($filters['warehouse_id'])) { @@ -386,18 +415,24 @@ class InventoryService implements InventoryServiceInterface $paginated = $query->paginate($perPage)->withQueryString(); // 為每筆紀錄附加最後入庫/出庫時間 + 狀態 - $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'); + $items = collect($paginated->items())->map(function ($item) use ($today, $expiryThreshold, $isGrouped) { + // 如果是聚合模式,Transaction 查詢也需要調整或略過單一批次查詢 + $lastIn = null; + $lastOut = null; - $lastOut = \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'); + } + + // 計算狀態 $statuses = []; if ($item->quantity < 0) { $statuses[] = 'negative'; @@ -427,10 +462,11 @@ class InventoryService implements InventoryServiceInterface 'safety_stock' => $item->safety_stock, 'expiry_date' => $item->expiry_date ? \Carbon\Carbon::parse($item->expiry_date)->toDateString() : null, 'location' => $item->location, - 'quality_status' => $item->quality_status, + 'quality_status' => $item->quality_status ?? null, '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 3d609af..0eeb948 100644 --- a/resources/js/Pages/Inventory/StockQuery/Index.tsx +++ b/resources/js/Pages/Inventory/StockQuery/Index.tsx @@ -40,6 +40,8 @@ interface InventoryItem { last_inbound: string | null; last_outbound: string | null; statuses: string[]; + is_grouped?: boolean; + location: string | null; } interface PaginationLink { @@ -491,18 +493,13 @@ export default function StockQueryIndex({ {item.warehouse_name} - + {item.batch_number || "—"} - + {item.location || "—"} - + {item.quantity}