countService = $countService; } public function index(Request $request) { $query = InventoryCountDoc::query() ->with(['createdBy', 'completedBy', 'warehouse']); if ($request->filled('warehouse_id')) { $query->where('warehouse_id', $request->warehouse_id); } if ($request->filled('search')) { $search = $request->search; $query->where(function ($q) use ($search) { $q->where('doc_no', 'like', "%{$search}%") ->orWhere('remarks', 'like', "%{$search}%"); }); } $perPage = $request->input('per_page', 10); if (!in_array($perPage, [10, 20, 50, 100])) { $perPage = 10; } $countQuery = function ($query) { $query->whereNotNull('counted_qty'); }; $docs = $query->withCount(['items', 'items as counted_items_count' => $countQuery]) ->orderByDesc('created_at') ->paginate($perPage) ->withQueryString() ->through(function ($doc) { return [ 'id' => (string) $doc->id, 'doc_no' => $doc->doc_no, 'status' => $doc->status, 'warehouse_name' => $doc->warehouse->name, 'snapshot_date' => $doc->snapshot_date ? $doc->snapshot_date->format('Y-m-d H:i') : '-', 'completed_at' => $doc->completed_at ? $doc->completed_at->format('Y-m-d H:i') : '-', 'created_by' => $doc->createdBy?->name, 'remarks' => $doc->remarks, 'total_items' => $doc->items_count, 'counted_items' => $doc->counted_items_count, ]; }); return Inertia::render('Inventory/Count/Index', [ 'docs' => $docs, 'warehouses' => Warehouse::all()->map(fn($w) => ['id' => (string)$w->id, 'name' => $w->name]), 'filters' => $request->only(['warehouse_id', 'search', 'per_page']), ]); } public function store(Request $request) { $validated = $request->validate([ 'warehouse_id' => 'required|exists:warehouses,id', 'remarks' => 'nullable|string|max:255', ]); $doc = $this->countService->createDoc( $validated['warehouse_id'], $validated['remarks'] ?? null, auth()->id() ); // 自動執行快照 $this->countService->snapshot($doc, false); return redirect()->route('inventory.count.show', [$doc->id]) ->with('success', '已建立盤點單並完成庫存快照'); } public function show(InventoryCountDoc $doc) { $doc->load(['items.product.baseUnit', 'createdBy', 'completedBy', 'warehouse']); // 預先抓取相關的 Inventory 資訊 (主要為了取得效期) $inventoryMap = \App\Modules\Inventory\Models\Inventory::withTrashed() ->where('warehouse_id', $doc->warehouse_id) ->whereIn('product_id', $doc->items->pluck('product_id')) ->whereIn('batch_number', $doc->items->pluck('batch_number')) ->get() ->mapWithKeys(function ($inv) { return [$inv->product_id . '-' . $inv->batch_number => $inv]; }); $docData = [ 'id' => (string) $doc->id, 'doc_no' => $doc->doc_no, 'warehouse_id' => (string) $doc->warehouse_id, 'warehouse_name' => $doc->warehouse->name, 'status' => $doc->status, 'remarks' => $doc->remarks, 'snapshot_date' => $doc->snapshot_date ? $doc->snapshot_date->format('Y-m-d H:i') : null, 'created_by' => $doc->createdBy?->name, 'items' => $doc->items->map(function ($item) use ($inventoryMap) { $key = $item->product_id . '-' . $item->batch_number; $inv = $inventoryMap->get($key); return [ 'id' => (string) $item->id, 'product_name' => $item->product->name, 'product_code' => $item->product->code, 'batch_number' => $item->batch_number, 'expiry_date' => $inv && $inv->expiry_date ? $inv->expiry_date->format('Y-m-d') : null, // 新增效期 'unit' => $item->product->baseUnit?->name, 'system_qty' => (float) $item->system_qty, 'counted_qty' => is_null($item->counted_qty) ? '' : (float) $item->counted_qty, 'diff_qty' => (float) $item->diff_qty, 'notes' => $item->notes, ]; }), ]; return Inertia::render('Inventory/Count/Show', [ 'doc' => $docData, ]); } public function print(InventoryCountDoc $doc) { $doc->load(['items.product.baseUnit', 'createdBy', 'completedBy', 'warehouse']); $docData = [ 'id' => (string) $doc->id, 'doc_no' => $doc->doc_no, 'warehouse_name' => $doc->warehouse->name, 'snapshot_date' => $doc->snapshot_date ? $doc->snapshot_date->format('Y-m-d') : date('Y-m-d'), // Use date only 'created_at' => $doc->created_at->format('Y-m-d'), 'print_date' => date('Y-m-d'), 'created_by' => $doc->createdBy?->name, 'items' => $doc->items->map(function ($item) { return [ 'id' => (string) $item->id, 'product_name' => $item->product->name, 'product_code' => $item->product->code, 'specification' => $item->product->specification, 'unit' => $item->product->baseUnit?->name, 'quantity' => (float) ($item->counted_qty ?? $item->system_qty), // Default to system qty if counted is null, or just counted? User wants "Count Sheet" -> maybe blank if not counted? // Actually, if it's "Completed", we show counted. If it's "Pending", we usually show blank or system. // The 'Show' page logic suggests we show counted_qty. 'counted_qty' => $item->counted_qty, 'notes' => $item->notes, ]; }), ]; return Inertia::render('Inventory/Count/Print', [ 'doc' => $docData, ]); } public function update(Request $request, InventoryCountDoc $doc) { if ($doc->status === 'completed') { return redirect()->back()->with('error', '此盤點單已完成,無法修改'); } $validated = $request->validate([ 'items' => 'array', 'items.*.id' => 'required|exists:inventory_count_items,id', 'items.*.counted_qty' => 'nullable|numeric|min:0', 'items.*.notes' => 'nullable|string', ]); if (isset($validated['items'])) { $this->countService->updateCount($doc, $validated['items']); } // 重新讀取以獲取最新狀態 $doc->refresh(); if ($doc->status === 'completed') { return redirect()->route('inventory.count.index') ->with('success', '盤點完成,單據已自動存檔並完成。'); } return redirect()->back()->with('success', '盤點資料已暫存'); } public function reopen(InventoryCountDoc $doc) { // 權限檢查 (通常僅允許有權限者執行,例如 inventory.adjust) // 注意:前端已經用 保護按鈕,後端這裡最好也加上檢查 if (!auth()->user()->can('inventory.adjust')) { abort(403); } if (!in_array($doc->status, ['completed', 'no_adjust'])) { return redirect()->back()->with('error', '僅能針對已完成或無需盤調的盤點單重新開啟盤點'); } // 執行取消核准邏輯 $doc->update([ 'status' => 'counting', // 回復為盤點中 'completed_at' => null, // 清除完成時間 'completed_by' => null, // 清除完成者 ]); return redirect()->back()->with('success', '已重新開啟盤點,單據回復為盤點中狀態'); } public function destroy(InventoryCountDoc $doc) { if ($doc->status === 'completed') { return redirect()->back()->with('error', '已完成的盤點單無法刪除'); } // Activity Log handled by Model Trait $doc->items()->delete(); $doc->delete(); return redirect()->route('inventory.count.index') ->with('success', '盤點單已刪除'); } }