WIP - Improved requested assets (#5289)

* WIP - beginning of improved requested assets

- Use Ajax tables for faster loading
- Use new notifications for requesting an asset

TODO:
- Use ajax tables for requestable asset models
- Use new notifications for canceling an asset request
- Expire requests once the asset has been checked out to the requesting user

* Only show asset name in email if it has one

* Refactor requested method to only include non-canceled requests

* Refactored requestable assets to log request and cancelation

* Added softdeletes on checkout requests

* Differentiate between canceling and deleting requests

* Added asset request cancelation notification

* Added timestamps and corrected unique key on requests table

* Improved requests view

* Re-use blade for cancel/request email

* Refactored BS table formatter for requested assets

* Location name min reduced to 2

* Added PAT test as maintenance option

This needs to be refactored into database-driven options with a UI

* Better slack message

* Added getImageUrl method for assets

* Include qty in request notifications

TODO:
- Try to pull requested info from original request for cancelation, otherwise it will default to 1

* Removed old asset request/cancel emails

* Added user profile asset request routes

* Added profile controller requested assets method

* Added blade link to requested assets for profile view

* Sort user history desc

* Added requested assets blade

* Added canceled at to checkoutRequest method

* Include qty in request

* Fixed comment, removed allowed_columns

* Removed Queable methods, since we don’t use a queue

* Fixed return type in method doc

* Fixed version number

* Changed id to user_id for clarity
This commit is contained in:
snipe 2018-04-04 17:33:02 -07:00 committed by GitHub
parent 201efecafa
commit 8a6713d5c0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 786 additions and 279 deletions

View file

@ -95,10 +95,6 @@ class AssetsController extends Controller
'model.category', 'model.manufacturer', 'model.fieldset','supplier');
// These are used by the API to query against specific ID numbers.
// They are also used by the individual searches on detail pages like
// locations, etc.
@ -106,6 +102,10 @@ class AssetsController extends Controller
$assets->where('assets.status_id', '=', $request->input('status_id'));
}
if ($request->input('requestable')=='true') {
$assets->where('assets.requestable', '=', '1');
}
if ($request->has('model_id')) {
$assets->InModelList([$request->input('model_id')]);
}
@ -736,5 +736,50 @@ class AssetsController extends Controller
}
/**
* Returns JSON listing of all requestable assets
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.0]
* @return JsonResponse
*/
public function requestable(Request $request)
{
$assets = Company::scopeCompanyables(Asset::select('assets.*'),"company_id","assets")
->with('location', 'assetstatus', 'assetlog', 'company', 'defaultLoc','assignedTo',
'model.category', 'model.manufacturer', 'model.fieldset','supplier')->where('assets.requestable', '=', '1');
$offset = request('offset', 0);
$limit = $request->input('limit', 50);
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
$assets->TextSearch($request->input('search'));
switch ($request->input('sort')) {
case 'model':
$assets->OrderModels($order);
break;
case 'model_number':
$assets->OrderModelNumber($order);
break;
case 'category':
$assets->OrderCategory($order);
break;
case 'manufacturer':
$assets->OrderManufacturer($order);
break;
default:
$assets->orderBy('assets.created_at', $order);
break;
}
$total = $assets->count();
$assets = $assets->skip($offset)->take($limit)->get();
return (new AssetsTransformer)->transformRequestedAssets($assets, $total);
}
}

View file

@ -0,0 +1,44 @@
<?php
namespace App\Http\Controllers\Api;
use App\Models\CheckoutRequest;
use App\Http\Controllers\Controller;
use Auth;
use App\Helpers\Helper;
class ProfileController extends Controller
{
/**
* Display a listing of requested assets.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.3.0]
*
* @return Array
*/
public function requestedAssets()
{
$checkoutRequests = CheckoutRequest::where('user_id', '=', Auth::user()->id)->get();
$results = [];
$results['total'] = $checkoutRequests->count();
foreach ($checkoutRequests as $checkoutRequest) {
$results['rows'][] = [
'image' => $checkoutRequest->itemRequested()->present()->getImageUrl(),
'name' => $checkoutRequest->itemRequested()->present()->name(),
'type' => $checkoutRequest->itemType(),
'qty' => $checkoutRequest->quantity,
'location' => ($checkoutRequest->location()) ? $checkoutRequest->location()->name : null,
'expected_checkin' => Helper::getFormattedDateObject($checkoutRequest->itemRequested()->expected_checkin, 'datetime'),
'request_date' => Helper::getFormattedDateObject($checkoutRequest->created_at, 'datetime'),
];
}
return $results;
}
}

View file

