input('tab', 'machines'); $per_page = $request->input('per_page', 10); $search = $request->input('search'); // 1. 處理機台清單 (Machines Tab) $machineQuery = Machine::query()->with(['machineModel', 'paymentConfig', 'company']); if ($tab === 'machines' && $search) { $machineQuery->where(function ($q) use ($search) { $q->where('name', 'like', "%{$search}%") ->orWhere('serial_no', 'like', "%{$search}%"); }); } $machines = $machineQuery->latest()->paginate($per_page)->withQueryString(); // 2. 處理型號清單 (Models Tab) $modelQuery = MachineModel::query()->withCount('machines'); if ($tab === 'models' && $search) { $modelQuery->where('name', 'like', "%{$search}%"); } $models_list = $modelQuery->latest()->paginate($per_page)->withQueryString(); // 3. 處理使用者清單 (Accounts Tab - 授權帳號) $userQuery = User::query()->with('machines')->whereNotNull('company_id'); // 僅列出租戶帳號以供分配 if ($tab === 'accounts' && $search) { $userQuery->where(function ($q) use ($search) { $q->where('name', 'like', "%{$search}%") ->orWhere('username', 'like', "%{$search}%") ->orWhere('email', 'like', "%{$search}%"); }); } $users_list = $userQuery->latest()->paginate($per_page)->withQueryString(); // 4. 基礎下拉資料 (用於新增/編輯機台的彈窗) $models = MachineModel::select('id', 'name')->get(); $paymentConfigs = PaymentConfig::select('id', 'name')->get(); $companies = \App\Models\System\Company::select('id', 'name', 'code')->get(); return view('admin.basic-settings.machines.index', compact( 'machines', 'models_list', 'users_list', 'models', 'paymentConfigs', 'companies', 'tab' )); } /** * 儲存新機台 (僅核心欄位) */ public function store(Request $request): RedirectResponse { $validated = $request->validate([ 'name' => 'required|string|max:255', 'serial_no' => 'required|string|unique:machines,serial_no', 'company_id' => 'nullable|exists:companies,id', 'machine_model_id' => 'required|exists:machine_models,id', 'payment_config_id' => 'nullable|exists:payment_configs,id', 'location' => 'nullable|string|max:255', 'images.*' => 'image|mimes:jpeg,png,jpg,gif|max:2048', ]); $imagePaths = []; if ($request->hasFile('images')) { foreach (array_slice($request->file('images'), 0, 3) as $image) { $imagePaths[] = $this->storeAsWebp($image, 'machines'); } } $machine = Machine::create(array_merge($validated, [ 'status' => 'offline', 'api_token' => \Illuminate\Support\Str::random(60), 'creator_id' => auth()->id(), 'updater_id' => auth()->id(), 'card_reader_seconds' => 30, // 預設值 'card_reader_checkout_time_1' => '22:30:00', 'card_reader_checkout_time_2' => '23:45:00', 'payment_buffer_seconds' => 5, 'images' => $imagePaths, ])); return redirect()->route('admin.basic-settings.machines.index') ->with('success', __('Machine created successfully.')); } /** * 顯示詳細編輯頁面 */ public function edit(Machine $machine): View { $models = MachineModel::select('id', 'name')->get(); $paymentConfigs = PaymentConfig::select('id', 'name')->get(); $companies = \App\Models\System\Company::select('id', 'name', 'code')->get(); return view('admin.basic-settings.machines.edit', compact('machine', 'models', 'paymentConfigs', 'companies')); } /** * 更新機台詳細參數 */ public function update(Request $request, Machine $machine): RedirectResponse { Log::info('Machine Update Request', ['machine_id' => $machine->id, 'data' => $request->all()]); try { $validated = $request->validate([ 'name' => 'required|string|max:255', 'serial_no' => 'sometimes|required|string|unique:machines,serial_no,' . $machine->id, 'card_reader_seconds' => 'required|integer|min:0', 'payment_buffer_seconds' => 'required|integer|min:0', 'card_reader_checkout_time_1' => 'nullable|string', 'card_reader_checkout_time_2' => 'nullable|string', 'heating_start_time' => 'nullable|string', 'heating_end_time' => 'nullable|string', 'card_reader_no' => 'nullable|string|max:255', 'key_no' => 'nullable|string|max:255', 'invoice_status' => 'required|integer|in:0,1,2', 'welcome_gift_enabled' => 'boolean', 'is_spring_slot_1_10' => 'boolean', 'is_spring_slot_11_20' => 'boolean', 'is_spring_slot_21_30' => 'boolean', 'is_spring_slot_31_40' => 'boolean', 'is_spring_slot_41_50' => 'boolean', 'is_spring_slot_51_60' => 'boolean', 'member_system_enabled' => 'boolean', 'machine_model_id' => 'required|exists:machine_models,id', 'payment_config_id' => 'nullable|exists:payment_configs,id', 'location' => 'nullable|string|max:255', ]); // 僅限系統管理員可修改公司 if (auth()->user()->isSystemAdmin()) { $companyRule = ['company_id' => 'nullable|exists:companies,id']; $companyData = $request->validate($companyRule); $validated = array_merge($validated, $companyData); } Log::info('Machine Update Validated Data', ['data' => $validated]); } catch (\Illuminate\Validation\ValidationException $e) { Log::error('Machine Update Validation Failed', ['errors' => $e->errors()]); throw $e; } $machine->update(array_merge($validated, [ 'updater_id' => auth()->id(), ])); // 處理圖片更新 (支援 3 個獨立槽位) if ($request->hasFile('images')) { $currentImages = $machine->images ?? []; $newImages = $request->file('images'); $updated = false; foreach ($newImages as $index => $file) { // 限制 3 個槽位 (0, 1, 2) if ($index < 0 || $index > 2) continue; // 刪除該槽位的舊圖 if (isset($currentImages[$index]) && !empty($currentImages[$index])) { \Illuminate\Support\Facades\Storage::disk('public')->delete($currentImages[$index]); } // 處理並儲存新圖 $currentImages[$index] = $this->storeAsWebp($file, 'machines'); $updated = true; } if ($updated) { ksort($currentImages); $machine->update(['images' => array_values($currentImages)]); } } return redirect()->route('admin.basic-settings.machines.index') ->with('success', __('Machine settings updated successfully.')); } public function regenerateToken(Request $request, $serial): \Illuminate\Http\JsonResponse { $machine = Machine::where('serial_no', $serial)->firstOrFail(); $newToken = \Illuminate\Support\Str::random(60); $machine->update(['api_token' => $newToken]); Log::info('Machine API Token Regenerated', [ 'machine_id' => $machine->id, 'serial_no' => $machine->serial_no, 'user_id' => auth()->id() ]); return response()->json([ 'success' => true, 'message' => __('API Token regenerated successfully.'), 'api_token' => $newToken ]); } /** * AJAX: 取得特定帳號的機台分配狀態 (從 MachineController 遷移) */ public function getAccountMachines(User $user): \Illuminate\Http\JsonResponse { $currentUser = auth()->user(); // 安全檢查:只能操作自己公司的帳號(除非是系統管理員) if (!$currentUser->isSystemAdmin() && $user->company_id !== $currentUser->company_id) { return response()->json(['error' => 'Unauthorized'], 403); } // 取得該使用者所屬公司之所有機台 $machines = Machine::where('company_id', $user->company_id) ->get(['id', 'name', 'serial_no']); $assignedIds = $user->machines()->pluck('machines.id')->toArray(); return response()->json([ 'user' => $user, 'machines' => $machines, 'assigned_ids' => $assignedIds ]); } /** * AJAX: 儲存特定帳號的機台分配 (從 MachineController 遷移) */ public function syncAccountMachines(Request $request, User $user): \Illuminate\Http\JsonResponse { $currentUser = auth()->user(); // 安全檢查 if (!$currentUser->isSystemAdmin() && $user->company_id !== $currentUser->company_id) { return response()->json(['error' => 'Unauthorized'], 403); } $request->validate([ 'machine_ids' => 'nullable|array', 'machine_ids.*' => 'exists:machines,id' ]); // 加固驗證:確保所有機台 ID 都屬於該使用者的公司 if ($request->has('machine_ids')) { $machineIds = array_unique($request->machine_ids); $validCount = Machine::where('company_id', $user->company_id) ->whereIn('id', $machineIds) ->count(); if ($validCount !== count($machineIds)) { return response()->json(['error' => 'Invalid machine IDs provided.'], 422); } } $user->machines()->sync($request->machine_ids ?? []); return response()->json([ 'success' => true, 'message' => __('Permissions updated successfully'), 'assigned_machines' => $user->machines()->select('machines.id', 'machines.name', 'machines.serial_no')->get() ]); } }