feat: 統一度量衡,確保儀表板統計與庫存查詢清單數據精確一致

This commit is contained in:
2026-02-10 11:09:22 +08:00
parent a6393e03d8
commit 38642cc58b
2 changed files with 55 additions and 22 deletions

View File

@@ -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,7 +415,12 @@ class InventoryService implements InventoryServiceInterface
$paginated = $query->paginate($perPage)->withQueryString();
// 為每筆紀錄附加最後入庫/出庫時間 + 狀態
$items = collect($paginated->items())->map(function ($item) use ($today, $expiryThreshold) {
$items = collect($paginated->items())->map(function ($item) use ($today, $expiryThreshold, $isGrouped) {
// 如果是聚合模式Transaction 查詢也需要調整或略過單一批次查詢
$lastIn = null;
$lastOut = null;
if (!$isGrouped) {
$lastIn = \App\Modules\Inventory\Models\InventoryTransaction::where('inventory_id', $item->id)
->where('type', '入庫')
->orderByDesc('actual_time')
@@ -396,8 +430,9 @@ class InventoryService implements InventoryServiceInterface
->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,
];
});

View File

@@ -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({
<TableCell>
{item.warehouse_name}
</TableCell>
<TableCell className="text-gray-500 text-sm">
<TableCell className="text-gray-500 text-sm italic">
{item.batch_number || "—"}
</TableCell>
<TableCell className="text-sm text-gray-500">
<TableCell className="text-sm text-gray-500 italic">
{item.location || "—"}
</TableCell>
<TableCell
className={`text-right font-medium ${item.quantity < 0
? "text-red-600"
: ""
}`}
>
<TableCell className={`text-right font-medium ${item.quantity < 0 ? "text-red-600" : ""}`}>
{item.quantity}
</TableCell>
<TableCell className="text-right text-gray-500">