@ -134,12 +134,6 @@ class SettingsController extends Controller
if (!config('app.lock_passwords')) {
try {
Notification::send(Setting::first(), new MailTest());
/*Mail::send('emails.test', [], function ($m) {
$m->to(config('mail.reply_to.address'), config('mail.reply_to.name'));
$m->replyTo(config('mail.reply_to.address'), config('mail.reply_to.name'));
$m->subject(trans('mail.test_email'));
});*/
return response()->json(['message' => 'Mail sent to '.config('mail.reply_to.address')], 200);
} catch (Exception $e) {
return response()->json(['message' => $e->getMessage()], 500);

View file

@ -1305,12 +1305,16 @@ class AssetsController extends Controller
}
}
public function getRequestedIndex($id = null)
public function getRequestedIndex($user_id = null)
{
if ($id) {
$requestedItems = CheckoutRequest::where('user_id', $id)->with('user', 'requestedItem')->get();
$requestedItems = CheckoutRequest::with('user', 'requestedItem')->whereNull('canceled_at')->with('user', 'requestedItem');
if ($user_id) {
$requestedItems->where('user_id', $user_id)->get();
}
$requestedItems = CheckoutRequest::with('user', 'requestedItem')->get();
$requestedItems = $requestedItems->orderBy('created_at', 'desc')->get();
return view('hardware/requested', compact('requestedItems'));
}

View file

@ -12,6 +12,8 @@ use App\Models\Consumable;
use App\Models\License;
use App\Models\Setting;
use App\Models\User;
use App\Notifications\RequestAssetNotification;
use App\Notifications\RequestAssetCancelationNotification;
use Auth;
use Config;
use DB;
@ -80,116 +82,71 @@ class ViewAssetsController extends Controller
{
$item = null;
$fullItemType = 'App\\Models\\' . studly_case($itemType);
if ($itemType == "asset_model") {
$itemType = "model";
}
$item = call_user_func(array($fullItemType, 'find'), $itemId);
$user = Auth::user();
$quantity = $data['item_quantity'] = Input::has('request-quantity') ? e(Input::get('request-quantity')) : 1;
$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::user()->id;
$logaction->target_type = User::class;
$data['item_quantity'] = Input::has('request-quantity') ? e(Input::get('request-quantity')) : 1;
$data['requested_by'] = $user->present()->fullName();
$data['item_name'] = $item->name;
$data['item'] = $item;
$data['item_type'] = $itemType;
$data['target'] = Auth::user();
if ($fullItemType == Asset::class) {
$data['item_url'] = route('hardware.show', $item->id);
$slackMessage = ' Asset <'.url('/').'/hardware/'.$item->id.'/view'.'|'.$item->present()->name().'> requested by <'.url('/').'/users/'.$item->user_id.'/view'.'|'.$user->present()->fullName().'>.';
} else {
$data['item_url'] = route("view/${itemType}", $item->id);
$slackMessage = $quantity. ' ' . class_basename(strtoupper($logaction->item_type)).' <'.$data['item_url'].'|'.$item->name.'> requested by <'.url('/').'/user/'.$item->id.'/view'.'|'.$user->present()->fullName().'>.';
}
$settings = Setting::getSettings();
if ($settings->slack_endpoint) {
$slack_settings = [
'username' => $settings->botname,
'channel' => $settings->slack_channel,
'link_names' => true
];
$slackClient = new \Maknz\Slack\Client($settings->slack_endpoint, $slack_settings);
}
if ($item->isRequestedBy($user)) {
$item->cancelRequest();
$log = $logaction->logaction('request_canceled');
if ($item_request = $item->isRequestedBy($user)) {
$item->cancelRequest();
$data['item_quantity'] = $item_request->qty;
$logaction->logaction('request_canceled');
if (($settings->alert_email!='') && ($settings->alerts_enabled=='1') && (!config('app.lock_passwords'))) {
Mail::send('emails.asset-canceled', $data, function ($m) use ($user, $settings) {
$m->to(explode(',', $settings->alert_email), $settings->site_name);
$m->replyTo(config('mail.reply_to.address'), config('mail.reply_to.name'));
$m->subject(trans('mail.Item_Request_Canceled'));
});
}
if ($settings->slack_endpoint) {
try {
$slackClient->attach([
'color' => 'good',
'fields' => [
[
'title' => 'CANCELED:',
'value' => $slackMessage
]
]
])->send('Item Request Canceled');
} catch (Exception $e) {
}
$settings->notify(new RequestAssetCancelationNotification($data));
}
return redirect()->route('requestable-assets')->with('success')->with('success', trans('admin/hardware/message.requests.canceled'));
} else {
$item->request();
$log = $logaction->logaction('requested');
if (($settings->alert_email!='') && ($settings->alerts_enabled=='1') && (!config('app.lock_passwords'))) {
Mail::send('emails.asset-requested', $data, function ($m) use ($user, $settings) {
$m->to(explode(',', $settings->alert_email), $settings->site_name);
$m->replyTo(config('mail.reply_to.address'), config('mail.reply_to.name'));
$m->subject(trans('mail.Item_Requested'));
});
$logaction->logaction('requested');
$settings->notify(new RequestAssetNotification($data));
}
if ($settings->slack_endpoint) {
try {
$slackClient->attach([
'color' => 'good',
'fields' => [
[
'title' => 'REQUESTED:',
'value' => $slackMessage
]
]
])->send('Item Requested');
} catch (Exception $e) {
}
}
return redirect()->route('requestable-assets')->with('success')->with('success', trans('admin/hardware/message.requests.success'));
}
}
public function getRequestAsset($assetId = null)
{
@ -197,74 +154,45 @@ class ViewAssetsController extends Controller
// Check if the asset exists and is requestable
if (is_null($asset = Asset::RequestableAssets()->find($assetId))) {
// Redirect to the asset management page
return redirect()->route('requestable-assets')->with('error', trans('admin/hardware/message.does_not_exist_or_not_requestable'));
return redirect()->route('requestable-assets')
->with('error', trans('admin/hardware/message.does_not_exist_or_not_requestable'));
} elseif (!Company::isCurrentUserHasAccess($asset)) {
return redirect()->route('requestable-assets')->with('error', trans('general.insufficient_permissions'));
return redirect()->route('requestable-assets')
->with('error', trans('general.insufficient_permissions'));
}
// If it's requested, cancel the request.
$data['item'] = $asset;
$data['target'] = Auth::user();
$data['item_quantity'] = 1;
$settings = Setting::getSettings();
$logaction = new Actionlog();
$logaction->item_id = $data['asset_id'] = $asset->id;
$logaction->item_type = $data['item_type'] = Asset::class;
$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::user()->id;
$logaction->target_type = User::class;
// If it's already requested, cancel the request.
if ($asset->isRequestedBy(Auth::user())) {
$asset->cancelRequest();
return redirect()->route('requestable-assets')->with('success')->with('success', trans('admin/hardware/message.requests.success'));
$logaction->logaction('request canceled');
$settings->notify(new RequestAssetCancelationNotification($data));
return redirect()->route('requestable-assets')
->with('success')->with('success', trans('admin/hardware/message.requests.cancel-success'));
} else {
$logaction = new Actionlog();
$logaction->item_id = $data['asset_id'] = $asset->id;
$logaction->item_type = Asset::class;
$logaction->created_at = $data['requested_date'] = date("Y-m-d H:i:s");
$data['asset_type'] = 'hardware';
if ($user->location_id) {
$logaction->location_id = $user->location_id;
}
$logaction->target_id = $data['user_id'] = Auth::user()->id;
$logaction->target_type = User::class;
$log = $logaction->logaction('requested');
$data['requested_by'] = $user->present()->fullName();
$data['asset_name'] = $asset->present()->name();
$settings = Setting::getSettings();
if (($settings->alert_email!='') && ($settings->alerts_enabled=='1') && (!config('app.lock_passwords'))) {
Mail::send('emails.asset-requested', $data, function ($m) use ($user, $settings) {
$m->to(explode(',', $settings->alert_email), $settings->site_name);
$m->replyTo(config('mail.reply_to.address'), config('mail.reply_to.name'));
$m->subject(trans('mail.asset_requested'));
});
}
$logaction->logaction('requested');
$asset->request();
$settings->notify(new RequestAssetNotification($data));
if ($settings->slack_endpoint) {
$slack_settings = [
'username' => $settings->botname,
'channel' => $settings->slack_channel,
'link_names' => true
];
$client = new \Maknz\Slack\Client($settings->slack_endpoint, $slack_settings);
try {
$client->attach([
'color' => 'good',
'fields' => [
[
'title' => 'REQUESTED:',
'value' => class_basename(strtoupper($logaction->item_type)).' asset <'.url('/').'/hardware/'.$asset->id.'/view'.'|'.$asset->present()->name().'> requested by <'.url('/').'/hardware/'.$asset->id.'/view'.'|'.Auth::user()->present()->fullName().'>.'
]
]
])->send('Asset Requested');
} catch (Exception $e) {
}
}
return redirect()->route('requestable-assets')->with('success')->with('success', trans('admin/hardware/message.requests.success'));
}
@ -273,13 +201,10 @@ class ViewAssetsController extends Controller
public function getRequestedAssets()
{
$checkoutrequests = CheckoutRequest::all();
return view('account/requested-items', compact($checkoutrequests));
return view('account/requested');
}
// Get the acceptance screen
public function getAcceptAsset($logID = null)
{

View file

@ -160,4 +160,39 @@ class AssetsTransformer
'type' => $asset->assignedType()
] : null;
}
public function transformRequestedAssets(Collection $assets, $total)
{
$array = array();
foreach ($assets as $asset) {
$array[] = self::transformRequestedAsset($asset);
}
return (new DatatablesTransformer)->transformDatatables($array, $total);
}
public function transformRequestedAsset(Asset $asset) {
$array = [
'id' => (int) $asset->id,
'name' => e($asset->name),
'asset_tag' => e($asset->asset_tag),
'serial' => e($asset->serial),
'image' => ($asset->getImageUrl()) ? $asset->getImageUrl() : null,
'model' => ($asset->model) ? e($asset->model->name) : null,
'model_number' => (($asset->model) && ($asset->model->model_number)) ? e($asset->model->model_number) : null,
'expected_checkin' => Helper::getFormattedDateObject($asset->expected_checkin, 'date'),
'location' => ($asset->location) ? e($asset->location->name) : null,
'status'=> ($asset->assetstatus) ? $asset->present()->statusMeta : null,
];
$permissions_array['available_actions'] = [
'cancel' => ($asset->isRequestedBy(\Auth::user())) ? true : false,
'request' => ($asset->isRequestedBy(\Auth::user())) ? false : true,
];
$array += $permissions_array;
return $array;
}
}

View file

@ -52,7 +52,8 @@ class AssetMaintenance extends Model implements ICompanyableChild
return [
trans('admin/asset_maintenances/general.maintenance') => trans('admin/asset_maintenances/general.maintenance'),
trans('admin/asset_maintenances/general.repair') => trans('admin/asset_maintenances/general.repair'),
trans('admin/asset_maintenances/general.upgrade') => trans('admin/asset_maintenances/general.upgrade')
trans('admin/asset_maintenances/general.upgrade') => trans('admin/asset_maintenances/general.upgrade'),
'PAT test' => 'PAT test',
];
}

