feat: 統一度量衡,確保儀表板統計與庫存查詢清單數據精確一致
This commit is contained in:
@@ -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,
|
||||
];
|
||||
});
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user