'required|string|max:255|min:2', 'email' => 'required|email|unique:users,email,' . $this->user->id . '|max:255', 'employee_id' => 'nullable|string|max:50|unique:users,employee_id,' . $this->user->id . '|regex:/^[A-Z0-9-]+$/', 'phone' => 'nullable|string|max:20|regex:/^[\+]?[0-9\s\-\(\)]+$/', 'department' => 'nullable|string|max:100', 'position' => 'nullable|string|max:100', 'branch_code' => 'required|string|max:10|exists:branches,code', 'hire_date' => 'nullable|date|before_or_equal:today', 'salary' => 'nullable|numeric|min:0|max:999999.99', 'status' => 'required|in:active,inactive,suspended', 'emergency_contact_name' => 'nullable|string|max:255', 'emergency_contact_phone' => 'nullable|string|max:20|regex:/^[\+]?[0-9\s\-\(\)]+$/', 'address' => 'nullable|string|max:500', 'date_of_birth' => 'nullable|date|before:-18 years', 'national_id' => 'nullable|string|max:50|unique:users,national_id,' . $this->user->id, 'selectedRoles' => 'array', 'selectedRoles.*' => 'exists:roles,id', 'selectedPermissions' => 'array', 'selectedPermissions.*' => 'exists:permissions,id', ]; if ($this->changePassword) { $rules['password'] = ['required', 'confirmed', Password::min(8)->letters()->mixedCase()->numbers()->symbols()]; } return $rules; } protected $messages = [ 'name.min' => 'Name must be at least 2 characters long.', 'branch_code.required' => 'Branch code is required.', 'branch_code.exists' => 'Selected branch code does not exist.', 'email.unique' => 'This email address is already registered.', 'employee_id.unique' => 'This employee ID is already in use.', 'employee_id.regex' => 'Employee ID can only contain letters, numbers, and hyphens.', 'phone.regex' => 'Please enter a valid phone number.', 'emergency_contact_phone.regex' => 'Please enter a valid emergency contact phone number.', 'date_of_birth.before' => 'Employee must be at least 18 years old.', 'hire_date.before_or_equal' => 'Hire date cannot be in the future.', 'national_id.unique' => 'This national ID is already registered.', 'salary.max' => 'Salary cannot exceed 999,999.99.', ]; public function mount(User $user) { $this->user = $user; // Store original data for change tracking $this->originalData = [ 'name' => $user->name, 'email' => $user->email, 'employee_id' => $user->employee_id, 'phone' => $user->phone, 'department' => $user->department, 'position' => $user->position, 'branch_code' => $user->branch_code, 'status' => $user->status, ]; // Load user data $this->name = $user->name; $this->email = $user->email; $this->employee_id = $user->employee_id; $this->phone = $user->phone; $this->department = $user->department; $this->position = $user->position; $this->branch_code = $user->branch_code; $this->hire_date = $user->hire_date ? $user->hire_date->format('Y-m-d') : ''; $this->salary = $user->salary; $this->status = $user->status; $this->emergency_contact_name = $user->emergency_contact_name; $this->emergency_contact_phone = $user->emergency_contact_phone; $this->address = $user->address; $this->date_of_birth = $user->date_of_birth ? $user->date_of_birth->format('Y-m-d') : ''; $this->national_id = $user->national_id; // Load current roles $this->selectedRoles = $user->roles() ->where('user_roles.is_active', true) ->where(function ($q) { $q->whereNull('user_roles.expires_at') ->orWhere('user_roles.expires_at', '>', now()); }) ->pluck('roles.id') ->toArray(); // Load current direct permissions $this->selectedPermissions = $user->permissions() ->where('user_permissions.granted', true) ->where(function ($q) { $q->whereNull('user_permissions.expires_at') ->orWhere('user_permissions.expires_at', '>', now()); }) ->pluck('permissions.id') ->toArray(); } public function render() { $roles = Role::where('is_active', true) ->orderBy('display_name') ->get(); $permissions = Permission::where('is_active', true) ->orderBy('module') ->orderBy('name') ->get() ->groupBy('module'); $departments = User::select('department') ->distinct() ->whereNotNull('department') ->where('department', '!=', '') ->orderBy('department') ->pluck('department'); $branches = \DB::table('branches') ->where('is_active', true) ->orderBy('name') ->get(['code', 'name']); $positions = $this->getPositionsForDepartment($this->department); // Get user activity logs $causedByUser = \Spatie\Activitylog\Models\Activity::where('causer_id', $this->user->id) ->where('causer_type', \App\Models\User::class) ->latest() ->limit(5) ->get(); $performedOnUser = \Spatie\Activitylog\Models\Activity::where('subject_id', $this->user->id) ->where('subject_type', \App\Models\User::class) ->latest() ->limit(5) ->get(); $recentActivity = $causedByUser->merge($performedOnUser) ->sortByDesc('created_at') ->take(10); return view('livewire.users.edit', [ 'availableRoles' => $roles, 'permissions' => $permissions, 'departments' => $departments, 'branches' => $branches, 'positions' => $positions, 'recentActivity' => $recentActivity, ]); } public function save() { $this->saving = true; $this->validate(); DB::beginTransaction(); try { // Prepare update data $userData = [ 'name' => trim($this->name), 'email' => strtolower(trim($this->email)), 'employee_id' => $this->employee_id ? strtoupper(trim($this->employee_id)) : null, 'phone' => $this->phone ? preg_replace('/[^0-9+\-\s\(\)]/', '', $this->phone) : null, 'department' => $this->department ?: null, 'position' => $this->position ?: null, 'branch_code' => $this->branch_code, 'hire_date' => $this->hire_date ?: null, 'salary' => $this->salary ?: null, 'status' => $this->status, 'emergency_contact_name' => trim($this->emergency_contact_name) ?: null, 'emergency_contact_phone' => $this->emergency_contact_phone ? preg_replace('/[^0-9+\-\s\(\)]/', '', $this->emergency_contact_phone) : null, 'address' => trim($this->address) ?: null, 'date_of_birth' => $this->date_of_birth ?: null, 'national_id' => $this->national_id ? trim($this->national_id) : null, 'updated_by' => auth()->id(), ]; // Update password if requested if ($this->changePassword && $this->password) { $userData['password'] = Hash::make($this->password); $userData['password_changed_at'] = now(); } // Track changes for activity log $changes = $this->getChangedData($userData); $this->user->update($userData); // Sync roles with branch code $roleData = []; foreach ($this->selectedRoles as $roleId) { $roleData[$roleId] = [ 'branch_code' => $this->branch_code, 'is_active' => true, 'assigned_at' => now(), 'expires_at' => null, ]; } $this->user->roles()->sync($roleData); // Sync direct permissions $permissionData = []; foreach ($this->selectedPermissions as $permissionId) { $permissionData[$permissionId] = [ 'granted' => true, 'branch_code' => $this->branch_code, 'assigned_at' => now(), 'expires_at' => null, ]; } $this->user->permissions()->sync($permissionData); // Log the update with changes if (!empty($changes) || $this->changePassword) { $logProperties = ['changes' => $changes]; if ($this->changePassword) { $logProperties['password_changed'] = true; } activity() ->performedOn($this->user) ->causedBy(auth()->user()) ->withProperties($logProperties) ->log('User updated'); } DB::commit(); session()->flash('success', "User '{$this->user->name}' updated successfully!"); $this->saving = false; return redirect()->route('users.show', $this->user); } catch (\Exception $e) { DB::rollBack(); $this->saving = false; \Log::error('Failed to update user', [ 'user_id' => $this->user->id, 'error' => $e->getMessage(), ]); session()->flash('error', 'Failed to update user: ' . $e->getMessage()); } } public function cancel() { return redirect()->route('users.show', $this->user); } public function generatePassword() { $this->generatedPassword = $this->generateSecurePassword(); $this->password = $this->generatedPassword; $this->password_confirmation = $this->generatedPassword; $this->changePassword = true; $this->showPasswordGenerator = true; } public function generateSecurePassword($length = 12) { // Generate a secure password with mixed case, numbers, and symbols $uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; $lowercase = 'abcdefghijklmnopqrstuvwxyz'; $numbers = '0123456789'; $symbols = '!@#$%^&*()_+-=[]{}|;:,.<>?'; $password = ''; $password .= $uppercase[random_int(0, strlen($uppercase) - 1)]; $password .= $lowercase[random_int(0, strlen($lowercase) - 1)]; $password .= $numbers[random_int(0, strlen($numbers) - 1)]; $password .= $symbols[random_int(0, strlen($symbols) - 1)]; $allChars = $uppercase . $lowercase . $numbers . $symbols; for ($i = 4; $i < $length; $i++) { $password .= $allChars[random_int(0, strlen($allChars) - 1)]; } return str_shuffle($password); } public function resetPassword() { try { $newPassword = \Str::random(12); $this->user->update([ 'password' => Hash::make($newPassword) ]); // TODO: Send password reset email // $this->user->notify(new PasswordResetNotification($newPassword)); session()->flash('success', 'Password reset successfully. New password: ' . $newPassword); } catch (\Exception $e) { session()->flash('error', 'Failed to reset password: ' . $e->getMessage()); } } public function impersonateUser() { if ($this->user->id === auth()->id()) { session()->flash('error', 'You cannot impersonate yourself.'); return; } // Store original user ID for returning later session(['impersonate_original_user' => auth()->id()]); auth()->loginUsingId($this->user->id); session()->flash('success', 'Now impersonating ' . $this->user->name); return redirect()->route('dashboard'); } public function getAvailableRolesProperty() { return Role::orderBy('name')->get(); } public function confirmDelete() { $this->showDeleteModal = true; } public function deleteUser() { // Prevent self-deletion if ($this->user->id === auth()->id()) { session()->flash('error', 'You cannot delete your own account.'); return; } try { $this->user->delete(); session()->flash('success', 'User deleted successfully.'); return redirect()->route('users.index'); } catch (\Exception $e) { session()->flash('error', 'Failed to delete user: ' . $e->getMessage()); } $this->showDeleteModal = false; } public function setActiveTab($tab) { $this->currentTab = $tab; } public function getChangedData($newData) { $changes = []; foreach ($newData as $key => $value) { if (isset($this->originalData[$key]) && $this->originalData[$key] != $value) { $changes[$key] = [ 'old' => $this->originalData[$key], 'new' => $value ]; } } return $changes; } public function getPositionsForDepartment($department) { $positions = [ 'Service' => ['Service Advisor', 'Service Manager', 'Service Coordinator', 'Service Writer'], 'Technician' => ['Lead Technician', 'Senior Technician', 'Junior Technician', 'Apprentice Technician'], 'Parts' => ['Parts Manager', 'Parts Associate', 'Inventory Specialist', 'Parts Counter Person'], 'Administration' => ['Administrator', 'Office Manager', 'Receptionist', 'Data Entry Clerk'], 'Management' => ['General Manager', 'Assistant Manager', 'Supervisor', 'Team Lead'], 'Sales' => ['Sales Manager', 'Sales Associate', 'Sales Coordinator'], 'Finance' => ['Finance Manager', 'Accountant', 'Cashier', 'Billing Specialist'], ]; return $positions[$department] ?? []; } public function updatedDepartment() { // Clear position when department changes unless it's valid for new department $validPositions = $this->getPositionsForDepartment($this->department); if (!in_array($this->position, $validPositions)) { $this->position = ''; } } public function hasUnsavedChanges() { $currentData = [ 'name' => $this->name, 'email' => $this->email, 'employee_id' => $this->employee_id, 'phone' => $this->phone, 'department' => $this->department, 'position' => $this->position, 'branch_code' => $this->branch_code, 'status' => $this->status, ]; return !empty($this->getChangedData($currentData)) || $this->changePassword; } public function copyPasswordToClipboard() { // This will be handled by Alpine.js on the frontend $this->dispatch('password-copied'); } public function togglePasswordVisibility() { $this->showPasswordGenerator = !$this->showPasswordGenerator; } }