mirror of
https://github.com/snipe/snipe-it.git
synced 2025-01-12 06:17:28 -08:00
Merge pull request #15071 from snipe/fixes/small_consumables_optimizations
Small consumables optimizations
This commit is contained in:
commit
c5c5ac58e5
|
@ -5,6 +5,7 @@ namespace App\Http\Controllers\Api;
|
|||
use App\Events\CheckoutableCheckedOut;
|
||||
use App\Helpers\Helper;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\StoreConsumableRequest;
|
||||
use App\Http\Transformers\ConsumablesTransformer;
|
||||
use App\Http\Transformers\SelectlistTransformer;
|
||||
use App\Models\Company;
|
||||
|
@ -27,27 +28,8 @@ class ConsumablesController extends Controller
|
|||
{
|
||||
$this->authorize('index', Consumable::class);
|
||||
|
||||
// This array is what determines which fields should be allowed to be sorted on ON the table itself, no relations
|
||||
// Relations will be handled in query scopes a little further down.
|
||||
$allowed_columns =
|
||||
[
|
||||
'id',
|
||||
'name',
|
||||
'order_number',
|
||||
'min_amt',
|
||||
'purchase_date',
|
||||
'purchase_cost',
|
||||
'company',
|
||||
'category',
|
||||
'model_number',
|
||||
'item_no',
|
||||
'qty',
|
||||
'image',
|
||||
'notes',
|
||||
];
|
||||
|
||||
$consumables = Consumable::select('consumables.*')
|
||||
->with('company', 'location', 'category', 'users', 'manufacturer');
|
||||
$consumables = Consumable::with('company', 'location', 'category', 'supplier', 'manufacturer')
|
||||
->withCount('users as consumables_users_count');
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$consumables = $consumables->TextSearch(e($request->input('search')));
|
||||
|
@ -89,15 +71,9 @@ class ConsumablesController extends Controller
|
|||
// Make sure the offset and limit are actually integers and do not exceed system limits
|
||||
$offset = ($request->input('offset') > $consumables->count()) ? $consumables->count() : app('api_offset_value');
|
||||
$limit = app('api_limit_value');
|
||||
|
||||
$allowed_columns = ['id', 'name', 'order_number', 'min_amt', 'purchase_date', 'purchase_cost', 'company', 'category', 'model_number', 'item_no', 'manufacturer', 'location', 'qty', 'image'];
|
||||
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
|
||||
|
||||
$sort_override = $request->input('sort');
|
||||
$column_sort = in_array($sort_override, $allowed_columns) ? $sort_override : 'created_at';
|
||||
|
||||
|
||||
switch ($sort_override) {
|
||||
switch ($request->input('sort')) {
|
||||
case 'category':
|
||||
$consumables = $consumables->OrderCategory($order);
|
||||
break;
|
||||
|
@ -111,10 +87,30 @@ class ConsumablesController extends Controller
|
|||
$consumables = $consumables->OrderCompany($order);
|
||||
break;
|
||||
case 'supplier':
|
||||
$components = $consumables->OrderSupplier($order);
|
||||
$consumables = $consumables->OrderSupplier($order);
|
||||
break;
|
||||
default:
|
||||
$consumables = $consumables->orderBy($column_sort, $order);
|
||||
// This array is what determines which fields should be allowed to be sorted on ON the table itself.
|
||||
// These must match a column on the consumables table directly.
|
||||
$allowed_columns = [
|
||||
'id',
|
||||
'name',
|
||||
'order_number',
|
||||
'min_amt',
|
||||
'purchase_date',
|
||||
'purchase_cost',
|
||||
'company',
|
||||
'category',
|
||||
'model_number',
|
||||
'item_no',
|
||||
'manufacturer',
|
||||
'location',
|
||||
'qty',
|
||||
'image'
|
||||
];
|
||||
|
||||
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'created_at';
|
||||
$consumables = $consumables->orderBy($sort, $order);
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -131,7 +127,7 @@ class ConsumablesController extends Controller
|
|||
* @since [v4.0]
|
||||
* @param \App\Http\Requests\ImageUploadRequest $request
|
||||
*/
|
||||
public function store(ImageUploadRequest $request) : JsonResponse
|
||||
public function store(StoreConsumableRequest $request) : JsonResponse
|
||||
{
|
||||
$this->authorize('create', Consumable::class);
|
||||
$consumable = new Consumable;
|
||||
|
@ -167,7 +163,7 @@ class ConsumablesController extends Controller
|
|||
* @param \App\Http\Requests\ImageUploadRequest $request
|
||||
* @param int $id
|
||||
*/
|
||||
public function update(ImageUploadRequest $request, $id) : JsonResponse
|
||||
public function update(StoreConsumableRequest $request, $id) : JsonResponse
|
||||
{
|
||||
$this->authorize('update', Consumable::class);
|
||||
$consumable = Consumable::findOrFail($id);
|
||||
|
|
|
@ -4,12 +4,11 @@ namespace App\Http\Controllers\Consumables;
|
|||
|
||||
use App\Events\CheckoutableCheckedOut;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Accessory;
|
||||
use App\Models\Consumable;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Input;
|
||||
use \Illuminate\Contracts\View\View;
|
||||
use \Illuminate\Http\RedirectResponse;
|
||||
|
||||
class ConsumableCheckoutController extends Controller
|
||||
{
|
||||
|
@ -20,13 +19,11 @@ class ConsumableCheckoutController extends Controller
|
|||
* @see ConsumableCheckoutController::store() method that stores the data.
|
||||
* @since [v1.0]
|
||||
* @param int $id
|
||||
* @return \Illuminate\Contracts\View\View
|
||||
* @throws \Illuminate\Auth\Access\AuthorizationException
|
||||
*/
|
||||
public function create($id)
|
||||
public function create($id) : View | RedirectResponse
|
||||
{
|
||||
|
||||
if ($consumable = Consumable::with('users')->find($id)) {
|
||||
if ($consumable = Consumable::find($id)) {
|
||||
|
||||
$this->authorize('checkout', $consumable);
|
||||
|
||||
|
|
|
@ -8,8 +8,10 @@ use App\Http\Requests\ImageUploadRequest;
|
|||
use App\Models\Company;
|
||||
use App\Models\Consumable;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Input;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use \Illuminate\Contracts\View\View;
|
||||
use App\Http\Requests\StoreConsumableRequest;
|
||||
|
||||
/**
|
||||
* This controller handles all actions related to Consumables for
|
||||
|
@ -62,7 +64,7 @@ class ConsumablesController extends Controller
|
|||
* @return \Illuminate\Http\RedirectResponse
|
||||
* @throws \Illuminate\Auth\Access\AuthorizationException
|
||||
*/
|
||||
public function store(ImageUploadRequest $request)
|
||||
public function store(StoreConsumableRequest $request)
|
||||
{
|
||||
$this->authorize('create', Consumable::class);
|
||||
$consumable = new Consumable();
|
||||
|
@ -99,10 +101,8 @@ class ConsumablesController extends Controller
|
|||
* @param int $consumableId
|
||||
* @see ConsumablesController::postEdit() method that stores the form data.
|
||||
* @since [v1.0]
|
||||
* @return \Illuminate\Contracts\View\View
|
||||
* @throws \Illuminate\Auth\Access\AuthorizationException
|
||||
*/
|
||||
public function edit($consumableId = null)
|
||||
public function edit($consumableId = null) : View | RedirectResponse
|
||||
{
|
||||
if ($item = Consumable::find($consumableId)) {
|
||||
$this->authorize($item);
|
||||
|
@ -124,7 +124,7 @@ class ConsumablesController extends Controller
|
|||
* @see ConsumablesController::getEdit() method that stores the form data.
|
||||
* @since [v1.0]
|
||||
*/
|
||||
public function update(ImageUploadRequest $request, $consumableId = null)
|
||||
public function update(StoreConsumableRequest $request, $consumableId = null)
|
||||
{
|
||||
if (is_null($consumable = Consumable::find($consumableId))) {
|
||||
return redirect()->route('consumables.index')->with('error', trans('admin/consumables/message.does_not_exist'));
|
||||
|
@ -182,6 +182,7 @@ class ConsumablesController extends Controller
|
|||
return redirect()->route('consumables.index')->with('error', trans('admin/consumables/message.not_found'));
|
||||
}
|
||||
$this->authorize($consumable);
|
||||
|
||||
$consumable->delete();
|
||||
// Redirect to the locations management page
|
||||
return redirect()->route('consumables.index')->with('success', trans('admin/consumables/message.delete.success'));
|
||||
|
|
56
app/Http/Requests/StoreConsumableRequest.php
Normal file
56
app/Http/Requests/StoreConsumableRequest.php
Normal file
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Models\Consumable;
|
||||
use App\Models\Category;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
|
||||
class StoreConsumableRequest extends ImageUploadRequest
|
||||
{
|
||||
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return Gate::allows('create', new Consumable);
|
||||
}
|
||||
|
||||
public function prepareForValidation(): void
|
||||
{
|
||||
|
||||
if ($this->category_id) {
|
||||
if ($category = Category::find($this->category_id)) {
|
||||
$this->merge([
|
||||
'category_type' => $category->category_type ?? null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return array_merge(
|
||||
['category_type' => 'in:consumable'],
|
||||
parent::rules(),
|
||||
);
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
$messages = ['category_type.in' => trans('admin/consumables/message.invalid_category_type')];
|
||||
return $messages;
|
||||
}
|
||||
|
||||
public function response(array $errors)
|
||||
{
|
||||
return $this->redirector->back()->withInput()->withErrors($errors, $this->errorBag);
|
||||
}
|
||||
}
|
|
@ -10,12 +10,21 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Watson\Validating\ValidatingTrait;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use App\Presenters\ConsumablePresenter;
|
||||
use App\Models\Actionlog;
|
||||
use App\Models\ConsumableAssignment;
|
||||
use App\Models\User;
|
||||
use App\Models\Location;
|
||||
use App\Models\Manufacturer;
|
||||
use App\Models\Supplier;
|
||||
use App\Models\Category;
|
||||
|
||||
class Consumable extends SnipeModel
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $presenter = \App\Presenters\ConsumablePresenter::class;
|
||||
protected $presenter = ConsumablePresenter::class;
|
||||
use CompanyableTrait;
|
||||
use Loggable, Presentable;
|
||||
use SoftDeletes;
|
||||
|
@ -37,10 +46,10 @@ class Consumable extends SnipeModel
|
|||
*/
|
||||
public $rules = [
|
||||
'name' => 'required|min:3|max:255',
|
||||
'qty' => 'required|integer|min:0',
|
||||
'qty' => 'required|integer|min:0|max:99999',
|
||||
'category_id' => 'required|integer',
|
||||
'company_id' => 'integer|nullable',
|
||||
'min_amt' => 'integer|min:0|nullable',
|
||||
'min_amt' => 'integer|min:0|max:99999|nullable',
|
||||
'purchase_cost' => 'numeric|nullable|gte:0',
|
||||
'purchase_date' => 'date_format:Y-m-d|nullable',
|
||||
];
|
||||
|
@ -109,7 +118,7 @@ class Consumable extends SnipeModel
|
|||
*/
|
||||
public function uploads()
|
||||
{
|
||||
return $this->hasMany(\App\Models\Actionlog::class, 'item_id')
|
||||
return $this->hasMany(Actionlog::class, 'item_id')
|
||||
->where('item_type', '=', self::class)
|
||||
->where('action_type', '=', 'uploaded')
|
||||
->whereNotNull('filename')
|
||||
|
@ -147,7 +156,7 @@ class Consumable extends SnipeModel
|
|||
*/
|
||||
public function admin()
|
||||
{
|
||||
return $this->belongsTo(\App\Models\User::class, 'user_id');
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -159,7 +168,7 @@ class Consumable extends SnipeModel
|
|||
*/
|
||||
public function consumableAssignments()
|
||||
{
|
||||
return $this->hasMany(\App\Models\ConsumableAssignment::class);
|
||||
return $this->hasMany(ConsumableAssignment::class);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -183,7 +192,7 @@ class Consumable extends SnipeModel
|
|||
*/
|
||||
public function manufacturer()
|
||||
{
|
||||
return $this->belongsTo(\App\Models\Manufacturer::class, 'manufacturer_id');
|
||||
return $this->belongsTo(Manufacturer::class, 'manufacturer_id');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -195,7 +204,7 @@ class Consumable extends SnipeModel
|
|||
*/
|
||||
public function location()
|
||||
{
|
||||
return $this->belongsTo(\App\Models\Location::class, 'location_id');
|
||||
return $this->belongsTo(Location::class, 'location_id');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -207,7 +216,7 @@ class Consumable extends SnipeModel
|
|||
*/
|
||||
public function category()
|
||||
{
|
||||
return $this->belongsTo(\App\Models\Category::class, 'category_id');
|
||||
return $this->belongsTo(Category::class, 'category_id');
|
||||
}
|
||||
|
||||
|
||||
|
@ -220,7 +229,7 @@ class Consumable extends SnipeModel
|
|||
*/
|
||||
public function assetlog()
|
||||
{
|
||||
return $this->hasMany(\App\Models\Actionlog::class, 'item_id')->where('item_type', self::class)->orderBy('created_at', 'desc')->withTrashed();
|
||||
return $this->hasMany(Actionlog::class, 'item_id')->where('item_type', self::class)->orderBy('created_at', 'desc')->withTrashed();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -244,11 +253,10 @@ class Consumable extends SnipeModel
|
|||
*
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @since [v3.0]
|
||||
* @return \Illuminate\Database\Eloquent\Relations\Relation
|
||||
*/
|
||||
public function users()
|
||||
public function users() : Relation
|
||||
{
|
||||
return $this->belongsToMany(\App\Models\User::class, 'consumables_users', 'consumable_id', 'assigned_to')->withPivot('user_id')->withTrashed()->withTimestamps();
|
||||
return $this->belongsToMany(User::class, 'consumables_users', 'consumable_id', 'assigned_to')->withPivot('user_id')->withTrashed()->withTimestamps();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -260,7 +268,7 @@ class Consumable extends SnipeModel
|
|||
*/
|
||||
public function supplier()
|
||||
{
|
||||
return $this->belongsTo(\App\Models\Supplier::class, 'supplier_id');
|
||||
return $this->belongsTo(Supplier::class, 'supplier_id');
|
||||
}
|
||||
|
||||
|
||||
|
@ -317,10 +325,7 @@ class Consumable extends SnipeModel
|
|||
*/
|
||||
public function numCheckedOut()
|
||||
{
|
||||
$checkedout = 0;
|
||||
$checkedout = $this->users->count();
|
||||
|
||||
return $checkedout;
|
||||
return $this->consumables_users_count ?? $this->users()->count();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -332,7 +337,7 @@ class Consumable extends SnipeModel
|
|||
*/
|
||||
public function numRemaining()
|
||||
{
|
||||
$checkedout = $this->users->count();
|
||||
$checkedout = $this->numCheckedOut();
|
||||
$total = $this->qty;
|
||||
$remaining = $total - $checkedout;
|
||||
|
||||
|
|
|
@ -3,13 +3,19 @@
|
|||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Watson\Validating\ValidatingTrait;
|
||||
|
||||
class ConsumableAssignment extends Model
|
||||
{
|
||||
use CompanyableTrait;
|
||||
use ValidatingTrait;
|
||||
|
||||
protected $table = 'consumables_users';
|
||||
|
||||
public $rules = [
|
||||
'assigned_to' => 'required|exists:users,id',
|
||||
];
|
||||
|
||||
public function consumable()
|
||||
{
|
||||
return $this->belongsTo(\App\Models\Consumable::class);
|
||||
|
|
|
@ -5,6 +5,8 @@ namespace App\Observers;
|
|||
use App\Models\Actionlog;
|
||||
use App\Models\Consumable;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class ConsumableObserver
|
||||
{
|
||||
|
@ -16,12 +18,26 @@ class ConsumableObserver
|
|||
*/
|
||||
public function updated(Consumable $consumable)
|
||||
{
|
||||
$logAction = new Actionlog();
|
||||
$logAction->item_type = Consumable::class;
|
||||
$logAction->item_id = $consumable->id;
|
||||
$logAction->created_at = date('Y-m-d H:i:s');
|
||||
$logAction->user_id = Auth::id();
|
||||
$logAction->logaction('update');
|
||||
|
||||
$changed = [];
|
||||
|
||||
foreach ($consumable->getRawOriginal() as $key => $value) {
|
||||
// Check and see if the value changed
|
||||
if ($consumable->getRawOriginal()[$key] != $consumable->getAttributes()[$key]) {
|
||||
$changed[$key]['old'] = $consumable->getRawOriginal()[$key];
|
||||
$changed[$key]['new'] = $consumable->getAttributes()[$key];
|
||||
}
|
||||
}
|
||||
|
||||
if (count($changed) > 0) {
|
||||
$logAction = new Actionlog();
|
||||
$logAction->item_type = Consumable::class;
|
||||
$logAction->item_id = $consumable->id;
|
||||
$logAction->created_at = date('Y-m-d H:i:s');
|
||||
$logAction->user_id = Auth::id();
|
||||
$logAction->log_meta = json_encode($changed);
|
||||
$logAction->logaction('update');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -52,6 +68,32 @@ class ConsumableObserver
|
|||
*/
|
||||
public function deleting(Consumable $consumable)
|
||||
{
|
||||
|
||||
$consumable->users()->detach();
|
||||
$uploads = $consumable->uploads;
|
||||
|
||||
foreach ($uploads as $file) {
|
||||
try {
|
||||
Storage::delete('private_uploads/consumables/'.$file->filename);
|
||||
$file->delete();
|
||||
} catch (\Exception $e) {
|
||||
Log::info($e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
try {
|
||||
Storage::disk('public')->delete('consumables/'.$consumable->image);
|
||||
} catch (\Exception $e) {
|
||||
Log::info($e);
|
||||
}
|
||||
|
||||
$consumable->image = null;
|
||||
$consumable->save();
|
||||
|
||||
|
||||
|
||||
$logAction = new Actionlog();
|
||||
$logAction->item_type = Consumable::class;
|
||||
$logAction->item_id = $consumable->id;
|
||||
|
|
|
@ -75,7 +75,7 @@ class ActionlogPresenter extends Presenter
|
|||
}
|
||||
|
||||
if ($this->actionType()=='delete') {
|
||||
return 'fa-solid fa-user-xmark';
|
||||
return 'fa-solid fa-trash';
|
||||
}
|
||||
|
||||
if ($this->actionType()=='update') {
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
return array(
|
||||
|
||||
'invalid_category_type' => 'The category must be a consumable category.',
|
||||
'does_not_exist' => 'Consumable does not exist.',
|
||||
|
||||
'create' => array(
|
||||
|
|
|
@ -37,6 +37,25 @@
|
|||
</div>
|
||||
@endif
|
||||
|
||||
<!-- total -->
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">{{ trans('admin/components/general.total') }}</label>
|
||||
<div class="col-md-6">
|
||||
<p class="form-control-static">{{ $consumable->qty }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- remaining -->
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">{{ trans('admin/components/general.remaining') }}</label>
|
||||
<div class="col-md-6">
|
||||
<p class="form-control-static">{{ $consumable->numRemaining() }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- User -->
|
||||
@include ('partials.forms.edit.user-select', ['translated_name' => trans('general.select_user'), 'fieldname' => 'assigned_to', 'required'=> 'true'])
|
||||
|
||||
|
|
|
@ -45,6 +45,17 @@
|
|||
</li>
|
||||
@endcan
|
||||
|
||||
<li>
|
||||
<a href="#history" data-toggle="tab">
|
||||
<span class="hidden-lg hidden-md">
|
||||
<i class="fas fa-history fa-2x" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span class="hidden-xs hidden-sm">
|
||||
{{ trans('general.history') }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@can('update', $consumable)
|
||||
<li class="pull-right">
|
||||
<a href="#" data-toggle="modal" data-target="#uploadFileModal">
|
||||
|
@ -95,7 +106,56 @@
|
|||
</div> <!-- close tab-pane div -->
|
||||
|
||||
|
||||
@can('consumables.files', $consumable)
|
||||
<div class="tab-pane fade" id="history">
|
||||
<!-- checked out assets table -->
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<table
|
||||
class="table table-striped snipe-table"
|
||||
id="consumableHistory"
|
||||
data-pagination="true"
|
||||
data-id-table="consumableHistory"
|
||||
data-search="true"
|
||||
data-side-pagination="server"
|
||||
data-show-columns="true"
|
||||
data-show-fullscreen="true"
|
||||
data-show-refresh="true"
|
||||
data-sort-order="desc"
|
||||
data-sort-name="created_at"
|
||||
data-show-export="true"
|
||||
data-export-options='{
|
||||
"fileName": "export-consumable-{{ $consumable->id }}-history",
|
||||
"ignoreColumn": ["actions","image","change","checkbox","checkincheckout","icon"]
|
||||
}'
|
||||
|
||||
data-url="{{ route('api.activity.index', ['item_id' => $consumable->id, 'item_type' => 'consumable']) }}"
|
||||
data-cookie-id-table="assetHistory"
|
||||
data-cookie="true">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-visible="true" data-field="icon" style="width: 40px;" class="hidden-xs" data-formatter="iconFormatter">{{ trans('admin/hardware/table.icon') }}</th>
|
||||
<th data-visible="true" data-field="action_date" data-sortable="true" data-formatter="dateDisplayFormatter">{{ trans('general.date') }}</th>
|
||||
<th data-visible="true" data-field="admin" data-formatter="usersLinkObjFormatter">{{ trans('general.admin') }}</th>
|
||||
<th data-visible="true" data-field="action_type">{{ trans('general.action') }}</th>
|
||||
<th class="col-sm-2" data-field="file" data-visible="false" data-formatter="fileUploadNameFormatter">{{ trans('general.file_name') }}</th>
|
||||
<th data-visible="true" data-field="item" data-formatter="polymorphicItemFormatter">{{ trans('general.item') }}</th>
|
||||
<th data-visible="true" data-field="target" data-formatter="polymorphicItemFormatter">{{ trans('general.target') }}</th>
|
||||
<th data-field="note">{{ trans('general.notes') }}</th>
|
||||
<th data-field="signature_file" data-visible="false" data-formatter="imageFormatter">{{ trans('general.signature') }}</th>
|
||||
<th data-visible="false" data-field="file" data-visible="false" data-formatter="fileUploadFormatter">{{ trans('general.download') }}</th>
|
||||
<th data-field="log_meta" data-visible="true" data-formatter="changeLogFormatter">{{ trans('admin/hardware/table.changed')}}</th>
|
||||
<th data-field="remote_ip" data-visible="false" data-sortable="true">{{ trans('admin/settings/general.login_ip') }}</th>
|
||||
<th data-field="user_agent" data-visible="false" data-sortable="true">{{ trans('admin/settings/general.login_user_agent') }}</th>
|
||||
<th data-field="action_source" data-visible="false" data-sortable="true">{{ trans('general.action_source') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
</div> <!-- /.row -->
|
||||
</div> <!-- /.tab-pane history -->
|
||||
|
||||
|
||||
@can('consumables.files', $consumable)
|
||||
<div class="tab-pane" id="files">
|
||||
|
||||
<div class="table-responsive">
|
||||
|
@ -176,6 +236,7 @@
|
|||
<i class="fas fa-trash icon-white" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{ trans('general.delete') }}</span>
|
||||
</a>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
|
@ -254,6 +315,19 @@
|
|||
</div>
|
||||
@endif
|
||||
|
||||
@if ($consumable->notes)
|
||||
|
||||
<div class="col-md-12">
|
||||
<strong>
|
||||
{{ trans('general.notes') }}:
|
||||
</strong>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
{!! nl2br(Helper::parseEscapedMarkedownInline($consumable->notes)) !!}
|
||||
</div>
|
||||
|
||||
@endif
|
||||
|
||||
@can('checkout', \App\Models\Consumable::class)
|
||||
|
||||
<div class="col-md-12">
|
||||
|
@ -268,22 +342,24 @@
|
|||
</button>
|
||||
@endif
|
||||
</div>
|
||||
@can('update', \App\Models\Consumable::class)
|
||||
<div class="col-md-12">
|
||||
<a href="{{ route('consumables.edit', $consumable->id) }}" style="width: 100%;" class="btn btn-sm btn-primary hidden-print">{{ trans('button.edit') }}</a>
|
||||
</div>
|
||||
@endcan
|
||||
|
||||
@can('delete', $consumable)
|
||||
<div class="col-md-12" style="padding-top: 30px; padding-bottom: 30px;">
|
||||
@if ($consumable->deleted_at=='')
|
||||
<button class="btn btn-sm btn-block btn-danger delete-asset" data-toggle="modal" data-title="{{ trans('general.delete') }}" data-content="{{ trans('general.sure_to_delete_var', ['item' => $consumable->name]) }}" data-target="#dataConfirmModal">{{ trans('general.delete') }}
|
||||
</button>
|
||||
<span class="sr-only">{{ trans('general.delete') }}</span>
|
||||
@endif
|
||||
</div>
|
||||
@endcan
|
||||
|
||||
@endcan
|
||||
|
||||
@if ($consumable->notes)
|
||||
|
||||
<div class="col-md-12">
|
||||
<strong>
|
||||
{{ trans('general.notes') }}:
|
||||
</strong>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
{!! nl2br(Helper::parseEscapedMarkedownInline($consumable->notes)) !!}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
|
||||
</div> <!-- /.col-md-3-->
|
||||
|
@ -297,5 +373,16 @@
|
|||
@stop
|
||||
|
||||
@section('moar_scripts')
|
||||
<script>
|
||||
|
||||
$('#dataConfirmModal').on('show.bs.modal', function (event) {
|
||||
var content = $(event.relatedTarget).data('content');
|
||||
var title = $(event.relatedTarget).data('title');
|
||||
$(this).find(".modal-body").text(content);
|
||||
$(this).find(".modal-header").text(title);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
@include ('partials.bootstrap-table', ['exportFile' => 'consumable' . $consumable->name . '-export', 'search' => false])
|
||||
@stop
|
||||
|
|
|
@ -976,7 +976,7 @@ dir="{{ in_array(app()->getLocale(),['ar-SA','fa-IR', 'he-IL']) ? 'rtl' : 'ltr'
|
|||
// Reference: https://jqueryvalidation.org/validate/
|
||||
$('#create-form').validate({
|
||||
ignore: 'input[type=hidden]',
|
||||
errorClass: 'help-block form-error',
|
||||
errorClass: 'alert-msg',
|
||||
errorElement: 'span',
|
||||
errorPlacement: function(error, element) {
|
||||
$(element).hasClass('select2') || $(element).hasClass('js-data-ajax')
|
||||
|
|
|
@ -25,4 +25,6 @@
|
|||
|
||||
|
||||
{!! $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>') !!}
|
||||
|
||||
{!! $errors->first('category_type', '<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>
|
||||
|
|
|
@ -3,13 +3,13 @@
|
|||
<label for="min_amt" class="col-md-3 control-label">{{ trans('general.min_amt') }}</label>
|
||||
<div class="col-md-9{{ (Helper::checkIfRequired($item, 'min_amt')) ? ' required' : '' }}">
|
||||
<div class="col-md-2" style="padding-left:0px">
|
||||
<input class="form-control col-md-3" type="text" name="min_amt" id="min_amt" aria-label="min_amt" value="{{ old('min_amt', $item->min_amt) }}" />
|
||||
<input class="form-control col-md-3" maxlength="5" type="text" name="min_amt" id="min_amt" aria-label="min_amt" value="{{ old('min_amt', $item->min_amt) }}" />
|
||||
</div>
|
||||
<div class="col-md-7" style="margin-left: -15px;">
|
||||
|
||||
<a href="#" data-tooltip="true" title="{{ trans('general.min_amt_help') }}"><i class="fas fa-info-circle" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{ trans('general.min_amt_help') }}</span>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
{!! $errors->first('min_amt', '<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!}
|
||||
|
|
|
@ -4,8 +4,10 @@
|
|||
<label for="qty" class="col-md-3 control-label">{{ trans('general.quantity') }}</label>
|
||||
<div class="col-md-7{{ (Helper::checkIfRequired($item, 'qty')) ? ' required' : '' }}">
|
||||
<div class="col-md-3" style="padding-left:0px">
|
||||
<input class="form-control" type="text" name="qty" aria-label="qty" id="qty" value="{{ old('qty', $item->qty) }}" {!! (Helper::checkIfRequired($item, 'qty')) ? ' required ' : '' !!}/>
|
||||
<input class="form-control" maxlength="5" type="text" name="qty" aria-label="qty" id="qty" value="{{ old('qty', $item->qty) }}" {!! (Helper::checkIfRequired($item, 'qty')) ? ' required ' : '' !!}/>
|
||||
</div>
|
||||
<div class="col-md-12" style="padding-left:0px">
|
||||
{!! $errors->first('qty', '<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -32,7 +32,6 @@ class IndexCategoriesTest extends TestCase
|
|||
'limit' => '20',
|
||||
]))
|
||||
->assertOk()
|
||||
->assertOk()
|
||||
->assertJsonStructure([
|
||||
'total',
|
||||
'rows',
|
||||
|
|
|
@ -54,4 +54,29 @@ class ConsumableIndexTest extends TestCase
|
|||
->assertResponseDoesNotContainInRows($consumableA)
|
||||
->assertResponseContainsInRows($consumableB);
|
||||
}
|
||||
|
||||
public function testConsumableIndexReturnsExpectedSearchResults()
|
||||
{
|
||||
Consumable::factory()->count(10)->create();
|
||||
Consumable::factory()->count(1)->create(['name' => 'My Test Consumable']);
|
||||
|
||||
$this->actingAsForApi(User::factory()->superuser()->create())
|
||||
->getJson(
|
||||
route('api.consumables.index', [
|
||||
'search' => 'My Test Consumable',
|
||||
'sort' => 'name',
|
||||
'order' => 'asc',
|
||||
'offset' => '0',
|
||||
'limit' => '20',
|
||||
]))
|
||||
->assertOk()
|
||||
->assertJsonStructure([
|
||||
'total',
|
||||
'rows',
|
||||
])
|
||||
->assertJson([
|
||||
'total' => 1,
|
||||
]);
|
||||
|
||||
}
|
||||
}
|
||||
|
|
52
tests/Feature/Consumables/Api/ConsumableUpdateTest.php
Normal file
52
tests/Feature/Consumables/Api/ConsumableUpdateTest.php
Normal file
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature\Consumables\Api;
|
||||
|
||||
use App\Models\Consumable;
|
||||
use App\Models\Category;
|
||||
use App\Models\User;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ConsumableUpdateTest extends TestCase
|
||||
{
|
||||
|
||||
public function testCanUpdateConsumableViaPatchWithoutCategoryType()
|
||||
{
|
||||
$consumable = Consumable::factory()->create();
|
||||
|
||||
$this->actingAsForApi(User::factory()->superuser()->create())
|
||||
->patchJson(route('api.consumables.update', $consumable), [
|
||||
'name' => 'Test Consumable',
|
||||
])
|
||||
->assertOk()
|
||||
->assertStatusMessageIs('success')
|
||||
->assertStatus(200)
|
||||
->json();
|
||||
|
||||
$consumable->refresh();
|
||||
$this->assertEquals('Test Consumable', $consumable->name, 'Name was not updated');
|
||||
|
||||
}
|
||||
|
||||
public function testCannotUpdateConsumableViaPatchWithInvalidCategoryType()
|
||||
{
|
||||
$category = Category::factory()->create(['category_type' => 'asset']);
|
||||
$consumable = Consumable::factory()->create();
|
||||
|
||||
$this->actingAsForApi(User::factory()->superuser()->create())
|
||||
->patchJson(route('api.consumables.update', $consumable), [
|
||||
'name' => 'Test Consumable',
|
||||
'category_id' => $category->id,
|
||||
])
|
||||
->assertOk()
|
||||
->assertStatusMessageIs('error')
|
||||
->assertStatus(200)
|
||||
->json();
|
||||
|
||||
$category->refresh();
|
||||
$this->assertNotEquals('Test Consumable', $consumable->name, 'Name was not updated');
|
||||
$this->assertNotEquals('consumable', $consumable->category_id, 'Category was not updated');
|
||||
|
||||
}
|
||||
|
||||
}
|
51
tests/Feature/Consumables/Api/ConsumableViewTest.php
Normal file
51
tests/Feature/Consumables/Api/ConsumableViewTest.php
Normal file
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature\Consumables\Api;
|
||||
|
||||
use App\Models\Company;
|
||||
use App\Models\Consumable;
|
||||
use App\Models\User;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ConsumableViewTest extends TestCase
|
||||
{
|
||||
public function testConsumableViewAdheresToCompanyScoping()
|
||||
{
|
||||
[$companyA, $companyB] = Company::factory()->count(2)->create();
|
||||
|
||||
$consumableA = Consumable::factory()->for($companyA)->create();
|
||||
$consumableB = Consumable::factory()->for($companyB)->create();
|
||||
|
||||
$superUser = $companyA->users()->save(User::factory()->superuser()->make());
|
||||
$userInCompanyA = $companyA->users()->save(User::factory()->viewConsumables()->make());
|
||||
$userInCompanyB = $companyB->users()->save(User::factory()->viewConsumables()->make());
|
||||
|
||||
$this->settings->disableMultipleFullCompanySupport();
|
||||
|
||||
$this->actingAsForApi($superUser)
|
||||
->getJson(route('api.consumables.show', $consumableA))
|
||||
->assertOk();
|
||||
|
||||
$this->actingAsForApi($userInCompanyA)
|
||||
->getJson(route('api.consumables.show', $consumableA))
|
||||
->assertOk();
|
||||
|
||||
$this->actingAsForApi($userInCompanyB)
|
||||
->getJson(route('api.consumables.show', $consumableB))
|
||||
->assertOk();
|
||||
|
||||
$this->settings->enableMultipleFullCompanySupport();
|
||||
|
||||
$this->actingAsForApi($superUser)
|
||||
->getJson(route('api.consumables.show', $consumableA))
|
||||
->assertOk();
|
||||
|
||||
$this->actingAsForApi($userInCompanyA)
|
||||
->getJson(route('api.consumables.index'))
|
||||
->assertOk();
|
||||
|
||||
$this->actingAsForApi($userInCompanyB)
|
||||
->getJson(route('api.consumables.index'))
|
||||
->assertOk();
|
||||
}
|
||||
}
|
26
tests/Feature/Consumables/Ui/ConsumableViewTest.php
Normal file
26
tests/Feature/Consumables/Ui/ConsumableViewTest.php
Normal file
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature\Consumables\Ui;
|
||||
|
||||
use App\Models\Consumable;
|
||||
use App\Models\User;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ConsumableViewTest extends TestCase
|
||||
{
|
||||
public function testPermissionRequiredToViewConsumable()
|
||||
{
|
||||
$consumable = Consumable::factory()->create();
|
||||
$this->actingAs(User::factory()->create())
|
||||
->get(route('consumables.show', $consumable))
|
||||
->assertForbidden();
|
||||
}
|
||||
|
||||
public function testUserCanListConsumables()
|
||||
{
|
||||
$consumable = Consumable::factory()->create();
|
||||
$this->actingAs(User::factory()->superuser()->create())
|
||||
->get(route('consumables.show', $consumable))
|
||||
->assertOk();
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue