diff --git a/app/Http/Controllers/SettingsController.php b/app/Http/Controllers/SettingsController.php index b652455a57..d2cd2f1997 100644 --- a/app/Http/Controllers/SettingsController.php +++ b/app/Http/Controllers/SettingsController.php @@ -352,6 +352,7 @@ class SettingsController extends Controller $setting->dash_chart_type = $request->input('dash_chart_type'); $setting->profile_edit = $request->input('profile_edit', 0); $setting->require_checkinout_notes = $request->input('require_checkinout_notes', 0); + $setting->manager_view_enabled = $request->input('manager_view_enabled', 0); if ($request->input('per_page') != '') { diff --git a/app/Http/Controllers/ViewAssetsController.php b/app/Http/Controllers/ViewAssetsController.php index bbff6ba4f7..c4e72971b4 100755 --- a/app/Http/Controllers/ViewAssetsController.php +++ b/app/Http/Controllers/ViewAssetsController.php @@ -27,50 +27,126 @@ use Exception; class ViewAssetsController extends Controller { /** - * Redirect to the profile page. + * Extract custom fields that should be displayed in user view. + * + * @param User $user + * @return array + */ + private function extractCustomFields(User $user): array + { + $fieldArray = []; + foreach ($user->assets as $asset) { + if ($asset->model && $asset->model->fieldset) { + foreach ($asset->model->fieldset->fields as $field) { + if ($field->display_in_user_view == '1') { + $fieldArray[$field->db_column] = $field->name; + } + } + } + } + return array_unique($fieldArray); + } + + /** + * Get list of users viewable by the current user. + * + * @param User $authUser + * @return \Illuminate\Support\Collection + */ + private function getViewableUsers(User $authUser): \Illuminate\Support\Collection + { + // SuperAdmin sees all users + if ($authUser->isSuperUser()) { + return User::select('id', 'first_name', 'last_name', 'username') + ->where('activated', 1) + ->orderBy('last_name') + ->orderBy('first_name') + ->get(); + } + + // Regular manager sees only their subordinates + self + $managedUsers = $authUser->getAllSubordinates(); + + // If user has subordinates, show them with self at beginning + if ($managedUsers->count() > 0) { + return collect([$authUser])->merge($managedUsers) + ->sortBy('last_name') + ->sortBy('first_name'); + } + + // User has no subordinates, only sees themselves + return collect([$authUser]); + } + + /** + * Get the selected user ID from request or default to current user. + * + * @param Request $request + * @param \Illuminate\Support\Collection $subordinates + * @param int $defaultUserId + * @return int + */ + private function getSelectedUserId(Request $request, \Illuminate\Support\Collection $subordinates, int $defaultUserId): int + { + // If no subordinates or no user_id in request, return default + if ($subordinates->count() <= 1 || !$request->filled('user_id')) { + return $defaultUserId; + } + + $requestedUserId = (int) $request->input('user_id'); + + // Validate if the requested user is allowed + if ($subordinates->contains('id', $requestedUserId)) { + return $requestedUserId; + } + + // If invalid ID or not authorized, return default + return $defaultUserId; + } + + /** + * Show user's assigned assets with optional manager view functionality. * */ - public function getIndex() : View | RedirectResponse + public function getIndex(Request $request) : View | RedirectResponse { - $user = User::with( + $authUser = auth()->user(); + $settings = Setting::getSettings(); + $subordinates = collect(); + $selectedUserId = $authUser->id; + + // Process manager view if enabled + if ($settings->manager_view_enabled) { + $subordinates = $this->getViewableUsers($authUser); + $selectedUserId = $this->getSelectedUserId($request, $subordinates, $authUser->id); + } + + // Load the data for the user to be viewed (either auth user or selected subordinate) + $userToView = User::with([ 'assets', 'assets.model', 'assets.model.fieldset.fields', 'consumables', 'accessories', - 'licenses', - )->find(auth()->id()); - - $field_array = array(); - - // Loop through all the custom fields that are applied to any model the user has assigned - foreach ($user->assets as $asset) { - - // Make sure the model has a custom fieldset before trying to loop through the associated fields - if ($asset->model->fieldset) { - - foreach ($asset->model->fieldset->fields as $field) { - // check and make sure they're allowed to see the value of the custom field - if ($field->display_in_user_view == '1') { - $field_array[$field->db_column] = $field->name; - } - - } - } + 'licenses' + ])->find($selectedUserId); + // If the user to view couldn't be found (shouldn't happen with proper logic), redirect with error + if (!$userToView) { + return redirect()->route('view-assets')->with('error', trans('admin/users/message.user_not_found')); } - // Since some models may re-use the same fieldsets/fields, let's make the array unique so we don't repeat columns - array_unique($field_array); + // Process custom fields for the user being viewed + $fieldArray = $this->extractCustomFields($userToView); - if (isset($user->id)) { - return view('account/view-assets', compact('user', 'field_array' )) - ->with('settings', Setting::getSettings()); - } - - // Redirect to the user management page - return redirect()->route('users.index') - ->with('error', trans('admin/users/message.user_not_found', $user->id)); + // Pass the necessary data to the view + return view('account/view-assets', [ + 'user' => $userToView, // Use 'user' for compatibility with the existing view + 'field_array' => $fieldArray, + 'settings' => $settings, + 'subordinates' => $subordinates, + 'selectedUserId' => $selectedUserId + ]); } /** diff --git a/app/Models/Setting.php b/app/Models/Setting.php index 199aee33dc..73fcada1e6 100755 --- a/app/Models/Setting.php +++ b/app/Models/Setting.php @@ -67,11 +67,13 @@ class Setting extends Model 'google_login', 'google_client_id', 'google_client_secret', + 'manager_view_enabled', ]; protected $casts = [ 'label2_asset_logo' => 'boolean', 'require_checkinout_notes' => 'boolean', + 'manager_view_enabled' => 'boolean', ]; /** diff --git a/app/Models/User.php b/app/Models/User.php index 278c801bed..f6bbcfaded 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -975,5 +975,75 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo + } + + /** + * Get all direct and indirect subordinates for this user. + * + * @return \Illuminate\Support\Collection + */ + public function getAllSubordinates() + { + $subordinates = collect(); + $this->fetchSubordinatesRecursive($this, $subordinates); + return $subordinates->unique('id'); + } + + /** + * Get all direct and indirect subordinates for this user, including self. + * + * @return \Illuminate\Support\Collection + */ + public function getAllSubordinatesIncludingSelf() + { + $subordinates = collect([$this]); + $this->fetchSubordinatesRecursive($this, $subordinates); + return $subordinates->unique('id'); + } + + /** + * Recursive helper function to fetch subordinates. + * + * @param User $manager + * @param \Illuminate\Support\Collection $subs + */ + protected function fetchSubordinatesRecursive(User $manager, \Illuminate\Support\Collection &$subs) + { + // Eager load 'managesUsers' to prevent N+1 queries in recursion + $directSubordinates = $manager->managesUsers()->with('managesUsers')->get(); + + foreach ($directSubordinates as $directSubordinate) { + // Add subordinate if not already in the collection + if (!$subs->contains('id', $directSubordinate->id)) { + $subs->push($directSubordinate); + // Recursive call for this subordinate's subordinates + $this->fetchSubordinatesRecursive($directSubordinate, $subs); + } + } + } + + /** + * Check if the current user is a direct or indirect manager of the given user. + * + * @param User $userToCheck + * @return bool + */ + public function isManagerOf(User $userToCheck): bool + { + // Optimization: If it's the same user, they are not their own manager + if ($this->id === $userToCheck->id) { + return false; + } + + // Eager load manager relationship to potentially reduce queries in the loop + $manager = $userToCheck->load('manager')->manager; + while ($manager) { + if ($manager->id === $this->id) { + return true; + } + // Move up the hierarchy (load relationship if not already loaded) + $manager = $manager->load('manager')->manager; + } + return false; } } diff --git a/database/migrations/2025_06_03_000000_add_manager_view_enabled_to_settings_table.php b/database/migrations/2025_06_03_000000_add_manager_view_enabled_to_settings_table.php new file mode 100644 index 0000000000..345c52a7dd --- /dev/null +++ b/database/migrations/2025_06_03_000000_add_manager_view_enabled_to_settings_table.php @@ -0,0 +1,34 @@ +boolean('manager_view_enabled') + ->default(false) + ->comment('Allow managers to view assets assigned to their subordinates'); + }); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + if (Schema::hasColumn('settings', 'manager_view_enabled')) { + Schema::table('settings', function (Blueprint $table) { + $table->dropColumn('manager_view_enabled'); + }); + } + } +}; diff --git a/resources/lang/en-US/admin/settings/general.php b/resources/lang/en-US/admin/settings/general.php index 8c4ae35365..149667a5ce 100644 --- a/resources/lang/en-US/admin/settings/general.php +++ b/resources/lang/en-US/admin/settings/general.php @@ -403,6 +403,9 @@ return [ 'due_checkin_days_help' => 'How many days before the expected checkin of an asset should it be listed in the "Due for checkin" page?', 'no_groups' => 'No groups have been created yet. Visit Admin Settings > Permission Groups to add one.', 'text' => 'Text', + 'manager_view' => 'Manager View', + 'manager_view_enabled_text' => 'Enable Manager View', + 'manager_view_enabled_help' => 'Allow managers to view assigned items to their direct and indirect reports in their account view.', 'username_formats' => [ 'username_format' => 'Username Format', diff --git a/resources/lang/en-US/general.php b/resources/lang/en-US/general.php index b21caa8168..7080203967 100644 --- a/resources/lang/en-US/general.php +++ b/resources/lang/en-US/general.php @@ -323,6 +323,9 @@ return [ 'viewall' => 'View All', 'viewassets' => 'View Assigned Assets', 'viewassetsfor' => 'View Assets for :name', + 'view_user_assets' => 'View User Assets', + 'select_user' => 'Select User', + 'me' => 'Me', 'website' => 'Website', 'welcome' => 'Welcome, :name', 'years' => 'years', diff --git a/resources/views/account/view-assets.blade.php b/resources/views/account/view-assets.blade.php index 6ad355e04c..9bc2d80f57 100755 --- a/resources/views/account/view-assets.blade.php +++ b/resources/views/account/view-assets.blade.php @@ -9,6 +9,31 @@ {{-- Account page content --}} @section('content') +{{-- Manager View Dropdown --}} +@if (isset($settings) && $settings->manager_view_enabled && isset($subordinates) && $subordinates->count() > 1) +
+
+
+ @csrf +
+ + +
+
+
+
+@endif @if ($acceptances = \App\Models\CheckoutAcceptance::forUser(Auth::user())->pending()->count())
diff --git a/resources/views/settings/general.blade.php b/resources/views/settings/general.blade.php index 931fa47bcc..c9964b581f 100644 --- a/resources/views/settings/general.blade.php +++ b/resources/views/settings/general.blade.php @@ -325,6 +325,21 @@

{{ trans('admin/settings/general.require_checkinout_notes_help_text') }}

+ + +
+
+ {{ trans('admin/settings/general.manager_view') }} +
+
+ +

{{ trans('admin/settings/general.manager_view_enabled_help') }}

+ {!! $errors->first('manager_view_enabled', '') !!} +
+