View file

@ -98,6 +98,14 @@ class AssetModel extends SnipeModel
return $this->belongsTo('\App\Models\CustomFieldset', 'fieldset_id');
}
public function getImageUrl() {
if ($this->image) {
return url('/').'/uploads/models/'.$this->image;
}
return false;
}
/**
* -----------------------------------------------
* BEGIN QUERY SCOPES

View file

@ -1,12 +1,13 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Model;
class CheckoutRequest extends Model
{
//
use SoftDeletes;
protected $fillable = ['user_id'];
protected $table = 'checkout_requests';

View file

@ -19,9 +19,7 @@ trait Requestable
public function isRequestedBy(User $user)
{
$requests = $this->requests->where('user_id', $user->id);
return $requests->count() > 0;
return $this->requests->where('canceled_at', NULL)->where('user_id', $user->id)->first();
}
public function scopeRequestedBy($query, User $user)
@ -31,15 +29,20 @@ trait Requestable
});
}
public function request()
public function request($qty = 1)
{
$this->requests()->save(
new CheckoutRequest(['user_id' => Auth::id()])
new CheckoutRequest(['user_id' => Auth::id(), 'qty' => $qty])
);
}
public function deleteRequest()
{
$this->requests()->where('user_id', Auth::id())->delete();
}
public function cancelRequest()
{
$this->requests()->where('user_id', Auth::id())->delete();
$this->requests()->where('user_id', Auth::id())->update(['canceled_at' => \Carbon\Carbon::now()]);
}
}

View file

@ -292,7 +292,7 @@ class User extends SnipeModel implements AuthenticatableContract, CanResetPasswo
*/
public function checkoutRequests()
{
return $this->belongsToMany(Asset::class, 'checkout_requests');
return $this->belongsToMany(Asset::class, 'checkout_requests', 'user_id', 'requestable_id')->whereNull('canceled_at');
}
public function throttle()

