orderByRequest($equipments, $request); $this->filterRequest( builder: $equipments, request: $request, filterKeys: [ 'code' => self::F_TEXT, 'name' => self::F_TEXT, 'note' => self::F_TEXT, ] ); $this->searchRequest( builder: $equipments, value: $request->get('search'), fields: ['equipments.code', 'equipments.name', 'equipments.note'] ); // Optional filter: assigned (in use) vs warehouse $assignedFilter = $request->get('assigned'); if ($assignedFilter === 'in_use') { $equipments->whereNotNull('equipments.current_user_id'); } elseif ($assignedFilter === 'warehouse') { $equipments->whereNull('equipments.current_user_id'); } $responseData = $equipments ->leftJoin('users as holder', 'equipments.current_user_id', '=', 'holder.id') ->orderBy('equipments.updated_at', 'desc') ->select( 'equipments.*', 'holder.name as current_user_name' ) ->paginate($request->get('per_page', 15)); return AbstractController::ResultSuccess($responseData); } catch (\Exception $e) { Log::error('Error fetching equipments: ' . $e->getMessage()); return AbstractController::ResultError($e->getMessage()); } } /** * Get a single equipment by code (for barcode scan). */ public function showByCode(Request $request) { try { $code = trim((string) $request->get('code')); if ($code === '') { return AbstractController::ResultError('Code is required', 422); } $equipment = Equipment::leftJoin('users as holder', 'equipments.current_user_id', '=', 'holder.id') ->where('equipments.code', $code) ->select('equipments.*', 'holder.name as current_user_name') ->first(); if (!$equipment) { return AbstractController::ResultSuccess(null, 'Equipment not found'); } $history = EquipmentAssignment::leftJoin('users', 'equipment_assignments.user_id', '=', 'users.id') ->where('equipment_assignments.equipment_id', $equipment->id) ->orderBy('equipment_assignments.assigned_at', 'desc') ->select( 'equipment_assignments.*', 'users.name as user_name' ) ->get(); $equipment->history = $history; return AbstractController::ResultSuccess($equipment); } catch (\Exception $e) { Log::error('Error fetching equipment by code: ' . $e->getMessage()); return AbstractController::ResultError($e->getMessage()); } } /** * Get assignment history for an equipment. */ public function history($id) { try { $history = EquipmentAssignment::leftJoin('users', 'equipment_assignments.user_id', '=', 'users.id') ->where('equipment_assignments.equipment_id', $id) ->orderBy('equipment_assignments.assigned_at', 'desc') ->select( 'equipment_assignments.*', 'users.name as user_name' ) ->get(); return AbstractController::ResultSuccess($history); } catch (\Exception $e) { Log::error('Error fetching equipment history: ' . $e->getMessage()); return AbstractController::ResultError($e->getMessage()); } } /** * Get all users for assignment selectbox. */ public function userOptions() { try { $users = Admin::orderBy('name', 'asc') ->select('id', 'name', 'email') ->get(); return AbstractController::ResultSuccess($users); } catch (\Exception $e) { Log::error('Error fetching user options: ' . $e->getMessage()); return AbstractController::ResultError($e->getMessage()); } } /** * Create new equipment. Accepts multipart with optional image file. */ public function create(Request $request) { try { $validated = $request->validate([ 'code' => 'required|string|max:255|unique:equipments,code', 'name' => 'required|string|max:255', 'note' => 'nullable|string', 'current_user_id' => 'nullable|integer|exists:users,id', 'image' => 'nullable|file|mimes:jpg,jpeg,png,gif,webp|max:10240', ]); $authId = auth('admins')->user()->id ?? null; $imagePath = null; if ($request->hasFile('image')) { $directory = 'equipments/images'; if (!Storage::disk('public')->exists($directory)) { Storage::disk('public')->makeDirectory($directory); } $imagePath = $request->file('image')->store($directory, 'public'); } $equipment = DB::transaction(function () use ($validated, $authId, $imagePath) { $now = Carbon::now(); $currentUserId = $validated['current_user_id'] ?? null; $equipment = Equipment::create([ 'code' => $validated['code'], 'name' => $validated['name'], 'note' => $validated['note'] ?? null, 'image_path' => $imagePath, 'current_user_id' => $currentUserId, 'assigned_at' => $now, 'created_by' => $authId, ]); // Always create an initial assignment record so the history // shows the very first state (warehouse or user). EquipmentAssignment::create([ 'equipment_id' => $equipment->id, 'user_id' => $currentUserId, // null = at warehouse 'assigned_at' => $now, 'note' => $currentUserId ? 'Initial assignment' : 'Initial — at warehouse', 'created_by' => $authId, ]); return $equipment; }); return AbstractController::ResultSuccess($equipment, 'Equipment created successfully'); } catch (\Illuminate\Validation\ValidationException $e) { return AbstractController::ResultError($e->getMessage(), 422); } catch (\Exception $e) { Log::error('Error creating equipment: ' . $e->getMessage()); return AbstractController::ResultError($e->getMessage()); } } /** * Upload / replace the equipment image (optional, post-create or edit). */ public function uploadImage(Request $request, $id) { try { $request->validate([ 'image' => 'required|file|mimes:jpg,jpeg,png,gif,webp|max:10240', ]); $equipment = Equipment::findOrFail($id); // Delete old image if present if ($equipment->image_path && Storage::disk('public')->exists($equipment->image_path)) { Storage::disk('public')->delete($equipment->image_path); } $directory = 'equipments/images'; if (!Storage::disk('public')->exists($directory)) { Storage::disk('public')->makeDirectory($directory); } $path = $request->file('image')->store($directory, 'public'); $equipment->update([ 'image_path' => $path, 'updated_by' => auth('admins')->user()->id ?? null, ]); return AbstractController::ResultSuccess($equipment, 'Image uploaded'); } catch (\Exception $e) { Log::error('Error uploading equipment image: ' . $e->getMessage()); return AbstractController::ResultError($e->getMessage()); } } /** * Delete the equipment image. */ public function deleteImage($id) { try { $equipment = Equipment::findOrFail($id); if ($equipment->image_path && Storage::disk('public')->exists($equipment->image_path)) { Storage::disk('public')->delete($equipment->image_path); } $equipment->update([ 'image_path' => null, 'updated_by' => auth('admins')->user()->id ?? null, ]); return AbstractController::ResultSuccess($equipment, 'Image deleted'); } catch (\Exception $e) { Log::error('Error deleting equipment image: ' . $e->getMessage()); return AbstractController::ResultError($e->getMessage()); } } /** * Update equipment basic info (code, name, note). */ public function update(Request $request, $id) { try { $equipment = Equipment::findOrFail($id); $validated = $request->validate([ 'code' => 'sometimes|string|max:255|unique:equipments,code,' . $id, 'name' => 'sometimes|string|max:255', 'note' => 'nullable|string', ]); $equipment->update(array_merge($validated, [ 'updated_by' => auth('admins')->user()->id ?? null, ])); return AbstractController::ResultSuccess($equipment, 'Equipment updated successfully'); } catch (\Exception $e) { Log::error('Error updating equipment: ' . $e->getMessage()); return AbstractController::ResultError($e->getMessage()); } } /** * Assign equipment to a user, or return to warehouse (user_id = null). * Every state change is recorded: closes the previous open record AND opens * a new one for the new holder (user or warehouse). */ public function assign(Request $request, $id) { try { $validated = $request->validate([ 'user_id' => 'nullable|integer|exists:users,id', 'note' => 'nullable|string', ]); $authId = auth('admins')->user()->id ?? null; $equipment = DB::transaction(function () use ($id, $validated, $authId) { $equipment = Equipment::lockForUpdate()->findOrFail($id); $now = Carbon::now(); $newUserId = $validated['user_id'] ?? null; // No-op guard: if assigning to the same holder, skip to avoid duplicate history rows if ($equipment->current_user_id == $newUserId) { return $equipment; } // Close any currently open assignment (records who/Kho was holding it until now) EquipmentAssignment::where('equipment_id', $equipment->id) ->whereNull('returned_at') ->update([ 'returned_at' => $now, 'updated_at' => $now, ]); // Always open a new assignment row — for a user OR for the warehouse. // user_id = null means "currently at warehouse". EquipmentAssignment::create([ 'equipment_id' => $equipment->id, 'user_id' => $newUserId, 'assigned_at' => $now, 'note' => $validated['note'] ?? ($newUserId === null ? 'Returned to warehouse' : null), 'created_by' => $authId, ]); $equipment->update([ 'current_user_id' => $newUserId, 'assigned_at' => $now, 'updated_by' => $authId, ]); return $equipment->fresh(); }); return AbstractController::ResultSuccess($equipment, 'Equipment assignment updated'); } catch (\Exception $e) { Log::error('Error assigning equipment: ' . $e->getMessage()); return AbstractController::ResultError($e->getMessage()); } } /** * Statistics: list users grouped with the equipments they are currently holding. */ public function userStatistics(Request $request) { try { $search = trim((string) $request->get('search', '')); $usersQuery = DB::table('equipments') ->join('users', 'equipments.current_user_id', '=', 'users.id') ->whereNotNull('equipments.current_user_id'); if ($search !== '') { $usersQuery->where(function ($q) use ($search) { $q->where('users.name', 'like', "%{$search}%") ->orWhere('users.email', 'like', "%{$search}%") ->orWhere('equipments.code', 'like', "%{$search}%") ->orWhere('equipments.name', 'like', "%{$search}%"); }); } $userTotals = (clone $usersQuery) ->select( 'users.id as user_id', 'users.name as user_name', 'users.email as user_email', DB::raw('COUNT(equipments.id) as equipment_count') ) ->groupBy('users.id', 'users.name', 'users.email') ->orderByDesc('equipment_count') ->orderBy('users.name') ->get(); $userIds = $userTotals->pluck('user_id')->all(); $devices = Equipment::leftJoin('users as holder', 'equipments.current_user_id', '=', 'holder.id') ->whereIn('equipments.current_user_id', $userIds) ->select( 'equipments.id', 'equipments.code', 'equipments.name', 'equipments.note', 'equipments.image_path', 'equipments.current_user_id', 'equipments.assigned_at' ) ->orderBy('equipments.assigned_at', 'desc') ->get() ->groupBy('current_user_id'); $result = $userTotals->map(function ($u) use ($devices) { return [ 'user_id' => $u->user_id, 'user_name' => $u->user_name, 'user_email' => $u->user_email, 'equipment_count' => (int) $u->equipment_count, 'equipments' => ($devices->get($u->user_id) ?? collect())->values(), ]; }); // Also include warehouse summary $warehouseCount = Equipment::whereNull('current_user_id')->count(); return AbstractController::ResultSuccess([ 'users' => $result, 'warehouse_count' => $warehouseCount, 'total_equipment' => Equipment::count(), ]); } catch (\Exception $e) { Log::error('Error fetching equipment user statistics: ' . $e->getMessage()); return AbstractController::ResultError($e->getMessage()); } } /** * Delete equipment (and its history via cascade). */ public function delete($id) { try { $equipment = Equipment::findOrFail($id); $equipment->delete(); return AbstractController::ResultSuccess(null, 'Equipment deleted successfully'); } catch (\Exception $e) { Log::error('Error deleting equipment: ' . $e->getMessage()); return AbstractController::ResultError($e->getMessage()); } } /** * Export the (filtered) equipment list as CSV. */ public function export(Request $request) { try { $equipments = new Equipment; $this->filterRequest( builder: $equipments, request: $request, filterKeys: [ 'code' => self::F_TEXT, 'name' => self::F_TEXT, 'note' => self::F_TEXT, ] ); $this->searchRequest( builder: $equipments, value: $request->get('search'), fields: ['equipments.code', 'equipments.name', 'equipments.note'] ); $assignedFilter = $request->get('assigned'); if ($assignedFilter === 'in_use') { $equipments->whereNotNull('equipments.current_user_id'); } elseif ($assignedFilter === 'warehouse') { $equipments->whereNull('equipments.current_user_id'); } $rows = $equipments ->leftJoin('users as holder', 'equipments.current_user_id', '=', 'holder.id') ->orderBy('equipments.updated_at', 'desc') ->select( 'equipments.id', 'equipments.code', 'equipments.name', 'equipments.note', 'equipments.assigned_at', 'equipments.created_at', 'equipments.updated_at', 'holder.name as current_user_name' ) ->get(); $filename = 'equipments_' . date('Y-m-d_His') . '.csv'; $callback = function () use ($rows) { $out = fopen('php://output', 'w'); // BOM for Excel UTF-8 fwrite($out, "\xEF\xBB\xBF"); fputcsv($out, ['ID', 'Code', 'Name', 'Note', 'Current Holder', 'Assigned At', 'Created At', 'Updated At']); foreach ($rows as $r) { fputcsv($out, [ $r->id, $r->code, $r->name, $r->note, $r->current_user_name ?: 'Kho', $r->assigned_at, $r->created_at, $r->updated_at, ]); } fclose($out); }; return response()->stream($callback, 200, [ 'Content-Type' => 'text/csv; charset=UTF-8', 'Content-Disposition' => 'attachment; filename="' . $filename . '"', ]); } catch (\Exception $e) { Log::error('Error exporting equipments: ' . $e->getMessage()); return AbstractController::ResultError($e->getMessage()); } } }