refactor(inventory): 重構倉庫管理邏輯,移除 is_sellable 欄位並改由類型判定可用庫存

This commit is contained in:
2026-01-27 10:23:49 +08:00
parent 1ed3d6a29d
commit 293358df62
6 changed files with 47 additions and 42 deletions

View File

@@ -26,9 +26,12 @@ class WarehouseController extends Controller
$warehouses = $query->withSum('inventories as book_stock', 'quantity') // 帳面庫存 = 所有庫存總和 $warehouses = $query->withSum('inventories as book_stock', 'quantity') // 帳面庫存 = 所有庫存總和
->withSum(['inventories as available_stock' => function ($query) { ->withSum(['inventories as available_stock' => function ($query) {
// 可用庫存 = 庫存 > 0 且 品質正常 且 (未過期 或 無效期) // 可用庫存 = 庫存 > 0 且 品質正常 且 (未過期 或 無效期) 且 倉庫類型不為瑕疵倉
$query->where('quantity', '>', 0) $query->where('quantity', '>', 0)
->where('quality_status', 'normal') ->where('quality_status', 'normal')
->whereHas('warehouse', function ($q) {
$q->where('type', '!=', \App\Enums\WarehouseType::QUARANTINE);
})
->where(function ($q) { ->where(function ($q) {
$q->whereNull('expiry_date') $q->whereNull('expiry_date')
->orWhere('expiry_date', '>=', now()); ->orWhere('expiry_date', '>=', now());
@@ -38,20 +41,15 @@ class WarehouseController extends Controller
->paginate(10) ->paginate(10)
->withQueryString(); ->withQueryString();
// 修正各倉庫列表中的可用庫存計算:若倉庫不可銷售,則可用庫存為 0 // 移除原本對 is_sellable 的手動修正邏輯,現在由 type 自動過濾
$warehouses->getCollection()->transform(function ($w) {
if (!$w->is_sellable) {
$w->available_stock = 0;
}
return $w;
});
// 計算全域總計 (不分頁) // 計算全域總計 (不分頁)
$totals = [ $totals = [
'available_stock' => \App\Modules\Inventory\Models\Inventory::where('quantity', '>', 0) 'available_stock' => \App\Modules\Inventory\Models\Inventory::where('quantity', '>', 0)
->where('quality_status', 'normal') ->where('quality_status', 'normal')
->whereHas('warehouse', function ($q) { ->whereHas('warehouse', function ($q) {
$q->where('is_sellable', true); $q->where('type', '!=', \App\Enums\WarehouseType::QUARANTINE);
}) })
->where(function ($q) { ->where(function ($q) {
$q->whereNull('expiry_date') $q->whereNull('expiry_date')
@@ -73,7 +71,6 @@ class WarehouseController extends Controller
'name' => 'required|string|max:50', 'name' => 'required|string|max:50',
'address' => 'nullable|string|max:255', 'address' => 'nullable|string|max:255',
'description' => 'nullable|string', 'description' => 'nullable|string',
'is_sellable' => 'nullable|boolean',
'type' => 'required|string', 'type' => 'required|string',
'license_plate' => 'nullable|string|max:20', 'license_plate' => 'nullable|string|max:20',
'driver_name' => 'nullable|string|max:50', 'driver_name' => 'nullable|string|max:50',
@@ -98,7 +95,6 @@ class WarehouseController extends Controller
'name' => 'required|string|max:50', 'name' => 'required|string|max:50',
'address' => 'nullable|string|max:255', 'address' => 'nullable|string|max:255',
'description' => 'nullable|string', 'description' => 'nullable|string',
'is_sellable' => 'nullable|boolean',
'type' => 'required|string', 'type' => 'required|string',
'license_plate' => 'nullable|string|max:20', 'license_plate' => 'nullable|string|max:20',
'driver_name' => 'nullable|string|max:50', 'driver_name' => 'nullable|string|max:50',

View File

@@ -18,13 +18,11 @@ class Warehouse extends Model
'type', 'type',
'address', 'address',
'description', 'description',
'is_sellable',
'license_plate', 'license_plate',
'driver_name', 'driver_name',
]; ];
protected $casts = [ protected $casts = [
'is_sellable' => 'boolean',
'type' => \App\Enums\WarehouseType::class, 'type' => \App\Enums\WarehouseType::class,
]; ];

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('warehouses', function (Blueprint $table) {
$table->dropColumn('is_sellable');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('warehouses', function (Blueprint $table) {
$table->boolean('is_sellable')->default(true)->after('description')->comment('是否可銷售');
});
}
};

View File

@@ -100,12 +100,18 @@ export default function WarehouseCard({
{/* 統計區塊 - 狀態標籤 */} {/* 統計區塊 - 狀態標籤 */}
<div className="space-y-3"> <div className="space-y-3">
{/* 銷售狀態 */} {/* 銷售狀態與可用性說明 */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-sm text-gray-500"></span> <span className="text-sm text-gray-500"></span>
<Badge variant={warehouse.is_sellable ? "default" : "secondary"} className={warehouse.is_sellable ? "bg-green-600" : "bg-gray-400"}> {warehouse.type === 'quarantine' ? (
{warehouse.is_sellable ? "可銷售" : "暫停銷售"} <Badge variant="secondary" className="bg-red-100 text-red-700 border-red-200">
</Badge> </Badge>
) : (
<Badge variant="default" className="bg-green-600">
</Badge>
)}
</div> </div>
{/* 低庫存警告狀態 */} {/* 低庫存警告狀態 */}

View File

@@ -62,7 +62,6 @@ export default function WarehouseDialog({
address: string; address: string;
description: string; description: string;
type: WarehouseType; type: WarehouseType;
is_sellable: boolean;
license_plate: string; license_plate: string;
driver_name: string; driver_name: string;
}>({ }>({
@@ -71,7 +70,6 @@ export default function WarehouseDialog({
address: "", address: "",
description: "", description: "",
type: "standard", type: "standard",
is_sellable: true,
license_plate: "", license_plate: "",
driver_name: "", driver_name: "",
}); });
@@ -86,7 +84,6 @@ export default function WarehouseDialog({
address: warehouse.address || "", address: warehouse.address || "",
description: warehouse.description || "", description: warehouse.description || "",
type: warehouse.type || "standard", type: warehouse.type || "standard",
is_sellable: warehouse.is_sellable ?? true,
license_plate: warehouse.license_plate || "", license_plate: warehouse.license_plate || "",
driver_name: warehouse.driver_name || "", driver_name: warehouse.driver_name || "",
}); });
@@ -97,7 +94,6 @@ export default function WarehouseDialog({
address: "", address: "",
description: "", description: "",
type: "standard", type: "standard",
is_sellable: true,
license_plate: "", license_plate: "",
driver_name: "", driver_name: "",
}); });
@@ -219,25 +215,7 @@ export default function WarehouseDialog({
</div> </div>
)} )}
{/* 銷售設定 */}
<div className="space-y-4">
<div className="border-b pb-2">
<h4 className="text-sm text-gray-700"></h4>
</div>
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="is_sellable"
className="h-4 w-4 rounded border-gray-300 text-primary-main focus:ring-primary-main"
checked={formData.is_sellable}
onChange={(e) => setFormData({ ...formData, is_sellable: e.target.checked })}
/>
<Label htmlFor="is_sellable"></Label>
</div>
<p className="text-xs text-gray-500 ml-6">
POS
</p>
</div>
{/* 區塊 B位置 */} {/* 區塊 B位置 */}
<div className="space-y-4"> <div className="space-y-4">

View File

@@ -25,7 +25,6 @@ export interface Warehouse {
total_quantity?: number; total_quantity?: number;
low_stock_count?: number; low_stock_count?: number;
type?: WarehouseType; type?: WarehouseType;
is_sellable?: boolean;
license_plate?: string; // 車牌號碼 (移動倉) license_plate?: string; // 車牌號碼 (移動倉)
driver_name?: string; // 司機姓名 (移動倉) driver_name?: string; // 司機姓名 (移動倉)
book_stock?: number; book_stock?: number;