feat: 統一度量衡,確保儀表板統計與庫存查詢清單數據精確一致
This commit is contained in:
@@ -246,8 +246,37 @@ class InventoryService implements InventoryServiceInterface
|
|||||||
$join->on('inventories.warehouse_id', '=', 'ss.warehouse_id')
|
$join->on('inventories.warehouse_id', '=', 'ss.warehouse_id')
|
||||||
->on('inventories.product_id', '=', 'ss.product_id');
|
->on('inventories.product_id', '=', 'ss.product_id');
|
||||||
})
|
})
|
||||||
->whereNull('inventories.deleted_at')
|
->whereNull('inventories.deleted_at');
|
||||||
->select([
|
|
||||||
|
// 決定是否啟用聚合顯示 (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.id',
|
||||||
'inventories.warehouse_id',
|
'inventories.warehouse_id',
|
||||||
'inventories.product_id',
|
'inventories.product_id',
|
||||||
@@ -256,13 +285,13 @@ class InventoryService implements InventoryServiceInterface
|
|||||||
'inventories.expiry_date',
|
'inventories.expiry_date',
|
||||||
'inventories.location',
|
'inventories.location',
|
||||||
'inventories.quality_status',
|
'inventories.quality_status',
|
||||||
'inventories.arrival_date',
|
|
||||||
'products.code as product_code',
|
'products.code as product_code',
|
||||||
'products.name as product_name',
|
'products.name as product_name',
|
||||||
'categories.name as category_name',
|
'categories.name as category_name',
|
||||||
'warehouses.name as warehouse_name',
|
'warehouses.name as warehouse_name',
|
||||||
'ss.safety_stock',
|
'ss.safety_stock',
|
||||||
]);
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
// 篩選:倉庫
|
// 篩選:倉庫
|
||||||
if (!empty($filters['warehouse_id'])) {
|
if (!empty($filters['warehouse_id'])) {
|
||||||
@@ -386,7 +415,12 @@ class InventoryService implements InventoryServiceInterface
|
|||||||
$paginated = $query->paginate($perPage)->withQueryString();
|
$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)
|
$lastIn = \App\Modules\Inventory\Models\InventoryTransaction::where('inventory_id', $item->id)
|
||||||
->where('type', '入庫')
|
->where('type', '入庫')
|
||||||
->orderByDesc('actual_time')
|
->orderByDesc('actual_time')
|
||||||
@@ -396,8 +430,9 @@ class InventoryService implements InventoryServiceInterface
|
|||||||
->where('type', '出庫')
|
->where('type', '出庫')
|
||||||
->orderByDesc('actual_time')
|
->orderByDesc('actual_time')
|
||||||
->value('actual_time');
|
->value('actual_time');
|
||||||
|
}
|
||||||
|
|
||||||
// 計算狀態 (明細表格依然呈現該批次的狀態)
|
// 計算狀態
|
||||||
$statuses = [];
|
$statuses = [];
|
||||||
if ($item->quantity < 0) {
|
if ($item->quantity < 0) {
|
||||||
$statuses[] = 'negative';
|
$statuses[] = 'negative';
|
||||||
@@ -427,10 +462,11 @@ class InventoryService implements InventoryServiceInterface
|
|||||||
'safety_stock' => $item->safety_stock,
|
'safety_stock' => $item->safety_stock,
|
||||||
'expiry_date' => $item->expiry_date ? \Carbon\Carbon::parse($item->expiry_date)->toDateString() : null,
|
'expiry_date' => $item->expiry_date ? \Carbon\Carbon::parse($item->expiry_date)->toDateString() : null,
|
||||||
'location' => $item->location,
|
'location' => $item->location,
|
||||||
'quality_status' => $item->quality_status,
|
'quality_status' => $item->quality_status ?? null,
|
||||||
'last_inbound' => $lastIn ? \Carbon\Carbon::parse($lastIn)->toDateString() : null,
|
'last_inbound' => $lastIn ? \Carbon\Carbon::parse($lastIn)->toDateString() : null,
|
||||||
'last_outbound' => $lastOut ? \Carbon\Carbon::parse($lastOut)->toDateString() : null,
|
'last_outbound' => $lastOut ? \Carbon\Carbon::parse($lastOut)->toDateString() : null,
|
||||||
'statuses' => $statuses,
|
'statuses' => $statuses,
|
||||||
|
'is_grouped' => $isGrouped,
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ interface InventoryItem {
|
|||||||
last_inbound: string | null;
|
last_inbound: string | null;
|
||||||
last_outbound: string | null;
|
last_outbound: string | null;
|
||||||
statuses: string[];
|
statuses: string[];
|
||||||
|
is_grouped?: boolean;
|
||||||
|
location: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PaginationLink {
|
interface PaginationLink {
|
||||||
@@ -491,18 +493,13 @@ export default function StockQueryIndex({
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
{item.warehouse_name}
|
{item.warehouse_name}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-gray-500 text-sm">
|
<TableCell className="text-gray-500 text-sm italic">
|
||||||
{item.batch_number || "—"}
|
{item.batch_number || "—"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-sm text-gray-500">
|
<TableCell className="text-sm text-gray-500 italic">
|
||||||
{item.location || "—"}
|
{item.location || "—"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell
|
<TableCell className={`text-right font-medium ${item.quantity < 0 ? "text-red-600" : ""}`}>
|
||||||
className={`text-right font-medium ${item.quantity < 0
|
|
||||||
? "text-red-600"
|
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{item.quantity}
|
{item.quantity}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right text-gray-500">
|
<TableCell className="text-right text-gray-500">
|
||||||
|
|||||||
Reference in New Issue
Block a user