Skip to content

Added: Manager View functionality for viewing subordinates' assets #16578

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 47 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
b141945
Updated branch
snipe Feb 25, 2025
881f4e3
Merge remote-tracking branch 'origin/develop'
snipe Feb 25, 2025
21e9f2b
Merge remote-tracking branch 'origin/develop'
snipe Feb 26, 2025
b4f70d9
Merge remote-tracking branch 'origin/develop'
snipe Feb 26, 2025
8bc7390
Merge remote-tracking branch 'origin/develop'
snipe Feb 26, 2025
ebae637
Merge remote-tracking branch 'origin/develop'
snipe Feb 26, 2025
3ba20a8
Merge remote-tracking branch 'origin/develop'
snipe Feb 26, 2025
c8e401f
Merge remote-tracking branch 'origin/develop'
snipe Feb 26, 2025
e863d3e
Merge remote-tracking branch 'origin/develop'
snipe Feb 26, 2025
138e7ac
Merge remote-tracking branch 'origin/develop'
snipe Feb 26, 2025
7603a93
Merge remote-tracking branch 'origin/develop'
snipe Feb 26, 2025
44dd061
Merge remote-tracking branch 'origin/develop'
snipe Feb 26, 2025
df38d7e
Merge remote-tracking branch 'origin/develop'
snipe Feb 27, 2025
9924553
Merge remote-tracking branch 'origin/develop'
snipe Feb 27, 2025
234f7d0
Merge remote-tracking branch 'origin/develop'
snipe Feb 27, 2025
f9f06d2
Merge remote-tracking branch 'origin/develop'
snipe Feb 27, 2025
e007db3
Merge remote-tracking branch 'origin/develop'
snipe Mar 3, 2025
88e1d8a
Merge remote-tracking branch 'origin/develop'
snipe Mar 4, 2025
3d3c13f
Merge remote-tracking branch 'origin/develop'
snipe Mar 4, 2025
4b05e55
Merge remote-tracking branch 'origin/develop'
snipe Mar 4, 2025
8f31597
Merge remote-tracking branch 'origin/develop'
snipe Mar 4, 2025
2c8b8bf
Merge remote-tracking branch 'origin/develop'
snipe Mar 4, 2025
5eda673
Merge remote-tracking branch 'origin/develop'
snipe Mar 4, 2025
5cfd1f6
Merge remote-tracking branch 'origin/develop'
snipe Mar 4, 2025
bdbaea7
Merge remote-tracking branch 'origin/develop'
snipe Mar 4, 2025
1ab0911
Merge remote-tracking branch 'origin/develop'
snipe Mar 4, 2025
ed8a486
Merge remote-tracking branch 'origin/develop'
snipe Mar 4, 2025
548ef97
Merge remote-tracking branch 'origin/develop'
snipe Mar 4, 2025
cc73b98
Merge remote-tracking branch 'origin/develop'
snipe Mar 4, 2025
9251007
Merge remote-tracking branch 'origin/develop'
snipe Mar 4, 2025
9fa855c
Prod assets
snipe Mar 4, 2025
5ba94c6
Merge remote-tracking branch 'origin/develop'
snipe Mar 5, 2025
7ee9a69
Merge remote-tracking branch 'origin/develop'
snipe Mar 5, 2025
a61dd8a
Merge remote-tracking branch 'origin/develop'
snipe Mar 5, 2025
a20d104
Merge remote-tracking branch 'origin/develop'
snipe Mar 5, 2025
c29bdbd
Merge remote-tracking branch 'origin/develop'
snipe Mar 5, 2025
91f3e07
Merge remote-tracking branch 'origin/develop'
snipe Mar 5, 2025
ef8d5ff
Merge remote-tracking branch 'origin/develop'
snipe Mar 6, 2025
deeb2fa
Merge remote-tracking branch 'origin/develop'
snipe Mar 11, 2025
11abb0f
Merge remote-tracking branch 'origin/develop'
snipe Mar 11, 2025
07602f6
Merge remote-tracking branch 'origin/develop'
snipe Mar 12, 2025
faeb037
Merge remote-tracking branch 'origin/develop'
snipe Mar 12, 2025
eb9cfba
Merge pull request #16498 from snipe/develop
snipe Mar 12, 2025
473ce15
Merge pull request #16526 from snipe/develop
snipe Mar 19, 2025
b4798e7
Added: Manager View for Assigned Assets
Mar 28, 2025
64883b5
Removed unnecessary file
Mar 28, 2025
39549f2
Merge branch 'snipe:master' into feature/manager-view
lukaskraic Mar 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/Http/Controllers/SettingsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,7 @@ public function postSettings(Request $request) : RedirectResponse
$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') != '') {
Expand Down
77 changes: 48 additions & 29 deletions app/Http/Controllers/ViewAssetsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,47 +27,66 @@ class ViewAssetsController extends Controller
* Redirect to the profile page.
*
*/
public function getIndex() : View | RedirectResponse
public function getIndex(Request $request) : View | RedirectResponse
{
$user = User::with(
'assets',
'assets.model',
'assets.model.fieldset.fields',
'consumables',
'accessories',
'licenses',
)->find(auth()->id());
$authUser = auth()->user();
$settings = Setting::getSettings();
$subordinates = collect();
$selectedUserId = $authUser->id;

$field_array = array();
// 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
$subordinates = $authUser->getAllSubordinates(true)->sortBy('last_name')->sortBy('first_name');

// Loop through all the custom fields that are applied to any model the user has assigned
foreach ($user->assets as $asset) {
// 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');

// Make sure the model has a custom fieldset before trying to loop through the associated fields
if ($asset->model->fieldset) {
// 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($selectedUserId);

// If the user to view couldn't be found (shouldn't happen with proper logic), redirect with error
if (!$userToView) {
return redirect()->route('home')->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;
}

}
}

}

