mirror of
https://github.com/snipe/snipe-it.git
synced 2026-08-18 11:15:42 +00:00
Accessories can now be flagged as requestable and requested by users from the requestable items page, the same way assets and asset models already can. No migration is needed since the accessories table already carries a requestable column. I kept the semantics deliberately simple: a request just records intent (and the requested quantity) and notifies the admins. It does not touch or reserve stock. The admin still performs the normal checkout, which is what actually decrements quantity. That way requests behave the same for unique assets and for quantity-based accessories, instead of inventing a separate "reserved" state. The admin "Requested" queue already lists every checkout request polymorphically (it showed asset models too), so accessory requests appear there as well; I extended that view to render the accessory name, image and a checkout action so an admin can actually see and fulfil the request. While wiring this up I also fixed a pre-existing bug in the request flow: the Requestable trait saved a 'qty' key, but the column is 'quantity' and wasn't fillable, so requested quantities were being silently dropped (this affected asset models too). Quantity is now persisted and read back correctly. Components and licenses can follow the same pattern; they each just need a small migration to add the requestable column.
292 lines
11 KiB
PHP
Executable File
292 lines
11 KiB
PHP
Executable File
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Actions\CheckoutRequests\CancelCheckoutRequestAction;
|
|
use App\Actions\CheckoutRequests\CreateCheckoutRequestAction;
|
|
use App\Enums\ActionType;
|
|
use App\Exceptions\AssetNotRequestable;
|
|
use App\Models\Accessory;
|
|
use App\Models\Actionlog;
|
|
use App\Models\Asset;
|
|
use App\Models\AssetModel;
|
|
use App\Models\Setting;
|
|
use App\Models\User;
|
|
use App\Notifications\RequestAssetCancelation;
|
|
use App\Notifications\RequestAssetNotification;
|
|
use Exception;
|
|
use Illuminate\Auth\Access\AuthorizationException;
|
|
use Illuminate\Contracts\View\View;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
/**
|
|
* This controller handles all actions related to the ability for users
|
|
* to view their own assets in the Snipe-IT Asset Management application.
|
|
*
|
|
* @version v1.0
|
|
*/
|
|
class ViewAssetsController extends Controller
|
|
{
|
|
/**
|
|
* Extract custom fields that should be displayed in user view.
|
|
*/
|
|
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.
|
|
*/
|
|
private function getViewableUsers(User $authUser): 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.
|
|
*/
|
|
private function getSelectedUserId(Request $request, 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(Request $request): View|RedirectResponse
|
|
{
|
|
$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',
|
|
'companies',
|
|
])->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'));
|
|
}
|
|
|
|
// Process custom fields for the user being viewed
|
|
$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' => $fieldArray,
|
|
'settings' => $settings,
|
|
'subordinates' => $subordinates,
|
|
'selectedUserId' => $selectedUserId,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Returns view of requestable items for a user.
|
|
*/
|
|
public function getRequestableIndex(): View
|
|
{
|
|
$assets = Asset::with('model', 'defaultLoc', 'location', 'assignedTo', 'requests')->Hardware()->RequestableAssets();
|
|
$models = AssetModel::with([
|
|
'category',
|
|
'requests',
|
|
'assets' => function ($q) {
|
|
$q->where('requestable', 1)
|
|
->whereHas('status', fn ($s) => $s->where('archived', 0)
|
|
->where(fn ($s) => $s->where('deployable', 1)->orWhere('pending', 1)
|
|
)
|
|
);
|
|
},
|
|
])->RequestableModels()->get();
|
|
|
|
$accessories = Accessory::with('category', 'location', 'requests')
|
|
->withCount('checkouts as checkouts_count')
|
|
->RequestableAccessories()
|
|
->get();
|
|
|
|
return view('account/requestable-assets', compact('assets', 'models', 'accessories'));
|
|
}
|
|
|
|
public function getRequestItem(Request $request, $itemType, $itemId = null, $cancel_by_admin = false, $requestingUser = null): RedirectResponse
|
|
{
|
|
$data = [];
|
|
$item = null;
|
|
$fullItemType = 'App\\Models\\'.studly_case($itemType);
|
|
|
|
if ($itemType == 'asset_model') {
|
|
$itemType = 'model';
|
|
}
|
|
$item = call_user_func([$fullItemType, 'find'], $itemId);
|
|
|
|
$user = auth()->user();
|
|
|
|
$logaction = new Actionlog;
|
|
$logaction->item_id = $data['asset_id'] = $item->id;
|
|
$logaction->item_type = $fullItemType;
|
|
$logaction->created_at = $data['requested_date'] = date('Y-m-d H:i:s');
|
|
|
|
if ($user->location_id) {
|
|
$logaction->location_id = $user->location_id;
|
|
}
|
|
|
|
$logaction->target_id = $data['user_id'] = auth()->id();
|
|
$logaction->target_type = User::class;
|
|
|
|
$data['item_quantity'] = $request->has('request-quantity') ? e($request->input('request-quantity')) : 1;
|
|
$data['requested_by'] = $user->display_name;
|
|
$data['item'] = $item;
|
|
$data['item_type'] = $itemType;
|
|
$data['target'] = auth()->user();
|
|
|
|
$data['item_url'] = match ($fullItemType) {
|
|
Asset::class => route('hardware.show', $item->id),
|
|
AssetModel::class => route('view/model', $item->id),
|
|
Accessory::class => route('accessories.show', $item->id),
|
|
default => route("view/{$itemType}", $item->id),
|
|
};
|
|
|
|
$settings = Setting::getSettings();
|
|
|
|
$is_admin = $user->isSuperUser() || $user->isAdmin();
|
|
|
|
if ($cancel_by_admin && ! $is_admin) {
|
|
return redirect()->back()->with('error', trans('general.insufficient_permissions'));
|
|
}
|
|
|
|
if (($item_request = $item->isRequestedBy($user)) || ($is_admin && $cancel_by_admin)) {
|
|
$item->cancelRequest($is_admin && $cancel_by_admin ? $requestingUser : null);
|
|
$data['item_quantity'] = ($item_request) ? $item_request->quantity : 1;
|
|
$logaction->logaction(ActionType::RequestCanceled);
|
|
|
|
if (($settings->alert_email != '') && ($settings->alerts_enabled == '1') && (! config('app.lock_passwords'))) {
|
|
try {
|
|
$settings->notify((new RequestAssetCancelation($data))->locale($settings->locale));
|
|
} catch (Exception $e) {
|
|
Log::warning('Could not send request cancellation notification: '.$e->getMessage());
|
|
}
|
|
}
|
|
|
|
return redirect()->back()->with('success')->with('success', trans('admin/hardware/message.requests.canceled'));
|
|
} else {
|
|
if (($fullItemType === Asset::class && is_null(Asset::RequestableAssets()->find($item->id)))
|
|
|| ($fullItemType === Accessory::class && is_null(Accessory::RequestableAccessories()->find($item->id)))) {
|
|
return redirect()->back()->with('error', trans('admin/hardware/message.requests.error'));
|
|
}
|
|
|
|
$item->request($data['item_quantity']);
|
|
if (($settings->alert_email != '') && ($settings->alerts_enabled == '1') && (! config('app.lock_passwords'))) {
|
|
$logaction->logaction('requested');
|
|
try {
|
|
$settings->notify((new RequestAssetNotification($data))->locale($settings->locale));
|
|
} catch (Exception $e) {
|
|
Log::warning('Could not send asset request notification: '.$e->getMessage());
|
|
}
|
|
}
|
|
|
|
return redirect()->route('requestable-assets')->with('success')->with('success', trans('admin/hardware/message.requests.success'));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Process a specific requested asset
|
|
*
|
|
* @param null $assetId
|
|
*/
|
|
public function store(Asset $asset): RedirectResponse
|
|
{
|
|
try {
|
|
CreateCheckoutRequestAction::run($asset, auth()->user());
|
|
|
|
return redirect()->route('requestable-assets')->with('success')->with('success', trans('admin/hardware/message.requests.success'));
|
|
} catch (AssetNotRequestable $e) {
|
|
return redirect()->back()->with('error', 'Asset is not requestable');
|
|
} catch (AuthorizationException $e) {
|
|
return redirect()->back()->with('error', trans('admin/hardware/message.requests.error'));
|
|
} catch (Exception $e) {
|
|
report($e);
|
|
|
|
return redirect()->back()->with('error', trans('general.something_went_wrong'));
|
|
}
|
|
}
|
|
|
|
public function destroy(Asset $asset): RedirectResponse
|
|
{
|
|
try {
|
|
CancelCheckoutRequestAction::run($asset, auth()->user());
|
|
|
|
return redirect()->route('requestable-assets')->with('success')->with('success', trans('admin/hardware/message.requests.canceled'));
|
|
} catch (Exception $e) {
|
|
report($e);
|
|
|
|
return redirect()->back()->with('error', trans('general.something_went_wrong'));
|
|
}
|
|
}
|
|
|
|
public function getRequestedAssets(): View
|
|
{
|
|
return view('account/requested');
|
|
}
|
|
}
|