更新:優化配方詳情彈窗 UI 與一般修正
This commit is contained in:
@@ -25,6 +25,7 @@ class ProductController extends Controller
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('name', 'like', "%{$search}%")
|
||||
->orWhere('code', 'like', "%{$search}%")
|
||||
->orWhere('barcode', 'like', "%{$search}%")
|
||||
->orWhere('brand', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
@@ -66,6 +67,7 @@ class ProductController extends Controller
|
||||
return (object) [
|
||||
'id' => (string) $product->id,
|
||||
'code' => $product->code,
|
||||
'barcode' => $product->barcode,
|
||||
'name' => $product->name,
|
||||
'categoryId' => $product->category_id,
|
||||
'category' => $product->category ? (object) [
|
||||
@@ -110,6 +112,7 @@ class ProductController extends Controller
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'code' => 'required|string|max:2|unique:products,code',
|
||||
'barcode' => 'required|string|unique:products,barcode',
|
||||
'name' => 'required|string|max:255',
|
||||
'category_id' => 'required|exists:categories,id',
|
||||
'brand' => 'nullable|string|max:255',
|
||||
@@ -123,6 +126,8 @@ class ProductController extends Controller
|
||||
'code.required' => '商品代號為必填',
|
||||
'code.max' => '商品代號最多 2 碼',
|
||||
'code.unique' => '商品代號已存在',
|
||||
'barcode.required' => '條碼編號為必填',
|
||||
'barcode.unique' => '條碼編號已存在',
|
||||
'name.required' => '商品名稱為必填',
|
||||
'category_id.required' => '請選擇分類',
|
||||
'category_id.exists' => '所選分類不存在',
|
||||
@@ -145,6 +150,7 @@ class ProductController extends Controller
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'code' => 'required|string|max:2|unique:products,code,' . $product->id,
|
||||
'barcode' => 'required|string|unique:products,barcode,' . $product->id,
|
||||
'name' => 'required|string|max:255',
|
||||
'category_id' => 'required|exists:categories,id',
|
||||
'brand' => 'nullable|string|max:255',
|
||||
@@ -157,6 +163,8 @@ class ProductController extends Controller
|
||||
'code.required' => '商品代號為必填',
|
||||
'code.max' => '商品代號最多 2 碼',
|
||||
'code.unique' => '商品代號已存在',
|
||||
'barcode.required' => '條碼編號為必填',
|
||||
'barcode.unique' => '條碼編號已存在',
|
||||
'name.required' => '商品名稱為必填',
|
||||
'category_id.required' => '請選擇分類',
|
||||
'category_id.exists' => '所選分類不存在',
|
||||
|
||||
@@ -17,6 +17,7 @@ class Product extends Model
|
||||
|
||||
protected $fillable = [
|
||||
'code',
|
||||
'barcode',
|
||||
'name',
|
||||
'category_id',
|
||||
'brand',
|
||||
|
||||
@@ -8,20 +8,28 @@ use App\Modules\Production\Models\ProductionOrder;
|
||||
use App\Modules\Production\Models\ProductionOrderItem;
|
||||
use App\Modules\Inventory\Contracts\InventoryServiceInterface;
|
||||
use App\Modules\Core\Contracts\CoreServiceInterface;
|
||||
use App\Modules\Procurement\Contracts\ProcurementServiceInterface;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
|
||||
class ProductionOrderController extends Controller
|
||||
{
|
||||
protected $inventoryService;
|
||||
protected $coreService;
|
||||
protected $procurementService;
|
||||
|
||||
public function __construct(InventoryServiceInterface $inventoryService, CoreServiceInterface $coreService)
|
||||
public function __construct(
|
||||
InventoryServiceInterface $inventoryService,
|
||||
CoreServiceInterface $coreService,
|
||||
ProcurementServiceInterface $procurementService
|
||||
)
|
||||
{
|
||||
$this->inventoryService = $inventoryService;
|
||||
$this->coreService = $coreService;
|
||||
$this->procurementService = $procurementService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -37,9 +45,6 @@ class ProductionOrderController extends Controller
|
||||
if ($request->filled('search')) {
|
||||
$search = $request->search;
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('code', 'like', "%{$search}%")
|
||||
->orWhere('output_batch_number', 'like', "%{$search}%");
|
||||
// 若要搜尋產品名稱,現在需先從 Inventory 查出 IDs
|
||||
$q->where('code', 'like', "%{$search}%")
|
||||
->orWhere('output_batch_number', 'like', "%{$search}%");
|
||||
// 若要搜尋產品名稱,現在需先從 Inventory 查出 IDs
|
||||
@@ -205,15 +210,29 @@ class ProductionOrderController extends Controller
|
||||
// 手動水和明細資料
|
||||
$items = $productionOrder->items;
|
||||
$inventoryIds = $items->pluck('inventory_id')->unique()->filter()->toArray();
|
||||
|
||||
// 修正: 移除跨模組關聯 sourcePurchaseOrder.vendor
|
||||
$inventories = $this->inventoryService->getInventoriesByIds(
|
||||
$inventoryIds,
|
||||
['product.baseUnit', 'sourcePurchaseOrder.vendor']
|
||||
['product.baseUnit']
|
||||
)->keyBy('id');
|
||||
|
||||
// 手動載入 Purchase Orders
|
||||
$poIds = $inventories->pluck('source_purchase_order_id')->unique()->filter()->toArray();
|
||||
$purchaseOrders = collect();
|
||||
if (!empty($poIds)) {
|
||||
$purchaseOrders = $this->procurementService->getPurchaseOrdersByIds($poIds, ['vendor'])->keyBy('id');
|
||||
}
|
||||
|
||||
$units = $this->inventoryService->getUnits()->keyBy('id');
|
||||
|
||||
foreach ($items as $item) {
|
||||
$item->inventory = $inventories->get($item->inventory_id);
|
||||
if ($item->inventory) {
|
||||
// 手動掛載 PO
|
||||
$poId = $item->inventory->source_purchase_order_id;
|
||||
$item->inventory->sourcePurchaseOrder = $purchaseOrders->get($poId);
|
||||
}
|
||||
$item->unit = $units->get($item->unit_id);
|
||||
}
|
||||
|
||||
|
||||
@@ -188,4 +188,118 @@ class RecipeController extends Controller
|
||||
$recipe->delete();
|
||||
return redirect()->back()->with('success', '配方已刪除');
|
||||
}
|
||||
|
||||
/**
|
||||
* 獲取配方詳細資料 (API)
|
||||
*/
|
||||
/**
|
||||
* 獲取配方詳細資料 (API)
|
||||
*/
|
||||
public function show(Recipe $recipe)
|
||||
{
|
||||
// Manual Hydration for strict modularity
|
||||
$recipe->product = $this->inventoryService->getProduct($recipe->product_id);
|
||||
|
||||
$items = $recipe->items;
|
||||
$productIds = $items->pluck('product_id')->unique()->toArray();
|
||||
$products = $this->inventoryService->getProductsByIds($productIds)->keyBy('id');
|
||||
$units = $this->inventoryService->getUnits()->keyBy('id');
|
||||
|
||||
foreach ($items as $item) {
|
||||
$item->product = $products->get($item->product_id);
|
||||
$item->unit = $units->get($item->unit_id);
|
||||
}
|
||||
|
||||
return response()->json($recipe);
|
||||
}
|
||||
|
||||
/**
|
||||
* 獲取商品最新有效配方 (API)
|
||||
*/
|
||||
public function getLatestByProduct($productId)
|
||||
{
|
||||
// 放寬條件,只要 product_id 相符就抓最新的
|
||||
$recipe = Recipe::where('product_id', (int)$productId)
|
||||
->orderBy('created_at', 'desc')
|
||||
->first();
|
||||
|
||||
if (!$recipe) {
|
||||
return response()->json(null);
|
||||
}
|
||||
|
||||
// Load items with product info
|
||||
$items = $recipe->items;
|
||||
$productIds = $items->pluck('product_id')->unique()->toArray();
|
||||
$products = $this->inventoryService->getProductsByIds($productIds)->keyBy('id');
|
||||
|
||||
$formattedItems = $items->map(function ($item) use ($products) {
|
||||
$product = $products->get($item->product_id);
|
||||
return [
|
||||
'product_id' => $item->product_id,
|
||||
'product_name' => $product->name ?? '未知商品',
|
||||
'product_code' => $product->code ?? '',
|
||||
'quantity' => $item->quantity,
|
||||
'unit_id' => $item->unit_id,
|
||||
'unit_name' => $product->baseUnit->name ?? '',
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'id' => $recipe->id,
|
||||
'name' => $recipe->name,
|
||||
'code' => $recipe->code,
|
||||
'yield_quantity' => $recipe->yield_quantity,
|
||||
'items' => $formattedItems,
|
||||
]);
|
||||
}
|
||||
/**
|
||||
* 獲取商品所有有效配方列表 (API)
|
||||
*/
|
||||
public function getByProduct($productId)
|
||||
{
|
||||
$recipes = Recipe::where('product_id', (int)$productId)
|
||||
->where('is_active', true)
|
||||
->orderBy('created_at', 'desc')
|
||||
->get();
|
||||
|
||||
if ($recipes->isEmpty()) {
|
||||
return response()->json([]);
|
||||
}
|
||||
|
||||
// 預先載入必要的關聯與數據
|
||||
// 為了效能,我們只在列表顯示基本資訊,詳細 Item 資料等選中後再透過 getLatestByProduct (或是重構為 getDetails) 獲取
|
||||
// 不過為了前端方便,若配方不多,直接回傳完整結構也可以。
|
||||
// 這裡選擇回傳完整結構,因為配方通常不會太多
|
||||
|
||||
$recipes->load('items');
|
||||
|
||||
// 收集所有 recipe items 中的 product ids
|
||||
$allProductIds = $recipes->pluck('items')->flatten()->pluck('product_id')->unique()->toArray();
|
||||
$products = $this->inventoryService->getProductsByIds($allProductIds)->keyBy('id');
|
||||
|
||||
$result = $recipes->map(function ($recipe) use ($products) {
|
||||
$formattedItems = $recipe->items->map(function ($item) use ($products) {
|
||||
$product = $products->get($item->product_id);
|
||||
return [
|
||||
'product_id' => $item->product_id,
|
||||
'product_name' => $product->name ?? '未知商品',
|
||||
'product_code' => $product->code ?? '',
|
||||
'quantity' => $item->quantity,
|
||||
'unit_id' => $item->unit_id,
|
||||
'unit_name' => $product->baseUnit->name ?? '',
|
||||
];
|
||||
});
|
||||
|
||||
return [
|
||||
'id' => $recipe->id,
|
||||
'name' => $recipe->name,
|
||||
'code' => $recipe->code,
|
||||
'yield_quantity' => $recipe->yield_quantity,
|
||||
'items' => $formattedItems,
|
||||
'created_at' => $recipe->created_at->toIso8601String(),
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json($result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,5 +27,13 @@ class RecipeItem extends Model
|
||||
return $this->belongsTo(Recipe::class);
|
||||
}
|
||||
|
||||
public function product()
|
||||
{
|
||||
return $this->belongsTo(\App\Modules\Inventory\Models\Product::class);
|
||||
}
|
||||
|
||||
public function unit()
|
||||
{
|
||||
return $this->belongsTo(\App\Modules\Inventory\Models\Unit::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,4 +29,10 @@ Route::middleware('auth')->group(function () {
|
||||
Route::get('/api/production/warehouses/{warehouse}/inventories', [ProductionOrderController::class, 'getWarehouseInventories'])
|
||||
->middleware('permission:production_orders.create')
|
||||
->name('api.production.warehouses.inventories');
|
||||
|
||||
Route::get('/api/production/recipes/latest-by-product/{productId}', [RecipeController::class, 'getLatestByProduct'])
|
||||
->name('api.production.recipes.latest-by-product');
|
||||
|
||||
Route::get('/api/production/recipes/by-product/{productId}', [RecipeController::class, 'getByProduct'])
|
||||
->name('api.production.recipes.by-product');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user