inventoryService = $inventoryService; $this->coreService = $coreService; } public function index(Request $request) { // 1. 從關聯中移除 'warehouse' 與 'user' $query = PurchaseOrder::with(['vendor']); // 搜尋 if ($request->search) { $query->where(function($q) use ($request) { $q->where('code', 'like', "%{$request->search}%") ->orWhereHas('vendor', function($vq) use ($request) { $vq->where('name', 'like', "%{$request->search}%"); }); }); } // 篩選 if ($request->status && $request->status !== 'all') { $query->where('status', $request->status); } if ($request->warehouse_id && $request->warehouse_id !== 'all') { $query->where('warehouse_id', $request->warehouse_id); } // 日期範圍 if ($request->date_start) { $query->whereDate('created_at', '>=', $request->date_start); } if ($request->date_end) { $query->whereDate('created_at', '<=', $request->date_end); } // 排序 $sortField = $request->sort_field ?? 'id'; $sortDirection = $request->sort_direction ?? 'desc'; $allowedSortFields = ['id', 'code', 'status', 'total_amount', 'created_at', 'expected_delivery_date']; if (in_array($sortField, $allowedSortFields)) { $query->orderBy($sortField, $sortDirection); } $perPage = $request->input('per_page', 10); $orders = $query->paginate($perPage)->withQueryString(); // 2. 手動注入倉庫與使用者資料 $warehouses = $this->inventoryService->getAllWarehouses(); $userIds = $orders->getCollection()->pluck('user_id')->unique()->toArray(); $users = $this->coreService->getUsersByIds($userIds)->keyBy('id'); $orders->getCollection()->transform(function ($order) use ($warehouses, $users) { // 水和倉庫 $warehouse = $warehouses->firstWhere('id', $order->warehouse_id); $order->setRelation('warehouse', $warehouse); // 水和使用者 $user = $users->get($order->user_id); $order->setRelation('user', $user); // 轉換為前端期望的格式 (camelCase) return (object) [ 'id' => (string) $order->id, 'poNumber' => $order->code, 'supplierId' => (string) $order->vendor_id, 'supplierName' => $order->vendor?->name ?? 'Unknown', 'orderDate' => $order->order_date?->format('Y-m-d'), // 新增 'expectedDate' => $order->expected_delivery_date?->toISOString(), 'status' => $order->status, 'totalAmount' => (float) $order->total_amount, 'taxAmount' => (float) $order->tax_amount, 'grandTotal' => (float) $order->grand_total, 'createdAt' => $order->created_at->toISOString(), 'createdBy' => $user?->name ?? 'System', 'warehouse_id' => (int) $order->warehouse_id, 'warehouse_name' => $warehouse?->name ?? 'Unknown', 'remark' => $order->remark, ]; }); return Inertia::render('PurchaseOrder/Index', [ 'orders' => $orders, 'filters' => $request->only(['search', 'status', 'warehouse_id', 'date_start', 'date_end', 'sort_field', 'sort_direction', 'per_page']), 'warehouses' => $warehouses->map(fn($w)=>(object)['id'=>$w->id, 'name'=>$w->name]), ]); } public function create() { // 1. 獲取廠商(無關聯) $vendors = Vendor::all(); // 2. 手動注入:獲取 Pivot 資料 $vendorIds = $vendors->pluck('id')->toArray(); $pivots = DB::table('product_vendor')->whereIn('vendor_id', $vendorIds)->get(); $productIds = $pivots->pluck('product_id')->unique()->toArray(); // 3. 從服務獲取商品 $products = $this->inventoryService->getProductsByIds($productIds)->keyBy('id'); // 4. 重建前端結構 $vendors = $vendors->map(function ($vendor) use ($pivots, $products) { $vendorProductPivots = $pivots->where('vendor_id', $vendor->id); $commonProducts = $vendorProductPivots->map(function($pivot) use ($products) { $product = $products[$pivot->product_id] ?? null; if (!$product) return null; return [ 'productId' => (string) $product->id, 'productName' => $product->name, 'base_unit_id' => $product->base_unit_id, 'base_unit_name' => $product->baseUnit?->name, 'large_unit_id' => $product->large_unit_id, 'large_unit_name' => $product->largeUnit?->name, 'purchase_unit_id' => $product->purchase_unit_id, 'conversion_rate' => (float) $product->conversion_rate, 'lastPrice' => (float) $pivot->last_price, ]; })->filter()->values(); return [ 'id' => (string) $vendor->id, 'name' => $vendor->name, 'commonProducts' => $commonProducts ]; }); $warehouses = $this->inventoryService->getAllWarehouses()->map(function ($w) { return [ 'id' => (string) $w->id, 'name' => $w->name, ]; }); return Inertia::render('PurchaseOrder/Create', [ 'suppliers' => $vendors, 'warehouses' => $warehouses, ]); } public function store(Request $request) { $validated = $request->validate([ 'vendor_id' => 'required|exists:vendors,id', 'warehouse_id' => 'required|exists:warehouses,id', 'order_date' => 'required|date', // 新增驗證 'expected_delivery_date' => 'nullable|date', 'remark' => 'nullable|string', 'invoice_number' => ['nullable', 'string', 'max:11', 'regex:/^[A-Z]{2}-\d{8}$/'], 'invoice_date' => 'nullable|date', 'invoice_amount' => 'nullable|numeric|min:0', 'items' => 'required|array|min:1', 'items.*.productId' => 'required|exists:products,id', 'items.*.quantity' => 'required|numeric|min:0.01', 'items.*.subtotal' => 'required|numeric|min:0', // 總金額 'items.*.unitId' => 'nullable|exists:units,id', 'tax_amount' => 'nullable|numeric|min:0', ]); try { DB::beginTransaction(); // 生成單號:YYYYMMDD001 $today = now()->format('Ymd'); $lastOrder = PurchaseOrder::where('code', 'like', $today . '%') ->lockForUpdate() // 鎖定以避免並發衝突 ->orderBy('code', 'desc') ->first(); if ($lastOrder) { // 取得最後 3 碼序號並加 1 $lastSequence = intval(substr($lastOrder->code, -3)); $sequence = str_pad($lastSequence + 1, 3, '0', STR_PAD_LEFT); } else { $sequence = '001'; } $code = $today . $sequence; $totalAmount = 0; foreach ($validated['items'] as $item) { $totalAmount += $item['subtotal']; } // 稅額計算 $taxAmount = isset($validated['tax_amount']) ? $validated['tax_amount'] : round($totalAmount * 0.05, 2); $grandTotal = $totalAmount + $taxAmount; // 確保有一個有效的使用者 ID $userId = auth()->id(); if (!$userId) { $user = $this->coreService->ensureSystemUserExists(); $userId = $user->id; } $order = PurchaseOrder::create([ 'code' => $code, 'vendor_id' => $validated['vendor_id'], 'warehouse_id' => $validated['warehouse_id'], 'user_id' => $userId, 'status' => 'draft', 'order_date' => $validated['order_date'], // 新增 'expected_delivery_date' => $validated['expected_delivery_date'], 'total_amount' => $totalAmount, 'tax_amount' => $taxAmount, 'grand_total' => $grandTotal, 'remark' => $validated['remark'], 'invoice_number' => $validated['invoice_number'] ?? null, 'invoice_date' => $validated['invoice_date'] ?? null, 'invoice_amount' => $validated['invoice_amount'] ?? null, ]); foreach ($validated['items'] as $item) { // 反算單價 $unitPrice = $item['quantity'] > 0 ? $item['subtotal'] / $item['quantity'] : 0; $order->items()->create([ 'product_id' => $item['productId'], 'quantity' => $item['quantity'], 'unit_id' => $item['unitId'] ?? null, 'unit_price' => $unitPrice, 'subtotal' => $item['subtotal'], ]); } DB::commit(); return redirect()->route('purchase-orders.index')->with('success', '採購單已成功建立'); } catch (\Exception $e) { DB::rollBack(); return back()->withErrors(['error' => '建立失敗:' . $e->getMessage()]); } } public function show($id) { $order = PurchaseOrder::with(['vendor', 'items'])->findOrFail($id); // 手動注入 $order->setRelation('warehouse', $this->inventoryService->getWarehouse($order->warehouse_id)); $order->setRelation('user', $this->coreService->getUser($order->user_id)); $productIds = $order->items->pluck('product_id')->unique()->toArray(); $products = $this->inventoryService->getProductsByIds($productIds)->keyBy('id'); $formattedItems = $order->items->map(function ($item) use ($order, $products) { $product = $products[$item->product_id] ?? null; return (object) [ 'productId' => (string) $item->product_id, 'productName' => $product?->name ?? 'Unknown', 'quantity' => (float) $item->quantity, 'unitId' => $item->unit_id, 'base_unit_id' => $product?->base_unit_id, 'base_unit_name' => $product?->baseUnit?->name, 'large_unit_id' => $product?->large_unit_id, 'large_unit_name' => $product?->largeUnit?->name, 'purchase_unit_id' => $product?->purchase_unit_id, 'conversion_rate' => (float) ($product?->conversion_rate ?? 1), 'selectedUnit' => ($item->unit_id && $product?->large_unit_id && $item->unit_id == $product->large_unit_id) ? 'large' : 'base', 'unitPrice' => (float) $item->unit_price, 'previousPrice' => (float) (DB::table('product_vendor')->where('vendor_id', $order->vendor_id)->where('product_id', $item->product_id)->value('last_price') ?? 0), 'subtotal' => (float) $item->subtotal, ]; }); $formattedOrder = (object) [ 'id' => (string) $order->id, 'poNumber' => $order->code, 'supplierId' => (string) $order->vendor_id, 'supplierName' => $order->vendor?->name ?? 'Unknown', 'orderDate' => $order->order_date?->format('Y-m-d'), // 新增 'expectedDate' => $order->expected_delivery_date?->toISOString(), 'status' => $order->status, 'items' => $formattedItems, 'totalAmount' => (float) $order->total_amount, 'taxAmount' => (float) $order->tax_amount, 'grandTotal' => (float) $order->grand_total, 'createdAt' => $order->created_at->toISOString(), 'createdBy' => $order->user?->name ?? 'System', 'warehouse_id' => (int) $order->warehouse_id, 'warehouse_name' => $order->warehouse?->name ?? 'Unknown', 'remark' => $order->remark, 'invoiceNumber' => $order->invoice_number, 'invoiceDate' => $order->invoice_date, 'invoiceAmount' => (float) $order->invoice_amount, ]; return Inertia::render('PurchaseOrder/Show', [ 'order' => $formattedOrder ]); } public function edit($id) { // 1. 獲取訂單 $order = PurchaseOrder::with(['items'])->findOrFail($id); // 2. 獲取廠商與商品(與 create 邏輯一致) $vendors = Vendor::all(); $vendorIds = $vendors->pluck('id')->toArray(); $pivots = DB::table('product_vendor')->whereIn('vendor_id', $vendorIds)->get(); $productIds = $pivots->pluck('product_id')->unique()->toArray(); $products = $this->inventoryService->getProductsByIds($productIds)->keyBy('id'); $vendors = $vendors->map(function ($vendor) use ($pivots, $products) { $vendorProductPivots = $pivots->where('vendor_id', $vendor->id); $commonProducts = $vendorProductPivots->map(function($pivot) use ($products) { $product = $products[$pivot->product_id] ?? null; if (!$product) return null; return [ 'productId' => (string) $product->id, 'productName' => $product->name, 'base_unit_id' => $product->base_unit_id, 'base_unit_name' => $product->baseUnit?->name, 'large_unit_id' => $product->large_unit_id, 'large_unit_name' => $product->largeUnit?->name, 'purchase_unit_id' => $product->purchase_unit_id, 'conversion_rate' => (float) $product->conversion_rate, 'lastPrice' => (float) $pivot->last_price, ]; })->filter()->values(); return [ 'id' => (string) $vendor->id, 'name' => $vendor->name, 'commonProducts' => $commonProducts ]; }); // 3. 獲取倉庫 $warehouses = $this->inventoryService->getAllWarehouses()->map(function ($w) { return [ 'id' => (string) $w->id, 'name' => $w->name, ]; }); // 4. 注入訂單項目特定資料 // 2. 注入訂單項目 $itemProductIds = $order->items->pluck('product_id')->toArray(); $itemProducts = $this->inventoryService->getProductsByIds($itemProductIds)->keyBy('id'); $vendorId = $order->vendor_id; $formattedItems = $order->items->map(function ($item) use ($vendorId, $itemProducts) { $product = $itemProducts[$item->product_id] ?? null; return (object) [ 'productId' => (string) $item->product_id, 'productName' => $product?->name ?? 'Unknown', 'quantity' => (float) $item->quantity, 'unitId' => $item->unit_id, 'base_unit_id' => $product?->base_unit_id, 'base_unit_name' => $product?->baseUnit?->name, 'large_unit_id' => $product?->large_unit_id, 'large_unit_name' => $product?->largeUnit?->name, 'conversion_rate' => (float) ($product?->conversion_rate ?? 1), 'selectedUnit' => ($item->unit_id && $product?->large_unit_id && $item->unit_id == $product->large_unit_id) ? 'large' : 'base', 'unitPrice' => (float) $item->unit_price, 'previousPrice' => (float) (DB::table('product_vendor')->where('vendor_id', $vendorId)->where('product_id', $item->product_id)->value('last_price') ?? 0), 'subtotal' => (float) $item->subtotal, ]; }); $formattedOrder = (object) [ 'id' => (string) $order->id, 'poNumber' => $order->code, 'supplierId' => (string) $order->vendor_id, 'warehouse_id' => (int) $order->warehouse_id, 'orderDate' => $order->order_date?->format('Y-m-d'), // 新增 'expectedDate' => $order->expected_delivery_date?->format('Y-m-d'), 'status' => $order->status, 'items' => $formattedItems, 'remark' => $order->remark, 'invoiceNumber' => $order->invoice_number, 'invoiceDate' => $order->invoice_date, 'invoiceAmount' => (float) $order->invoice_amount, 'taxAmount' => (float) $order->tax_amount, ]; return Inertia::render('PurchaseOrder/Create', [ 'order' => $formattedOrder, 'suppliers' => $vendors, 'warehouses' => $warehouses, ]); } public function update(Request $request, $id) { $order = PurchaseOrder::findOrFail($id); $validated = $request->validate([ 'vendor_id' => 'required|exists:vendors,id', 'warehouse_id' => 'required|exists:warehouses,id', 'order_date' => 'required|date', // 新增驗證 'expected_delivery_date' => 'nullable|date', 'remark' => 'nullable|string', 'status' => 'required|string|in:draft,pending,processing,shipping,confirming,completed,cancelled', 'invoice_number' => ['nullable', 'string', 'max:11', 'regex:/^[A-Z]{2}-\d{8}$/'], 'invoice_date' => 'nullable|date', 'invoice_amount' => 'nullable|numeric|min:0', 'items' => 'required|array|min:1', 'items.*.productId' => 'required|exists:products,id', 'items.*.quantity' => 'required|numeric|min:0.01', 'items.*.subtotal' => 'required|numeric|min:0', // 總金額 'items.*.unitId' => 'nullable|exists:units,id', // 允許 tax_amount 和 taxAmount 以保持相容性 'tax_amount' => 'nullable|numeric|min:0', 'taxAmount' => 'nullable|numeric|min:0', ]); try { DB::beginTransaction(); $totalAmount = 0; foreach ($validated['items'] as $item) { $totalAmount += $item['subtotal']; } // 稅額計算(處理兩個鍵) $inputTax = $validated['tax_amount'] ?? $validated['taxAmount'] ?? null; $taxAmount = !is_null($inputTax) ? $inputTax : round($totalAmount * 0.05, 2); $grandTotal = $totalAmount + $taxAmount; // 1. 填充屬性但暫不儲存以捕捉變更 $order->fill([ 'vendor_id' => $validated['vendor_id'], 'warehouse_id' => $validated['warehouse_id'], 'order_date' => $validated['order_date'], // 新增 'expected_delivery_date' => $validated['expected_delivery_date'], 'total_amount' => $totalAmount, 'tax_amount' => $taxAmount, 'grand_total' => $grandTotal, 'remark' => $validated['remark'], 'status' => $validated['status'], 'invoice_number' => $validated['invoice_number'] ?? null, 'invoice_date' => $validated['invoice_date'] ?? null, 'invoice_amount' => $validated['invoice_amount'] ?? null, ]); // 捕捉變更屬性以進行手動記錄 $dirty = $order->getDirty(); $oldAttributes = []; $newAttributes = []; foreach ($dirty as $key => $value) { $oldAttributes[$key] = $order->getOriginal($key); $newAttributes[$key] = $value; } // 儲存但不觸發事件(防止重複記錄) $order->saveQuietly(); // 2. 捕捉包含商品名稱的舊項目以進行比對 $oldItems = $order->items()->with('product', 'unit')->get()->map(function($item) { return [ 'id' => $item->id, 'product_id' => $item->product_id, 'product_name' => $item->product?->name, 'quantity' => (float) $item->quantity, 'unit_id' => $item->unit_id, 'unit_name' => $item->unit?->name, 'subtotal' => (float) $item->subtotal, ]; })->keyBy('product_id'); // 同步項目(原始邏輯) $order->items()->delete(); $newItemsData = []; foreach ($validated['items'] as $item) { // 反算單價 $unitPrice = $item['quantity'] > 0 ? $item['subtotal'] / $item['quantity'] : 0; $newItem = $order->items()->create([ 'product_id' => $item['productId'], 'quantity' => $item['quantity'], 'unit_id' => $item['unitId'] ?? null, 'unit_price' => $unitPrice, 'subtotal' => $item['subtotal'], ]); $newItemsData[] = $newItem; } // 3. 計算項目差異 $itemDiffs = [ 'added' => [], 'removed' => [], 'updated' => [], ]; // 重新獲取新項目以確保擁有最新的關聯 $newItemsFormatted = $order->items()->with('product', 'unit')->get()->map(function($item) { return [ 'product_id' => $item->product_id, 'product_name' => $item->product?->name, 'quantity' => (float) $item->quantity, 'unit_id' => $item->unit_id, 'unit_name' => $item->unit?->name, 'subtotal' => (float) $item->subtotal, ]; })->keyBy('product_id'); // 找出已移除的項目 foreach ($oldItems as $productId => $oldItem) { if (!$newItemsFormatted->has($productId)) { $itemDiffs['removed'][] = $oldItem; } } // 找出新增和更新的項目 foreach ($newItemsFormatted as $productId => $newItem) { if (!$oldItems->has($productId)) { $itemDiffs['added'][] = $newItem; } else { $oldItem = $oldItems[$productId]; // 比對欄位 if ( $oldItem['quantity'] != $newItem['quantity'] || $oldItem['unit_id'] != $newItem['unit_id'] || $oldItem['subtotal'] != $newItem['subtotal'] ) { $itemDiffs['updated'][] = [ 'product_name' => $newItem['product_name'], 'old' => [ 'quantity' => $oldItem['quantity'], 'unit_name' => $oldItem['unit_name'], 'subtotal' => $oldItem['subtotal'], ], 'new' => [ 'quantity' => $newItem['quantity'], 'unit_name' => $newItem['unit_name'], 'subtotal' => $newItem['subtotal'], ] ]; } } } // 4. 手動記錄活動(單一整合記錄) // 如果有屬性變更或項目變更則記錄 if (!empty($newAttributes) || !empty($itemDiffs['added']) || !empty($itemDiffs['removed']) || !empty($itemDiffs['updated'])) { activity() ->performedOn($order) ->causedBy(auth()->user()) ->event('updated') ->withProperties([ 'attributes' => $newAttributes, 'old' => $oldAttributes, 'items_diff' => $itemDiffs, 'snapshot' => [ 'po_number' => $order->code, 'vendor_name' => $order->vendor?->name, 'warehouse_name' => $order->warehouse?->name, 'user_name' => $order->user?->name, ] ]) ->log('updated'); } DB::commit(); return redirect()->route('purchase-orders.index')->with('success', '採購單已更新'); } catch (\Exception $e) { DB::rollBack(); return back()->withErrors(['error' => '更新失敗:' . $e->getMessage()]); } } public function destroy($id) { try { DB::beginTransaction(); $order = PurchaseOrder::with(['items'])->findOrFail($id); // 為記錄注入資料 $productIds = $order->items->pluck('product_id')->unique()->toArray(); $products = $this->inventoryService->getProductsByIds($productIds)->keyBy('id'); // 捕捉項目以進行記錄 $items = $order->items->map(function ($item) use ($products) { $product = $products[$item->product_id] ?? null; return [ 'product_name' => $product?->name ?? 'Unknown', 'quantity' => floatval($item->quantity), 'unit_name' => 'N/A', 'subtotal' => floatval($item->subtotal), ]; })->toArray(); // 手動記錄包含項目的刪除操作 activity() ->performedOn($order) ->causedBy(auth()->user()) ->event('deleted') ->withProperties([ 'attributes' => $order->getAttributes(), 'items_diff' => [ 'added' => [], 'removed' => $items, 'updated' => [], ], 'snapshot' => [ 'po_number' => $order->code, 'vendor_name' => $order->vendor?->name, 'warehouse_name' => $order->warehouse?->name, 'user_name' => $order->user?->name, ] ]) ->log('deleted'); // 對此操作停用自動記錄 $order->disableLogging(); // 先刪除關聯項目 $order->items()->delete(); $order->delete(); DB::commit(); return redirect()->route('purchase-orders.index')->with('success', '採購單已刪除'); } catch (\Exception $e) { DB::rollBack(); return back()->withErrors(['error' => '刪除失敗:' . $e->getMessage()]); } } }