View file

@ -0,0 +1,137 @@
<?php
namespace App\Notifications;
use App\Models\Setting;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Messages\SlackMessage;
use Illuminate\Notifications\Notification;
class RequestAssetCancelationNotification extends Notification
{
/**
* @var
*/
private $params;
/**
* Create a new notification instance.
*
* @param $params
*/
public function __construct($params)
{
$this->target = $params['target'];
$this->item = $params['item'];
$this->note = '';
$this->last_checkout = '';
$this->item_quantity = $params['item_quantity'];
$this->expected_checkin = '';
$this->requested_date = \App\Helpers\Helper::getFormattedDateObject($params['requested_date'], 'datetime',
false);
$this->settings = Setting::getSettings();
if (array_key_exists('note', $params)) {
$this->note = $params['note'];
}
if ($this->item->last_checkout) {
$this->last_checkout = \App\Helpers\Helper::getFormattedDateObject($this->item->last_checkout, 'date',
false);
}
if ($this->item->expected_checkin) {
$this->expected_checkin = \App\Helpers\Helper::getFormattedDateObject($this->item->expected_checkin, 'date',
false);
}
}
/**
* Get the notification's delivery channels.
*
* @param mixed $notifiable
* @return array
*/
public function via()
{
$notifyBy = [];
if (Setting::getSettings()->slack_endpoint!='') {
\Log::debug('use slack');
$notifyBy[] = 'slack';
}
$notifyBy[] = 'mail';
return $notifyBy;
}
public function toSlack()
{
$target = $this->target;
$item = $this->item;
$note = $this->note;
$qty = $this->item_quantity;
$botname = ($this->settings->slack_botname) ? $this->settings->slack_botname : 'Snipe-Bot' ;
$fields = [
'QTY' => $qty,
'Canceled By' => '<'.$target->present()->viewUrl().'|'.$target->present()->fullName().'>',
];
if (($this->expected_checkin) && ($this->expected_checkin!='')) {
$fields['Expected Checkin'] = $this->expected_checkin;
}
return (new SlackMessage)
->content( trans('mail.a_user_canceled'))
->from($botname)
->attachment(function ($attachment) use ($item, $note, $fields) {
$attachment->title(htmlspecialchars_decode($item->present()->name), $item->present()->viewUrl())
->fields($fields)
->content($note);
});
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail()
{
$fields = [];
// Check if the item has custom fields associated with it
if (($this->item->model) && ($this->item->model->fieldset)) {
$fields = $this->item->model->fieldset->fields;
}
$message = (new MailMessage)->markdown('notifications.markdown.asset-requested',
[
'item' => $this->item,
'note' => $this->note,
'requested_by' => $this->target,
'requested_date' => $this->requested_date,
'fields' => $fields,
'qty' => $this->item_quantity,
'last_checkout' => $this->last_checkout,
'expected_checkin' => $this->expected_checkin,
'intro_text' => trans('mail.a_user_canceled'),
])
->subject(trans('Item Request Canceled'));
return $message;
}
}

View file

@ -0,0 +1,134 @@
<?php
namespace App\Notifications;
use App\Models\Setting;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Messages\SlackMessage;
use Illuminate\Notifications\Notification;
class RequestAssetNotification extends Notification
{
/**
* @var
*/
private $params;
/**
* Create a new notification instance.
*
* @param $params
*/
public function __construct($params)
{
$this->target = $params['target'];
$this->item = $params['item'];
$this->item_type = $params['item_type'];
$this->item_quantity = $params['item_quantity'];
$this->note = '';
$this->last_checkout = '';
$this->expected_checkin = '';
$this->requested_date = \App\Helpers\Helper::getFormattedDateObject($params['requested_date'], 'datetime',
false);
$this->settings = Setting::getSettings();
if (array_key_exists('note', $params)) {
$this->note = $params['note'];
}
if ($this->item->last_checkout) {
$this->last_checkout = \App\Helpers\Helper::getFormattedDateObject($this->item->last_checkout, 'date',
false);
}
if ($this->item->expected_checkin) {
$this->expected_checkin = \App\Helpers\Helper::getFormattedDateObject($this->item->expected_checkin, 'date',
false);
}
}
/**
* Get the notification's delivery channels.
*
* @param mixed $notifiable
* @return array
*/
public function via()
{
$notifyBy = [];
if (Setting::getSettings()->slack_endpoint!='') {
\Log::debug('use slack');
$notifyBy[] = 'slack';
}
$notifyBy[] = 'mail';
return $notifyBy;
}
public function toSlack()
{
$target = $this->target;
$qty = $this->item_quantity;
$item = $this->item;
$note = $this->note;
$botname = ($this->settings->slack_botname) ? $this->settings->slack_botname : 'Snipe-Bot' ;
$fields = [
'QTY' => $qty,
'Requested By' => '<'.$target->present()->viewUrl().'|'.$target->present()->fullName().'>',
];
return (new SlackMessage)
->content(trans('mail.Item_Requested'))
->from($botname)
->attachment(function ($attachment) use ($item, $note, $fields) {
$attachment->title(htmlspecialchars_decode($item->present()->name), $item->present()->viewUrl())
->fields($fields)
->content($note);
});
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail()
{
$fields = [];
// Check if the item has custom fields associated with it
if (($this->item->model) && ($this->item->model->fieldset)) {
$fields = $this->item->model->fieldset->fields;
}
$message = (new MailMessage)->markdown('notifications.markdown.asset-requested',
[
'item' => $this->item,
'note' => $this->note,
'requested_by' => $this->target,
'requested_date' => $this->requested_date,
'fields' => $fields,
'last_checkout' => $this->last_checkout,
'expected_checkin' => $this->expected_checkin,
'intro_text' => trans('mail.a_user_requested'),
'qty' => $this->item_quantity,
])
->subject(trans('mail.Item_Requested'));
return $message;
}
}

View file

@ -0,0 +1,36 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddCanceledAtAndFulfilledAtInRequests extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('checkout_requests', function (Blueprint $table) {
$table->dateTime('canceled_at')->nullable()->default(null);
$table->dateTime('fulfilled_at')->nullable()->default(null);
$table->dateTime('deleted_at')->nullable()->default(null);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('checkout_requests', function (Blueprint $table) {
$table->dropColumn('canceled_at');
$table->dropColumn('fulfilled_at');
$table->dropColumn('deleted_at');
});
}
}

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddDropUniqueRequests extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('checkout_requests', function (Blueprint $table) {
$table->dropUnique('checkout_requests_user_id_requestable_id_requestable_type_unique');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('checkout_requests', function (Blueprint $table) {
$table->index(['user_id','requestable_id','requestable_type'], 'checkout_requests_user_id_requestable_id_requestable_type_unique')->unique();
});
}
}

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddNewIndexRequestable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('checkout_requests', function (Blueprint $table) {
$table->index(['user_id','requestable_id','requestable_type'], 'checkout_requests_user_id_requestable_id_requestable_type');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('checkout_requests', function (Blueprint $table) {
$table->dropIndex('checkout_requests_user_id_requestable_id_requestable_type');
});
}
}

View file

@ -29,97 +29,40 @@
<div class="tab-pane fade in active" id="assets">
<div class="row">
<div class="col-md-12">
@if ($assets->count() > 0)
<div class="table-responsive">
<table
name="requested-assets"
data-toolbar="#toolbar"
class="table table-striped snipe-table"
id="table"
data-advanced-search="true"
data-id-table="advancedTable"
data-cookie-id-table="requestableAssets">
data-click-to-select="true"
data-cookie-id-table="requestableAssetsListingTable"
data-pagination="true"
data-id-table="requestableAssetsListingTable"
data-search="true"
data-side-pagination="server"
data-show-columns="true"
data-show-export="false"
data-show-footer="false"
data-show-refresh="true"
data-sort-order="asc"
data-sort-name="name"
data-toolbar="#toolbar"
id="assetsListingTable"
class="table table-striped snipe-table"
data-url="{{ route('api.assets.requestable', ['requestable' => true]) }}">
<thead>
<tr role="row">
<th class="col-md-1" data-sortable="true">{{ trans('general.image') }}</th>
<th class="col-md-2" data-sortable="true">{{ trans('admin/hardware/table.asset_model') }}</th>
<th class="col-md-2" data-sortable="true">{{ trans('admin/models/table.modelnumber') }}</th>
@if ($snipeSettings->display_asset_name)
<th class="col-md-2" data-sortable="true">{{ trans('admin/hardware/form.name') }}</th>
@endif
<th class="col-md-3" data-sortable="true">{{ trans('admin/hardware/table.serial') }}</th>
<th class="col-md-2" data-sortable="true">{{ trans('admin/hardware/table.location') }}</th>
<th class="col-md-2" data-sortable="true">{{ trans('admin/hardware/table.status') }}</th>
<th class="col-md-2" data-sortable="true">{{ trans('admin/hardware/form.expected_checkin') }}</th>
<th class="col-md-1 actions" data-sortable="false">{{ trans('table.actions') }}</th>
<tr>
<th class="col-md-1" data-field="image" data-formatter="imageFormatter" data-sortable="true">{{ trans('general.image') }}</th>
<th class="col-md-2" data-field="model" data-sortable="true">{{ trans('admin/hardware/table.asset_model') }}</th>
<th class="col-md-2" data-field="model_number" data-sortable="true">{{ trans('admin/models/table.modelnumber') }}</th>
<th class="col-md-2" data-field="name" data-sortable="true">{{ trans('admin/hardware/form.name') }}</th>
<th class="col-md-3" data-field="serial" data-sortable="true">{{ trans('admin/hardware/table.serial') }}</th>
<th class="col-md-2" data-field="location" data-sortable="true">{{ trans('admin/hardware/table.location') }}</th>
<th class="col-md-2" data-field="status" data-sortable="true">{{ trans('admin/hardware/table.status') }}</th>
<th class="col-md-2" data-field="expected_checkin" data-formatter="dateDisplayFormatter" data-sortable="true">{{ trans('admin/hardware/form.expected_checkin') }}</th>
<th class="col-md-1" data-formatter="assetRequestActionsFormatter" data-field="actions" data-sortable="false">{{ trans('table.actions') }}</th>
</tr>
</thead>
<tbody>
@foreach ($assets as $asset)
<tr>
<td>
@if ($asset->getImageUrl())
<a href="{{ $asset->getImageUrl() }}" data-toggle="lightbox" data-type="image">
<img src="{{ $asset->getImageUrl() }}" style="max-height: {{ $snipeSettings->thumbnail_max_h }}px; width: auto;" class="img-responsive">
</a>
@endif
</td>
<td>{{ $asset->model->name }}
</td>
<td>
{{ $asset->model->model_number }}
</td>
@if ($snipeSettings->display_asset_name)
<td>{{ $asset->name }}</td>
@endif
<td>{{ $asset->serial }}</td>
<td>
@if ($asset->location)
{{ $asset->location->name }}
@endif
</td>
@if ($asset->assigned_to != '' && $asset->assigned_to > 0)
<td>Checked out</td>
@else
<td>{{ trans('admin/hardware/general.requestable') }}</td>
@endif
<td>{{ $asset->expected_checkin }}</td>
<td>
<form action="{{route('account/request-item', ['itemType' => 'asset', 'itemId' => $asset->id])}}" method="POST" accept-charset="utf-8">
{{ csrf_field() }}
@if ($asset->isRequestedBy(Auth::user()))
{{Form::submit(trans('button.cancel'), ['class' => 'btn btn-danger btn-sm'])}}
@else
{{Form::submit(trans('button.request'), ['class' => 'btn btn-primary btn-sm'])}}
@endif
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@else
<div class="alert alert-info alert-block">
<i class="fa fa-info-circle"></i>
{{ trans('general.no_results') }}
</div>
@endif
</div>
</div>
</div>

View file

@ -0,0 +1,54 @@
@extends('layouts/default')
{{-- Page title --}}
@section('title')
Requested Assets
@stop
{{-- Account page content --}}
@section('content')
<div class="row">
<div class="col-md-12">
<div class="box box-default">
<div class="box-body">
<table
data-cookie-id-table="userRequests"
data-pagination="true"
data-id-table="userRequests"
data-side-pagination="server"
data-show-columns="true"
data-show-export="true"
data-show-refresh="true"
data-sort-order="desc"
id="userRequests"
class="table table-striped snipe-table"
data-url="{{ route('api.assets.requested') }}"
data-export-options='{
"fileName": "my-requested-assets-{{ date('Y-m-d') }}",
"ignoreColumn": ["actions","image","change","checkbox","checkincheckout","icon"]
}'>
<thead>
<tr>
<th class="col-md-1" data-field="image" data-formatter="imageFormatter">Image</th>
<th class="col-md-2" data-field="name">Item Name</th>
<th class="col-md-2" data-field="type">Type</th>
<th class="col-md-2" data-field="qty">{{ trans('general.qty') }}</th>
<th class="col-md-2" data-field="location">{{ trans('admin/hardware/table.location') }}</th>
<th class="col-md-2" data-field="expected_checkin" data-formatter="dateDisplayFormatter"> {{ trans('admin/hardware/form.expected_checkin') }}</th>
<th class="col-md-2" data-field="request_date" data-formatter="dateDisplayFormatter">Requested Date</th>
</tr>
</thead>
</table>
</div> <!-- .box-body -->
</div> <!-- .box-default -->
</div> <!-- .col-md-9 -->
</div> <!-- .row-->
@stop
@section('moar_scripts')
@include ('partials.bootstrap-table')
@stop

View file

@ -266,7 +266,7 @@ View Assets for {{ $user->present()->fullName() }}
data-show-columns="true"
data-show-export="true"
data-show-refresh="true"
data-sort-order="asc"
data-sort-order="desc"
id="userActivityReport"
class="table table-striped snipe-table"
data-url="{{route('api.activity.index', ['target_id' => $user->id, 'target_type' => 'User', 'order' => 'desc']) }}"

View file

@ -1,17 +0,0 @@
@extends('emails/layouts/default')
@section('content')
<p>{{ trans('mail.a_user_canceled') }} <a href="{{ url('/') }}"> {{ $snipeSettings->site_name }}</a>. </p>
<p>{{ trans('mail.user') }} <a href="{{ route('users.show', $user_id) }}">{{ $requested_by }}</a><br>
{{ trans('mail.item') }} <a href="{{ $item_url }}">{{ $item_name }}</a> ({{ $item_type }}) <br>
{{ trans('mail.canceled') }} {{ $requested_date }}
</p>
@if ($snipeSettings->show_url_in_emails=='1')
<p><a href="{{ url('/') }}">{{ $snipeSettings->site_name }}</a></p>
@else
<p>{{ $snipeSettings->site_name }}</p>
@endif
@stop

View file

@ -1,20 +0,0 @@
@extends('emails/layouts/default')
@section('content')
<p>{{ trans('mail.a_user_requested') }} <a href="{{ url('/') }}"> {{ $snipeSettings->site_name }}</a>. </p>
<p>{{ trans('mail.user') }} <a href="{{ route('users.show', $user_id) }}">{{ $requested_by }}</a><br>
{{ trans('mail.item') }} <a href="{{ $item_url }}">{{ $item_name }}</a> ({{ $item_type }}) <br>
{{ trans('general.requested') }} {{ $requested_date }}
@if ($item_quantity > 1)
<br> {{ trans('general.qty') }} {{ $item_quantity}}
@endif
@if ($snipeSettings->show_url_in_emails=='1')
<p><a href="{{ url('/') }}">{{ $snipeSettings->site_name }}</a></p>
@else
<p>{{ $snipeSettings->site_name }}</p>
@endif
@stop

View file

@ -28,13 +28,22 @@
@if ($requestedItems->count() > 0)
<div class="table-responsive">
<table
name="requested-assets"
name="requestedAssets"
data-toolbar="#toolbar"
class="table table-striped snipe-table"
id="table"
id="requestedAssets"
data-advanced-search="true"
data-id-table="advancedTable"
data-cookie-id-table="requestedAssets">
data-search="true"
data-show-columns="true"
data-show-export="true"
data-pagination="true"
data-id-table="requestedAssets"
data-cookie-id-table="requestedAssets"
data-url="{{ route('api.consumables.index') }}"
data-export-options='{
"fileName": "export-assetrequests-{{ date('Y-m-d') }}",
"ignoreColumn": ["actions","image","change","checkbox","checkincheckout","icon"]
}'>
<thead>
<tr role="row">
<th class="col-md-1">Image</th>
@ -44,6 +53,7 @@
<th class="col-md-3" data-sortable="true">Requesting User</th>
<th class="col-md-2">Requested Date</th>
<th class="col-md-1"></th>
<th class="col-md-1"></th>
</tr>
</thead>
<tbody>

