From 84ec5aea26458e37b0919b13fa902e596323089e Mon Sep 17 00:00:00 2001 From: Lukas Kraic Date: Wed, 4 Jun 2025 09:05:27 +0200 Subject: [PATCH 01/16] Manager View Feature --- app/Http/Controllers/SettingsController.php | 1 + app/Http/Controllers/ViewAssetsController.php | 91 +++++++++++++------ app/Models/Setting.php | 2 + app/Models/User.php | 65 +++++++++++++ ...manager_view_enabled_to_settings_table.php | 34 +++++++ .../lang/en-US/admin/settings/general.php | 3 + resources/lang/en-US/general.php | 3 + resources/views/account/view-assets.blade.php | 24 +++++ resources/views/settings/general.blade.php | 15 +++ 9 files changed, 212 insertions(+), 26 deletions(-) create mode 100644 database/migrations/2025_06_03_000000_add_manager_view_enabled_to_settings_table.php diff --git a/app/Http/Controllers/SettingsController.php b/app/Http/Controllers/SettingsController.php index 747f7b7284..2729b14a7e 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..a54dff0881 100755 --- a/app/Http/Controllers/ViewAssetsController.php +++ b/app/Http/Controllers/ViewAssetsController.php @@ -27,50 +27,89 @@ use Exception; class ViewAssetsController extends Controller { /** - * Redirect to the profile page. + * 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; + + // Check if manager view is enabled and get subordinates if applicable + if ($settings->manager_view_enabled) { + // Get all subordinates including self, sorted for the dropdown + if ($authUser->isSuperUser()) { + // SuperAdmin sees all users + $subordinates = User::select('id', 'first_name', 'last_name', 'username') + ->where('activated', 1) + ->orderBy('last_name') + ->orderBy('first_name') + ->get(); + } else { + // Regular manager sees only their subordinates + self (recursive) + $managedUsers = $authUser->getAllSubordinates(false); // Don't include self yet + + // Only show dropdown if user actually has subordinates + if ($managedUsers->count() > 0) { + $subordinates = collect([$authUser])->merge($managedUsers) // Add self at beginning + ->sortBy('last_name') + ->sortBy('first_name'); + } else { + // User has no subordinates, so they only see themselves + $subordinates = collect([$authUser]); + } + } + + // If the user has subordinates and a user_id is provided in the request + if ($subordinates->count() > 1 && $request->filled('user_id')) { + $requestedUserId = (int) $request->input('user_id'); + + // Validate if the requested user is allowed (self or subordinate) + if ($subordinates->contains('id', $requestedUserId)) { + $selectedUserId = $requestedUserId; + } + // If invalid ID or not authorized, $selectedUserId remains $authUser->id (default) + } + } + + // 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()); + 'licenses' + ])->find($selectedUserId); - $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) { + // 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')); + } + // Process custom fields for the user being viewed + $field_array = []; + foreach ($userToView->assets as $asset) { + if ($asset->model && $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; } - } } - } + array_unique($field_array); // Remove duplicate field names - // 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); - - 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' => $field_array, + '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 9b211e6a04..7856e5e53f 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -956,5 +956,70 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo + } + + /** + * Get all direct and indirect subordinates for this user. + * + * @param bool $includeSelf Include the current user in the results + * @return \Illuminate\Support\Collection + */ + public function getAllSubordinates($includeSelf = false) + { + $subordinates = collect(); + if ($includeSelf) { + $subordinates->push($this); + } + + // Use a recursive helper function to avoid scope issues + $this->fetchSubordinatesRecursive($this, $subordinates); + + return $subordinates->unique('id'); // Ensure uniqueness + } + + /** + * Recursive helper function to fetch subordinates. + * + * @param User $manager + * @param \Illuminate\Support\Collection $subordinatesCollection + */ + protected function fetchSubordinatesRecursive(User $manager, \Illuminate\Support\Collection &$subordinatesCollection) + { + // 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 (!$subordinatesCollection->contains('id', $directSubordinate->id)) { + $subordinatesCollection->push($directSubordinate); + // Recursive call for this subordinate's subordinates + $this->fetchSubordinatesRecursive($directSubordinate, $subordinatesCollection); + } + } + } + + /** + * 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 5da86537d6..fc45c61291 100644 --- a/resources/lang/en-US/admin/settings/general.php +++ b/resources/lang/en-US/admin/settings/general.php @@ -401,6 +401,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 assets assigned 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 599a15c139..e4adad7fd9 100644 --- a/resources/lang/en-US/general.php +++ b/resources/lang/en-US/general.php @@ -318,6 +318,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 050f53263e..4c356778cf 100755 --- a/resources/views/account/view-assets.blade.php +++ b/resources/views/account/view-assets.blade.php @@ -9,6 +9,30 @@ {{-- Account page content --}} @section('content') +{{-- Manager View Dropdown --}} +@if (isset($settings) && $settings->manager_view_enabled && isset($subordinates) && $subordinates->count() > 1) +
+
+
+
+ + +
+
+
+
+@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', '') !!} +
+
From 60989d67664a14c407d861e9b9b678a02fcf296a Mon Sep 17 00:00:00 2001 From: Lukas Kraic Date: Wed, 4 Jun 2025 15:17:55 +0200 Subject: [PATCH 02/16] Fix Codacy warnings --- app/Http/Controllers/ViewAssetsController.php | 37 ++++++++++++------- app/Models/User.php | 14 +++---- resources/views/account/view-assets.blade.php | 1 + 3 files changed, 32 insertions(+), 20 deletions(-) diff --git a/app/Http/Controllers/ViewAssetsController.php b/app/Http/Controllers/ViewAssetsController.php index a54dff0881..49503f370f 100755 --- a/app/Http/Controllers/ViewAssetsController.php +++ b/app/Http/Controllers/ViewAssetsController.php @@ -26,6 +26,27 @@ use Exception; */ class ViewAssetsController extends Controller { + /** + * 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; + } + } //end foreach + } + } //end foreach + return array_unique($fieldArray); + } + /** * Show user's assigned assets with optional manager view functionality. * @@ -59,7 +80,7 @@ class ViewAssetsController extends Controller } else { // User has no subordinates, so they only see themselves $subordinates = collect([$authUser]); - } + } //end if } // If the user has subordinates and a user_id is provided in the request @@ -90,22 +111,12 @@ class ViewAssetsController extends Controller } // Process custom fields for the user being viewed - $field_array = []; - foreach ($userToView->assets as $asset) { - if ($asset->model && $asset->model->fieldset) { - foreach ($asset->model->fieldset->fields as $field) { - if ($field->display_in_user_view == '1') { - $field_array[$field->db_column] = $field->name; - } - } - } - } - array_unique($field_array); // Remove duplicate field names + $fieldArray = $this->extractCustomFields($userToView); // Pass the necessary data to the view return view('account/view-assets', [ 'user' => $userToView, // Use 'user' for compatibility with the existing view - 'field_array' => $field_array, + 'field_array' => $fieldArray, 'settings' => $settings, 'subordinates' => $subordinates, 'selectedUserId' => $selectedUserId diff --git a/app/Models/User.php b/app/Models/User.php index 7856e5e53f..43ff8fdf1f 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -964,7 +964,7 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo * @param bool $includeSelf Include the current user in the results * @return \Illuminate\Support\Collection */ - public function getAllSubordinates($includeSelf = false) + public function getAllSubordinates($includeSelf=false) { $subordinates = collect(); if ($includeSelf) { @@ -981,21 +981,21 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo * Recursive helper function to fetch subordinates. * * @param User $manager - * @param \Illuminate\Support\Collection $subordinatesCollection + * @param \Illuminate\Support\Collection $subs */ - protected function fetchSubordinatesRecursive(User $manager, \Illuminate\Support\Collection &$subordinatesCollection) + 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 (!$subordinatesCollection->contains('id', $directSubordinate->id)) { - $subordinatesCollection->push($directSubordinate); + if (!$subs->contains('id', $directSubordinate->id)) { + $subs->push($directSubordinate); // Recursive call for this subordinate's subordinates - $this->fetchSubordinatesRecursive($directSubordinate, $subordinatesCollection); + $this->fetchSubordinatesRecursive($directSubordinate, $subs); } - } + } //end foreach } /** diff --git a/resources/views/account/view-assets.blade.php b/resources/views/account/view-assets.blade.php index 4c356778cf..742fab66b3 100755 --- a/resources/views/account/view-assets.blade.php +++ b/resources/views/account/view-assets.blade.php @@ -14,6 +14,7 @@
+ @csrf