// 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));
array_unique($field_array); // Remove duplicate field names

// 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
]);
}

/**
Expand Down
1 change: 1 addition & 0 deletions app/Models/Setting.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ class Setting extends Model
'google_login',
'google_client_id',
'google_client_secret',
'manager_view_enabled',
];

protected $casts = [
Expand Down
67 changes: 67 additions & 0 deletions app/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -868,6 +868,73 @@ public function getUserTotalCost(){

return $this;
}

/**
* 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;
}


public function scopeUserLocation($query, $location, $search){


Expand Down
65 changes: 65 additions & 0 deletions app/Providers/AppServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@
use App\Observers\LicenseObserver;
use App\Observers\SettingObserver;
use Illuminate\Routing\UrlGenerator;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\Facades\View;

/**
* This service provider handles setting the observers on models
Expand Down Expand Up @@ -73,6 +75,69 @@ public function boot(UrlGenerator $url)
Consumable::observe(ConsumableObserver::class);
License::observe(LicenseObserver::class);
Setting::observe(SettingObserver::class);

// Share manager view data with the default layout if enabled and user is logged in
View::composer('layouts.default', function ($view) {
if (Auth::check()) {
$settings = Setting::getSettings();
$user = Auth::user();
$subordinates = collect(); // Initialize as empty collection
$selectedUserId = $user->id; // Default to current user

// Use getAllSubordinates(true) which includes the manager themselves
if ($settings->manager_view_enabled) {
if ($user->isSuperUser()) {
// SuperAdmin sees all users
$subordinates = User::select('id', 'first_name', 'last_name', 'username')->get();
} else {
// Regular manager sees their subordinates + self
$subordinates = $user->getAllSubordinates(true); // Use the correct method, includes self
}

// Check if a specific user ID is requested via query parameter
if (request()->filled('user_id') && $subordinates->contains('id', request('user_id'))) {
$selectedUserId = (int) request('user_id');
} elseif (session()->has('manager_view_user_id') && $subordinates->contains('id', session('manager_view_user_id'))) {
// Optionally check session if you store the selection there
$selectedUserId = (int) session('manager_view_user_id');
}

// Store the selected ID in session if needed for persistence across requests
// session(['manager_view_user_id' => $selectedUserId]);

}


// Only pass subordinates if the count is greater than 1
if ($subordinates->count() > 1) {
$view->with('subordinates', $subordinates->sortBy('username')); // Sort for better display
$view->with('selectedUserId', $selectedUserId);
} else {
// Pass empty collection if not applicable
$view->with('subordinates', collect());
$view->with('selectedUserId', $user->id); // Default to self if no subordinates applicable
}

// Always pass settings
$view->with('settings', $settings);

// RE-ADD TEMPORARY DEBUG LOGGING
Log::debug('View Composer for layouts.default (After Fixes):');
Log::debug(' - User ID: ' . ($user->id ?? 'null'));
Log::debug(' - Is SuperUser: ' . ($user->isSuperUser() ? 'true' : 'false'));
Log::debug(' - manager_view_enabled: ' . (isset($settings->manager_view_enabled) ? $settings->manager_view_enabled : 'not set'));
Log::debug(' - subordinates count: ' . ($subordinates->count() ?? 'null'));
Log::debug(' - selectedUserId: ' . ($selectedUserId ?? 'null'));
// END TEMPORARY DEBUG LOGGING


} else {
// Pass default/empty values if user is not logged in
$view->with('settings', Setting::getSettings()); // Still might need settings for guests
$view->with('subordinates', collect());
$view->with('selectedUserId', null);
}
});
}

/**
Expand Down
2 changes: 1 addition & 1 deletion config/version.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
'prerelease_version' => '',
'hash_version' => 'gc29bdbdac',
'full_hash' => 'v8.0.4-29-gc29bdbdac',
'branch' => 'develop',
'branch' => 'master',
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
// Check if the column already exists before trying to add it
if (!Schema::hasColumn('settings', 'manager_view_enabled')) {
Schema::table('settings', function (Blueprint $table) {
// Add the new column, defaulting to false (0)
// Place it after 'show_images_in_email' for organization if that column exists
if (Schema::hasColumn('settings', 'show_images_in_email')) {
$table->boolean('manager_view_enabled')->default(false)->after('show_images_in_email');
} else {
$table->boolean('manager_view_enabled')->default(false);
}
});
}
}

/**
* Reverse the migrations.
*/
public function down(): void
{
// Check if the column exists before trying to drop it
if (Schema::hasColumn('settings', 'manager_view_enabled')) {
Schema::table('settings', function (Blueprint $table) {
$table->dropColumn('manager_view_enabled');
});
}
}
};
Loading