Merge remote-tracking branch 'origin/develop'

Signed-off-by: snipe <snipe@snipe.net>

# Conflicts:
#	.all-contributorsrc
#	CONTRIBUTORS.md
This commit is contained in:
snipe 2024-07-11 18:38:57 +01:00
commit 1a541ce220
23 changed files with 527 additions and 145 deletions

View file

@ -3137,6 +3137,15 @@
"doc"
]
},
{
"login": "FlorentDotMe",
"name": "Florent Bervas",
"avatar_url": "https://avatars.githubusercontent.com/u/292081?v=4",
"profile": "http://spoontux.net",
"contributions": [
"code"
]
},
{
"login": "Galaxy102",
"name": "Konstantin Köhring",
@ -3146,5 +3155,6 @@
"code"
]
}
]
}

View file

@ -2,7 +2,7 @@ Thanks goes to all of these wonderful people ([emoji key](https://github.com/ken
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
| [<img src="https://avatars3.githubusercontent.com/u/197404?v=3" width="110px;"/><br /><sub>snipe</sub>](http://www.snipe.net)<br />[💻](https://github.com/snipe/snipe-it/commits?author=snipe "Code") [🚇](#infra-snipe "Infrastructure (Hosting, Build-Tools, etc)") [📖](https://github.com/snipe/snipe-it/commits?author=snipe "Documentation") [⚠️](https://github.com/snipe/snipe-it/commits?author=snipe "Tests") [🐛](https://github.com/snipe/snipe-it/issues?q=author%3Asnipe "Bug reports") [🎨](#design-snipe "Design") [👀](#review-snipe "Reviewed Pull Requests") | [<img src="https://avatars0.githubusercontent.com/u/36335?v=3" width="110px;"/><br /><sub>Brady Wetherington</sub>](http://www.uberbrady.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=uberbrady "Code") [📖](https://github.com/snipe/snipe-it/commits?author=uberbrady "Documentation") [🚇](#infra-uberbrady "Infrastructure (Hosting, Build-Tools, etc)") [👀](#review-uberbrady "Reviewed Pull Requests") | [<img src="https://avatars0.githubusercontent.com/u/3803132?v=3" width="110px;"/><br /><sub>Daniel Meltzer</sub>](https://github.com/dmeltzer)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dmeltzer "Code") [⚠️](https://github.com/snipe/snipe-it/commits?author=dmeltzer "Tests") [📖](https://github.com/snipe/snipe-it/commits?author=dmeltzer "Documentation") | [<img src="https://avatars0.githubusercontent.com/u/1609106?v=3" width="110px;"/><br /><sub>Michael T</sub>](http://www.tuckertechonline.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mtucker6784 "Code") | [<img src="https://avatars2.githubusercontent.com/u/3274937?v=3" width="110px;"/><br /><sub>madd15</sub>](https://github.com/madd15)<br />[📖](https://github.com/snipe/snipe-it/commits?author=madd15 "Documentation") [💬](#question-madd15 "Answering Questions") | [<img src="https://avatars2.githubusercontent.com/u/894126?v=3" width="110px;"/><br /><sub>Vincent Sposato</sub>](https://github.com/vsposato)<br />[💻](https://github.com/snipe/snipe-it/commits?author=vsposato "Code") | [<img src="https://avatars0.githubusercontent.com/u/1639757?v=3" width="110px;"/><br /><sub>Andrea Bergamasco</sub>](https://github.com/vjandrea)<br />[💻](https://github.com/snipe/snipe-it/commits?author=vjandrea "Code") |
| :---: | :---: | :---: | :---: | :---: | :---: | :---: |
| :---: | :---: | :---: |:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:| :---: | :---: | :---: |
| [<img src="https://avatars0.githubusercontent.com/u/10640152?v=3" width="110px;"/><br /><sub>Karol</sub>](https://github.com/kpawelski)<br />[🌍](#translation-kpawelski "Translation") [💻](https://github.com/snipe/snipe-it/commits?author=kpawelski "Code") | [<img src="https://avatars3.githubusercontent.com/u/600106?v=3" width="110px;"/><br /><sub>morph027</sub>](http://blog.morph027.de/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=morph027 "Code") | [<img src="https://avatars3.githubusercontent.com/u/22935755?v=3" width="110px;"/><br /><sub>fvleminckx</sub>](https://github.com/fvleminckx)<br />[🚇](#infra-fvleminckx "Infrastructure (Hosting, Build-Tools, etc)") | [<img src="https://avatars2.githubusercontent.com/u/15633547?v=3" width="110px;"/><br /><sub>itsupportcmsukorg</sub>](https://github.com/itsupportcmsukorg)<br />[💻](https://github.com/snipe/snipe-it/commits?author=itsupportcmsukorg "Code") [🐛](https://github.com/snipe/snipe-it/issues?q=author%3Aitsupportcmsukorg "Bug reports") | [<img src="https://avatars3.githubusercontent.com/u/12373799?v=3" width="110px;"/><br /><sub>Frank</sub>](https://override.io)<br />[💻](https://github.com/snipe/snipe-it/commits?author=base-zero "Code") | [<img src="https://avatars0.githubusercontent.com/u/10137?v=3" width="110px;"/><br /><sub>Deleted user</sub>](https://github.com/ghost)<br />[🌍](#translation-ghost "Translation") [💻](https://github.com/snipe/snipe-it/commits?author=ghost "Code") | [<img src="https://avatars1.githubusercontent.com/u/10802313?v=3" width="110px;"/><br /><sub>tiagom62</sub>](https://github.com/tiagom62)<br />[💻](https://github.com/snipe/snipe-it/commits?author=tiagom62 "Code") [🚇](#infra-tiagom62 "Infrastructure (Hosting, Build-Tools, etc)") |
| [<img src="https://avatars3.githubusercontent.com/u/2389047?v=3" width="110px;"/><br /><sub>Ryan Stafford</sub>](https://github.com/rystaf)<br />[💻](https://github.com/snipe/snipe-it/commits?author=rystaf "Code") | [<img src="https://avatars2.githubusercontent.com/u/10345935?v=3" width="110px;"/><br /><sub>Eammon Hanlon</sub>](https://github.com/ehanlon)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ehanlon "Code") | [<img src="https://avatars0.githubusercontent.com/u/441924?v=3" width="110px;"/><br /><sub>zjean</sub>](https://github.com/zjean)<br />[💻](https://github.com/snipe/snipe-it/commits?author=zjean "Code") | [<img src="https://avatars0.githubusercontent.com/u/12660103?v=3" width="110px;"/><br /><sub>Matthias Frei</sub>](http://www.frei.media)<br />[💻](https://github.com/snipe/snipe-it/commits?author=FREImedia "Code") | [<img src="https://avatars0.githubusercontent.com/u/3767518?v=3" width="110px;"/><br /><sub>opsydev</sub>](https://github.com/opsydev)<br />[💻](https://github.com/snipe/snipe-it/commits?author=opsydev "Code") | [<img src="https://avatars1.githubusercontent.com/u/82290?v=3" width="110px;"/><br /><sub>Daniel Dreier</sub>](http://www.ddreier.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ddreier "Code") | [<img src="https://avatars0.githubusercontent.com/u/23448?v=3" width="110px;"/><br /><sub>Nikolai Prokoschenko</sub>](http://rassie.org)<br />[💻](https://github.com/snipe/snipe-it/commits?author=rassie "Code") |
| [<img src="https://avatars0.githubusercontent.com/u/13452757?v=3" width="110px;"/><br /><sub>Drew</sub>](https://github.com/YetAnotherCodeMonkey)<br />[💻](https://github.com/snipe/snipe-it/commits?author=YetAnotherCodeMonkey "Code") | [<img src="https://avatars0.githubusercontent.com/u/1342320?v=3" width="110px;"/><br /><sub>Walter</sub>](https://github.com/merid14)<br />[💻](https://github.com/snipe/snipe-it/commits?author=merid14 "Code") | [<img src="https://avatars3.githubusercontent.com/u/11254614?v=3" width="110px;"/><br /><sub>Petr Baloun</sub>](https://github.com/balous)<br />[💻](https://github.com/snipe/snipe-it/commits?author=balous "Code") | [<img src="https://avatars0.githubusercontent.com/u/6117660?v=3" width="110px;"/><br /><sub>reidblomquist</sub>](https://github.com/reidblomquist)<br />[📖](https://github.com/snipe/snipe-it/commits?author=reidblomquist "Documentation") | [<img src="https://avatars0.githubusercontent.com/u/539914?v=3" width="110px;"/><br /><sub>Mathieu Kooiman</sub>](https://github.com/mathieuk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mathieuk "Code") | [<img src="https://avatars3.githubusercontent.com/u/6606421?v=3" width="110px;"/><br /><sub>csayre</sub>](https://github.com/csayre)<br />[📖](https://github.com/snipe/snipe-it/commits?author=csayre "Documentation") | [<img src="https://avatars1.githubusercontent.com/u/768488?v=3" width="110px;"/><br /><sub>Adam Dunson</sub>](https://github.com/adamdunson)<br />[💻](https://github.com/snipe/snipe-it/commits?author=adamdunson "Code") |
@ -51,7 +51,7 @@ Thanks goes to all of these wonderful people ([emoji key](https://github.com/ken
| [<img src="https://avatars.githubusercontent.com/u/111287779?v=4" width="110px;"/><br /><sub>NojoudAlshehri</sub>](https://github.com/NojoudAlshehri)<br />[💻](https://github.com/snipe/snipe-it/commits?author=NojoudAlshehri "Code") | [<img src="https://avatars.githubusercontent.com/u/54367449?v=4" width="110px;"/><br /><sub>Stefan Stidl</sub>](https://github.com/stefanstidlffg)<br />[💻](https://github.com/snipe/snipe-it/commits?author=stefanstidlffg "Code") | [<img src="https://avatars.githubusercontent.com/u/87803479?v=4" width="110px;"/><br /><sub>Quentin Aymard</sub>](https://github.com/qay21)<br />[💻](https://github.com/snipe/snipe-it/commits?author=qay21 "Code") | [<img src="https://avatars.githubusercontent.com/u/5396871?v=4" width="110px;"/><br /><sub>Grant Le Roux</sub>](https://github.com/cram42)<br />[💻](https://github.com/snipe/snipe-it/commits?author=cram42 "Code") | [<img src="https://avatars.githubusercontent.com/u/58479551?v=4" width="110px;"/><br /><sub>Bogdan</sub>](http://@singrity)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Singrity "Code") | [<img src="https://avatars.githubusercontent.com/u/3483684?v=4" width="110px;"/><br /><sub>mmanjos</sub>](https://github.com/mmanjos)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mmanjos "Code") | [<img src="https://avatars.githubusercontent.com/u/7429229?v=4" width="110px;"/><br /><sub>Abdelaziz Faki</sub>](https://azooz2014.github.io/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Azooz2014 "Code") |
| [<img src="https://avatars.githubusercontent.com/u/47315739?v=4" width="110px;"/><br /><sub>bilias</sub>](https://github.com/bilias)<br />[💻](https://github.com/snipe/snipe-it/commits?author=bilias "Code") | [<img src="https://avatars.githubusercontent.com/u/2565989?v=4" width="110px;"/><br /><sub>coach1988</sub>](https://github.com/coach1988)<br />[💻](https://github.com/snipe/snipe-it/commits?author=coach1988 "Code") | [<img src="https://avatars.githubusercontent.com/u/11910225?v=4" width="110px;"/><br /><sub>MrM</sub>](https://github.com/mauro-miatello)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mauro-miatello "Code") | [<img src="https://avatars.githubusercontent.com/u/60405354?v=4" width="110px;"/><br /><sub>koiakoia</sub>](https://github.com/koiakoia)<br />[💻](https://github.com/snipe/snipe-it/commits?author=koiakoia "Code") | [<img src="https://avatars.githubusercontent.com/u/5323832?v=4" width="110px;"/><br /><sub>Mustafa Online</sub>](https://github.com/mustafa-online)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mustafa-online "Code") | [<img src="https://avatars.githubusercontent.com/u/104601439?v=4" width="110px;"/><br /><sub>franceslui</sub>](https://github.com/franceslui)<br />[💻](https://github.com/snipe/snipe-it/commits?author=franceslui "Code") | [<img src="https://avatars.githubusercontent.com/u/125313163?v=4" width="110px;"/><br /><sub>Q4kK</sub>](https://github.com/Q4kK)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Q4kK "Code") |
| [<img src="https://avatars.githubusercontent.com/u/55590532?v=4" width="110px;"/><br /><sub>squintfox</sub>](https://github.com/squintfox)<br />[💻](https://github.com/snipe/snipe-it/commits?author=squintfox "Code") | [<img src="https://avatars.githubusercontent.com/u/1380084?v=4" width="110px;"/><br /><sub>Jeff Clay</sub>](https://github.com/jeffclay)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jeffclay "Code") | [<img src="https://avatars.githubusercontent.com/u/52716446?v=4" width="110px;"/><br /><sub>Phil J R</sub>](https://github.com/PP-JN-RL)<br />[💻](https://github.com/snipe/snipe-it/commits?author=PP-JN-RL "Code") | [<img src="https://avatars.githubusercontent.com/u/1496725?v=4" width="110px;"/><br /><sub>i_virus</sub>](https://www.corelight.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=chandanchowdhury "Code") | [<img src="https://avatars.githubusercontent.com/u/1020541?v=4" width="110px;"/><br /><sub>Paul Grime</sub>](https://github.com/gitgrimbo)<br />[💻](https://github.com/snipe/snipe-it/commits?author=gitgrimbo "Code") | [<img src="https://avatars.githubusercontent.com/u/922815?v=4" width="110px;"/><br /><sub>Lee Porte</sub>](https://leeporte.co.uk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=LeePorte "Code") | [<img src="https://avatars.githubusercontent.com/u/23613427?v=4" width="110px;"/><br /><sub>BRYAN </sub>](https://github.com/bryanlopezinc)<br />[💻](https://github.com/snipe/snipe-it/commits?author=bryanlopezinc "Code") [⚠️](https://github.com/snipe/snipe-it/commits?author=bryanlopezinc "Tests") |
| [<img src="https://avatars.githubusercontent.com/u/64061710?v=4" width="110px;"/><br /><sub>U-H-T</sub>](https://github.com/U-H-T)<br />[💻](https://github.com/snipe/snipe-it/commits?author=U-H-T "Code") | [<img src="https://avatars.githubusercontent.com/u/5395363?v=4" width="110px;"/><br /><sub>Matt Tyree</sub>](https://github.com/Tyree)<br />[📖](https://github.com/snipe/snipe-it/commits?author=Tyree "Documentation") | [<img src="https://avatars.githubusercontent.com/u/18245993?v=4" width="110px;"/><br /><sub>Konstantin Köhring</sub>](https://www.galaxy102.de)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Galaxy102 "Code") |
| [<img src="https://avatars.githubusercontent.com/u/64061710?v=4" width="110px;"/><br /><sub>U-H-T</sub>](https://github.com/U-H-T)<br />[💻](https://github.com/snipe/snipe-it/commits?author=U-H-T "Code") | [<img src="https://avatars.githubusercontent.com/u/5395363?v=4" width="110px;"/><br /><sub>Matt Tyree</sub>](https://github.com/Tyree)<br />[📖](https://github.com/snipe/snipe-it/commits?author=Tyree "Documentation") | [<img src="https://avatars.githubusercontent.com/u/292081?v=4" width="110px;"/><br /><sub>Florent Bervas</sub>](http://spoontux.net)<br />[💻](https://github.com/snipe/snipe-it/commits?author=FlorentDotMe "Code") | [<img src="https://avatars.githubusercontent.com/u/18245993?v=4" width="110px;"/><br /><sub>Konstantin Köhring</sub>](https://www.galaxy102.de)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Galaxy102 "Code") |
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome!

View file

@ -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);

View file

@ -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);

View file

@ -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'));

View 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);
}
}

View file

@ -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;

View file

@ -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);

View file

@ -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,13 +18,27 @@ class ConsumableObserver
*/
public function updated(Consumable $consumable)
{
$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');
}
}
/**
* Listen to the Consumable created event when
@ -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;

View file

@ -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') {

View file

@ -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(

View file

@ -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'])

View file

@ -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,6 +106,55 @@
</div> <!-- close tab-pane div -->
<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">
@ -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,21 +342,23 @@
</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
@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>
@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
</div>
@ -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

View file

@ -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')

View file

@ -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>

View file

@ -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>') !!}

View file

@ -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>

View file

@ -151,20 +151,25 @@
<tr>
<td>{{ $counter }}.{{ $assignedCounter }}</td>
<td data-formatter="imageFormatter">
<td>
@if ($asset->getImageUrl())
<img src="{{ $asset->getImageUrl() }}" class="thumbnail" style="max-height: 50px;">
@endif
</td>
<td>{{ $asset->asset_tag }}</td>
<td>{{ $asset->name }}</td>
<td>{{ $asset->model->category->name }}</td>
<td>{{ (($asset->model) && ($asset->model->category)) ? $asset->model->category->name : trans('general.invalid_category') }}</td>
<td>{{ ($asset->model) ? $asset->model->name : trans('general.invalid_model') }}</td>
<td>{{ ($asset->defaultLoc) ? $asset->defaultLoc->name : '' }}</td>
<td>{{ ($asset->location) ? $asset->location->name : '' }}</td>
<td>{{ $asset->model->name }}</td>
<td>{{ $asset->serial }}</td>
<td>{{ $asset->last_checkout }}</td>
<td><img style="width:auto;height:100px;" src="{{ asset('/') }}display-sig/{{ $asset->assetlog->first()->accept_signature }}"></td>
<td>
{{ Helper::getFormattedDateObject($asset->last_checkout, 'datetime', false) }}</td>
<td>
@if (($asset->assetlog->first()) && ($asset->assetlog->first()->accept_signature!=''))
<img style="width:auto;height:100px;" src="{{ asset('/') }}display-sig/{{ $asset->assetlog->first()->accept_signature }}">
@endif
</td>
</tr>
@php
$assignedCounter++

View file

@ -32,7 +32,6 @@ class IndexCategoriesTest extends TestCase
'limit' => '20',
]))
->assertOk()
->assertOk()
->assertJsonStructure([
'total',
'rows',

View file

@ -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,
]);
}
}

View 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');
}
}

View 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();
}
}

View 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();
}
}