mirror of
https://github.com/snipe/snipe-it.git
synced 2026-02-04 18:15:43 +00:00
Merge pull request #17096 from lukaskraic/feature/manager-view-v2
Manager View Feature
This commit is contained in:
@ -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') != '') {
|
||||
|
||||
@ -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
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,34 @@
|
||||
<?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
|
||||
{
|
||||
if (!Schema::hasColumn('settings', 'manager_view_enabled')) {
|
||||
Schema::table('settings', function (Blueprint $table) {
|
||||
$table->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');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -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 <code>Admin Settings > Permission Groups</code> 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',
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -9,6 +9,31 @@
|
||||
{{-- Account page content --}}
|
||||
@section('content')
|
||||
|
||||
{{-- Manager View Dropdown --}}
|
||||
@if (isset($settings) && $settings->manager_view_enabled && isset($subordinates) && $subordinates->count() > 1)
|
||||
<div class="row hidden-print" style="margin-bottom: 15px;">
|
||||
<div class="col-md-12">
|
||||
<form method="GET" action="{{ route('view-assets') }}" class="pull-right" role="form">
|
||||
@csrf
|
||||
<div class="form-group" style="margin-bottom: 0;">
|
||||
<label for="user_id" class="control-label" style="margin-right: 10px;">
|
||||
<i class="fas fa-users"></i> {{ trans('general.view_user_assets') }}:
|
||||
</label>
|
||||
<select name="user_id" id="user_id" class="form-control select2" onchange="this.form.submit()" style="width: 250px; display: inline-block;">
|
||||
@foreach ($subordinates as $subordinate)
|
||||
<option value="{{ $subordinate->id }}" {{ (int)$selectedUserId === (int)$subordinate->id ? ' selected' : '' }}>
|
||||
{{ $subordinate->present()->fullName() }}
|
||||
@if ($subordinate->id == auth()->id())
|
||||
({{ trans('general.me') }})
|
||||
@endif
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($acceptances = \App\Models\CheckoutAcceptance::forUser(Auth::user())->pending()->count())
|
||||
<div class="row">
|
||||
|
||||
@ -325,6 +325,21 @@
|
||||
<p class="help-block">{{ trans('admin/settings/general.require_checkinout_notes_help_text') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Manager View -->
|
||||
<div class="form-group {{ $errors->has('manager_view_enabled') ? 'error' : '' }}">
|
||||
<div class="col-md-3">
|
||||
<strong>{{ trans('admin/settings/general.manager_view') }}</strong>
|
||||
</div>
|
||||
<div class="col-md-8 col-md-offset-3">
|
||||
<label class="form-control">
|
||||
<input type="checkbox" value="1" name="manager_view_enabled" {{ (old('manager_view_enabled', $setting->manager_view_enabled)) == '1' ? ' checked="checked"' : '' }} aria-label="manager_view_enabled">
|
||||
{{ trans('admin/settings/general.manager_view_enabled_text') }}
|
||||
</label>
|
||||
<p class="help-block">{{ trans('admin/settings/general.manager_view_enabled_help') }}</p>
|
||||
{!! $errors->first('manager_view_enabled', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
|
||||
</div>
|
||||
</div>
|
||||
<!-- /.form-group -->
|
||||
</fieldset>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user