Major update of assets bulk checkin/checkout feature

Replicate the assets selection method of Bulk Checkout to the Quick Scan Checkin view:
- API: filter dynamic assets list to show only Deployed assets

Add Checkin and Checkout as Bulk actions for selected items:
- API: add ability to handle assets tag or id as parameters to prefill the asset tags list
- Scripts: share commun javascript functions by moving them to snipe-it.js

Add additional fields to be set on Bulk checkin: Status, Checkin Date

Ability to scan assets tags is preserved.
This commit is contained in:
Florent Bervas 2024-08-09 13:46:49 +00:00
parent b2a6349243
commit 44e2581f56
17 changed files with 465 additions and 317 deletions

View file

@ -524,6 +524,10 @@ class AssetsController extends Controller
$assets = $assets->RTD();
}
if ($request->filled('assetStatusType') && $request->input('assetStatusType') === 'Deployed') {
$assets = $assets->Deployed();
}
if ($request->filled('search')) {
$assets = $assets->AssignedSearch($request->input('search'));
}
@ -545,7 +549,7 @@ class AssetsController extends Controller
if ($asset->assetstatus->getStatuslabelType() == 'pending') {
$asset->use_text .= '('.$asset->assetstatus->getStatuslabelType().')';
$asset->use_text .= ' ('.$asset->assetstatus->getStatuslabelType().')';
}
$asset->use_image = ($asset->getImageUrl()) ? $asset->getImageUrl() : null;
@ -800,12 +804,19 @@ class AssetsController extends Controller
* @param string $tag
* @since [v6.0.5]
*/
public function checkoutByTag(AssetCheckoutRequest $request, $tag) : JsonResponse
public function checkoutByTag(AssetCheckoutRequest $request, $tag = null) : JsonResponse
{
if ($asset = Asset::where('asset_tag', $tag)->first()) {
$this->authorize('checkout', Asset::class);
if(null == $tag && null !== ($request->input('asset_tag'))) {
$tag = $request->input('asset_tag');
}
$asset = Asset::where('asset_tag', $tag)->first();
if ($asset) {
return $this->checkout($request, $asset->id);
}
return response()->json(Helper::formatStandardApiResponse('error', null, 'Asset not found'), 200);
return response()->json(Helper::formatStandardApiResponse('error', ['asset_tag' => e($tag)], 'Asset with tag '.e($tag).' not found'));
}
/**
@ -815,22 +826,30 @@ class AssetsController extends Controller
* @param int $assetId
* @since [v4.0]
*/
public function checkout(AssetCheckoutRequest $request, $asset_id) : JsonResponse
public function checkout(AssetCheckoutRequest $request, $asset_id = null) : JsonResponse
{
$this->authorize('checkout', Asset::class);
if(null == $asset_id && null !== ($request->input('asset_id'))) {
$asset_id = $request->input('asset_id');
}
$asset = Asset::findOrFail($asset_id);
$payload = [
'id' => e($asset->id),
'asset_tag'=> e($asset->asset_tag),
'name'=> e($asset->name),
'model' => e($asset->model->name),
'model_number' => e($asset->model->model_number)
];
if (! $asset->availableForCheckout()) {
return response()->json(Helper::formatStandardApiResponse('error', ['asset'=> e($asset->asset_tag)], trans('admin/hardware/message.checkout.not_available')));
return response()->json(Helper::formatStandardApiResponse('error', $payload, trans('admin/hardware/message.checkout.not_available')));
}
$this->authorize('checkout', $asset);
$error_payload = [];
$error_payload['asset'] = [
'id' => $asset->id,
'asset_tag' => $asset->asset_tag,
];
// This item is checked out to a location
if (request('checkout_to_type') == 'location') {
@ -859,7 +878,7 @@ class AssetsController extends Controller
}
if (! isset($target)) {
return response()->json(Helper::formatStandardApiResponse('error', $error_payload, 'Checkout target for asset '.e($asset->asset_tag).' is invalid - '.$error_payload['target_type'].' does not exist.'));
return response()->json(Helper::formatStandardApiResponse('error', $payload, 'Checkout target for asset '.e($asset->asset_tag).' is invalid - '.$error_payload['target_type'].' does not exist.'));
}
$checkout_at = request('checkout_at', date('Y-m-d H:i:s'));
@ -876,12 +895,11 @@ class AssetsController extends Controller
// if ((isset($target->rtd_location_id)) && ($asset->rtd_location_id!='')) {
// $asset->location_id = $target->rtd_location_id;
// }
if ($asset->checkOut($target, auth()->user(), $checkout_at, $expected_checkin, $note, $asset_name, $asset->location_id)) {
return response()->json(Helper::formatStandardApiResponse('success', ['asset'=> e($asset->asset_tag)], trans('admin/hardware/message.checkout.success')));
return response()->json(Helper::formatStandardApiResponse('success', $payload, trans('admin/hardware/message.checkout.success')));
}
return response()->json(Helper::formatStandardApiResponse('error', ['asset'=> e($asset->asset_tag)], trans('admin/hardware/message.checkout.error')));
return response()->json(Helper::formatStandardApiResponse('error', $payload, trans('admin/hardware/message.checkout.error')));
}
@ -892,18 +910,26 @@ class AssetsController extends Controller
* @param int $assetId
* @since [v4.0]
*/
public function checkin(Request $request, $asset_id) : JsonResponse
public function checkin(Request $request, $asset_id = null) : JsonResponse
{
$this->authorize('checkin', Asset::class);
if(null == $asset_id && null !== ($request->input('asset_id'))) {
$asset_id = $request->input('asset_id');
}
$asset = Asset::with('model')->findOrFail($asset_id);
$this->authorize('checkin', $asset);
$payload = [
'id'=> e($asset->id),
'asset_tag'=> e($asset->asset_tag),
'name'=> e($asset->name),
'model' => e($asset->model->name),
'model_number' => e($asset->model->model_number)
];
$target = $asset->assignedTo;
if (is_null($target)) {
return response()->json(Helper::formatStandardApiResponse('error', [
'asset_tag'=> e($asset->asset_tag),
'model' => e($asset->model->name),
'model_number' => e($asset->model->model_number)
], trans('admin/hardware/message.checkin.already_checked_in')));
return response()->json(Helper::formatStandardApiResponse('error', $payload, trans('admin/hardware/message.checkin.already_checked_in')));
}
$asset->expected_checkin = null;
@ -932,11 +958,13 @@ class AssetsController extends Controller
$asset->status_id = $request->input('status_id');
}
$checkin_at = $request->filled('checkin_at') ? $request->input('checkin_at').' '. date('H:i:s') : date('Y-m-d H:i:s');
$originalValues = $asset->getRawOriginal();
if (($request->filled('checkin_at')) && ($request->get('checkin_at') != date('Y-m-d'))) {
// Fixed non-matching condition for bulk-checkin
$checkin_at = date('Y-m-d H:i:s');
if (($request->filled('checkin_at')) && ($request->input('checkin_at') != date('Y-m-d'))) {
$originalValues['action_date'] = $checkin_at;
$checkin_at = $request->input('checkin_at');
}
$asset->licenseseats->each(function (LicenseSeat $seat) {
@ -961,6 +989,7 @@ class AssetsController extends Controller
return response()->json(Helper::formatStandardApiResponse('success', [
'asset_tag'=> e($asset->asset_tag),
'name'=> e($asset->name),
'model' => e($asset->model->name),
'model_number' => e($asset->model->model_number)
], trans('admin/hardware/message.checkin.success')));

View file

@ -146,6 +146,20 @@ class BulkAssetsController extends Controller
->with('statuslabel_list', Helper::statusLabelList())
->with('models', $models->pluck(['model']))
->with('modelNames', $modelNames);
case 'checkin':
$this->authorize('checkin', Asset::class);
return view('hardware/quickscan-checkin')
->with('assets', $assets)
->with('statusLabel_list', Helper::statusLabelList());
case 'checkout':
$this->authorize('checkout', Asset::class);
return view('hardware/bulk-checkout')
->with('assets', $assets)
->with('statusLabel_list', Helper::deployableStatusLabelList());
}
}
@ -167,6 +181,7 @@ class BulkAssetsController extends Controller
break;
case 'location':
$assets->OrderLocation($order);
break;
case 'rtd_location':
$assets->OrderRtdLocation($order);
break;
@ -576,7 +591,7 @@ class BulkAssetsController extends Controller
// Redirect to the asset management page with error
return redirect()->route('hardware.bulkcheckout.show')->with('error', trans('admin/hardware/message.checkout.error'))->withErrors($errors);
} catch (ModelNotFoundException $e) {
return redirect()->route('hardware.bulkcheckout.show')->with('error', $e->getErrors());
return redirect()->route('hardware.bulkcheckout.show')->with('error', trans('admin/hardware/message.update.assets_do_not_exist_or_are_invalid'));
}
}

