2026-01-26 14:59:24 +08:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Modules\Production\Controllers;
|
|
|
|
|
|
|
|
|
|
use App\Http\Controllers\Controller;
|
|
|
|
|
use App\Modules\Production\Models\Recipe;
|
|
|
|
|
use App\Modules\Production\Models\RecipeItem;
|
|
|
|
|
use App\Modules\Inventory\Contracts\InventoryServiceInterface;
|
|
|
|
|
use Illuminate\Http\Request;
|
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
|
use Inertia\Inertia;
|
|
|
|
|
use Inertia\Response;
|
|
|
|
|
|
|
|
|
|
class RecipeController extends Controller
|
|
|
|
|
{
|
|
|
|
|
protected $inventoryService;
|
|
|
|
|
|
|
|
|
|
public function __construct(InventoryServiceInterface $inventoryService)
|
|
|
|
|
{
|
|
|
|
|
$this->inventoryService = $inventoryService;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 配方列表
|
|
|
|
|
*/
|
|
|
|
|
public function index(Request $request): Response
|
|
|
|
|
{
|
|
|
|
|
$query = Recipe::query();
|
|
|
|
|
|
|
|
|
|
if ($request->filled('search')) {
|
|
|
|
|
$search = $request->search;
|
|
|
|
|
$query->where(function ($q) use ($search) {
|
|
|
|
|
$q->where('code', 'like', "%{$search}%")
|
|
|
|
|
->orWhere('name', 'like', "%{$search}%");
|
|
|
|
|
|
|
|
|
|
$productIds = $this->inventoryService->getProductsByName($search)->pluck('id');
|
|
|
|
|
$q->orWhereIn('product_id', $productIds);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$query->orderBy($request->input('sort_field', 'created_at'), $request->input('sort_direction', 'desc'));
|
|
|
|
|
|
|
|
|
|
$recipes = $query->paginate($request->input('per_page', 10))->withQueryString();
|
|
|
|
|
|
|
|
|
|
// Manual Hydration
|
|
|
|
|
$productIds = $recipes->pluck('product_id')->unique()->filter()->toArray();
|
|
|
|
|
$products = $this->inventoryService->getProductsByIds($productIds)->keyBy('id');
|
|
|
|
|
|
|
|
|
|
$recipes->getCollection()->transform(function ($recipe) use ($products) {
|
|
|
|
|
$recipe->product = $products->get($recipe->product_id);
|
|
|
|
|
return $recipe;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return Inertia::render('Production/Recipe/Index', [
|
|
|
|
|
'recipes' => $recipes,
|
|
|
|
|
'filters' => $request->only(['search', 'per_page', 'sort_field', 'sort_direction']),
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 新增配方表單
|
|
|
|
|
*/
|
|
|
|
|
public function create(): Response
|
|
|
|
|
{
|
|
|
|
|
return Inertia::render('Production/Recipe/Create', [
|
|
|
|
|
'products' => $this->inventoryService->getAllProducts(),
|
|
|
|
|
'units' => $this->inventoryService->getUnits(),
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 儲存配方
|
|
|
|
|
*/
|
|
|
|
|
public function store(Request $request)
|
|
|
|
|
{
|
|
|
|
|
$validated = $request->validate([
|
|
|
|
|
'product_id' => 'required|exists:products,id',
|
|
|
|
|
'code' => 'required|string|max:50|unique:recipes,code',
|
|
|
|
|
'name' => 'required|string|max:255',
|
|
|
|
|
'description' => 'nullable|string',
|
|
|
|
|
'yield_quantity' => 'required|numeric|min:0.01',
|
|
|
|
|
'items' => 'required|array|min:1',
|
|
|
|
|
'items.*.product_id' => 'required|exists:products,id',
|
|
|
|
|
'items.*.quantity' => 'required|numeric|min:0.0001',
|
|
|
|
|
'items.*.unit_id' => 'nullable|exists:units,id',
|
|
|
|
|
'items.*.remark' => 'nullable|string',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
DB::transaction(function () use ($validated) {
|
|
|
|
|
$recipe = Recipe::create([
|
|
|
|
|
'product_id' => $validated['product_id'],
|
|
|
|
|
'code' => $validated['code'],
|
|
|
|
|
'name' => $validated['name'],
|
|
|
|
|
'description' => $validated['description'],
|
|
|
|
|
'yield_quantity' => $validated['yield_quantity'],
|
|
|
|
|
'is_active' => true,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
foreach ($validated['items'] as $item) {
|
|
|
|
|
RecipeItem::create([
|
|
|
|
|
'recipe_id' => $recipe->id,
|
|
|
|
|
'product_id' => $item['product_id'],
|
|
|
|
|
'quantity' => $item['quantity'],
|
|
|
|
|
'unit_id' => $item['unit_id'],
|
|
|
|
|
'remark' => $item['remark'],
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return redirect()->route('recipes.index')->with('success', '配方已建立');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 編輯配方表單
|
|
|
|
|
*/
|
|
|
|
|
public function edit(Recipe $recipe): Response
|
|
|
|
|
{
|
|
|
|
|
// Hydrate Product
|
|
|
|
|
$recipe->product = $this->inventoryService->getProduct($recipe->product_id);
|
|
|
|
|
|
|
|
|
|
// Load items with details
|
|
|
|
|
$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 Inertia::render('Production/Recipe/Edit', [
|
|
|
|
|
'recipe' => $recipe,
|
|
|
|
|
'products' => $this->inventoryService->getAllProducts(),
|
|
|
|
|
'units' => $this->inventoryService->getUnits(),
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 更新配方
|
|
|
|
|
*/
|
|
|
|
|
public function update(Request $request, Recipe $recipe)
|
|
|
|
|
{
|
|
|
|
|
$validated = $request->validate([
|
|
|
|
|
'product_id' => 'required|exists:products,id',
|
|
|
|
|
'code' => 'required|string|max:50|unique:recipes,code,' . $recipe->id,
|
|
|
|
|
'name' => 'required|string|max:255',
|
|
|
|
|
'description' => 'nullable|string',
|
|
|
|
|
'yield_quantity' => 'required|numeric|min:0.01',
|
|
|
|
|
'items' => 'required|array|min:1',
|
|
|
|
|
'items.*.product_id' => 'required|exists:products,id',
|
|
|
|
|
'items.*.quantity' => 'required|numeric|min:0.0001',
|
|
|
|
|
'items.*.unit_id' => 'nullable|exists:units,id',
|
|
|
|
|
'items.*.remark' => 'nullable|string',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
DB::transaction(function () use ($validated, $recipe) {
|
|
|
|
|
$recipe->update([
|
|
|
|
|
'product_id' => $validated['product_id'],
|
|
|
|
|
'code' => $validated['code'],
|
|
|
|
|
'name' => $validated['name'],
|
|
|
|
|
'description' => $validated['description'],
|
|
|
|
|
'yield_quantity' => $validated['yield_quantity'],
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// Sync items (Delete all and recreate)
|
|
|
|
|
$recipe->items()->delete();
|
|
|
|
|
|
|
|
|
|
foreach ($validated['items'] as $item) {
|
|
|
|
|
RecipeItem::create([
|
|
|
|
|
'recipe_id' => $recipe->id,
|
|
|
|
|
'product_id' => $item['product_id'],
|
|
|
|
|
'quantity' => $item['quantity'],
|
|
|
|
|
'unit_id' => $item['unit_id'],
|
|
|
|
|
'remark' => $item['remark'],
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return redirect()->route('recipes.index')->with('success', '配方已更新');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 刪除配方
|
|
|
|
|
*/
|
|
|
|
|
public function destroy(Recipe $recipe)
|
|
|
|
|
{
|
|
|
|
|
$recipe->delete();
|
|
|
|
|
return redirect()->back()->with('success', '配方已刪除');
|
|
|
|
|
}
|
2026-01-29 16:13:56 +08:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 獲取配方詳細資料 (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);
|
|
|
|
|
}
|
2026-01-26 14:59:24 +08:00
|
|
|
}
|