inventoryService = $inventoryService; $this->coreService = $coreService; $this->procurementService = $procurementService; } /** * 生產工單列表 */ public function index(Request $request): Response { // 不再使用 with(),避免跨模組 Eager Loading $query = ProductionOrder::query(); // 搜尋 (此處 orWhereHas 暫時保留,因 Laravel query builder 仍可作用於資料表層級, // 但實務上若模組完全隔離,應考慮搜尋引擎或 ID 預選) 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 $productIds = $this->inventoryService->getProductsByName($search)->pluck('id'); $q->orWhereIn('product_id', $productIds); }); } // 狀態篩選 if ($request->filled('status') && $request->status !== 'all') { $query->where('status', $request->status); } // 排除軟刪除 $query->orderBy($request->input('sort_field', 'created_at'), $request->input('sort_direction', 'desc')); // 分頁 $perPage = $request->input('per_page', 10); $productionOrders = $query->paginate($perPage)->withQueryString(); // --- 手動資料水和 (Manual Hydration) --- $productIds = $productionOrders->pluck('product_id')->unique()->filter()->toArray(); $warehouseIds = $productionOrders->pluck('warehouse_id')->unique()->filter()->toArray(); $userIds = $productionOrders->pluck('user_id')->unique()->filter()->toArray(); $products = $this->inventoryService->getProductsByIds($productIds)->keyBy('id'); $warehouses = $this->inventoryService->getAllWarehouses()->whereIn('id', $warehouseIds)->keyBy('id'); $users = $this->coreService->getUsersByIds($userIds)->keyBy('id'); $productionOrders->getCollection()->transform(function ($order) use ($products, $warehouses, $users) { $order->product = $products->get($order->product_id); $order->warehouse = $warehouses->get($order->warehouse_id); $order->user = $users->get($order->user_id); return $order; }); return Inertia::render('Production/Index', [ 'productionOrders' => $productionOrders, 'filters' => $request->only(['search', 'status', 'per_page', 'sort_field', 'sort_direction']), ]); } /** * 新增生產單表單 */ public function create(): Response { return Inertia::render('Production/Create', [ 'products' => $this->inventoryService->getAllProducts(), 'warehouses' => $this->inventoryService->getAllWarehouses(), 'units' => $this->inventoryService->getUnits(), ]); } /** * 儲存生產單(含自動扣料與成品入庫) */ public function store(Request $request) { $status = $request->input('status', 'draft'); $rules = [ 'product_id' => 'required', 'status' => 'nullable|in:draft,completed', 'warehouse_id' => $status === 'completed' ? 'required' : 'nullable', 'output_quantity' => $status === 'completed' ? 'required|numeric|min:0.01' : 'nullable|numeric', 'items' => 'nullable|array', 'items.*.inventory_id' => $status === 'completed' ? 'required' : 'nullable', 'items.*.quantity_used' => $status === 'completed' ? 'required|numeric|min:0.0001' : 'nullable|numeric', ]; $validated = $request->validate($rules); DB::transaction(function () use ($validated, $request, $status) { // 1. 建立生產工單 $productionOrder = ProductionOrder::create([ 'code' => ProductionOrder::generateCode(), 'product_id' => $validated['product_id'], 'warehouse_id' => $validated['warehouse_id'] ?? null, 'output_quantity' => $validated['output_quantity'] ?? 0, 'output_batch_number' => $request->output_batch_number, // 建立時改為選填 'output_box_count' => $request->output_box_count, 'production_date' => $request->production_date, 'expiry_date' => $request->expiry_date, 'user_id' => auth()->id(), 'status' => ProductionOrder::STATUS_DRAFT, // 一律存為草稿 'remark' => $request->remark, ]); activity() ->performedOn($productionOrder) ->causedBy(auth()->user()) ->log('created'); // 2. 處理明細 if (!empty($request->items)) { foreach ($request->items as $item) { ProductionOrderItem::create([ 'production_order_id' => $productionOrder->id, 'inventory_id' => $item['inventory_id'], 'quantity_used' => $item['quantity_used'] ?? 0, 'unit_id' => $item['unit_id'] ?? null, ]); } } }); return redirect()->route('production-orders.index') ->with('success', '生產單草稿已建立'); } /** * 檢視生產單詳情 */ public function show(ProductionOrder $productionOrder): Response { // 手動水和主表資料 $productionOrder->product = $this->inventoryService->getProduct($productionOrder->product_id); if ($productionOrder->product) { $productionOrder->product->base_unit = $this->inventoryService->getUnits()->where('id', $productionOrder->product->base_unit_id)->first(); } $productionOrder->warehouse = $productionOrder->warehouse_id ? $this->inventoryService->getWarehouse($productionOrder->warehouse_id) : null; $productionOrder->user = $this->coreService->getUser($productionOrder->user_id); // 手動水和明細資料 $items = $productionOrder->items; $inventoryIds = $items->pluck('inventory_id')->unique()->filter()->toArray(); // 修正: 移除跨模組關聯 sourcePurchaseOrder.vendor $inventories = $this->inventoryService->getInventoriesByIds( $inventoryIds, ['product.baseUnit', 'warehouse'] )->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); } return Inertia::render('Production/Show', [ 'productionOrder' => $productionOrder, 'warehouses' => $this->inventoryService->getAllWarehouses(), ]); } /** * 取得倉庫內可用庫存 */ public function getWarehouseInventories($warehouseId) { $inventories = $this->inventoryService->getInventoriesByWarehouse($warehouseId); $data = $inventories->map(function ($inv) { return [ 'id' => $inv->id, 'product_id' => $inv->product_id, 'product_name' => $inv->product->name ?? '未知商品', 'product_code' => $inv->product->code ?? '', 'batch_number' => $inv->batch_number, 'box_number' => $inv->box_number, 'quantity' => $inv->quantity, 'arrival_date' => $inv->arrival_date ? $inv->arrival_date->format('Y-m-d') : null, 'expiry_date' => $inv->expiry_date ? $inv->expiry_date->format('Y-m-d') : null, 'unit_name' => $inv->product->baseUnit->name ?? '', 'base_unit_id' => $inv->product->base_unit_id ?? null, 'large_unit_id' => $inv->product->large_unit_id ?? null, 'conversion_rate' => $inv->product->conversion_rate ?? 1, ]; }); return response()->json($data); } /** * 取得商品在各倉庫的庫存分佈 */ public function getProductWarehouses($productId) { $inventories = \App\Modules\Inventory\Models\Inventory::with(['warehouse', 'product.baseUnit']) ->where('product_id', $productId) ->where('quantity', '>', 0) ->get(); $data = $inventories->map(function ($inv) { return [ 'id' => $inv->id, // Inventory ID 'warehouse_id' => $inv->warehouse_id, 'warehouse_name' => $inv->warehouse->name ?? '未知倉庫', 'batch_number' => $inv->batch_number, 'quantity' => $inv->quantity, 'expiry_date' => $inv->expiry_date ? $inv->expiry_date->format('Y-m-d') : null, 'unit_name' => $inv->product->baseUnit->name ?? '', 'base_unit_id' => $inv->product->base_unit_id ?? null, 'conversion_rate' => $inv->product->conversion_rate ?? 1, ]; }); return response()->json($data); } /** * 編輯生產單 */ public function edit(ProductionOrder $productionOrder): Response { if ($productionOrder->status !== 'draft') { return redirect()->route('production-orders.show', $productionOrder->id) ->with('error', '只有草稿狀態的生產單可以編輯'); } // 基本水和 $productionOrder->product = $this->inventoryService->getProduct($productionOrder->product_id); $productionOrder->warehouse = $productionOrder->warehouse_id ? $this->inventoryService->getWarehouse($productionOrder->warehouse_id) : null; // 手動水和明細資料 $items = $productionOrder->items; $inventoryIds = $items->pluck('inventory_id')->unique()->filter()->toArray(); $inventories = $this->inventoryService->getInventoriesByIds( $inventoryIds, ['product.baseUnit'] )->keyBy('id'); $units = $this->inventoryService->getUnits()->keyBy('id'); foreach ($items as $item) { $item->inventory = $inventories->get($item->inventory_id); $item->unit = $units->get($item->unit_id); } return Inertia::render('Production/Edit', [ 'productionOrder' => $productionOrder, 'products' => $this->inventoryService->getAllProducts(), 'warehouses' => $this->inventoryService->getAllWarehouses(), 'units' => $this->inventoryService->getUnits(), ]); } /** * 更新生產單 */ public function update(Request $request, ProductionOrder $productionOrder) { if ($productionOrder->status !== 'draft') { return redirect()->route('production-orders.show', $productionOrder->id) ->with('error', '只有草稿可以修改'); } $status = $request->input('status', 'draft'); // 基礎驗證規則 $rules = [ 'product_id' => 'required', 'remark' => 'nullable|string', 'warehouse_id' => 'nullable', 'output_quantity' => 'nullable|numeric', 'items' => 'nullable|array', 'items.*.inventory_id' => 'required', 'items.*.quantity_used' => 'required|numeric', ]; $validated = $request->validate($rules); DB::transaction(function () use ($validated, $request, $productionOrder) { $productionOrder->update([ 'product_id' => $validated['product_id'], 'warehouse_id' => $validated['warehouse_id'] ?? $productionOrder->warehouse_id, 'output_quantity' => $validated['output_quantity'] ?? 0, 'output_batch_number' => $request->output_batch_number ?? $productionOrder->output_batch_number, 'output_box_count' => $request->output_box_count, 'production_date' => $request->production_date ?? $productionOrder->production_date, 'expiry_date' => $request->expiry_date ?? $productionOrder->expiry_date, 'remark' => $request->remark, ]); activity() ->performedOn($productionOrder) ->causedBy(auth()->user()) ->log('updated'); // 重新建立明細 $productionOrder->items()->delete(); if (!empty($request->items)) { foreach ($request->items as $item) { ProductionOrderItem::create([ 'production_order_id' => $productionOrder->id, 'inventory_id' => $item['inventory_id'], 'quantity_used' => $item['quantity_used'] ?? 0, 'unit_id' => $item['unit_id'] ?? null, ]); } } }); return redirect()->route('production-orders.index') ->with('success', '生產單已更新'); } /** * 更新生產工單狀態 */ public function updateStatus(Request $request, ProductionOrder $productionOrder) { $newStatus = $request->input('status'); if (!$productionOrder->canTransitionTo($newStatus)) { return response()->json(['error' => '不合法的狀態轉移或權限不足'], 403); } DB::transaction(function () use ($newStatus, $productionOrder, $request) { $oldStatus = $productionOrder->status; // 1. 執行特定狀態的業務邏輯 if ($oldStatus === ProductionOrder::STATUS_APPROVED && $newStatus === ProductionOrder::STATUS_IN_PROGRESS) { // 開始製作 -> 扣除原料庫存 $items = $productionOrder->items; foreach ($items as $item) { $this->inventoryService->decreaseInventoryQuantity( $item->inventory_id, $item->quantity_used, "生產單 #{$productionOrder->code} 開始製作 (扣料)", ProductionOrder::class, $productionOrder->id ); } } elseif ($oldStatus === ProductionOrder::STATUS_IN_PROGRESS && $newStatus === ProductionOrder::STATUS_COMPLETED) { // 完成製作 -> 成品入庫 $warehouseId = $request->input('warehouse_id'); // 由前端 Modal 傳來 $batchNumber = $request->input('output_batch_number'); // 由前端 Modal 傳來 $expiryDate = $request->input('expiry_date'); // 由前端 Modal 傳來 if (!$warehouseId) { throw new \Exception('必須選擇入庫倉庫'); } if (!$batchNumber) { throw new \Exception('必須提供成品批號'); } // 更新單據資訊:批號、效期與自動記錄生產日期 $productionOrder->output_batch_number = $batchNumber; $productionOrder->expiry_date = $expiryDate; $productionOrder->production_date = now()->toDateString(); $productionOrder->warehouse_id = $warehouseId; $this->inventoryService->createInventoryRecord([ 'warehouse_id' => $warehouseId, 'product_id' => $productionOrder->product_id, 'quantity' => $productionOrder->output_quantity, 'batch_number' => $batchNumber, 'box_number' => $productionOrder->output_box_count, 'arrival_date' => now()->toDateString(), 'expiry_date' => $expiryDate, 'reason' => "生產單 #{$productionOrder->code} 製作完成 (入庫)", 'reference_type' => ProductionOrder::class, 'reference_id' => $productionOrder->id, ]); } // 2. 更新狀態 $productionOrder->status = $newStatus; $productionOrder->save(); // 3. 紀錄 Activity Log activity() ->performedOn($productionOrder) ->causedBy(auth()->user()) ->withProperties([ 'old_status' => $oldStatus, 'new_status' => $newStatus ]) ->log("status_updated_to_{$newStatus}"); }); return back()->with('success', '狀態已更新'); } /** * 從儲存體中移除指定資源。 */ public function destroy(ProductionOrder $productionOrder) { // 僅允許刪除草稿或已作廢的單據 if (!in_array($productionOrder->status, [ProductionOrder::STATUS_DRAFT, ProductionOrder::STATUS_CANCELLED])) { return redirect()->back()->with('error', '僅有草稿或已作廢的生產單可以刪除'); } DB::transaction(function () use ($productionOrder) { $productionOrder->items()->delete(); $productionOrder->delete(); activity() ->performedOn($productionOrder) ->causedBy(auth()->user()) ->log('deleted'); }); return redirect()->route('production-orders.index')->with('success', '生產單已刪除'); } }