View file

@ -307,6 +307,16 @@
<i class="fa fa-check fa-fw"></i>
{{ trans('general.viewassets') }}
</a></li>
<li {!! (Request::is('account/requested') ? ' class="active"' : '') !!}>
<a href="{{ route('account.requested') }}">
<i class="fa fa-check fa-disk"></i>
Requested Assets
</a></li>
<li>
<a href="{{ route('profile') }}">
<i class="fa fa-user fa-fw"></i>

View file

@ -0,0 +1,62 @@
@component('mail::message')
# {{ trans('mail.hello') }},
{{ $intro_text }}.
@if (($snipeSettings->show_images_in_email =='1') && $item->getImageUrl())
<center><img src="{{ $item->getImageUrl() }}" alt="Asset" style="max-width: 570px;"></center>
@endif
@component('mail::table')
| | |
| ------------- | ------------- |
@if (isset($qty))
| **{{ trans('general.qty') }}** | {{ $qty }}
@endif
| **{{ trans('mail.user') }}** | [{{ $requested_by->present()->fullName() }}]({{ route('users.show', $requested_by->id) }}) |
| **{{ trans('general.requested') }}** | {{ $requested_date }} |
@if ((isset($item->asset_tag)) && ($item->asset_tag!=''))
| **{{ trans('mail.asset_tag') }}** | {{ $item->asset_tag }} |
@endif
@if ((isset($item->name)) && ($item->name!=''))
| **{{ trans('mail.asset_name') }}** | {{ $item->name }} |
@endif
@if (isset($item->assetstatus))
| **{{ trans('general.status') }}** | {{ $item->assetstatus->name }}
@endif
@if ($item->assignedTo)
| **Checked out to** | {!! $item->assignedTo->present()->nameUrl() !!} ({{ $item->present()->statusMeta }})
@endif
@if (isset($item->manufacturer))
| **{{ trans('general.manufacturer') }}** | {{ $item->manufacturer->name }} |
@endif
@if (isset($item->model))
| **{{ trans('general.asset_model') }}** | {{ $item->model->name }} |
@endif
@if (isset($item->model_no))
| **{{ trans('general.model_no') }}** | {{ $item->model_no }} |
@endif
@if (isset($item->serial))
| **{{ trans('mail.serial') }}** | {{ $item->serial }} |
@endif
@if ((isset($last_checkout)) && ($last_checkout!=''))
| **Last Checkout** | {{ $last_checkout }} |
@endif
@if ((isset($expected_checkin)) && ($expected_checkin!=''))
| **{{ trans('mail.expecting_checkin_date') }}** | {{ $expected_checkin }} |
@endif
@foreach($fields as $field)
@if (($item->{ $field->db_column_name() }!='') && ($field->show_in_email) && ($field->field_encrypted=='0'))
| **{{ $field->name }}** | {{ $item->{ $field->db_column_name() } }} |
@endif
@endforeach
@if ($note)
| **{{ trans('mail.additional_notes') }}** | {{ $note }} |
@endif
@endcomponent
Thanks,
{{ $snipeSettings->site_name }}
@endcomponent