View file

@ -23,7 +23,8 @@ class SelectlistTransformer
// Loop through the paginated collection to set the array values
foreach ($select_items as $select_item) {
$items_array[] = [
'id' => (int) $select_item->id,
'id' => $select_item->id,
'name' => ($select_item->asset_tag) ? $select_item->asset_tag : null,
'text' => ($select_item->use_text) ? $select_item->use_text : $select_item->name,
'image' => ($select_item->use_image) ? $select_item->use_image : null,

View file

@ -1411,7 +1411,7 @@ class Asset extends Depreciable
public function scopeDeployed($query)
{
return $query->where('assigned_to', '>', '0');
return $query->whereNotNull('assets.assigned_to');
}
/**

View file

@ -159,7 +159,8 @@ trait Loggable
if ($key == 'action_date' && $value != $action_date) {
$changed[$key]['old'] = $value;
$changed[$key]['new'] = is_string($action_date) ? $action_date : $action_date->format('Y-m-d H:i:s');
} elseif ($value != $this->getAttributes()[$key]) {
// Make sure the key exists to prevent php error
} elseif (array_key_exists($key, $this->getAttributes()) && $value != $this->getAttributes()[$key]) {
$changed[$key]['old'] = $value;
$changed[$key]['new'] = $this->getAttributes()[$key];
}

View file

@ -262,55 +262,14 @@ $(function () {
$(".select2-hidden-accessible").on('select2:closing', function (e) {
var element = $(this);
var value = getSelect2Value(element);
var assetStatusType = element.data("asset-status-type");
var noForceAjax = false;
var isMouseUp = false;
if(e.params.args.originalSelect2Event) noForceAjax = e.params.args.originalSelect2Event.noForceAjax;
if(e.params.args.originalEvent) isMouseUp = e.params.args.originalEvent.type == "mouseup";
if(value && !noForceAjax && !isMouseUp) {
var endpoint = element.data("endpoint");
var assetStatusType = element.data("asset-status-type");
$.ajax({
url: baseUrl + 'api/v1/' + endpoint + '/selectlist?search='+value+'&page=1' + (assetStatusType ? '&assetStatusType='+assetStatusType : ''),
dataType: 'json',
headers: {
"X-Requested-With": 'XMLHttpRequest',
"X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr('content')
},
}).done(function(response) {
var currentlySelected = element.select2('data').map(function (x){
return +x.id;
}).filter(function (x) {
return x !== 0;
});
// makes sure we're not selecting the same thing twice for multiples
var filteredResponse = response.results.filter(function(item) {
return currentlySelected.indexOf(+item.id) < 0;
});
var first = (currentlySelected.length > 0) ? filteredResponse[0] : response.results[0];
if(first && first.id) {
first.selected = true;
if($("option[value='" + first.id + "']", element).length < 1) {
var option = new Option(first.text, first.id, true, true);
element.append(option);
} else {
var isMultiple = element.attr("multiple") == "multiple";
element.val(isMultiple? element.val().concat(first.id) : element.val(first.id));
}
element.trigger('change');
element.trigger({
type: 'select2:select',
params: {
data: first
}
});
}
});
assetSearch($(this), value, assetStatusType);
}
});
@ -597,3 +556,58 @@ document.addEventListener('livewire:init', () => {
});
});
});
/* Set of functions to query assets informations
Mainly used to update the select2 field content
*/
function assetSearch(element, string, assetStatusType){
var endpoint = element.data("endpoint");
$.ajax({
url: baseUrl + 'api/v1/' + endpoint + '/selectlist?search=' + string + '&page=1' + (assetStatusType ? '&assetStatusType=' + assetStatusType : ''),
dataType: 'json',
headers: {
"X-Requested-With": 'XMLHttpRequest',
"X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr('content')
}
}).done(function (response) {
var currentlySelected = element.select2('data').map(function (x) {
return +x.id;
}).filter(function (x) {
return x !== 0;
});
// makes sure we're not selecting the same thing twice for multiples
var filteredResponse = response.results.filter(function (item) {
return currentlySelected.indexOf(+item.id) < 0;
});
var first = currentlySelected.length > 0 ? filteredResponse[0] : response.results[0];
if (first && first.id) {
first.selected = true;
if ($("option[value='" + first.id + "']", element).length < 1) {
var option = new Option(first.text, first.id, false, true);
element.append(option);
} else {
var isMultiple = element.attr("multiple") == "multiple";
element.val(isMultiple ? element.val().concat(first.id) : element.val(first.id));
}
element.trigger('change');
element.trigger({
type: 'select2:select',
params: {
data: first
}
});
}
});
}
/* Make this function available into global scope */
window.load_bulkassets = function (select_assets_id, assets){
// Add options to the select2 box
for(let k = 0; k < assets.length; k++){
var search_string = assets[k].asset_tag;
var element = $("#" + select_assets_id);
assetSearch(element, search_string, null);
}
return true;
}

View file

@ -5,6 +5,8 @@ return [
'add' => 'Add New',
'cancel' => 'Cancel',
'checkin_and_delete' => 'Checkin All / Delete User',
'checkin' => 'Checkin',
'checkout' => 'Checkout',
'delete' => 'Delete',
'edit' => 'Edit',
'clone' => 'Clone',

View file

@ -62,6 +62,7 @@ return [
'checkin' => 'Checkin',
'checkin_from' => 'Checkin from',
'checkout' => 'Checkout',
'checkout_status' => 'Checkout Status',
'checkouts_count' => 'Checkouts',
'checkins_count' => 'Checkins',
'user_requests_count' => 'Requests',

View file

@ -9,105 +9,124 @@
{{-- Page content --}}
@section('content')
<style>
.input-group {
padding-left: 0px !important;
}
</style>
<style>
.input-group {
padding-left: 0px !important;
width: 100% !important;
}
</style>
<div class="row">
<!-- left column -->
<div class="col-md-7">
<div class="box box-default">
<div class="box-header with-border">
<h2 class="box-title"> {{ trans('admin/hardware/form.tag') }} </h2>
</div>
<div class="box-body">
<form class="form-horizontal" method="post" action="" autocomplete="off">
{{ csrf_field() }}
<div class="row">
{{ Form::open(['method' => 'POST', 'class' => ['form-horizontal','checkout-form'], 'role' => 'form', 'id' => 'checkinout-form' ]) }}
<!-- left column -->
<div class="col-md-6">
<div class="box box-default">
<div class="box-header with-border">
<h2 class="box-title"> {{ trans('admin/hardware/form.tag') }} </h2>
</div>
<div class="box-body">
{{csrf_field()}}
<!-- Checkout selector -->
@include ('partials.forms.checkout-selector', ['user_select' => 'true','asset_select' => 'true', 'location_select' => 'true'])
<!-- Checkout selector -->
@include ('partials.forms.checkout-selector', ['user_select' => 'true','asset_select' => 'true', 'location_select' => 'true'])
@include ('partials.forms.edit.user-select', ['translated_name' => trans('general.user'), 'fieldname' => 'assigned_user', 'required'=>'true'])
@include ('partials.forms.edit.asset-select', ['translated_name' => trans('general.asset'), 'fieldname' => 'assigned_asset_select', 'unselect' => 'true', 'style' => 'display:none;', 'required'=>'true'])
@include ('partials.forms.edit.location-select', ['translated_name' => trans('general.location'), 'fieldname' => 'assigned_location', 'style' => 'display:none;', 'required'=>'true'])
@include ('partials.forms.edit.user-select', ['translated_name' => trans('general.user'), 'fieldname' => 'assigned_user', 'required'=>'true'])
@include ('partials.forms.edit.asset-select', ['translated_name' => trans('general.asset'), 'fieldname' => 'assigned_asset', 'unselect' => 'true', 'style' => 'display:none;', 'required'=>'true'])
@include ('partials.forms.edit.location-select', ['translated_name' => trans('general.location'), 'fieldname' => 'assigned_location', 'style' => 'display:none;', 'required'=>'true'])
<!-- Checkout/Checkin Date -->
<div class="form-group {{ $errors->has('checkout_at') ? 'error' : '' }}">
<label for="checkout_at" class="col-sm-3 control-label">
{{ trans('admin/hardware/form.checkout_date') }}
</label>
<div class="col-md-8">
<div class="input-group date col-md-5" data-provide="datepicker" data-date-format="yyyy-mm-dd" data-date-end-date="0d" data-date-clear-btn="true">
<input type="text" class="form-control" placeholder="{{ trans('general.select_date') }}" name="checkout_at" id="checkout_at" value="{{ old('checkout_at') }}">
<span class="input-group-addon"><i class="fas fa-calendar" aria-hidden="true"></i></span>
</div>
{!! $errors->first('checkout_at', '<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!}
</div>
</div>
<!-- Checkout/Checkin Date -->
<div class="form-group {{ $errors->has('checkout_at') ? 'error' : '' }}">
<label for="checkout_at" class="col-sm-3 control-label">
{{ trans('admin/hardware/form.checkout_date') }}
</label>
<div class="col-md-8">
<div class="input-group date col-md-5" data-provide="datepicker" data-date-format="yyyy-mm-dd" data-date-end-date="0d" data-date-clear-btn="true">
<input type="text" class="form-control" placeholder="{{ trans('general.select_date') }}" name="checkout_at" id="checkout_at" value="{{ old('checkout_at') }}">
<span class="input-group-addon"><x-icon type="calendar" /></span>
</div>
{!! $errors->first('checkout_at', '<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!}
</div>
</div>
<!-- Expected Checkin Date -->
<div class="form-group {{ $errors->has('expected_checkin') ? 'error' : '' }}">
<label for="expected_checkin" class="col-sm-3 control-label">
{{ trans('admin/hardware/form.expected_checkin') }}
</label>
<div class="col-md-8">
<div class="input-group date col-md-5" data-provide="datepicker" data-date-format="yyyy-mm-dd" data-date-start-date="0d" data-date-clear-btn="true">
<input type="text" class="form-control" placeholder="{{ trans('general.select_date') }}" name="expected_checkin" id="expected_checkin" value="{{ old('expected_checkin') }}">
<span class="input-group-addon"><x-icon type="calendar" /></span>
</div>
{!! $errors->first('expected_checkin', '<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!}
</div>
</div>
<!-- Expected Checkin Date -->
<div class="form-group {{ $errors->has('expected_checkin') ? 'error' : '' }}">
<label for="expected_checkin" class="col-sm-3 control-label">
{{ trans('admin/hardware/form.expected_checkin') }}
</label>
<div class="col-md-8">
<div class="input-group date col-md-5" data-provide="datepicker" data-date-format="yyyy-mm-dd" data-date-start-date="0d" data-date-clear-btn="true">
<input type="text" class="form-control" placeholder="{{ trans('general.select_date') }}" name="expected_checkin" id="expected_checkin" value="{{ old('expected_checkin') }}">
<span class="input-group-addon"><x-icon type="calendar" /></span>
</div>
{!! $errors->first('expected_checkin', '<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!}
</div>
</div>
<!-- Note -->
<div class="form-group {{ $errors->has('note') ? 'error' : '' }}">
<label for="note" class="col-sm-3 control-label">
{{ trans('general.notes') }}
</label>
<div class="col-md-8">
<textarea class="col-md-6 form-control" id="note" name="note">{{ old('note') }}</textarea>
{!! $errors->first('note', '<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!}
<!-- Note -->
<div class="form-group {{ $errors->has('note') ? 'error' : '' }}">
<label for="note" class="col-sm-3 control-label">
{{ trans('general.notes') }}
</label>
<div class="col-md-8">
<textarea class="col-md-6 form-control" id="note" name="note">{{ old('note') }}</textarea>
{!! $errors->first('note', '<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!}
</div>
</div>
@include ('partials.forms.edit.asset-select', [
'translated_name' => trans('general.assets'),
// Fieldname is empty so not included in the form
'fieldname' => '',
'multiple' => 'true',
'asset_status_type' => 'RTD',
'select_id' => 'bulk_assets_select',
'required' => true
])
</div> <!--./box-body-->
<div class="box-footer">
<a class="btn btn-link" href="{{ URL::previous() }}"> {{ trans('button.cancel') }}</a>
<button type="submit" id="checkinout_button" class="btn btn-primary pull-right"><i class="fas fa-check icon-white" aria-hidden="true"></i> {{ trans('general.checkout') }}</button>
</div>
</div>
</div>
{{Form::close()}}
</div> <!--/.col-md-6-->
@include ('partials.forms.edit.asset-select', [
'translated_name' => trans('general.assets'),
'fieldname' => 'selected_assets[]',
'multiple' => true,
'asset_status_type' => 'RTD',
'select_id' => 'assigned_assets_select',
])
<div class="col-md-6">
<div class="box box-default" id="checkedinout-div" style="display: none">
<div class="box-header with-border">
<h2 class="box-title"> {{ trans('general.checkout_status') }} (<span id="checkinout-counter">0</span> {{ trans('general.assets_checked_out_count') }}) </h2>
</div>
<div class="box-body">
</div> <!--./box-body-->
<div class="box-footer">
<a class="btn btn-link" href="{{ URL::previous() }}"> {{ trans('button.cancel') }}</a>
<button type="submit" class="btn btn-primary pull-right"><x-icon type="checkmark" /> {{ trans('general.checkout') }}</button>
</div>
</div>
</form>
</div> <!--/.col-md-7-->
<!-- right column -->
<div class="col-md-5" id="current_assets_box" style="display:none;">
<div class="box box-primary">
<div class="box-header with-border">
<h2 class="box-title">{{ trans('admin/users/general.current_assets') }}</h2>
</div>
<div class="box-body">
<div id="current_assets_content">
<table id="checkedinout" class="table table-striped snipe-table">
<thead>
<tr>
<th>{{ trans('general.asset_tag') }}</th>
<th>{{ trans('general.asset_name') }}</th>
<th>{{ trans('general.asset_model') }}</th>
<th>{{ trans('general.model_no') }}</th>
<th>{{ trans('general.checkout_status') }}</th>
<th></th>
</tr>
<tr id="checkinout-loader" style="display: none;">
<td colspan="3">
<i class="fas fa-spinner spin" aria-hidden="true"></i> {{ trans('general.processing') }}...
</td>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@stop
@section('moar_scripts')
@include('partials/assets-assigned')
@parent
@include('partials/assets-checkinout')
@include('partials/assets-assigned')
@stop

View file

@ -142,4 +142,4 @@
</div>
</div>
@stop
@stop

View file

@ -10,16 +10,13 @@
@section('content')
<style>
.input-group {
padding-left: 0px !important;
}
</style>
<div class="row">
{{ Form::open(['method' => 'POST', 'class' => 'form-horizontal', 'role' => 'form', 'id' => 'checkin-form' ]) }}
{{ Form::open(['method' => 'POST', 'class' => ['form-horizontal','checkin-form'], 'role' => 'form', 'id' => 'checkinout-form' ]) }}
<!-- left column -->
<div class="col-md-6">
<div class="box box-default">
@ -30,16 +27,16 @@
{{csrf_field()}}
<!-- Asset Tag -->
<div class="form-group {{ $errors->has('asset_tag') ? 'error' : '' }}">
{{ Form::label('asset_tag', trans('general.asset_tag'), array('class' => 'col-md-3 control-label', 'id' => 'checkin_tag')) }}
<div class="col-md-9">
<div class="input-group col-md-11 required">
<input type="text" class="form-control" name="asset_tag" id="asset_tag" value="{{ old('asset_tag') }}" required>
</div>
{!! $errors->first('asset_tag', '<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!}
</div>
</div>
@include ('partials.forms.edit.asset-select', [
'translated_name' => trans('general.asset_tag'),
// Fieldname is empty so not included in the form
'fieldname' => '',
'multiple' => true,
'asset_status_type' => 'Deployed',
'select_id' => 'bulk_assets_select',
'required' => true
])
{!! $errors->first('asset_tag', '<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!}
<!-- Status -->
<div class="form-group {{ $errors->has('status_id') ? 'error' : '' }}">
@ -55,6 +52,27 @@
<!-- Locations -->
@include ('partials.forms.edit.location-select', ['translated_name' => trans('general.location'), 'fieldname' => 'location_id'])
<!-- Checkout/Checkin Date -->
<div class="form-group{{ $errors->has('checkin_at') ? ' has-error' : '' }}">
<label for="checkin_at" class="col-sm-3 control-label">
{{ trans('admin/hardware/form.checkin_date') }}
</label>
<div class="col-md-8">
<div class="input-group col-md-5 required">
<div class="input-group date" data-provide="datepicker"
data-date-format="yyyy-mm-dd" data-autoclose="true">
<input type="text" class="form-control"
placeholder="{{ trans('general.select_date') }}"
name="checkin_at" id="checkin_at"
value="{{ old('checkin_at', date('Y-m-d')) }}">
<span class="input-group-addon"><i class="fas fa-calendar" aria-hidden="true"></i></span>
</div>
{!! $errors->first('checkin_at', '<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!}
</div>
</div>
</div>
<!-- Note -->
<div class="form-group {{ $errors->has('note') ? 'error' : '' }}">
{{ Form::label('note', trans('admin/hardware/form.notes'), array('class' => 'col-md-3 control-label')) }}
@ -63,17 +81,13 @@
{!! $errors->first('note', '<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!}
</div>
</div>
</div> <!--/.box-body-->
<div class="box-footer">
<a class="btn btn-link" href="{{ route('hardware.index') }}"> {{ trans('button.cancel') }}</a>
<button type="submit" id="checkin_button" class="btn btn-success pull-right"><x-icon type="checkmark" /> {{ trans('general.checkin') }}</button>
<a class="btn btn-link" href="{{ URL::previous() }}"> {{ trans('button.cancel') }}</a>
<button type="submit" id="checkinout_button" class="btn btn-success pull-right"><x-icon type="checkmark" /> {{ trans('general.checkin') }}</button>
</div>
</div>
@ -82,22 +96,23 @@
</div> <!--/.col-md-6-->
<div class="col-md-6">
<div class="box box-default" id="checkedin-div" style="display: none">
<div class="box box-default" id="checkedinout-div">
<div class="box-header with-border">
<h2 class="box-title"> {{ trans('general.quickscan_checkin_status') }} (<span id="checkin-counter">0</span> {{ trans('general.assets_checked_in_count') }}) </h2>
<h2 class="box-title"> {{ trans('general.quickscan_checkin_status') }} (<span id="checkinout-counter">0</span> {{ trans('general.assets_checked_in_count') }}) </h2>
</div>
<div class="box-body">
<table id="checkedin" class="table table-striped snipe-table">
<table id="checkedinout" class="table table-striped snipe-table">
<thead>
<tr>
<th>{{ trans('general.asset_tag') }}</th>
<th>{{ trans('general.asset_name') }}</th>
<th>{{ trans('general.asset_model') }}</th>
<th>{{ trans('general.model_no') }}</th>
<th>{{ trans('general.quickscan_checkin_status') }}</th>
<th></th>
</tr>
<tr id="checkin-loader" style="display: none;">
<tr id="checkinout-loader" style="display: none;">
<td colspan="3">
<x-icon type="spinner" /> {{ trans('general.processing') }}...
</td>
@ -109,92 +124,12 @@
</div>
</div>
</div>
</div>
@stop
@section('moar_scripts')
<script nonce="{{ csrf_token() }}">
$("#checkin-form").submit(function (event) {
$('#checkedin-div').show();
$('#checkin-loader').show();
event.preventDefault();
var form = $("#checkin-form").get(0);
var formData = $('#checkin-form').serializeArray();
$.ajax({
url: "{{ route('api.asset.checkinbytag') }}",
type : 'POST',
headers: {
"X-Requested-With": 'XMLHttpRequest',
"X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr('content')
},
dataType : 'json',
data : formData,
success : function (data) {
if (data.status == 'success') {
$('#checkedin tbody').prepend("<tr class='success'><td>" + data.payload.asset_tag + "</td><td>" + data.payload.model + "</td><td>" + data.payload.model_number + "</td><td>" + data.messages + "</td><td><i class='fas fa-check text-success'></i></td></tr>");
@if ($user->enable_sounds)
var audio = new Audio('{{ config('app.url') }}/sounds/success.mp3');
audio.play()
@endif
incrementOnSuccess();
} else {
handlecheckinFail(data);
}
$('input#asset_tag').val('');
},
error: function (data) {
handlecheckinFail(data);
},
complete: function() {
$('#checkin-loader').hide();
}
});
return false;
});
function handlecheckinFail (data) {
@if ($user->enable_sounds)
var audio = new Audio('{{ config('app.url') }}/sounds/error.mp3');
audio.play()
@endif
if (data.payload.asset_tag) {
var asset_tag = data.payload.asset_tag;
var model = data.payload.model;
var model_number = data.payload.model_number;
} else {
var asset_tag = '';
var model = '';
var model_number = '';
}
if (data.messages) {
var messages = data.messages;
} else {
var messages = '';
}
$('#checkedin tbody').prepend("<tr class='danger'><td>" + asset_tag + "</td><td>" + model + "</td><td>" + model_number + "</td><td>" + messages + "</td><td><i class='fas fa-times text-danger'></i></td></tr>");
}
function incrementOnSuccess() {
var x = parseInt($('#checkin-counter').html());
y = x + 1;
$('#checkin-counter').html(y);
}
$("#checkin_tag").focus();
</script>
@parent
@include('partials/assets-checkinout')
@stop

View file

@ -1194,57 +1194,40 @@
</div> <!-- /.tab-pane components -->
<div class="tab-pane fade" id="assets">
<div class="row{{($asset->assignedAssets->count() > 0 ) ? '' : ' hidden-print'}}">
<div class="col-md-12">
<div class="tab-pane fade" id="assets">
@if ($asset->assignedAssets->count() > 0)
<!-- checked out assets table -->
@if ($asset->assignedAssets->count() > 0)
@include('partials.asset-bulk-actions')
{{ Form::open([
'method' => 'POST',
'route' => ['hardware/bulkedit'],
'class' => 'form-inline',
'id' => 'bulkForm']) }}
<div id="toolbar">
<label for="bulk_actions"><span class="sr-only">{{ trans('general.bulk_actions')}}</span></label>
<select name="bulk_actions" class="form-control select2" style="width: 150px;" aria-label="bulk_actions">
<option value="edit">{{ trans('button.edit') }}</option>
<option value="delete">{{ trans('button.delete')}}</option>
<option value="labels">{{ trans_choice('button.generate_labels', 2) }}</option>
</select>
<button class="btn btn-primary" id="{{ (isset($id_button)) ? $id_button : 'bulkAssetEditButton' }}" disabled>{{ trans('button.go') }}</button>
</div>
<!-- checked out assets table -->
<div class="table-responsive">
<table
data-columns="{{ \App\Presenters\AssetPresenter::dataTableLayout() }}"
data-cookie-id-table="assetsTable"
data-pagination="true"
data-id-table="assetsTable"
data-search="true"
data-side-pagination="server"
data-show-columns="true"
data-show-fullscreen="true"
data-show-export="true"
data-show-refresh="true"
data-sort-order="asc"
data-bulk-button-id="#bulkAssetEditButton"
id="assetsListingTable"
class="table table-striped snipe-table"
data-url="{{route('api.assets.index',['assigned_to' => $asset->id, 'assigned_type' => 'App\Models\Asset']) }}"
data-export-options='{
"fileName": "export-assets-{{ str_slug($asset->name) }}-assets-{{ date('Y-m-d') }}",
"ignoreColumn": ["actions","image","change","checkbox","checkincheckout","icon"]
}'>
</table>
{{ Form::close() }}
</div>
<div class="table table-responsive">
<table
data-click-to-select="true"
data-columns="{{ \App\Presenters\AssetPresenter::dataTableLayout() }}"
data-cookie-id-table="assetsTable"
data-pagination="true"
data-id-table="assetsTable"
data-search="true"
data-side-pagination="server"
data-show-columns="true"
data-show-fullscreen="true"
data-show-export="true"
data-show-footer="true"
data-show-refresh="true"
data-sort-order="asc"
data-sort-name="name"
data-toolbar="#assetsBulkEditToolbar"
data-bulk-button-id="#bulkAssetEditButton"
data-bulk-form-id="#assetsBulkForm"
id="assetsListingTable"
class="table table-striped snipe-table"
data-url="{{ route('api.assets.index',['assigned_to' => e($asset->id), 'assigned_type' => 'App\Models\Asset']) }}"
data-export-options='{
"fileName": "export-{{ str_slug($asset->name) }}-assets-{{ date('Y-m-d') }}",
"ignoreColumn": ["actions","image","change","checkbox","checkincheckout","icon"]
}'>
</table>
</div>
@else
@ -1573,4 +1556,4 @@
</script>
@include ('partials.bootstrap-table')
@stop
@stop

View file

@ -14,7 +14,7 @@
{{ trans('button.bulk_actions') }}
</span>
</label>
<select name="bulk_actions" class="form-control select2" aria-label="bulk_actions" style="min-width: 350px;">
<select name="bulk_actions" class="form-control select2" aria-label="bulk_actions" style="width: 350px;">
@if((isset($status)) && ($status == 'Deleted'))
@can('delete', \App\Models\Asset::class)
<option value="restore">{{trans('button.restore')}}</option>
@ -26,6 +26,12 @@
@can('delete', \App\Models\Asset::class)
<option value="delete">{{ trans('button.delete') }}</option>
@endcan
@can('checkout', \App\Models\Asset::class)
<option value="checkout">{{ trans('button.checkout') }}</option>
@endcan
@can('checkin', \App\Models\Asset::class)
<option value="checkin">{{ trans('button.checkin') }}</option>
@endcan
<option value="labels" {{$snipeSettings->shortcuts_enabled == 1 ? "accesskey=l" : ''}}>{{ trans_choice('button.generate_labels', 2) }}</option>
@endif
</select>

View file

@ -0,0 +1,101 @@
<script nonce="{{ csrf_token() }}">
$("#checkinout-form").submit(function (event) {
$('#checkedinout-div').show();
$('#checkinout-loader').show();
event.preventDefault();
var form = $("#checkinout-form").get(0);
var formData = $('#checkinout-form').serializeArray();
var assets = $('#bulk_assets_select').val();
for(let i = 0; i < assets.length; i++){
// For each asset, override previous 'asset_id' value
formData = formData.filter(a => !(a.name === "asset_id"));
formData.push({name: 'asset_id', value: assets[i]});
if ($("#checkinout-form").hasClass("checkout-form")) {
checkinout_url = "{{ route('api.asset.checkoutbyid') }}";
} else if ($("#checkinout-form").hasClass("checkin-form")) {
checkinout_url = "{{ route('api.asset.checkinbyid') }}";
} else {
// Should handle the error
}
$.ajax({
url: checkinout_url,
type : 'POST',
headers: {
"X-Requested-With": 'XMLHttpRequest',
"X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr('content')
},
dataType : 'json',
data : formData,
success : function (data) {
if (data.status == 'success') {
$('#checkedinout tbody').prepend("<tr class='success'><td>" + data.payload.asset_tag + "</td><td>" + data.payload.name + "</td><td>" + data.payload.model + "</td><td>" + data.payload.model_number + "</td><td>" + data.messages + "</td><td><i class='fas fa-check text-success'></i></td></tr>");
@if ($user->enable_sounds)
if ( ! assets.length > 1 ) {
var audio = new Audio('{{ config('app.url') }}/sounds/success.mp3');
audio.play()
}
@endif
incrementOnSuccess();
} else {
handlecheckinoutFail(data);
}
$('input#asset_tag').val('');
},
error: function (data) {
handlecheckinoutFail(data);
},
complete: function() {
$('#checkinout-loader').hide();
}
});
}
$('#checkinout-loader').hide();
return false;
});
function handlecheckinoutFail (data) {
@if ($user->enable_sounds)
var audio = new Audio('{{ config('app.url') }}/sounds/error.mp3');
audio.play()
@endif
if (data.payload && data.payload.asset_tag) {
var asset_tag = data.payload.asset_tag;
var name = data.payload.name;
var model = data.payload.model;
var model_number = data.payload.model_number;
} else {
var asset_tag = '';
var name = '';
var model = '';
var model_number = '';
}
if (data.messages) {
var messages = data.messages;
} else {
var messages = '';
}
$('#checkedinout tbody').prepend("<tr class='danger'><td>" + asset_tag + "</td><td>" + name + "</td><td>" + model + "</td><td>" + model_number + "</td><td>" + messages + "</td><td><i class='fas fa-times text-danger'></i></td></tr>");
}
function incrementOnSuccess() {
var x = parseInt($('#checkinout-counter').html());
y = x + 1;
$('#checkinout-counter').html(y);
}
$("#checkinout_tag").focus();
</script>

View file

@ -955,7 +955,9 @@
});
};
$('.search button[name=clearSearch]').click(searchboxHighlighter);
$('.search button[name=clearSearch]').click(function(event) {
searchboxHighlighter(event);
});
searchboxHighlighter({ name:'pageload'});
$('.search-input').keyup(searchboxHighlighter);

View file

@ -1,8 +1,10 @@
<!-- Asset -->
<div id="assigned_asset" class="form-group{{ $errors->has($fieldname) ? ' has-error' : '' }}"{!! (isset($style)) ? ' style="'.e($style).'"' : '' !!}>
<div id="{{ isset($divname) ? $divname : 'assigned_asset' }}" class="form-group{{ $errors->has($fieldname) ? ' has-error' : '' }}"{!! (isset($style)) ? ' style="'.e($style).'"' : '' !!}>
{{ Form::label($fieldname, $translated_name, array('class' => 'col-md-3 control-label')) }}
<div class="col-md-8{{ ((isset($required) && ($required =='true'))) ? ' required' : '' }}">
<select class="js-data-ajax select2" data-endpoint="hardware" data-placeholder="{{ trans('general.select_asset') }}" aria-label="{{ $fieldname }}" name="{{ $fieldname }}" style="width: 100%" id="{{ (isset($select_id)) ? $select_id : 'assigned_asset_select' }}"{{ (isset($multiple)) ? ' multiple' : '' }}{!! (!empty($asset_status_type)) ? ' data-asset-status-type="' . $asset_status_type . '"' : '' !!}>
<select class="js-data-ajax select2" data-endpoint="hardware" data-placeholder="{{ trans('general.select_asset') }}" aria-label="{{ $fieldname }}" name="{{ $fieldname }}" style="width: 100%" id="{{ $select_id = (isset($select_id)) ? $select_id : 'assigned_asset_select' }}"{{ (isset($multiple)) ? ' multiple' : '' }}{!! (!empty($asset_status_type)) ? ' data-asset-status-type="' . $asset_status_type . '"' : '' !!}>
@if ((!isset($unselect)) && ($asset_id = old($fieldname, (isset($asset) ? $asset->id : (isset($item) ? $item->{$fieldname} : '')))))
<option value="{{ $asset_id }}" selected="selected" role="option" aria-selected="true" role="option">
@ -18,3 +20,19 @@
{!! $errors->first($fieldname, '<div class="col-md-8 col-md-offset-3"><span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span></div>') !!}
</div>
@section('moar_scripts')
@parent
@if (isset($assets) && isset($multiple))
<script nonce="{{ csrf_token() }}">
$(document).ready(function() {
var assets = [];
@foreach ($assets as $asset)
assets.push(<?php echo $asset; ?>);
@endforeach
window.load_bulkassets("{{ $select_id }}", assets);
});
</script>
@endif
@stop

View file

@ -473,6 +473,13 @@ Route::group(['prefix' => 'v1', 'middleware' => ['api', 'throttle:api']], functi
]
)->name('api.assets.checkout.bytag');
Route::post('checkoutbytag',
[
Api\AssetsController::class,
'checkoutbytag'
]
)->name('api.asset.checkoutbytag');
Route::post('bytag/{any}/checkin',
[
Api\AssetsController::class,
@ -537,6 +544,20 @@ Route::group(['prefix' => 'v1', 'middleware' => ['api', 'throttle:api']], functi
]
)->name('api.asset.checkout');
Route::post('checkout',
[
Api\AssetsController::class,
'checkout'
]
)->name('api.asset.checkoutbyid');
Route::post('checkin',
[
Api\AssetsController::class,
'checkin'
]
)->name('api.asset.checkinbyid');
Route::post('{asset_id}/restore',
[
Api\AssetsController::class,