View file

@ -10,7 +10,9 @@
@component('mail::table')
| | |
| ------------- | ------------- |
@if ((isset($item->name)) && ($item->name!=''))
| **{{ trans('mail.asset_name') }}** | {{ $item->name }} |
@endif
| **{{ trans('mail.asset_tag') }}** | {{ $item->asset_tag }} |
@if (isset($item->manufacturer))
| **{{ trans('general.manufacturer') }}** | {{ $item->manufacturer->name }} |

View file

@ -10,7 +10,9 @@
@component('mail::table')
| | |
| ------------- | ------------- |
@if ((isset($item->name)) && ($item->name!=''))
| **{{ trans('mail.asset_name') }}** | {{ $item->name }} |
@endif
| **{{ trans('mail.asset_tag') }}** | {{ $item->asset_tag }} |
@if (isset($item->manufacturer))
| **{{ trans('general.manufacturer') }}** | {{ $item->manufacturer->name }} |

View file

@ -301,6 +301,17 @@
}
// This is only used by the requestable assets section
function assetRequestActionsFormatter (row, value) {
if (value.available_actions.cancel == true) {
return '<form action="{{ url('/') }}/account/request-asset/'+ value.id + '" method="GET"><button class="btn btn-danger btn-sm" data-tooltip="true" title="Cancel this item request">{{ trans('button.cancel') }}</button></form>';
} else if (value.available_actions.request == true) {
return '<form action="{{ url('/') }}/account/request-asset/'+ value.id + '" method="GET"><button class="btn btn-primary btn-sm" data-tooltip="true" title="Request this item">{{ trans('button.request') }}</button></form>';
}
}
var formatters = [
'hardware',

View file

@ -16,8 +16,24 @@ use Illuminate\Http\Request;
Route::group(['prefix' => 'v1','namespace' => 'Api'], function () {
/*--- Accessories API ---*/
Route::group(['prefix' => 'account'], function () {
Route::get('requestable/hardware',
[
'as' => 'api.assets.requestable',
'uses' => 'AssetsController@requestable'
]
);
Route::get('requests',
[
'as' => 'api.assets.requested',
'uses' => 'ProfileController@requestedAssets'
]
);
});
/*--- Accessories API ---*/
Route::resource('accessories', 'AccessoriesController',
['names' =>
[
@ -716,4 +732,5 @@ Route::group(['prefix' => 'v1','namespace' => 'Api'], function () {
);
});

View file

@ -249,6 +249,8 @@ Route::group([ 'prefix' => 'account', 'middleware' => ['auth']], function () {
# View Assets
Route::get('view-assets', [ 'as' => 'view-assets', 'uses' => 'ViewAssetsController@getIndex' ]);
Route::get('requested', [ 'as' => 'account.requested', 'uses' => 'ViewAssetsController@getRequestedAssets' ]);
# Accept Asset
Route::get(
'accept-asset/{logID}',