Merge branch 'develop' into laravel_v9

This commit is contained in:
Brady Wetherington 2023-04-27 13:56:04 -07:00
commit 381890b578
93 changed files with 1305 additions and 248 deletions

View file

@ -2889,6 +2889,24 @@
"avatar_url": "https://avatars.githubusercontent.com/u/570639?v=4",
"profile": "https://github.com/Mezzle",
"contributions": []
},
{
"login": "dboth",
"name": "dboth",
"avatar_url": "https://avatars.githubusercontent.com/u/5731963?v=4",
"profile": "http://dboth.de",
"contributions": [
"code"
]
},
{
"login": "zacharyfleck",
"name": "Zachary Fleck",
"avatar_url": "https://avatars.githubusercontent.com/u/87536651?v=4",
"profile": "https://github.com/zacharyfleck",
"contributions": [
"code"
]
}
]
}

View file

@ -76,7 +76,7 @@ jobs:
with:
context: .
file: ./Dockerfile.alpine
platforms: linux/amd64
platforms: linux/amd64,linux/arm64
# For pull requests, we run the Docker build (to ensure no PR changes break the build),
# but we ONLY do an image push to DockerHub if it's NOT a PR
push: ${{ github.event_name != 'pull_request' }}

View file

@ -76,7 +76,7 @@ jobs:
with:
context: .
file: ./Dockerfile
platforms: linux/amd64
platforms: linux/amd64,linux/arm64
# For pull requests, we run the Docker build (to ensure no PR changes break the build),
# but we ONLY do an image push to DockerHub if it's NOT a PR
push: ${{ github.event_name != 'pull_request' }}

View file

@ -1,5 +1,5 @@
![Build Status](https://app.chipperci.com/projects/0e5f8979-31eb-4ee6-9abf-050b76ab0383/status/master) [![Crowdin](https://d322cqt584bo4o.cloudfront.net/snipe-it/localized.svg)](https://crowdin.com/project/snipe-it) [![Docker Pulls](https://img.shields.io/docker/pulls/snipe/snipe-it.svg)](https://hub.docker.com/r/snipe/snipe-it/) [![Twitter Follow](https://img.shields.io/twitter/follow/snipeitapp.svg?style=social)](https://twitter.com/snipeitapp) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/553ce52037fc43ea99149785afcfe641)](https://www.codacy.com/app/snipe/snipe-it?utm_source=github.com&utm_medium=referral&utm_content=snipe/snipe-it&utm_campaign=Badge_Grade)
[![All Contributors](https://img.shields.io/badge/all_contributors-318-orange.svg?style=flat-square)](#contributors) [![Discord](https://badgen.net/badge/icon/discord?icon=discord&label)](https://discord.gg/yZFtShAcKk) [![huntr](https://cdn.huntr.dev/huntr_security_badge_mono.svg)](https://huntr.dev)
[![All Contributors](https://img.shields.io/badge/all_contributors-320-orange.svg?style=flat-square)](#contributors) [![Discord](https://badgen.net/badge/icon/discord?icon=discord&label)](https://discord.gg/yZFtShAcKk) [![huntr](https://cdn.huntr.dev/huntr_security_badge_mono.svg)](https://huntr.dev)
## Snipe-IT - Open Source Asset Management System
@ -144,7 +144,7 @@ Thanks goes to all of these wonderful people ([emoji key](https://github.com/ken
| [<img src="https://avatars.githubusercontent.com/u/32363424?v=4" width="110px;"/><br /><sub>Peace</sub>](https://github.com/julian-piehl)<br />[💻](https://github.com/snipe/snipe-it/commits?author=julian-piehl "Code") | [<img src="https://avatars.githubusercontent.com/u/231528?v=4" width="110px;"/><br /><sub>Kyle Gordon</sub>](https://github.com/kylegordon)<br />[💻](https://github.com/snipe/snipe-it/commits?author=kylegordon "Code") | [<img src="https://avatars.githubusercontent.com/u/53009155?v=4" width="110px;"/><br /><sub>Katharina Drexel</sub>](http://www.bfh.ch)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sunflowerbofh "Code") | [<img src="https://avatars.githubusercontent.com/u/1931963?v=4" width="110px;"/><br /><sub>David Sferruzza</sub>](https://david.sferruzza.fr/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dsferruzza "Code") | [<img src="https://avatars.githubusercontent.com/u/19511639?v=4" width="110px;"/><br /><sub>Rick Nelson</sub>](https://github.com/rnelsonee)<br />[💻](https://github.com/snipe/snipe-it/commits?author=rnelsonee "Code") | [<img src="https://avatars.githubusercontent.com/u/94169344?v=4" width="110px;"/><br /><sub>BasO12</sub>](https://github.com/BasO12)<br />[💻](https://github.com/snipe/snipe-it/commits?author=BasO12 "Code") | [<img src="https://avatars.githubusercontent.com/u/111710123?v=4" width="110px;"/><br /><sub>Vautia</sub>](https://github.com/Vautia)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Vautia "Code") |
| [<img src="https://avatars.githubusercontent.com/u/28321?v=4" width="110px;"/><br /><sub>Chris Hartjes</sub>](http://www.littlehart.net/atthekeyboard)<br />[💻](https://github.com/snipe/snipe-it/commits?author=chartjes "Code") | [<img src="https://avatars.githubusercontent.com/u/2404584?v=4" width="110px;"/><br /><sub>geo-chen</sub>](https://github.com/geo-chen)<br />[💻](https://github.com/snipe/snipe-it/commits?author=geo-chen "Code") | [<img src="https://avatars.githubusercontent.com/u/6006620?v=4" width="110px;"/><br /><sub>Phan Nguyen</sub>](https://github.com/nh314)<br />[💻](https://github.com/snipe/snipe-it/commits?author=nh314 "Code") | [<img src="https://avatars.githubusercontent.com/u/115993812?v=4" width="110px;"/><br /><sub>Iisakki Jaakkola</sub>](https://github.com/StarlessNights)<br />[💻](https://github.com/snipe/snipe-it/commits?author=StarlessNights "Code") | [<img src="https://avatars.githubusercontent.com/u/22633385?v=4" width="110px;"/><br /><sub>Ikko Ashimine</sub>](https://bandism.net/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=eltociear "Code") | [<img src="https://avatars.githubusercontent.com/u/56871540?v=4" width="110px;"/><br /><sub>Lukas Fehling</sub>](https://github.com/lukasfehling)<br />[💻](https://github.com/snipe/snipe-it/commits?author=lukasfehling "Code") | [<img src="https://avatars.githubusercontent.com/u/1975990?v=4" width="110px;"/><br /><sub>Fernando Almeida</sub>](https://github.com/fernando-almeida)<br />[💻](https://github.com/snipe/snipe-it/commits?author=fernando-almeida "Code") |
| [<img src="https://avatars.githubusercontent.com/u/116301219?v=4" width="110px;"/><br /><sub>akemidx</sub>](https://github.com/akemidx)<br />[💻](https://github.com/snipe/snipe-it/commits?author=akemidx "Code") | [<img src="https://avatars.githubusercontent.com/u/144778?v=4" width="110px;"/><br /><sub>Oguz Bilgic</sub>](http://oguz.site)<br />[💻](https://github.com/snipe/snipe-it/commits?author=oguzbilgic "Code") | [<img src="https://avatars.githubusercontent.com/u/9262438?v=4" width="110px;"/><br /><sub>Scooter Crawford</sub>](https://github.com/scoo73r)<br />[💻](https://github.com/snipe/snipe-it/commits?author=scoo73r "Code") | [<img src="https://avatars.githubusercontent.com/u/5957345?v=4" width="110px;"/><br /><sub>subdriven</sub>](https://github.com/subdriven)<br />[💻](https://github.com/snipe/snipe-it/commits?author=subdriven "Code") | [<img src="https://avatars.githubusercontent.com/u/658865?v=4" width="110px;"/><br /><sub>Andrew Savinykh</sub>](https://github.com/AndrewSav)<br />[💻](https://github.com/snipe/snipe-it/commits?author=AndrewSav "Code") | [<img src="https://avatars.githubusercontent.com/u/1155067?v=4" width="110px;"/><br /><sub>Tadayuki Onishi</sub>](https://kenchan0130.github.io)<br />[💻](https://github.com/snipe/snipe-it/commits?author=kenchan0130 "Code") | [<img src="https://avatars.githubusercontent.com/u/112496896?v=4" width="110px;"/><br /><sub>Florian</sub>](https://github.com/floschoepfer)<br />[💻](https://github.com/snipe/snipe-it/commits?author=floschoepfer "Code") |
| [<img src="https://avatars.githubusercontent.com/u/7305753?v=4" width="110px;"/><br /><sub>Spencer Long</sub>](http://spencerlong.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=spencerrlongg "Code") | [<img src="https://avatars.githubusercontent.com/u/1141514?v=4" width="110px;"/><br /><sub>Marcus Moore</sub>](https://github.com/marcusmoore)<br />[💻](https://github.com/snipe/snipe-it/commits?author=marcusmoore "Code") | [<img src="https://avatars.githubusercontent.com/u/570639?v=4" width="110px;"/><br /><sub>Martin Meredith</sub>](https://github.com/Mezzle)<br /> |
| [<img src="https://avatars.githubusercontent.com/u/7305753?v=4" width="110px;"/><br /><sub>Spencer Long</sub>](http://spencerlong.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=spencerrlongg "Code") | [<img src="https://avatars.githubusercontent.com/u/1141514?v=4" width="110px;"/><br /><sub>Marcus Moore</sub>](https://github.com/marcusmoore)<br />[💻](https://github.com/snipe/snipe-it/commits?author=marcusmoore "Code") | [<img src="https://avatars.githubusercontent.com/u/570639?v=4" width="110px;"/><br /><sub>Martin Meredith</sub>](https://github.com/Mezzle)<br /> | [<img src="https://avatars.githubusercontent.com/u/5731963?v=4" width="110px;"/><br /><sub>dboth</sub>](http://dboth.de)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dboth "Code") | [<img src="https://avatars.githubusercontent.com/u/87536651?v=4" width="110px;"/><br /><sub>Zachary Fleck</sub>](https://github.com/zacharyfleck)<br />[💻](https://github.com/snipe/snipe-it/commits?author=zacharyfleck "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

@ -56,7 +56,7 @@ class CheckoutLicenseToAllUsers extends Command
return false;
}
$users = User::whereNull('deleted_at')->where('autoassign_licenses', '==', 1)->with('licenses')->get();
$users = User::whereNull('deleted_at')->where('autoassign_licenses', '=', 1)->with('licenses')->get();
if ($users->count() > $license->getAvailSeatsCountAttribute()) {
$this->info('You do not have enough free seats to complete this task, so we will check out as many as we can. ');

View file

@ -20,13 +20,14 @@ class CreateAdmin extends Command
* @property string $password
* @property boolean $activated
* @property boolean $show_in_list
* @property boolean $autoassign_licenses
* @property \Illuminate\Support\Carbon|null $created_at
* @property mixed $created_by
*/
protected $signature = 'snipeit:create-admin {--first_name=} {--last_name=} {--email=} {--username=} {--password=} {show_in_list?}';
protected $signature = 'snipeit:create-admin {--first_name=} {--last_name=} {--email=} {--username=} {--password=} {show_in_list?} {autoassign_licenses?}';
/**
* The console command description.
@ -54,6 +55,9 @@ class CreateAdmin extends Command
$email = $this->option('email');
$password = $this->option('password');
$show_in_list = $this->argument('show_in_list');
$autoassign_licenses = $this->argument('autoassign_licenses');
if (($first_name == '') || ($last_name == '') || ($username == '') || ($email == '') || ($password == '')) {
$this->info('ERROR: All fields are required.');
@ -70,6 +74,11 @@ class CreateAdmin extends Command
if ($show_in_list == 'false') {
$user->show_in_list = 0;
}
if ($autoassign_licenses == 'false') {
$user->autoassign_licenses = 0;
}
if ($user->save()) {
$this->info('New user created');
$user->groups()->attach(1);

View file

@ -62,6 +62,7 @@ class LdapSync extends Command
$ldap_result_phone = Setting::getSettings()->ldap_phone_field;
$ldap_result_jobtitle = Setting::getSettings()->ldap_jobtitle;
$ldap_result_country = Setting::getSettings()->ldap_country;
$ldap_result_location = Setting::getSettings()->ldap_location;
$ldap_result_dept = Setting::getSettings()->ldap_dept;
$ldap_result_manager = Setting::getSettings()->ldap_manager;
$ldap_default_group = Setting::getSettings()->ldap_default_group;
@ -209,8 +210,11 @@ class LdapSync extends Command
$item['country'] = $results[$i][$ldap_result_country][0] ?? '';
$item['department'] = $results[$i][$ldap_result_dept][0] ?? '';
$item['manager'] = $results[$i][$ldap_result_manager][0] ?? '';
$item['location'] = $results[$i][$ldap_result_location][0] ?? '';
$location = Location::firstOrCreate([
'name' => $item['location'],
]);
$department = Department::firstOrCreate([
'name' => $item['department'],
]);
@ -236,6 +240,7 @@ class LdapSync extends Command
$user->jobtitle = $item['jobtitle'];
$user->country = $item['country'];
$user->department_id = $department->id;
$user->location_id = $location->id;
if($item['manager'] != null) {
// Check Cache first

View file

@ -77,7 +77,7 @@ class AccessoriesController extends Controller
$accessory->manufacturer_id = request('manufacturer_id');
$accessory->model_number = request('model_number');
$accessory->purchase_date = request('purchase_date');
$accessory->purchase_cost = Helper::ParseCurrency(request('purchase_cost'));
$accessory->purchase_cost = request('purchase_cost');
$accessory->qty = request('qty');
$accessory->user_id = Auth::user()->id;
$accessory->supplier_id = request('supplier_id');
@ -180,7 +180,7 @@ class AccessoriesController extends Controller
$accessory->order_number = request('order_number');
$accessory->model_number = request('model_number');
$accessory->purchase_date = request('purchase_date');
$accessory->purchase_cost = Helper::ParseCurrency(request('purchase_cost'));
$accessory->purchase_cost = request('purchase_cost');
$accessory->qty = request('qty');
$accessory->supplier_id = request('supplier_id');
$accessory->notes = request('notes');

View file

@ -118,7 +118,7 @@ class AssetMaintenancesController extends Controller
$assetMaintenance = new AssetMaintenance();
$assetMaintenance->supplier_id = $request->input('supplier_id');
$assetMaintenance->is_warranty = $request->input('is_warranty');
$assetMaintenance->cost = Helper::ParseCurrency($request->input('cost'));
$assetMaintenance->cost = $request->input('cost');
$assetMaintenance->notes = e($request->input('notes'));
$asset = Asset::find(e($request->input('asset_id')));
@ -175,7 +175,7 @@ class AssetMaintenancesController extends Controller
$assetMaintenance->supplier_id = e($request->input('supplier_id'));
$assetMaintenance->is_warranty = e($request->input('is_warranty'));
$assetMaintenance->cost = Helper::ParseCurrency($request->input('cost'));
$assetMaintenance->cost = $request->input('cost');
$assetMaintenance->notes = e($request->input('notes'));
$asset = Asset::find(request('asset_id'));

View file

@ -550,7 +550,8 @@ class AssetsController extends Controller
$asset->depreciate = '0';
$asset->status_id = $request->get('status_id', 0);
$asset->warranty_months = $request->get('warranty_months', null);
$asset->purchase_cost = Helper::ParseCurrency($request->get('purchase_cost')); // this is the API's store method, so I don't know that I want to do this? Confusing. FIXME (or not?!)
$asset->purchase_cost = $request->get('purchase_cost');
$asset->asset_eol_date = $request->get('asset_eol_date', $asset->present()->eol_date());
$asset->purchase_date = $request->get('purchase_date', null);
$asset->assigned_to = $request->get('assigned_to', null);
$asset->supplier_id = $request->get('supplier_id');
@ -558,6 +559,7 @@ class AssetsController extends Controller
$asset->rtd_location_id = $request->get('rtd_location_id', null);
$asset->location_id = $request->get('rtd_location_id', null);
/**
* this is here just legacy reasons. Api\AssetController
* used image_source once to allow encoded image uploads.

View file

@ -23,10 +23,10 @@ class ManufacturersController extends Controller
public function index(Request $request)
{
$this->authorize('view', Manufacturer::class);
$allowed_columns = ['id', 'name', 'url', 'support_url', 'support_email', 'support_phone', 'created_at', 'updated_at', 'image', 'assets_count', 'consumables_count', 'components_count', 'licenses_count'];
$allowed_columns = ['id', 'name', 'url', 'support_url', 'support_email', 'warranty_lookup_url', 'support_phone', 'created_at', 'updated_at', 'image', 'assets_count', 'consumables_count', 'components_count', 'licenses_count'];
$manufacturers = Manufacturer::select(
['id', 'name', 'url', 'support_url', 'support_email', 'support_phone', 'created_at', 'updated_at', 'image', 'deleted_at']
['id', 'name', 'url', 'support_url', 'warranty_lookup_url', 'support_email', 'support_phone', 'created_at', 'updated_at', 'image', 'deleted_at']
)->withCount('assets as assets_count')->withCount('licenses as licenses_count')->withCount('consumables as consumables_count')->withCount('accessories as accessories_count');
if ($request->input('deleted') == 'true') {
@ -49,6 +49,10 @@ class ManufacturersController extends Controller
$manufacturers->where('support_url', '=', $request->input('support_url'));
}
if ($request->filled('warranty_lookup_url')) {
$manufacturers->where('warranty_lookup_url', '=', $request->input('warranty_lookup_url'));
}
if ($request->filled('support_phone')) {
$manufacturers->where('support_phone', '=', $request->input('support_phone'));
}

View file

@ -71,6 +71,7 @@ class UsersController extends Controller
'users.start_date',
'users.end_date',
'users.vip',
'users.autoassign_licenses',
])->with('manager', 'groups', 'userloc', 'company', 'department', 'assets', 'licenses', 'accessories', 'consumables', 'createdBy',)
->withCount('assets as assets_count', 'licenses as licenses_count', 'accessories as accessories_count', 'consumables as consumables_count');
@ -187,6 +188,10 @@ class UsersController extends Controller
$users->has('accessories', '=', $request->input('accessories_count'));
}
if ($request->filled('autoassign_licenses')) {
$users->where('autoassign_licenses', '=', $request->input('autoassign_licenses'));
}
if ($request->filled('search')) {
$users = $users->TextSearch($request->input('search'));
}
@ -259,6 +264,7 @@ class UsersController extends Controller
'vip',
'start_date',
'end_date',
'autoassign_licenses',
];
$sort = in_array($request->get('sort'), $allowed_columns) ? $request->get('sort') : 'first_name';
@ -356,7 +362,7 @@ class UsersController extends Controller
$user->permissions = $permissions_array;
}
$tmp_pass = substr(str_shuffle('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'), 0, 20);
$tmp_pass = substr(str_shuffle('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'), 0, 40);
$user->password = bcrypt($request->get('password', $tmp_pass));
app('App\Http\Requests\ImageUploadRequest')->handleImages($user, 600, 'image', 'avatars', 'avatar');

View file

@ -101,7 +101,7 @@ class AssetMaintenancesController extends Controller
$assetMaintenance = new AssetMaintenance();
$assetMaintenance->supplier_id = $request->input('supplier_id');
$assetMaintenance->is_warranty = $request->input('is_warranty');
$assetMaintenance->cost = Helper::ParseCurrency($request->input('cost'));
$assetMaintenance->cost = $request->input('cost');
$assetMaintenance->notes = $request->input('notes');
$asset = Asset::find($request->input('asset_id'));
@ -211,7 +211,7 @@ class AssetMaintenancesController extends Controller
$assetMaintenance->supplier_id = $request->input('supplier_id');
$assetMaintenance->is_warranty = $request->input('is_warranty');
$assetMaintenance->cost = Helper::ParseCurrency($request->input('cost'));
$assetMaintenance->cost = $request->input('cost');
$assetMaintenance->notes = $request->input('notes');
$asset = Asset::find(request('asset_id'));

View file

@ -140,9 +140,9 @@ class AssetsController extends Controller
$asset->depreciate = '0';
$asset->status_id = request('status_id');
$asset->warranty_months = request('warranty_months', null);
$asset->purchase_cost = Helper::ParseCurrency($request->get('purchase_cost'));
$asset->purchase_cost = request('purchase_cost');
$asset->purchase_date = request('purchase_date', null);
$asset->asset_eol_date = request('asset_eol_date', null);
$asset->asset_eol_date = request('asset_eol_date', $asset->present()->eol_date());
$asset->assigned_to = request('assigned_to', null);
$asset->supplier_id = request('supplier_id', null);
$asset->requestable = request('requestable', 0);
@ -312,7 +312,7 @@ class AssetsController extends Controller
$asset->status_id = $request->input('status_id', null);
$asset->warranty_months = $request->input('warranty_months', null);
$asset->purchase_cost = Helper::ParseCurrency($request->input('purchase_cost', null));
$asset->purchase_cost = $request->input('purchase_cost', null);
$asset->asset_eol_date = request('asset_eol_date', null);
$asset->purchase_date = $request->input('purchase_date', null);

View file

@ -29,7 +29,7 @@ class BulkAssetsController extends Controller
*/
public function edit(Request $request)
{
$this->authorize('update', Asset::class);
$this->authorize('view', Asset::class);
if (! $request->filled('ids')) {
return redirect()->back()->with('error', trans('admin/hardware/message.update.no_assets_selected'));
@ -44,6 +44,7 @@ class BulkAssetsController extends Controller
if ($request->filled('bulk_actions')) {
switch ($request->input('bulk_actions')) {
case 'labels':
$this->authorize('view', Asset::class);
return view('hardware/labels')
->with('assets', Asset::find($asset_ids))
->with('settings', Setting::getSettings())
@ -51,6 +52,7 @@ class BulkAssetsController extends Controller
->with('count', 0);
case 'delete':
$this->authorize('delete', Asset::class);
$assets = Asset::with('assignedTo', 'location')->find($asset_ids);
$assets->each(function ($asset) {
$this->authorize('delete', $asset);
@ -58,7 +60,8 @@ class BulkAssetsController extends Controller
return view('hardware/bulk-delete')->with('assets', $assets);
case 'restore':
case 'restore':
$this->authorize('update', Asset::class);
$assets = Asset::withTrashed()->find($asset_ids);
$assets->each(function ($asset) {
$this->authorize('delete', $asset);
@ -67,6 +70,7 @@ class BulkAssetsController extends Controller
return view('hardware/bulk-restore')->with('assets', $assets);
case 'edit':
$this->authorize('update', Asset::class);
return view('hardware/bulk')
->with('assets', $asset_ids)
->with('statuslabel_list', Helper::statusLabelList());
@ -145,7 +149,7 @@ class BulkAssetsController extends Controller
}
if ($request->filled('purchase_cost')) {
$this->update_array['purchase_cost'] = Helper::ParseCurrency($request->input('purchase_cost'));
$this->update_array['purchase_cost'] = $request->input('purchase_cost');
}
if ($request->filled('company_id')) {
@ -333,6 +337,7 @@ class BulkAssetsController extends Controller
}
public function restore(Request $request) {
$this->authorize('update', Asset::class);
$assetIds = $request->get('ids');
if (empty($assetIds)) {
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.restore.nothing_updated'));

View file

@ -78,7 +78,7 @@ class ComponentsController extends Controller
$component->min_amt = $request->input('min_amt', null);
$component->serial = $request->input('serial', null);
$component->purchase_date = $request->input('purchase_date', null);
$component->purchase_cost = Helper::ParseCurrency($request->input('purchase_cost', null));
$component->purchase_cost = $request->input('purchase_cost', null);
$component->qty = $request->input('qty');
$component->user_id = Auth::id();
$component->notes = $request->input('notes');
@ -153,7 +153,7 @@ class ComponentsController extends Controller
$component->min_amt = $request->input('min_amt');
$component->serial = $request->input('serial');
$component->purchase_date = $request->input('purchase_date');
$component->purchase_cost = Helper::ParseCurrency(request('purchase_cost'));
$component->purchase_cost = request('purchase_cost');
$component->qty = $request->input('qty');
$component->notes = $request->input('notes');

View file

@ -77,7 +77,7 @@ class ConsumablesController extends Controller
$consumable->model_number = $request->input('model_number');
$consumable->item_no = $request->input('item_no');
$consumable->purchase_date = $request->input('purchase_date');
$consumable->purchase_cost = Helper::ParseCurrency($request->input('purchase_cost'));
$consumable->purchase_cost = $request->input('purchase_cost');
$consumable->qty = $request->input('qty');
$consumable->user_id = Auth::id();
$consumable->notes = $request->input('notes');
@ -154,7 +154,7 @@ class ConsumablesController extends Controller
$consumable->model_number = $request->input('model_number');
$consumable->item_no = $request->input('item_no');
$consumable->purchase_date = $request->input('purchase_date');
$consumable->purchase_cost = Helper::ParseCurrency($request->input('purchase_cost'));
$consumable->purchase_cost = $request->input('purchase_cost');
$consumable->qty = Helper::ParseFloat($request->input('qty'));
$consumable->notes = $request->input('notes');

View file

@ -112,4 +112,54 @@ class LicenseCheckinController extends Controller
// Redirect to the license page with error
return redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.checkin.error'));
}
/**
* Bulk checkin all license seats
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @see LicenseCheckinController::create() method that provides the form view
* @since [v6.1.1]
* @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function bulkCheckin(Request $request, $licenseId) {
$license = License::findOrFail($licenseId);
$this->authorize('checkin', $license);
$licenseSeatsByUser = LicenseSeat::where('license_id', '=', $licenseId)
->whereNotNull('assigned_to')
->with('user')
->get();
foreach ($licenseSeatsByUser as $user_seat) {
$user_seat->assigned_to = null;
if ($user_seat->save()) {
\Log::debug('Checking in '.$license->name.' from user '.$user_seat->username);
$user_seat->logCheckin($user_seat->user, trans('admin/licenses/general.bulk.checkin_all.log_msg'));
}
}
$licenseSeatsByAsset = LicenseSeat::where('license_id', '=', $licenseId)
->whereNotNull('asset_id')
->with('asset')
->get();
$count = 0;
foreach ($licenseSeatsByAsset as $asset_seat) {
$asset_seat->asset_id = null;
if ($asset_seat->save()) {
\Log::debug('Checking in '.$license->name.' from asset '.$asset_seat->asset_tag);
$asset_seat->logCheckin($asset_seat->asset, trans('admin/licenses/general.bulk.checkin_all.log_msg'));
$count++;
}
}
return redirect()->back()->with('success', trans_choice('admin/licenses/general.bulk.checkin_all.success', 2, ['count' => $count] ));
}
}

View file

@ -126,4 +126,70 @@ class LicenseCheckoutController extends Controller
return false;
}
/**
* Bulk checkin all license seats
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @see LicenseCheckinController::create() method that provides the form view
* @since [v6.1.1]
* @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function bulkCheckout($licenseId) {
\Log::debug('Checking out '.$licenseId.' via bulk');
$license = License::findOrFail($licenseId);
$this->authorize('checkin', $license);
$avail_count = $license->getAvailSeatsCountAttribute();
$users = User::whereNull('deleted_at')->where('autoassign_licenses', '=', 1)->with('licenses')->get();
\Log::debug($avail_count.' will be assigned');
if ($users->count() > $avail_count) {
\Log::debug('You do not have enough free seats to complete this task, so we will check out as many as we can. ');
}
// If the license is valid, check that there is an available seat
if ($license->availCount()->count() < 1) {
return redirect()->back()->with('error', trans('admin/licenses/general.bulk.checkout_all.error_no_seats'));
}
$assigned_count = 0;
foreach ($users as $user) {
// Check to make sure this user doesn't already have this license checked out to them
if ($user->licenses->where('id', '=', $licenseId)->count()) {
\Log::debug($user->username.' already has this license checked out to them. Skipping... ');
continue;
}
$licenseSeat = $license->freeSeat();
// Update the seat with checkout info
$licenseSeat->assigned_to = $user->id;
if ($licenseSeat->save()) {
$avail_count--;
$assigned_count++;
$licenseSeat->logCheckout(trans('admin/licenses/general.bulk.checkout_all.log_msg'), $user);
\Log::debug('License '.$license->name.' seat '.$licenseSeat->id.' checked out to '.$user->username);
}
if ($avail_count == 0) {
return redirect()->back()->with('warning', trans('admin/licenses/general.bulk.checkout_all.warn_not_enough_seats', ['count' => $assigned_count]));
}
}
if ($assigned_count == 0) {
return redirect()->back()->with('warning', trans('admin/licenses/general.bulk.checkout_all.warn_no_avail_users', ['count' => $assigned_count]));
}
return redirect()->back()->with('success', trans_choice('admin/licenses/general.bulk.checkout_all.success', 2, ['count' => $assigned_count] ));
}
}

View file

@ -6,6 +6,8 @@ use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Models\Company;
use App\Models\License;
use App\Models\LicenseSeat;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
@ -86,7 +88,7 @@ class LicensesController extends Controller
$license->name = $request->input('name');
$license->notes = $request->input('notes');
$license->order_number = $request->input('order_number');
$license->purchase_cost = Helper::ParseCurrency($request->input('purchase_cost'));
$license->purchase_cost = $request->input('purchase_cost');
$license->purchase_date = $request->input('purchase_date');
$license->purchase_order = $request->input('purchase_order');
$license->purchase_order = $request->input('purchase_order');
@ -164,7 +166,7 @@ class LicensesController extends Controller
$license->name = $request->input('name');
$license->notes = $request->input('notes');
$license->order_number = $request->input('order_number');
$license->purchase_cost = Helper::ParseCurrency($request->input('purchase_cost'));
$license->purchase_cost = $request->input('purchase_cost');
$license->purchase_date = $request->input('purchase_date');
$license->purchase_order = $request->input('purchase_order');
$license->reassignable = $request->input('reassignable', 0);
@ -233,16 +235,40 @@ class LicensesController extends Controller
{
$license = License::with('assignedusers')->find($licenseId);
if ($license) {
$this->authorize('view', $license);
return view('licenses/view', compact('license'));
if (!$license) {
return redirect()->route('licenses.index')
->with('error', trans('admin/licenses/message.does_not_exist'));
}
return redirect()->route('licenses.index')
->with('error', trans('admin/licenses/message.does_not_exist'));
$users_count = User::where('autoassign_licenses', '1')->count();
$total_seats_count = $license->totalSeatsByLicenseID();
$available_seats_count = $license->availCount()->count();
$checkedout_seats_count = ($total_seats_count - $available_seats_count);
\Log::debug('Total: '.$total_seats_count);
\Log::debug('Users: '.$users_count);
\Log::debug('Available: '.$available_seats_count);
\Log::debug('Checkedout: '.$checkedout_seats_count);
$this->authorize('view', $license);
return view('licenses.view', compact('license'))
->with('users_count', $users_count)
->with('total_seats_count', $total_seats_count)
->with('available_seats_count', $available_seats_count)
->with('checkedout_seats_count', $checkedout_seats_count);
}
/**
* Returns a view with prepopulated data for clone
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $licenseId
* @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function getClone($licenseId = null)
{
if (is_null($license_to_clone = License::find($licenseId))) {

View file

@ -68,6 +68,7 @@ class ManufacturersController extends Controller
$manufacturer->user_id = Auth::id();
$manufacturer->url = $request->input('url');
$manufacturer->support_url = $request->input('support_url');
$manufacturer->warranty_lookup_url = $request->input('warranty_lookup_url');
$manufacturer->support_phone = $request->input('support_phone');
$manufacturer->support_email = $request->input('support_email');
$manufacturer = $request->handleImages($manufacturer);
@ -123,10 +124,11 @@ class ManufacturersController extends Controller
return redirect()->route('manufacturers.index')->with('error', trans('admin/manufacturers/message.does_not_exist'));
}
// Save the data
// Save the data
$manufacturer->name = $request->input('name');
$manufacturer->url = $request->input('url');
$manufacturer->support_url = $request->input('support_url');
$manufacturer->warranty_lookup_url = $request->input('warranty_lookup_url');
$manufacturer->support_phone = $request->input('support_phone');
$manufacturer->support_email = $request->input('support_email');

View file

@ -961,6 +961,7 @@ class SettingsController extends Controller
$setting->ldap_phone_field = $request->input('ldap_phone');
$setting->ldap_jobtitle = $request->input('ldap_jobtitle');
$setting->ldap_country = $request->input('ldap_country');
$setting->ldap_location = $request->input('ldap_location');
$setting->ldap_dept = $request->input('ldap_dept');
$setting->ldap_client_tls_cert = $request->input('ldap_client_tls_cert');
$setting->ldap_client_tls_key = $request->input('ldap_client_tls_key');

View file

@ -113,7 +113,8 @@ class BulkUsersController extends Controller
->conditionallyAddItem('locale')
->conditionallyAddItem('remote')
->conditionallyAddItem('ldap_import')
->conditionallyAddItem('activated');
->conditionallyAddItem('activated')
->conditionallyAddItem('autoassign_licenses');
// If the manager_id is one of the users being updated, generate a warning.

View file

@ -120,7 +120,7 @@ class UsersController extends Controller
$user->created_by = Auth::user()->id;
$user->start_date = $request->input('start_date', null);
$user->end_date = $request->input('end_date', null);
$user->autoassign_licenses= $request->input('autoassign_licenses', 1);
$user->autoassign_licenses = $request->input('autoassign_licenses', 0);
// Strip out the superuser permission if the user isn't a superadmin
$permissions_array = $request->input('permission');
@ -210,7 +210,6 @@ class UsersController extends Controller
*/
public function update(SaveUserRequest $request, $id = null)
{
// We need to reverse the UI specific logic for our
// permissions here before we update the user.
$permissions = $request->input('permissions', []);
@ -268,14 +267,15 @@ class UsersController extends Controller
$user->city = $request->input('city', null);
$user->state = $request->input('state', null);
$user->country = $request->input('country', null);
$user->activated = $request->input('activated', 0);
// if a user is editing themselves we should always keep activated true
$user->activated = $request->input('activated', $request->user()->is($user) ? 1 : 0);
$user->zip = $request->input('zip', null);
$user->remote = $request->input('remote', 0);
$user->vip = $request->input('vip', 0);
$user->website = $request->input('website', null);
$user->start_date = $request->input('start_date', null);
$user->end_date = $request->input('end_date', null);
$user->autoassign_licenses = $request->input('autoassign_licenses', 1);
$user->autoassign_licenses = $request->input('autoassign_licenses', 0);
// Update the location of any assets checked out to this user
Asset::where('assigned_type', User::class)
@ -670,4 +670,4 @@ class UsersController extends Controller
return redirect()->back()->with('error', 'User is not activated, is LDAP synced, or does not have an email address ');
}
}
}

View file

@ -56,7 +56,7 @@ class LicensesTransformer
'checkin' => Gate::allows('checkin', License::class),
'clone' => Gate::allows('create', License::class),
'update' => Gate::allows('update', License::class),
'delete' => Gate::allows('delete', License::class),
'delete' => (Gate::allows('delete', License::class) && ($license->seats == $license->availCount()->count())) ? true : false,
];
$array += $permissions_array;

View file

@ -29,6 +29,7 @@ class ManufacturersTransformer
'url' => e($manufacturer->url),
'image' => ($manufacturer->image) ? Storage::disk('public')->url('manufacturers/'.e($manufacturer->image)) : null,
'support_url' => e($manufacturer->support_url),
'warranty_lookup_url' => e($manufacturer->warranty_lookup_url),
'support_phone' => e($manufacturer->support_phone),
'support_email' => e($manufacturer->support_email),
'assets_count' => (int) $manufacturer->assets_count,

View file

@ -56,6 +56,7 @@ class UsersTransformer
'notes'=> e($user->notes),
'permissions' => $user->decodePermissions(),
'activated' => ($user->activated == '1') ? true : false,
'autoassign_licenses' => ($user->autoassign_licenses == '1') ? true : false,
'ldap_import' => ($user->ldap_import == '1') ? true : false,
'two_factor_enrolled' => ($user->two_factor_active_and_enrolled()) ? true : false,
'two_factor_optin' => ($user->two_factor_active()) ? true : false,

View file

@ -58,7 +58,8 @@ class UserImporter extends ItemImporter
$this->item['department_id'] = $this->createOrFetchDepartment($this->findCsvMatch($row, 'department'));
$this->item['manager_id'] = $this->fetchManager($this->findCsvMatch($row, 'manager_first_name'), $this->findCsvMatch($row, 'manager_last_name'));
$this->item['remote'] =($this->fetchHumanBoolean($this->findCsvMatch($row, 'remote')) ==1 ) ? '1' : 0;
$this->item['vip'] =($this->fetchHumanBoolean($this->findCsvMatch($row, 'vip')) ==1 ) ? '1' : 0;
$this->item['vip'] = ($this->fetchHumanBoolean($this->findCsvMatch($row, 'vip')) ==1 ) ? '1' : 0;
$this->item['autoassign_licenses'] = ($this->fetchHumanBoolean($this->findCsvMatch($row, 'autoassign_licenses')) ==1 ) ? '1' : 0;
$user_department = $this->findCsvMatch($row, 'department');

View file

@ -2,22 +2,21 @@
namespace App\Listeners;
use App\Events\CheckoutableCheckedOut;
use App\Models\Accessory;
use App\Models\Asset;
use App\Models\CheckoutAcceptance;
use App\Models\Component;
use App\Models\Consumable;
use App\Models\LicenseSeat;
use App\Models\Recipients\AdminRecipient;
use App\Models\Setting;
use App\Models\User;
use App\Notifications\CheckinAccessoryNotification;
use App\Notifications\CheckinAssetNotification;
use App\Notifications\CheckinLicenseNotification;
use App\Notifications\CheckinLicenseSeatNotification;
use App\Notifications\CheckoutAccessoryNotification;
use App\Notifications\CheckoutAssetNotification;
use App\Notifications\CheckoutConsumableNotification;
use App\Notifications\CheckoutLicenseNotification;
use App\Notifications\CheckoutLicenseSeatNotification;
use Illuminate\Support\Facades\Notification;
use Exception;
@ -25,18 +24,17 @@ use Log;
class CheckoutableListener
{
private array $skipNotificationsFor = [
Component::class,
];
/**
* Notify the user about the checked out checkoutable and add a record to the
* checkout_requests table.
* Notify the user and post to webhook about the checked out checkoutable
* and add a record to the checkout_requests table.
*/
public function onCheckedOut($event)
{
/**
* When the item wasn't checked out to a user, we can't send notifications
*/
if (! $event->checkedOutTo instanceof User) {
if ($this->shouldNotSendAnyNotifications($event->checkoutable)){
return;
}
@ -46,6 +44,11 @@ class CheckoutableListener
$acceptance = $this->getCheckoutAcceptance($event);
try {
if ($this->shouldSendWebhookNotification()) {
Notification::route('slack', Setting::getSettings()->webhook_endpoint)
->notify($this->getCheckoutNotification($event));
}
if (! $event->checkedOutTo->locale) {
Notification::locale(Setting::getSettings()->locale)->send(
$this->getNotifiables($event),
@ -63,16 +66,13 @@ class CheckoutableListener
}
/**
* Notify the user about the checked in checkoutable
* Notify the user and post to webhook about the checked in checkoutable
*/
public function onCheckedIn($event)
{
\Log::debug('onCheckedIn in the Checkoutable listener fired');
/**
* When the item wasn't checked out to a user, we can't send notifications
*/
if (! $event->checkedOutTo instanceof User) {
if ($this->shouldNotSendAnyNotifications($event->checkoutable)) {
return;
}
@ -90,6 +90,11 @@ class CheckoutableListener
}
try {
if ($this->shouldSendWebhookNotification()) {
Notification::route('slack', Setting::getSettings()->webhook_endpoint)
->notify($this->getCheckinNotification($event));
}
// Use default locale
if (! $event->checkedOutTo->locale) {
Notification::locale(Setting::getSettings()->locale)->send(
@ -182,11 +187,11 @@ class CheckoutableListener
/**
* Get the appropriate notification for the event
*
* @param CheckoutableCheckedIn $event
* @param CheckoutAcceptance $acceptance
* @param CheckoutableCheckedOut $event
* @param CheckoutAcceptance|null $acceptance
* @return Notification
*/
private function getCheckoutNotification($event, $acceptance)
private function getCheckoutNotification($event, $acceptance = null)
{
$notificationClass = null;
@ -225,4 +230,14 @@ class CheckoutableListener
'App\Listeners\CheckoutableListener@onCheckedOut'
);
}
private function shouldNotSendAnyNotifications($checkoutable): bool
{
return in_array(get_class($checkoutable), $this->skipNotificationsFor);
}
private function shouldSendWebhookNotification(): bool
{
return Setting::getSettings() && Setting::getSettings()->webhook_endpoint;
}
}

View file

@ -95,8 +95,8 @@ class AssetMaintenance extends Model implements ICompanyableChild
*/
public function setCostAttribute($value)
{
$value = Helper::ParseFloat($value);
if ($value == '0.0') {
$value = Helper::ParseCurrency($value);
if ($value == 0) {
$value = null;
}
$this->attributes['cost'] = $value;

View file

@ -213,6 +213,7 @@ class Ldap extends Model
$ldap_result_phone = Setting::getSettings()->ldap_phone;
$ldap_result_jobtitle = Setting::getSettings()->ldap_jobtitle;
$ldap_result_country = Setting::getSettings()->ldap_country;
$ldap_result_location = Setting::getSettings()->ldap_location;
$ldap_result_dept = Setting::getSettings()->ldap_dept;
$ldap_result_manager = Setting::getSettings()->ldap_manager;
// Get LDAP user data
@ -227,6 +228,7 @@ class Ldap extends Model
$item['country'] = $ldapattributes[$ldap_result_country][0] ?? '';
$item['department'] = $ldapattributes[$ldap_result_dept][0] ?? '';
$item['manager'] = $ldapattributes[$ldap_result_manager][0] ?? '';
$item['location'] = $ldapattributes[$ldap_result_location][0] ?? '';
return $item;
}

View file

@ -106,10 +106,10 @@ class License extends Depreciable
* @var array
*/
protected $searchableRelations = [
'manufacturer' => ['name'],
'company' => ['name'],
'category' => ['name'],
'depreciation' => ['name'],
'manufacturer' => ['name'],
'company' => ['name'],
'category' => ['name'],
'depreciation' => ['name'],
];
/**
@ -425,7 +425,7 @@ class License extends Depreciable
public static function assetcount()
{
return LicenseSeat::whereNull('deleted_at')
->count();
->count();
}
@ -441,8 +441,8 @@ class License extends Depreciable
public function totalSeatsByLicenseID()
{
return LicenseSeat::where('license_id', '=', $this->id)
->whereNull('deleted_at')
->count();
->whereNull('deleted_at')
->count();
}
/**
@ -486,11 +486,12 @@ class License extends Depreciable
public static function availassetcount()
{
return LicenseSeat::whereNull('assigned_to')
->whereNull('asset_id')
->whereNull('deleted_at')
->count();
->whereNull('asset_id')
->whereNull('deleted_at')
->count();
}
/**
* Returns the number of total available seats for this license
*
@ -533,7 +534,7 @@ class License extends Depreciable
{
return $this->licenseSeatsRelation()->where(function ($query) {
$query->whereNotNull('assigned_to')
->orWhereNotNull('asset_id');
->orWhereNotNull('asset_id');
});
}
@ -621,13 +622,13 @@ class License extends Depreciable
public function freeSeat()
{
return $this->licenseseats()
->whereNull('deleted_at')
->where(function ($query) {
$query->whereNull('assigned_to')
->whereNull('asset_id');
})
->orderBy('id', 'asc')
->first();
->whereNull('deleted_at')
->where(function ($query) {
$query->whereNull('assigned_to')
->whereNull('asset_id');
})
->orderBy('id', 'asc')
->first();
}
@ -657,11 +658,11 @@ class License extends Depreciable
$days = (is_null($days)) ? 60 : $days;
return self::whereNotNull('expiration_date')
->whereNull('deleted_at')
->whereRaw(DB::raw('DATE_SUB(`expiration_date`,INTERVAL '.$days.' DAY) <= DATE(NOW()) '))
->where('expiration_date', '>', date('Y-m-d'))
->orderBy('expiration_date', 'ASC')
->get();
->whereNull('deleted_at')
->whereRaw(DB::raw('DATE_SUB(`expiration_date`,INTERVAL '.$days.' DAY) <= DATE(NOW()) '))
->where('expiration_date', '>', date('Y-m-d'))
->orderBy('expiration_date', 'ASC')
->get();
}
/**
@ -705,4 +706,4 @@ class License extends Depreciable
return $query->leftJoin('companies as companies', 'licenses.company_id', '=', 'companies.id')->select('licenses.*')
->orderBy('companies.name', $order);
}
}
}

View file

@ -6,13 +6,15 @@ use App\Models\Traits\Acceptable;
use App\Notifications\CheckinLicenseNotification;
use App\Notifications\CheckoutLicenseNotification;
use App\Presenters\Presentable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
class LicenseSeat extends SnipeModel implements ICompanyableChild
{
use CompanyableChildTrait;
use SoftDeletes;
use HasFactory;
use Loggable;
use SoftDeletes;
protected $presenter = \App\Presenters\LicenseSeatPresenter::class;
use Presentable;

View file

@ -23,8 +23,9 @@ class Manufacturer extends SnipeModel
protected $rules = [
'name' => 'required|min:2|max:255|unique:manufacturers,name,NULL,id,deleted_at,NULL',
'url' => 'url|nullable',
'support_url' => 'url|nullable',
'support_email' => 'email|nullable',
'support_url' => 'nullable|url',
'warranty_lookup_url' => 'starts_with:http://,https://,afp://,facetime://,file://,irc://','nullable'
];
protected $hidden = ['user_id'];
@ -51,6 +52,7 @@ class Manufacturer extends SnipeModel
'support_phone',
'support_url',
'url',
'warranty_lookup_url',
];
use Searchable;

View file

@ -341,7 +341,15 @@ class Setting extends Model
'ad_domain',
'ad_append_domain',
'ldap_client_tls_key',
'ldap_client_tls_cert'
'ldap_client_tls_cert',
'ldap_default_group',
'ldap_dept',
'ldap_emp_num',
'ldap_phone_field',
'ldap_jobtitle',
'ldap_manager',
'ldap_country',
'ldap_location',
])->first()->getAttributes();
return collect($ldapSettings);

View file

@ -21,9 +21,9 @@ class SnipeModel extends Model
*/
public function setPurchaseCostAttribute($value)
{
$value = Helper::ParseFloat($value);
$value = Helper::ParseCurrency($value);
if ($value == '0.0') {
if ($value == 0) {
$value = null;
}
$this->attributes['purchase_cost'] = $value;

View file

@ -70,7 +70,11 @@ class SnipeSCIMConfig extends \ArieTimmerman\Laravel\SCIMServer\SCIMConfig
// Map a SCIM attribute to an attribute of the object.
'mapping' => [
'id' => AttributeMapping::eloquent("id")->disableWrite(),
'id' => (new AttributeMapping())->setRead(
function (&$object) {
return (string)$object->id;
}
)->disableWrite(),
'externalId' => AttributeMapping::eloquent('scim_externalid'), // FIXME - I have a PR that changes a lot of this.
@ -174,7 +178,6 @@ class SnipeSCIMConfig extends \ArieTimmerman\Laravel\SCIMServer\SCIMConfig
'$ref' => null,
'display' => null,
'type' => null,
'type' => null
]],
'entitlements' => null,

View file

@ -78,24 +78,7 @@ class Supplier extends SnipeModel
{
return $this->hasMany(Asset::class)->whereNull('deleted_at')->selectRaw('supplier_id, count(*) as count')->groupBy('supplier_id');
}
/**
* Sets the license seat count attribute
*
* @todo I don't see the licenseSeatsRelation here?
*
* @author A. Gianotto <snipe@snipe.net>
* @since [v1.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function getLicenseSeatsCountAttribute()
{
if ($this->licenseSeatsRelation->first()) {
return $this->licenseSeatsRelation->first()->count;
}
return 0;
}
/**
* Establishes the supplier -> assets relationship

View file

@ -65,6 +65,7 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
'avatar',
'gravatar',
'vip',
'autoassign_licenses',
];
protected $casts = [
@ -76,6 +77,7 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
'created_at' => 'datetime',
'updated_at' => 'datetime',
'deleted_at' => 'datetime',
'autoassign_licenses' => 'boolean',
];
/**
@ -95,6 +97,7 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
'location_id' => 'exists:locations,id|nullable',
'start_date' => 'nullable|date_format:Y-m-d',
'end_date' => 'nullable|date_format:Y-m-d|after_or_equal:start_date',
'autoassign_licenses' => 'boolean',
];
/**
@ -256,20 +259,6 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
return $this->last_name.', '.$this->first_name.' ('.$this->username.')';
}
/**
* The url for slack notifications.
* Used by Notifiable trait.
* @return mixed
*/
public function routeNotificationForSlack()
{
// At this point the endpoint is the same for everything.
// In the future this may want to be adapted for individual notifications.
$this->endpoint = \App\Models\Setting::getSettings()->webhook_endpoint;
return $this->endpoint;
}
/**
* Establishes the user -> assets relationship

View file

@ -534,6 +534,18 @@ class AssetPresenter extends Presenter
return false;
}
/**
* Used to take user created warranty URL and dynamically fill in the needed values per asset
* @return string
*/
public function dynamicWarrantyUrl()
{
$warranty_lookup_url = $this->model->model->manufacturer->warranty_lookup_url;
$url = (str_replace('{LOCALE}',\App\Models\Setting::getSettings()->locale,$warranty_lookup_url));
$url = (str_replace('{SERIAL}',$this->model->serial,$url));
return $url;
}
/**
* Url to view this item.
* @return string

View file

@ -33,7 +33,7 @@ class LicensePresenter extends Presenter
'field' => 'name',
'searchable' => true,
'sortable' => true,
'title' => trans('admin/licenses/table.title'),
'title' => trans('general.name'),
'formatter' => 'licensesLinkFormatter',
], [
'field' => 'product_key',

View file

@ -47,7 +47,7 @@ class ManufacturerPresenter extends Presenter
'switchable' => true,
'title' => trans('admin/manufacturers/table.url'),
'visible' => true,
'formatter' => 'linkFormatter',
'formatter' => 'externalLinkFormatter',
],
[
'field' => 'support_url',
@ -56,7 +56,7 @@ class ManufacturerPresenter extends Presenter
'switchable' => true,
'title' => trans('admin/manufacturers/table.support_url'),
'visible' => true,
'formatter' => 'linkFormatter',
'formatter' => 'externalLinkFormatter',
],
[
@ -78,6 +78,15 @@ class ManufacturerPresenter extends Presenter
'visible' => true,
'formatter' => 'emailFormatter',
],
[
'field' => 'warranty_lookup_url',
'searchable' => true,
'sortable' => true,
'switchable' => true,
'title' => trans('admin/manufacturers/table.warranty_lookup_url'),
'visible' => false,
'formatter' => 'externalLinkFormatter',
],
[
'field' => 'assets_count',

View file

@ -294,6 +294,15 @@ class UserPresenter extends Presenter
'visible' => true,
'formatter' => 'trueFalseFormatter',
],
[
'field' => 'autoassign_licenses',
'searchable' => false,
'sortable' => true,
'switchable' => true,
'title' => trans('general.autoassign_licenses'),
'visible' => false,
'formatter' => 'trueFalseFormatter',
],
[
'field' => 'created_by',
'searchable' => false,

View file

@ -130,7 +130,7 @@ return [
|
*/
'path' => '/',
'path' => env('SESSION_COOKIE_PATH', '/'),
/*
|--------------------------------------------------------------------------

View file

@ -0,0 +1,16 @@
<?php
namespace Database\Factories;
use App\Models\License;
use Illuminate\Database\Eloquent\Factories\Factory;
class LicenseSeatFactory extends Factory
{
public function definition()
{
return [
'license_id' => License::factory(),
];
}
}

View file

@ -38,6 +38,7 @@ class ManufacturerFactory extends Factory
'name' => 'Apple',
'url' => 'https://apple.com',
'support_url' => 'https://support.apple.com',
'warranty_lookup_url' => 'https://checkcoverage.apple.com',
'image' => 'apple.jpg',
];
});
@ -50,6 +51,7 @@ class ManufacturerFactory extends Factory
'name' => 'Microsoft',
'url' => 'https://microsoft.com',
'support_url' => 'https://support.microsoft.com',
'warranty_lookup_url' => 'https://account.microsoft.com/devices',
'image' => 'microsoft.png',
];
});
@ -62,6 +64,7 @@ class ManufacturerFactory extends Factory
'name' => 'Dell',
'url' => 'https://dell.com',
'support_url' => 'https://support.dell.com',
'warranty_lookup_url' => 'https://www.dell.com/support/home/en-us/Products/?app=warranty',
'image' => 'dell.png',
];
});

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddsLdapLocationToSettingsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('settings', function (Blueprint $table) {
$table->string('ldap_location')->after('ldap_country')->nullable()->default(null);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('settings', function (Blueprint $table) {
$table->dropColumn('ldap_location');
});
}
}

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddWarrantyUrlToManufacturers extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('manufacturers', function (Blueprint $table) {
$table->string('warranty_lookup_url')->after('support_url')->nullable()->default(null);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('manufacturers', function (Blueprint $table) {
$table->dropColumn('warranty_lookup_url');
});
}
}

12
package-lock.json generated
View file

@ -1329,9 +1329,9 @@
"dev": true
},
"@fortawesome/fontawesome-free": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.3.0.tgz",
"integrity": "sha512-qVtd5i1Cc7cdrqnTWqTObKQHjPWAiRwjUPaXObaeNPcy7+WKxJumGBx66rfSFgK6LNpIasVKkEgW8oyf0tmPLA=="
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.4.0.tgz",
"integrity": "sha512-0NyytTlPJwB/BF5LtRV8rrABDbe3TdTXqNB3PdZ+UUUZAEIrdOJdmABqKjt4AXwIoJNaRVVZEXxpNrqvE1GAYQ=="
},
"@jridgewell/gen-mapping": {
"version": "0.1.1",
@ -3154,9 +3154,9 @@
"integrity": "sha1-EQPWvADPv6jPyaJZmrUYxVZD2j8="
},
"bootstrap-table": {
"version": "1.21.3",
"resolved": "https://registry.npmjs.org/bootstrap-table/-/bootstrap-table-1.21.3.tgz",
"integrity": "sha512-y6PLHxJJVqIVXoMWrnwPsA8dKqvy9An8Iz7WuuimuLU1i0jIT9+Xzg6NXqBBilHOwp0dUAw9vfgNLvCVq2wdJQ=="
"version": "1.21.4",
"resolved": "https://registry.npmjs.org/bootstrap-table/-/bootstrap-table-1.21.4.tgz",
"integrity": "sha512-Vp0kwkCsZzBINiA1KJ46LSkIa4oA0r6GSuzERZEqkUU8/0JDij2aG4CdYxiOm/UFCPZ9+KuAE4zSjxJLxg5jWw=="
},
"brace-expansion": {
"version": "1.1.11",

View file

@ -24,7 +24,7 @@
"vue-template-compiler": "2.4.4"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^6.3.0",
"@fortawesome/fontawesome-free": "^6.4.0",
"acorn": "^8.8.2",
"acorn-import-assertions": "^1.8.0",
"admin-lte": "^2.4.18",
@ -34,7 +34,7 @@
"bootstrap-colorpicker": "^2.5.3",
"bootstrap-datepicker": "^1.9.0",
"bootstrap-less": "^3.3.8",
"bootstrap-table": "1.21.3",
"bootstrap-table": "1.21.4",
"chart.js": "^2.9.4",
"css-loader": "^4.0.0",
"ekko-lightbox": "^5.1.1",
@ -54,6 +54,6 @@
"tableexport.jquery.plugin": "1.27.0",
"tether": "^1.4.0",
"vue-resource": "^1.5.2",
"webpack": "^5.76.2"
"webpack": "^5.77.0"
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -18,20 +18,20 @@
"/css/dist/skins/skin-green.css": "/css/dist/skins/skin-green.css?id=0a82a6ae6bb4e58fe62d162c4fb50397",
"/css/dist/skins/skin-contrast.css": "/css/dist/skins/skin-contrast.css?id=da6c7997d9de2f8329142399f0ce50da",
"/css/dist/skins/skin-red.css": "/css/dist/skins/skin-red.css?id=44bf834f2110504a793dadec132a5898",
"/css/dist/all.css": "/css/dist/all.css?id=2b87a5b5f1e6f09861732fa41d159bc4",
"/css/dist/all.css": "/css/dist/all.css?id=41d8a74912c3b0d17cc508361530c597",
"/css/dist/signature-pad.css": "/css/dist/signature-pad.css?id=6a89d3cd901305e66ced1cf5f13147f7",
"/css/dist/signature-pad.min.css": "/css/dist/signature-pad.min.css?id=6a89d3cd901305e66ced1cf5f13147f7",
"/css/webfonts/fa-brands-400.ttf": "/css/webfonts/fa-brands-400.ttf?id=2df05d4beaa48550d71234e8dca79141",
"/css/webfonts/fa-brands-400.woff2": "/css/webfonts/fa-brands-400.woff2?id=682885a4f72597322017a9fcd0683831",
"/css/webfonts/fa-regular-400.ttf": "/css/webfonts/fa-regular-400.ttf?id=e7a7f9dd9376f68614860d920255d4df",
"/css/webfonts/fa-regular-400.woff2": "/css/webfonts/fa-regular-400.woff2?id=204fc700c679395e6aa9bebc3cada64e",
"/css/webfonts/fa-solid-900.ttf": "/css/webfonts/fa-solid-900.ttf?id=c9d3294ec75b843a31ef711069a0f0b6",
"/css/webfonts/fa-solid-900.woff2": "/css/webfonts/fa-solid-900.woff2?id=6707d0247b0bca1b4964bab435e3c0d6",
"/css/webfonts/fa-v4compatibility.ttf": "/css/webfonts/fa-v4compatibility.ttf?id=a947172f4fde88e43b4c1a60b01db061",
"/css/webfonts/fa-v4compatibility.woff2": "/css/webfonts/fa-v4compatibility.woff2?id=bbc23038a6067c78310d3f19432a3ebf",
"/css/dist/bootstrap-table.css": "/css/dist/bootstrap-table.css?id=418917c053841ab1aa1b78610a1825e0",
"/css/webfonts/fa-brands-400.ttf": "/css/webfonts/fa-brands-400.ttf?id=e2e2b1797606a266ed55549f5bb5a179",
"/css/webfonts/fa-brands-400.woff2": "/css/webfonts/fa-brands-400.woff2?id=fe912d1c4a7e0e1db87a64eb7e54c945",
"/css/webfonts/fa-regular-400.ttf": "/css/webfonts/fa-regular-400.ttf?id=31a6b5ecfc8d018d0e3a294f0c80e9e9",
"/css/webfonts/fa-regular-400.woff2": "/css/webfonts/fa-regular-400.woff2?id=bf8eabe300a00a3adb0293596987abc4",
"/css/webfonts/fa-solid-900.ttf": "/css/webfonts/fa-solid-900.ttf?id=cd687455c6d6c058e2e9f84f409e2965",
"/css/webfonts/fa-solid-900.woff2": "/css/webfonts/fa-solid-900.woff2?id=eea38615e7b5dbbaf88c263f2230cc32",
"/css/webfonts/fa-v4compatibility.ttf": "/css/webfonts/fa-v4compatibility.ttf?id=6ebbf5afc34f54463abc2b81ca637364",
"/css/webfonts/fa-v4compatibility.woff2": "/css/webfonts/fa-v4compatibility.woff2?id=67b8a78b7e80e805cfa4ee0421895ba4",
"/css/dist/bootstrap-table.css": "/css/dist/bootstrap-table.css?id=2265e072e44c782c901dce8e037d97fc",
"/js/build/vendor.js": "/js/build/vendor.js?id=3843eca1b2e670b29c1e1cb57e1d7aa7",
"/js/dist/bootstrap-table.js": "/js/dist/bootstrap-table.js?id=7a506bf59323cf5b5fe97f7080fc5ee0",
"/js/dist/bootstrap-table.js": "/js/dist/bootstrap-table.js?id=a0e44dba789031b34ef150a01318b865",
"/js/dist/all.js": "/js/dist/all.js?id=97b1034b75e3ac29a2eb9770d66c3370",
"/css/dist/skins/skin-green.min.css": "/css/dist/skins/skin-green.min.css?id=0a82a6ae6bb4e58fe62d162c4fb50397",
"/css/dist/skins/skin-green-dark.min.css": "/css/dist/skins/skin-green-dark.min.css?id=28b36223cf7b1d6e5f236859a4ef2b45",

View file

@ -23,6 +23,7 @@ return [
'restore' => 'Restore Asset',
'pending' => 'Pending',
'undeployable' => 'Undeployable',
'undeployable_tooltip' => 'This asset has a status label that is undeployable and cannot be checked out at this time.',
'view' => 'View Asset',
'csv_error' => 'You have an error in your CSV file:',
'import_text' => '

View file

@ -1,8 +1,8 @@
<?php
return array(
'about_licenses_title' => 'About Licenses',
'about_licenses' => 'Licenses are used to track software. They have a specified number of seats that can be checked out to individuals',
'about_licenses_title' => 'About Licenses',
'about_licenses' => 'Licenses are used to track software. They have a specified number of seats that can be checked out to individuals',
'checkin' => 'Checkin License Seat',
'checkout_history' => 'Checkout History',
'checkout' => 'Checkout License Seat',
@ -18,4 +18,30 @@ return array(
'software_licenses' => 'Software Licenses',
'user' => 'User',
'view' => 'View License',
'delete_disabled' => 'This license cannot be deleted yet because some seats are still checked out.',
'bulk' =>
[
'checkin_all' => [
'button' => 'Checkin All Seats',
'modal' => 'This will action checkin one seat. | This action will checkin all :checkedout_seats_count seats for this license.',
'enabled_tooltip' => 'Checkin ALL seats for this license from both users and assets',
'disabled_tooltip' => 'This is disabled because there are no seats currently checked out',
'success' => 'License successfully checked in! | All licenses were successfully checked in!',
'log_msg' => 'Checked in via bulk license checkout in license GUI',
],
'checkout_all' => [
'button' => 'Checkout All Seats',
'modal' => 'This action will checkout one seat to the first available user. | This action will checkout all :available_seats_count seats to the first available users. A user is considered available for this seat if they do not already have this license checked out to them, and the Auto-Assign License property is enabled on their user account.',
'enabled_tooltip' => 'Checkout ALL seats (or as many as are available) to ALL users',
'disabled_tooltip' => 'This is disabled because there are no seats currently available',
'success' => 'License successfully checked out! | :count licenses were successfully checked out!',
'error_no_seats' => 'There are no remaining seats left for this license.',
'warn_not_enough_seats' => ':count users were assigned this license, but we ran out of available license seats.',
'warn_no_avail_users' => 'Nothing to do. There are no users who do not already have this license assigned to them.',
'log_msg' => 'Checked out via bulk license checkout in license GUI',
],
],
);

View file

@ -2,6 +2,7 @@
return array(
'support_url_help' => 'Use <code>{LOCALE}</code> and <code>{SERIAL}</code> in your URL as variables to have those values auto-populate when viewing assets.',
'does_not_exist' => 'Manufacturer does not exist.',
'assoc_users' => 'This manufacturer is currently associated with at least one model and cannot be deleted. Please update your models to no longer reference this manufacturer and try again. ',

View file

@ -10,6 +10,7 @@ return array(
'support_email' => 'Support Email',
'support_phone' => 'Support Phone',
'support_url' => 'Support URL',
'warranty_lookup_url' => 'Warranty Lookup URL',
'update' => 'Update Manufacturer',
'url' => 'URL',

View file

@ -86,6 +86,8 @@ return [
'ldap_settings' => 'LDAP Settings',
'ldap_client_tls_cert_help' => 'Client-Side TLS Certificate and Key for LDAP connections are usually only useful in Google Workspace configurations with "Secure LDAP." Both are required.',
'ldap_client_tls_key' => 'LDAP Client-Side TLS key',
'ldap_location' => 'LDAP Location',
'ldap_location_help' => 'The Ldap Location field should be used if <strong>an OU is not being used in the Base Bind DN.</strong> Leave this blank if an OU search is being used.',
'ldap_login_test_help' => 'Enter a valid LDAP username and password from the base DN you specified above to test whether your LDAP login is configured correctly. YOU MUST SAVE YOUR UPDATED LDAP SETTINGS FIRST.',
'ldap_login_sync_help' => 'This only tests that LDAP can sync correctly. If your LDAP Authentication query is not correct, users may still not be able to login. YOU MUST SAVE YOUR UPDATED LDAP SETTINGS FIRST.',
'ldap_manager' => 'LDAP Manager',

View file

@ -439,4 +439,13 @@ return [
'setup_migration_output' => 'Migration output:',
'setup_migration_create_user' => 'Next: Create User',
'importer_generic_error' => 'Your file import is complete, but we did receive an error. This is usually caused by third-party API throttling from a notification webhook (such as Slack) and would not have interfered with the import itself, but you should confirm this.',
'confirm' => 'Confirm',
'autoassign_licenses' => 'Auto-Assign Licenses',
'autoassign_licenses_help' => 'Allow user to be have licenses assigned via the bulk-assign license UI or cli tools.',
'autoassign_licenses_help_long' => 'This allows a user to be have licenses assigned via the bulk-assign license UI or cli tools. (For example, you might not want contractors to be auto-assigned a license you would provide to only staff members. You can still individually assign licenses to those users, but they will not be included in the Checkout License to All Users functions.)',
'no_autoassign_licenses_help' => 'Do not include user for bulk-assigning through the license UI or cli tools.',
'modal_confirm_generic' => 'Are you sure?',
'cannot_be_deleted' => 'This item cannot be deleted',
'undeployable_tooltip' => 'This item cannot be checked out. Check the quantity remaining.',
];

View file

@ -21,7 +21,7 @@
<h2 class="box-title">{{ trans('admin/custom_fields/general.fieldsets') }}</h2>
<div class="box-tools pull-right">
@can('create', \App\Models\CustomFieldset::class)
<a href="{{ route('fieldsets.create') }}" class="btn btn-sm btn-primary" data-toggle="tooltip" title="{{ trans('admin/custom_fields/general.create_fieldset_title') }}">{{ trans('admin/custom_fields/general.create_fieldset') }}</a>
<a href="{{ route('fieldsets.create') }}" class="btn btn-sm btn-primary" data-tooltip="true" title="{{ trans('admin/custom_fields/general.create_fieldset_title') }}">{{ trans('admin/custom_fields/general.create_fieldset') }}</a>
@endcan
</div>
</div><!-- /.box-header -->
@ -111,7 +111,7 @@
<h2 class="box-title">{{ trans('admin/custom_fields/general.custom_fields') }}</h2>
<div class="box-tools pull-right">
@can('create', \App\Models\CustomField::class)
<a href="{{ route('fields.create') }}" class="btn btn-sm btn-primary" data-toggle="tooltip" title="{{ trans('admin/custom_fields/general.create_field_title') }}">{{ trans('admin/custom_fields/general.create_field') }}</a>
<a href="{{ route('fields.create') }}" class="btn btn-sm btn-primary" data-tooltip="true" title="{{ trans('admin/custom_fields/general.create_field_title') }}">{{ trans('admin/custom_fields/general.create_field') }}</a>
@endcan
</div>

View file

@ -77,7 +77,7 @@
<tbody class="permissions-group">
<tr class="header-row permissions-row">
<td class="col-md-5 tooltip-base permissions-item"
data-toggle="tooltip"
data-tooltip="true"
data-placement="right"
title="{{ $localPermission['note'] }}">
@unless (empty($localPermission['label']))
@ -100,7 +100,7 @@
<tbody class="permission-group">
<tr class="header-row permissions-row">
<td class="col-md-5 tooltip-base permissions-item header-name"
data-toggle="tooltip"
data-tooltip="true"
data-placement="right"
title="{{ $localPermission['note'] }}">
<h2>{{ $area }}</h2>
@ -122,7 +122,7 @@
<tr class="permissions-row">
<td
class="col-md-5 tooltip-base permissions-item"
data-toggle="tooltip"
data-tooltip="true"
data-placement="right"
title="{{ $this_permission['note'] }}">
{{ $this_permission['label'] }}

View file

@ -456,7 +456,7 @@
</div>
<div class="col-md-6{{ (($field->format=='URL') && ($asset->{$field->db_column_name()}!='')) ? ' ellipsis': '' }}">
@if ($field->field_encrypted=='1')
<i class="fas fa-lock" data-toggle="tooltip" data-placement="top" title="{{ trans('admin/custom_fields/general.value_encrypted') }}"></i>
<i class="fas fa-lock" data-tooltip="true" data-placement="top" title="{{ trans('admin/custom_fields/general.value_encrypted') }}"></i>
@endif
@if ($field->isFieldDecryptable($asset->{$field->db_column_name()} ))
@ -595,20 +595,10 @@
{{ $asset->warranty_months }}
{{ trans('admin/hardware/form.months') }}
@if ($asset->serial && $asset->model->manufacturer)
@if ((strtolower($asset->model->manufacturer->name) == "apple") || (str_starts_with(str_replace(' ','',strtolower($asset->model->manufacturer->name)),"appleinc")))
<a href="https://checkcoverage.apple.com/?locale={{ (str_replace('-','_',\App\Models\Setting::getSettings()->locale)) }}" target="_blank">
<i class="fa-brands fa-apple" aria-hidden="true"><span class="sr-only">{{ trans('hardware/general.mfg_warranty_lookup') }}</span></i>
@if (($asset->model->manufacturer) && ($asset->model->manufacturer->warranty_lookup_url!=''))
<a href="{{ $asset->present()->dynamicWarrantyUrl() }}" target="_blank">
<i class="fa fa-external-link"><span class="sr-only">{{ trans('admin/hardware/general.mfg_warranty_lookup', ['manufacturer' => $asset->model->manufacturer->name]) }}</span></i>
</a>
@elseif ((strtolower($asset->model->manufacturer->name) == "dell") || (str_starts_with(str_replace(' ','',strtolower($asset->model->manufacturer->name)),"dellinc")))
<a href="https://www.dell.com/support/home/en-us?app=warranty" target="_blank">
<img src="/img/demo/manufacturers/dellicon.png" style="width:25px;height:25px;"><span class="sr-only">{{ trans('hardware/general.mfg_warranty_lookup') }}</span></i>
</a>
@elseif ((strtolower($asset->model->manufacturer->name) == "lenovo") || (str_starts_with(str_replace(' ','',strtolower($asset->model->manufacturer->name)),"lenovoinc")))
<a href="https://pcsupport.lenovo.com/us/en/warrantylookup#/" target="_blank">
<img src="/img/demo/manufacturers/lenovoicon.png" style="width:25px;height:25px;"><span class="sr-only">{{ trans('hardware/general.mfg_warranty_lookup') }}</span></i>
</a>
@endif
@endif
</div>
</div>

View file

@ -483,8 +483,7 @@
class="fas fa-times text-red fa-fw"></i>
{{ trans('general.all') }}
{{ trans('general.undeployable') }}
({{ (isset($total_undeployable_sidebar)) ? $total_undeployable_sidebar : '' }}
)
({{ (isset($total_undeployable_sidebar)) ? $total_undeployable_sidebar : '' }})
</a>
</li>
<li{!! (Request::query('status') == 'byod' ? ' class="active"' : '') !!}><a
@ -974,7 +973,7 @@
$(function () {
$('[data-toggle="tooltip"]').tooltip();
$('[data-tooltip="true"]').tooltip();
$('[data-toggle="popover"]').popover();
$('.select2 span').addClass('needsclick');
$('.select2 span').removeAttr('title');

View file

@ -10,7 +10,7 @@
{{-- Page content --}}
@section('content')
<div class="row">
<div class="col-md-12">
<div class="col-md-9">
<!-- Custom Tabs -->
<div class="nav-tabs-custom">
@ -56,21 +56,7 @@
<span class="hidden-xs hidden-sm">{{ trans('general.history') }}</span>
</a>
</li>
@can('update', $license)
<li class="dropdown pull-right">
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
<i class="fas fa-cog" aria-hidden="true"></i> {{ trans('button.actions') }}
<span class="caret"></span>
</a>
<ul class="dropdown-menu">
<li><a href="{{ route('licenses.edit', $license->id) }}">{{ trans('admin/licenses/general.edit') }}</a></li>
<li><a href="{{ route('clone/license', $license->id) }}">{{ trans('admin/licenses/general.clone') }}</a></li>
</ul>
</li>
@endcan
@can('update', \App\Models\License::class)
<li class="pull-right"><a href="#" data-toggle="modal" data-target="#uploadFileModal">
<i class="fas fa-paperclip" aria-hidden="true"></i> {{ trans('button.upload') }}</a>
@ -332,7 +318,7 @@
</strong>
</div>
<div class="col-md-9">
{!! $license->maintained ? '<i class="fas fa-check text-success" aria-hidden="true"></i> '.trans('general.yes') : '<i class="fas fa-times text-danger" aria-hidden="true"></i> '.trans('general.no') !!}
{!! $license->maintained ? '<i class="fas fa-check fa-fw text-success" aria-hidden="true"></i> '.trans('general.yes') : '<i class="fas fa-times fa-fw text-danger" aria-hidden="true"></i> '.trans('general.no') !!}
</div>
</div>
@ -358,7 +344,7 @@
</strong>
</div>
<div class="col-md-9">
{!! $license->reassignable ? '<i class="fas fa-check text-success" aria-hidden="true"></i> '.trans('general.yes') : '<i class="fas fa-times text-danger" aria-hidden="true"></i> '.trans('general.no') !!}
{!! $license->reassignable ? '<i class="fas fa-check fa-fw text-success" aria-hidden="true"></i> '.trans('general.yes') : '<i class="fas fa-times fa-fw text-danger" aria-hidden="true"></i> '.trans('general.no') !!}
</div>
</div>
@ -378,8 +364,10 @@
</div> <!-- end row-striped -->
</div>
</div>
</div> <!-- end tab-pane -->
</div>
</div> <!-- end tab-pane -->
@ -550,13 +538,100 @@
</table>
</div>
</div> <!-- /.col-md-12-->
</div> <!-- /.row-->
</div> <!-- /.tab-pane -->
</div> <!-- /.tab-content -->
</div> <!-- nav-tabs-custom -->
</div> <!-- /.col -->
<div class="col-md-3">
@can('update', $license)
<a href="{{ route('licenses.edit', $license->id) }}" class="btn btn-block btn-primary" style="margin-bottom: 10px;">{{ trans('admin/licenses/general.edit') }}</a>
<a href="{{ route('clone/license', $license->id) }}" class="btn btn-block btn-primary" style="margin-bottom: 10px;">{{ trans('admin/licenses/general.clone') }}</a>
@endcan
@can('checkout', $license)
@if ($license->availCount()->count() > 0)
<a href="{{ route('licenses.checkout', $license->id) }}" class="btn-block btn bg-maroon" style="margin-bottom: 10px;">
{{ trans('general.checkout') }}
</a>
<a href="#" class="btn-block btn bg-maroon" style="margin-bottom: 10px;" data-toggle="modal" data-tooltip="true" title="{{ trans('admin/licenses/general.bulk.checkout_all.enabled_tooltip') }}" data-target="#checkoutFromAllModal">
{{ trans('admin/licenses/general.bulk.checkout_all.button') }}
</a>
@else
<a href="{{ route('licenses.checkout', $license->id) }}" class="btn btn-block bg-maroon disabled" style="margin-bottom: 10px;">
{{ trans('general.checkout') }}
</a>
<span data-tooltip="true" title=" {{ trans('admin/licenses/general.bulk.checkout_all.disabled_tooltip') }}">
<a href="#" class="btn btn-block bg-maroon disabled" style="margin-bottom: 10px;" data-tooltip="true" title="{{ trans('general.checkout') }}">
{{ trans('admin/licenses/general.bulk.checkout_all.button') }}
</a>
</span>
@endif
@endcan
@can('checkin', $license)
@if (($license->seats - $license->availCount()->count()) > 0 )
<a href="#" class="btn btn-block bg-purple" style="margin-bottom: 25px;" data-toggle="modal" data-tooltip="true" data-target="#checkinFromAllModal" data-content="{{ trans('general.sure_to_delete') }} data-title="{{ trans('general.delete') }}" onClick="return false;">
{{ trans('admin/licenses/general.bulk.checkin_all.button') }}
</a>
@else
<span data-tooltip="true" title=" {{ trans('admin/licenses/general.bulk.checkin_all.disabled_tooltip') }}">
<a href="#" class="btn btn-block bg-purple disabled" style="margin-bottom: 25px;">
{{ trans('admin/licenses/general.bulk.checkin_all.button') }}
</a>
</span>
@endif
@endcan
@can('delete', $license)
@if ($license->availCount()->count() == $license->seats)
<button class="btn btn-block btn-danger delete-asset" data-toggle="modal" data-title="{{ trans('general.delete') }}" data-content="{{ trans('general.delete_confirm', ['item' => $license->name]) }}" data-target="#dataConfirmModal">
{{ trans('general.delete') }}
</button>
@else
<span data-tooltip="true" title=" {{ trans('admin/licenses/general.delete_disabled') }}">
<a href="#" class="btn btn-block btn-danger disabled">
{{ trans('general.delete') }}
</a>
</span>
@endif
@endcan
</div>
</div> <!-- /.row -->
@can('checkin', \App\Models\License::class)
@include ('modals.confirm-action',
[
'modal_name' => 'checkinFromAllModal',
'route' => route('licenses.bulkcheckin', $license->id),
'title' => trans('general.modal_confirm_generic'),
'body' => trans_choice('admin/licenses/general.bulk.checkin_all.modal', 2, ['checkedout_seats_count' => $checkedout_seats_count])
])
@endcan
@can('checkout', \App\Models\License::class)
@include ('modals.confirm-action',
[
'modal_name' => 'checkoutFromAllModal',
'route' => route('licenses.bulkcheckout', $license->id),
'title' => trans('general.modal_confirm_generic'),
'body' => trans_choice('admin/licenses/general.bulk.checkout_all.modal', 2, ['available_seats_count' => $available_seats_count])
])
@endcan
@can('update', \App\Models\License::class)
@include ('modals.upload-file', ['item_type' => 'license', 'item_id' => $license->id])
@endcan
@ -565,5 +640,15 @@
@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')
@stop

View file

@ -30,6 +30,17 @@
</div>
</div>
<!-- Warranty Lookup URL -->
<div class="form-group {{ $errors->has('warranty_lookup_url') ? ' has-error' : '' }}">
<label for="support_url" class="col-md-3 control-label">{{ trans('admin/manufacturers/table.warranty_lookup_url') }}
</label>
<div class="col-md-6">
<input class="form-control" type="text" name="warranty_lookup_url" id="warranty_lookup_url" value="{{ old('warranty_lookup_url', $item->warranty_lookup_url) }}" />
<p class="help-block">{!! trans('admin/manufacturers/message.support_url_help') !!}</p>
{!! $errors->first('warranty_lookup_url', '<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!}
</div>
</div>
<!-- Support Phone -->
<div class="form-group {{ $errors->has('support_phone') ? ' has-error' : '' }}">
<label for="support_phone" class="col-md-3 control-label">{{ trans('admin/manufacturers/table.support_phone') }}

View file

@ -0,0 +1,27 @@
<!-- Modal -->
<div class="modal fade" id="{{ $modal_name }}" tabindex="-1" role="dialog" aria-labelledby="{{ $modal_name }}Label" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="{{ $modal_name }}Label">{{ $title }}</h4>
</div>
<form action="{{ $route }}" method="POST" class="form-horizontal">
<input type="hidden" name="_token" value="{{ csrf_token() }}" />
<div class="modal-body">
<div class="row">
<div class="col-md-12">
{{ $body }}
</div>
</div>
</div> <!-- /.modal-body-->
<div class="modal-footer">
<a href="#" class="pull-left" data-dismiss="modal">{{ trans('button.cancel') }}</a>
<button type="submit" class="btn btn-primary">{{ trans('general.confirm') }}</button>
</div>
</form>
</div>
</div>
</div>

View file

@ -4,7 +4,7 @@
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h2 class="modal-title" id="uploadFileModalLabel">{{ trans('general.file_upload') }}</h4>
<h4 class="modal-title" id="uploadFileModalLabel">{{ trans('general.file_upload') }}</h4>
</div>
{{ Form::open([
'method' => 'POST',

View file

@ -77,7 +77,7 @@
@if ($field->field_encrypted)
<div class="col-md-1 col-sm-1 text-left">
<i class="fas fa-lock" data-toggle="tooltip" data-placement="top" title="{{ trans('admin/custom_fields/general.value_encrypted') }}"></i>
<i class="fas fa-lock" data-tooltip="true" data-placement="top" title="{{ trans('admin/custom_fields/general.value_encrypted') }}"></i>
</div>
@endif

View file

@ -94,7 +94,7 @@
exportTypes: ['xlsx', 'excel', 'csv', 'pdf','json', 'xml', 'txt', 'sql', 'doc' ],
onLoadSuccess: function () {
$('[data-toggle="tooltip"]').tooltip(); // Needed to attach tooltips after ajax call
$('[data-tooltip="true"]').tooltip(); // Needed to attach tooltips after ajax call
}
});
@ -215,7 +215,7 @@
text_help = '';
}
return '<nobr><a href="{{ config('app.url') }}/' + destination + '/' + value.id + '" data-toggle="tooltip" title="'+ status_meta[value.status_meta] + '"> <i class="fa ' + icon_style + ' text-' + text_color + '"></i> ' + value.name + ' ' + text_help + ' </a> </nobr>';
return '<nobr><a href="{{ config('app.url') }}/' + destination + '/' + value.id + '" data-tooltip="true" title="'+ status_meta[value.status_meta] + '"> <i class="fa ' + icon_style + ' text-' + text_color + '"></i> ' + value.name + ' ' + text_help + ' </a> </nobr>';
} else if ((value) && (value.name)) {
// Add some overrides for any funny urls we have
@ -269,21 +269,28 @@
}
if ((row.available_actions) && (row.available_actions.delete === true)) {
// use the asset tag if no name is provided
var name_for_box = row.name
if (row.name=='') {
var name_for_box = row.asset_tag
}
actions += '<a href="{{ config('app.url') }}/' + dest + '/' + row.id + '" '
+ ' class="btn btn-danger btn-sm delete-asset" data-toggle="tooltip" '
+ ' class="btn btn-danger btn-sm delete-asset" data-tooltip="true" '
+ ' data-toggle="modal" '
+ ' data-content="{{ trans('general.sure_to_delete') }} ' + row.name + '?" '
+ ' data-content="{{ trans('general.sure_to_delete') }} ' + name_for_box + '?" '
+ ' data-title="{{ trans('general.delete') }}" onClick="return false;">'
+ '<i class="fas fa-trash" aria-hidden="true"></i><span class="sr-only">Delete</span></a>&nbsp;';
+ '<i class="fas fa-trash" aria-hidden="true"></i><span class="sr-only">{{ trans('general.delete') }}</span></a>&nbsp;';
} else {
actions += '<a class="btn btn-danger btn-sm delete-asset disabled" onClick="return false;"><i class="fas fa-trash"></i></a>&nbsp;';
actions += '<span data-tooltip="true" title="{{ trans('general.cannot_be_deleted') }}"><a class="btn btn-danger btn-sm delete-asset disabled" onClick="return false;"><i class="fas fa-trash"></i></a></span>&nbsp;';
}
if ((row.available_actions) && (row.available_actions.restore === true)) {
actions += '<form style="display: inline;" method="POST" action="{{ config('app.url') }}/' + dest + '/' + row.id + '/restore"> ';
actions += '@csrf';
actions += '<button class="btn btn-sm btn-warning" data-toggle="tooltip" title="{{ trans('general.restore') }}"><i class="fas fa-retweet"></i></button>&nbsp;';
actions += '<button class="btn btn-sm btn-warning" data-tooltip="true" title="{{ trans('general.restore') }}"><i class="fas fa-retweet"></i></button>&nbsp;';
}
actions +='</nobr>';
@ -364,9 +371,9 @@
function licenseSeatInOutFormatter(value, row) {
// The user is allowed to check the license seat out and it's available
if ((row.available_actions.checkout == true) && (row.user_can_checkout == true) && ((!row.asset_id) && (!row.assigned_to))) {
return '<a href="{{ config('app.url') }}/licenses/' + row.license_id + '/checkout/'+row.id+'" class="btn btn-sm bg-maroon" data-toggle="tooltip" title="{{ trans('general.checkout_tooltip') }}">{{ trans('general.checkout') }}</a>';
return '<a href="{{ config('app.url') }}/licenses/' + row.license_id + '/checkout/'+row.id+'" class="btn btn-sm bg-maroon" data-tooltip="true" title="{{ trans('general.checkout_tooltip') }}">{{ trans('general.checkout') }}</a>';
} else {
return '<a href="{{ config('app.url') }}/licenses/' + row.id + '/checkin" class="btn btn-sm bg-purple" data-toggle="tooltip" title="Check in this license seat.">{{ trans('general.checkin') }}</a>';
return '<a href="{{ config('app.url') }}/licenses/' + row.id + '/checkin" class="btn btn-sm bg-purple" data-tooltip="true" title="Check in this license seat.">{{ trans('general.checkin') }}</a>';
}
}
@ -376,18 +383,26 @@
// The user is allowed to check items out, AND the item is deployable
if ((row.available_actions.checkout == true) && (row.user_can_checkout == true) && ((!row.asset_id) && (!row.assigned_to))) {
return '<a href="{{ config('app.url') }}/' + destination + '/' + row.id + '/checkout" class="btn btn-sm bg-maroon" data-toggle="tooltip" title="{{ trans('general.checkout_tooltip') }}">{{ trans('general.checkout') }}</a>';
// The user is allowed to check items out, but the item is not deployable
return '<a href="{{ config('app.url') }}/' + destination + '/' + row.id + '/checkout" class="btn btn-sm bg-maroon" data-tooltip="true" title="{{ trans('general.checkout_tooltip') }}">{{ trans('general.checkout') }}</a>';
// The user is allowed to check items out, but the item is not able to be checked out
} else if (((row.user_can_checkout == false)) && (row.available_actions.checkout == true) && (!row.assigned_to)) {
return '<div data-toggle="tooltip" title="This item has a status label that is undeployable and cannot be checked out at this time."><a class="btn btn-sm bg-maroon disabled">{{ trans('general.checkout') }}</a></div>';
// We use slightly different language for assets versus other things, since they are the only
// item that has a status label
if (destination =='hardware') {
return '<span data-tooltip="true" title="{{ trans('admin/hardware/general.undeployable_tooltip') }}"><a class="btn btn-sm bg-maroon disabled">{{ trans('general.checkout') }}</a></span>';
} else {
return '<span data-tooltip="true" title="{{ trans('general.undeployable_tooltip') }}"><a class="btn btn-sm bg-maroon disabled">{{ trans('general.checkout') }}</a></span>';
}
// The user is allowed to check items in
} else if (row.available_actions.checkin == true) {
if (row.assigned_to) {
return '<a href="{{ config('app.url') }}/' + destination + '/' + row.id + '/checkin" class="btn btn-sm bg-purple" data-toggle="tooltip" title="Check this item in so it is available for re-imaging, re-issue, etc.">{{ trans('general.checkin') }}</a>';
return '<a href="{{ config('app.url') }}/' + destination + '/' + row.id + '/checkin" class="btn btn-sm bg-purple" data-tooltip="true" title="Check this item in so it is available for re-imaging, re-issue, etc.">{{ trans('general.checkin') }}</a>';
} else if (row.assigned_pivot_id) {
return '<a href="{{ config('app.url') }}/' + destination + '/' + row.assigned_pivot_id + '/checkin" class="btn btn-sm bg-purple" data-toggle="tooltip" title="Check this item in so it is available for re-imaging, re-issue, etc.">{{ trans('general.checkin') }}</a>';
return '<a href="{{ config('app.url') }}/' + destination + '/' + row.assigned_pivot_id + '/checkin" class="btn btn-sm bg-purple" data-tooltip="true" title="Check this item in so it is available for re-imaging, re-issue, etc.">{{ trans('general.checkin') }}</a>';
}
}
@ -401,11 +416,11 @@
// This is only used by the requestable assets section
function assetRequestActionsFormatter (row, value) {
if (value.assigned_to_self == true){
return '<button class="btn btn-danger btn-sm disabled" data-toggle="tooltip" title="Cancel this item request">{{ trans('button.cancel') }}</button>';
return '<button class="btn btn-danger btn-sm disabled" data-tooltip="true" title="Cancel this item request">{{ trans('button.cancel') }}</button>';
} else if (value.available_actions.cancel == true) {
return '<form action="{{ config('app.url') }}/account/request-asset/'+ value.id + '" method="POST">@csrf<button class="btn btn-danger btn-sm" data-toggle="tooltip" title="Cancel this item request">{{ trans('button.cancel') }}</button></form>';
return '<form action="{{ config('app.url') }}/account/request-asset/'+ value.id + '" method="POST">@csrf<button class="btn btn-danger btn-sm" data-tooltip="true" title="Cancel this item request">{{ trans('button.cancel') }}</button></form>';
} else if (value.available_actions.request == true) {
return '<form action="{{ config('app.url') }}/account/request-asset/'+ value.id + '" method="POST">@csrf<button class="btn btn-primary btn-sm" data-toggle="tooltip" title="Request this item">{{ trans('button.request') }}</button></form>';
return '<form action="{{ config('app.url') }}/account/request-asset/'+ value.id + '" method="POST">@csrf<button class="btn btn-primary btn-sm" data-tooltip="true" title="Request this item">{{ trans('button.request') }}</button></form>';
}
}
@ -496,8 +511,12 @@
}
function externalLinkFormatter(value) {
if (value) {
return '<a href="' + value + '" target="_blank">' + value + '</a>';
if ((value.indexOf("{") === -1) || (value.indexOf("}") ===-1)) {
return '<i class="fa fa-external-link"></i> <a href="' + value + '" target="_blank">' + value + '</a>';
}
return value;
}
}
@ -537,7 +556,7 @@
if ((row) && (row!=undefined)) {
return '<a href="{{ config('app.url') }}/locations/' + row.id + '">' + row.name + '</a>';
} else if (value.rtd_location) {
return '<a href="{{ config('app.url') }}/locations/' + value.rtd_location.id + '" data-toggle="tooltip" title="Default Location">' + value.rtd_location.name + '</a>';
return '<a href="{{ config('app.url') }}/locations/' + value.rtd_location.id + '" data-tooltip="true" title="Default Location">' + value.rtd_location.name + '</a>';
}
}
@ -779,7 +798,7 @@
// This is necessary to make the bootstrap tooltips work inside of the
// wenzhixin/bootstrap-table formatters
$('#table').on('post-body.bs.table', function () {
$('[data-toggle="tooltip"]').tooltip({
$('[data-tooltip="true"]').tooltip({
container: 'body'
});

View file

@ -6,7 +6,7 @@
<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) }}" />
</div>
<div class="col-md-7" style="margin-left: -15px;">
<a href="#" data-toggle="tooltip" title="{{ trans('general.min_amt_help') }}"><i class="fas fa-info-circle" aria-hidden="true"></i>
<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>

View file

@ -4,7 +4,7 @@
<tbody class="permissions-group">
<tr class="header-row permissions-row">
<td class="col-md-5 tooltip-base permissions-item"
data-toggle="tooltip"
data-tooltip="true"
data-placement="right"
title="{{ $localPermission['note'] }}"
>
@ -76,7 +76,7 @@
@if ($permission['display'])
<td
class="col-md-5 tooltip-base permissions-item"
data-toggle="tooltip"
data-tooltip="true"
data-placement="right"
title="{{ $permission['note'] }}"
>

View file

@ -499,6 +499,20 @@
@endif
</div>
</div>
<!-- LDAP Location -->
<div class="form-group {{ $errors->has('ldap_location') ? 'error' : '' }}">
<div class="col-md-3">
{{ Form::label('ldap_location', trans('admin/settings/general.ldap_location')) }}
</div>
<div class="col-md-9">
{{ Form::text('ldap_location', Request::old('ldap_location', $setting->ldap_location), ['class' => 'form-control','placeholder' => trans('general.example') .'physicaldeliveryofficename', $setting->demoMode]) }}
<p class="help-block">{!! trans('admin/settings/general.ldap_location_help') !!}</p>
{!! $errors->first('ldap_location', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
@if (config('app.lock_passwords')===true)
<p class="text-warning"><i class="fas fa-lock" aria-hidden="true"></i> {{ trans('general.feature_disabled') }}</p>
@endif
</div>
</div>
@if ($setting->ldap_enabled)
<!-- LDAP test -->

View file

@ -119,6 +119,29 @@
</div>
</div> <!--/form-group-->
<!-- activated -->
<div class="form-group">
<div class="col-sm-3 control-label">
{{ trans('general.autoassign_licenses') }}
</div>
<div class="col-sm-9">
<label for="no_change_autoassign_licenses" class="form-control">
{{ Form::radio('autoassign_licenses', '', true, ['id' => 'no_change_autoassign_licenses', 'aria-label'=>'no_change_autoassign_licenses']) }}
{{ trans('general.do_not_change') }}
</label>
<label for="autoassign_licenses" class="form-control">
{{ Form::radio('autoassign_licenses', '1', old('autoassign_licenses'), ['id' => 'autoassign_licenses', 'aria-label'=>'autoassign_licenses']) }}
{{ trans('general.autoassign_licenses_help')}}
</label>
<label for="dont_autoassign_licenses" class="form-control">
{{ Form::radio('autoassign_licenses', '0', old('autoassign_licenses'), ['id' => 'dont_autoassign_licenses', 'aria-label'=>'dont_autoassign_licenses']) }}
{{ trans('general.no_autoassign_licenses_help')}}
</label>
</div>
</div> <!--/form-group-->
<!-- activated -->
<div class="form-group">
<div class="col-sm-3 control-label">

View file

@ -370,6 +370,20 @@
</div>
</div>
<!-- Auto assign checkbox -->
<div class="form-group">
<div class="col-md-7 col-md-offset-3">
<label class="form-control" for="autoassign_licenses">
<input type="checkbox" value="1" name="autoassign_licenses" {{ (old('autoassign_licenses', $user->autoassign_licenses)) == '1' ? " checked='checked'" : '' }} aria-label="autoassign_licenses">
{{ trans('general.autoassign_licenses') }}
</label>
<p class="help-block">{{ trans('general.autoassign_licenses_help_long') }}</p>
</div>
</div>
<!-- remote checkbox -->
<div class="form-group">
@ -383,15 +397,6 @@
</div>
</div>
<!-- Auto Assign checkbox -->
<div class="form-group">
<div class="col-md-7 col-md-offset-3">
<label for="autoassign_licenses" class="form-control">
<input type="checkbox" value="1" name="autoassign_licenses" {{ (old('autoassign_licenses', $user->autoassign_licenses)) == '1' ? ' checked="checked"' : '' }} aria-label="autoassign_licenses">
{{ trans('admin/users/general.auto_assign_label') }}
</label>
</div>
</div>
<!-- Location -->
@include ('partials.forms.edit.location-select', ['translated_name' => trans('general.location'), 'fieldname' => 'location_id'])
@ -444,8 +449,8 @@
<!-- Country -->
<div class="form-group{{ $errors->has('country') ? ' has-error' : '' }}">
<label class="col-md-3 control-label" for="country">{{ trans('general.country') }}</label>
<div class="col-md-6">
{!! Form::countries('country', old('country', $user->country), 'col-md-6 select2') !!}
<div class="col-md-9">
{!! Form::countries('country', old('country', $user->country), 'col-md-12 select2') !!}
{!! $errors->first('country', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
</div>
</div>

View file

@ -122,7 +122,8 @@
@endcan
@can('update', \App\Models\User::class)
<li class="pull-right"><a href="#" data-toggle="modal" data-target="#uploadFileModal">
<li class="pull-right">
<a href="#" data-toggle="modal" data-target="#uploadFileModal">
<span class="hidden-xs"><i class="fas fa-paperclip" aria-hidden="true"></i></span>
<span class="hidden-lg hidden-md hidden-xl"><i class="fas fa-paperclip fa-2x" aria-hidden="true"></i></span>
<span class="hidden-xs hidden-sm">{{ trans('button.upload') }}</span>
@ -519,23 +520,23 @@
</div>
@endif
<!-- login enabled -->
<!-- vip -->
<div class="row">
<div class="col-md-3">
{{ trans('admin/users/general.vip_label') }}
</div>
<div class="col-md-9">
{!! ($user->vip=='1') ? '<i class="fas fa-check text-success" aria-hidden="true"></i> '.trans('general.yes') : '<i class="fas fa-times text-danger" aria-hidden="true"></i> '.trans('general.no') !!}
{!! ($user->vip=='1') ? '<i class="fas fa-check fa-fw fa-fw text-success" aria-hidden="true"></i> '.trans('general.yes') : '<i class="fas fa-times fa-fw text-danger" aria-hidden="true"></i> '.trans('general.no') !!}
</div>
</div>
<!-- login enabled -->
<!-- remote -->
<div class="row">
<div class="col-md-3">
{{ trans('admin/users/general.remote') }}
</div>
<div class="col-md-9">
{!! ($user->remote=='1') ? '<i class="fas fa-check text-success" aria-hidden="true"></i> '.trans('general.yes') : '<i class="fas fa-times text-danger" aria-hidden="true"></i> '.trans('general.no') !!}
{!! ($user->remote=='1') ? '<i class="fas fa-check fa-fw text-success" aria-hidden="true"></i> '.trans('general.yes') : '<i class="fas fa-times fa-fw text-danger" aria-hidden="true"></i> '.trans('general.no') !!}
</div>
</div>
@ -545,17 +546,28 @@
{{ trans('general.login_enabled') }}
</div>
<div class="col-md-9">
{!! ($user->activated=='1') ? '<i class="fas fa-check text-success" aria-hidden="true"></i> '.trans('general.yes') : '<i class="fas fa-times text-danger" aria-hidden="true"></i> '.trans('general.no') !!}
{!! ($user->activated=='1') ? '<i class="fas fa-check fa-fw text-success" aria-hidden="true"></i> '.trans('general.yes') : '<i class="fas fa-times fa-fw text-danger" aria-hidden="true"></i> '.trans('general.no') !!}
</div>
</div>
<!-- LDAP -->
<!-- auto assign license -->
<div class="row">
<div class="col-md-3">
{{ trans('general.autoassign_licenses') }}
</div>
<div class="col-md-9">
{!! ($user->autoassign_licenses=='1') ? '<i class="fas fa-check fa-fw text-success" aria-hidden="true"></i> '.trans('general.yes') : '<i class="fas fa-times fa-fw text-danger" aria-hidden="true"></i> '.trans('general.no') !!}
</div>
</div>
<!-- LDAP -->
<div class="row">
<div class="col-md-3">
LDAP
</div>
<div class="col-md-9">
{!! ($user->ldap_import=='1') ? '<i class="fas fa-check text-success" aria-hidden="true"></i> '.trans('general.yes') : '<i class="fas fa-times text-danger" aria-hidden="true"></i> '.trans('general.no') !!}
{!! ($user->ldap_import=='1') ? '<i class="fas fa-check fa-fw text-success" aria-hidden="true"></i> '.trans('general.yes') : '<i class="fas fa-times fa-fw text-danger" aria-hidden="true"></i> '.trans('general.no') !!}
</div>
</div>
@ -569,7 +581,7 @@
</div>
<div class="col-md-9">
{!! ($user->two_factor_active()) ? '<i class="fas fa-check text-success" aria-hidden="true"></i> '.trans('general.yes') : '<i class="fas fa-times text-danger" aria-hidden="true"></i> '.trans('general.no') !!}
{!! ($user->two_factor_active()) ? '<i class="fas fa-check fa-fw text-success" aria-hidden="true"></i> '.trans('general.yes') : '<i class="fas fa-times fa-fw text-danger" aria-hidden="true"></i> '.trans('general.no') !!}
</div>
</div>
@ -580,7 +592,7 @@
{{ trans('admin/users/general.two_factor_enrolled') }}
</div>
<div class="col-md-9" id="two_factor_reset_toggle">
{!! ($user->two_factor_active_and_enrolled()) ? '<i class="fas fa-check text-success" aria-hidden="true"></i> '.trans('general.yes') : '<i class="fas fa-times text-danger" aria-hidden="true"></i> '.trans('general.no') !!}
{!! ($user->two_factor_active_and_enrolled()) ? '<i class="fas fa-check fa-fw text-success" aria-hidden="true"></i> '.trans('general.yes') : '<i class="fas fa-times fa-fw text-danger" aria-hidden="true"></i> '.trans('general.no') !!}
</div>
</div>
@ -590,11 +602,11 @@
<!-- 2FA reset -->
<div class="row">
<div class="col-md-3">
</div>
<div class="col-md-9" style="margin-top: 10px;">
<div class="col-md-9">
<a class="btn btn-default btn-sm pull-left" id="two_factor_reset" style="margin-right: 10px;">
<a class="btn btn-default btn-sm" id="two_factor_reset" style="margin-right: 10px; margin-top: 10px;">
{{ trans('admin/settings/general.two_factor_reset') }}
</a>
<span id="two_factor_reseticon">
@ -1031,9 +1043,9 @@ $(function () {
dataType: 'json',
success: function (data) {
$("#two_factor_reset_toggle").html('').html('<i class="fas fa-times text-danger" aria-hidden="true"></i> {{ trans('general.no') }}');
$("#two_factor_reset_toggle").html('').html('<span class="text-danger"><i class="fas fa-times" aria-hidden="true"></i> {{ trans('general.no') }}</span>');
$("#two_factor_reseticon").html('');
$("#two_factor_resetstatus").html('<i class="fas fa-check text-success"></i>' + data.message);
$("#two_factor_resetstatus").html('<span class="text-success"><i class="fas fa-check"></i> ' + data.message + '</span>');
},

View file

@ -50,7 +50,7 @@ Route::group(['middleware' => 'auth'], function () {
Route::get('{locationId}/clone',
[LocationsController::class, 'getClone']
)->name('clone/license');
)->name('clone/location');
Route::get(
'{locationId}/printassigned',

View file

@ -25,10 +25,21 @@ Route::group(['prefix' => 'licenses', 'middleware' => ['auth']], function () {
[Licenses\LicenseCheckinController::class, 'store']
)->name('licenses.checkin.save');
Route::post(
'{licenseId}/bulkcheckin',
[Licenses\LicenseCheckinController::class, 'bulkCheckin']
)->name('licenses.bulkcheckin');
Route::post(
'{licenseId}/bulkcheckout',
[Licenses\LicenseCheckoutController::class, 'bulkCheckout']
)->name('licenses.bulkcheckout');
Route::post(
'{licenseId}/upload',
[Licenses\LicenseFilesController::class, 'store']
)->name('upload/license');
Route::delete(
'{licenseId}/deletefile/{fileId}',
[Licenses\LicenseFilesController::class, 'destroy']

View file

@ -0,0 +1,96 @@
<?php
namespace Tests\Feature\Notifications;
use App\Events\CheckoutableCheckedIn;
use App\Events\CheckoutableCheckedOut;
use App\Models\Accessory;
use App\Models\Setting;
use App\Models\User;
use App\Notifications\CheckinAccessoryNotification;
use App\Notifications\CheckoutAccessoryNotification;
use Illuminate\Notifications\AnonymousNotifiable;
use Illuminate\Support\Facades\Notification;
use Tests\Support\InteractsWithSettings;
use Tests\TestCase;
class AccessoryWebhookTest extends TestCase
{
use InteractsWithSettings;
public function testAccessoryCheckoutSendsWebhookNotificationWhenSettingEnabled()
{
Notification::fake();
$this->settings->enableWebhook();
event(new CheckoutableCheckedOut(
Accessory::factory()->appleBtKeyboard()->create(),
User::factory()->create(),
User::factory()->superuser()->create(),
''
));
Notification::assertSentTo(
new AnonymousNotifiable,
CheckoutAccessoryNotification::class,
function ($notification, $channels, $notifiable) {
return $notifiable->routes['slack'] === Setting::getSettings()->webhook_endpoint;
}
);
}
public function testAccessoryCheckoutDoesNotSendWebhookNotificationWhenSettingDisabled()
{
Notification::fake();
$this->settings->disableWebhook();
event(new CheckoutableCheckedOut(
Accessory::factory()->appleBtKeyboard()->create(),
User::factory()->create(),
User::factory()->superuser()->create(),
''
));
Notification::assertNotSentTo(new AnonymousNotifiable, CheckoutAccessoryNotification::class);
}
public function testAccessoryCheckinSendsWebhookNotificationWhenSettingEnabled()
{
Notification::fake();
$this->settings->enableWebhook();
event(new CheckoutableCheckedIn(
Accessory::factory()->appleBtKeyboard()->create(),
User::factory()->create(),
User::factory()->superuser()->create(),
''
));
Notification::assertSentTo(
new AnonymousNotifiable,
CheckinAccessoryNotification::class,
function ($notification, $channels, $notifiable) {
return $notifiable->routes['slack'] === Setting::getSettings()->webhook_endpoint;
}
);
}
public function testAccessoryCheckinDoesNotSendWebhookNotificationWhenSettingDisabled()
{
Notification::fake();
$this->settings->disableWebhook();
event(new CheckoutableCheckedIn(
Accessory::factory()->appleBtKeyboard()->create(),
User::factory()->create(),
User::factory()->superuser()->create(),
''
));
Notification::assertNotSentTo(new AnonymousNotifiable, CheckinAccessoryNotification::class);
}
}

View file

@ -0,0 +1,115 @@
<?php
namespace Tests\Feature\Notifications;
use App\Events\CheckoutableCheckedIn;
use App\Events\CheckoutableCheckedOut;
use App\Models\Asset;
use App\Models\Location;
use App\Models\Setting;
use App\Models\User;
use App\Notifications\CheckinAssetNotification;
use App\Notifications\CheckoutAssetNotification;
use Illuminate\Notifications\AnonymousNotifiable;
use Illuminate\Support\Facades\Notification;
use Tests\Support\InteractsWithSettings;
use Tests\TestCase;
class AssetWebhookTest extends TestCase
{
use InteractsWithSettings;
public function targets(): array
{
return [
'Asset checked out to user' => [fn() => User::factory()->create()],
'Asset checked out to asset' => [fn() => $this->createAsset()],
'Asset checked out to location' => [fn() => Location::factory()->create()],
];
}
/** @dataProvider targets */
public function testAssetCheckoutSendsWebhookNotificationWhenSettingEnabled($checkoutTarget)
{
Notification::fake();
$this->settings->enableWebhook();
event(new CheckoutableCheckedOut(
$this->createAsset(),
$checkoutTarget(),
User::factory()->superuser()->create(),
''
));
Notification::assertSentTo(
new AnonymousNotifiable,
CheckoutAssetNotification::class,
function ($notification, $channels, $notifiable) {
return $notifiable->routes['slack'] === Setting::getSettings()->webhook_endpoint;
}
);
}
/** @dataProvider targets */
public function testAssetCheckoutDoesNotSendWebhookNotificationWhenSettingDisabled($checkoutTarget)
{
Notification::fake();
$this->settings->disableWebhook();
event(new CheckoutableCheckedOut(
$this->createAsset(),
$checkoutTarget(),
User::factory()->superuser()->create(),
''
));
Notification::assertNotSentTo(new AnonymousNotifiable, CheckoutAssetNotification::class);
}
/** @dataProvider targets */
public function testAssetCheckinSendsWebhookNotificationWhenSettingEnabled($checkoutTarget)
{
Notification::fake();
$this->settings->enableWebhook();
event(new CheckoutableCheckedIn(
$this->createAsset(),
$checkoutTarget(),
User::factory()->superuser()->create(),
''
));
Notification::assertSentTo(
new AnonymousNotifiable,
CheckinAssetNotification::class,
function ($notification, $channels, $notifiable) {
return $notifiable->routes['slack'] === Setting::getSettings()->webhook_endpoint;
}
);
}
/** @dataProvider targets */
public function testAssetCheckinDoesNotSendWebhookNotificationWhenSettingDisabled($checkoutTarget)
{
Notification::fake();
$this->settings->disableWebhook();
event(new CheckoutableCheckedIn(
$this->createAsset(),
$checkoutTarget(),
User::factory()->superuser()->create(),
''
));
Notification::assertNotSentTo(new AnonymousNotifiable, CheckinAssetNotification::class);
}
private function createAsset()
{
return Asset::factory()->laptopMbp()->create();
}
}

View file

@ -0,0 +1,50 @@
<?php
namespace Tests\Feature\Notifications;
use App\Events\CheckoutableCheckedIn;
use App\Events\CheckoutableCheckedOut;
use App\Models\Asset;
use App\Models\Component;
use App\Models\User;
use Illuminate\Support\Facades\Notification;
use Tests\Support\InteractsWithSettings;
use Tests\TestCase;
class ComponentWebhookTest extends TestCase
{
use InteractsWithSettings;
public function testComponentCheckoutDoesNotSendWebhookNotification()
{
Notification::fake();
$this->settings->enableWebhook();
event(new CheckoutableCheckedOut(
Component::factory()->ramCrucial8()->create(),
Asset::factory()->laptopMbp()->create(),
User::factory()->superuser()->create(),
''
));
Notification::assertNothingSent();
}
public function testComponentCheckinDoesNotSendWebhookNotification()
{
Notification::fake();
$this->settings->enableWebhook();
event(new CheckoutableCheckedIn(
Component::factory()->ramCrucial8()->create(),
Asset::factory()->laptopMbp()->create(),
User::factory()->superuser()->create(),
''
));
Notification::assertNothingSent();
}
}

View file

@ -0,0 +1,56 @@
<?php
namespace Tests\Feature\Notifications;
use App\Events\CheckoutableCheckedOut;
use App\Models\Consumable;
use App\Models\Setting;
use App\Models\User;
use App\Notifications\CheckoutConsumableNotification;
use Illuminate\Notifications\AnonymousNotifiable;
use Illuminate\Support\Facades\Notification;
use Tests\Support\InteractsWithSettings;
use Tests\TestCase;
class ConsumableWebhookTest extends TestCase
{
use InteractsWithSettings;
public function testConsumableCheckoutSendsWebhookNotificationWhenSettingEnabled()
{
Notification::fake();
$this->settings->enableWebhook();
event(new CheckoutableCheckedOut(
Consumable::factory()->cardstock()->create(),
User::factory()->create(),
User::factory()->superuser()->create(),
''
));
Notification::assertSentTo(
new AnonymousNotifiable,
CheckoutConsumableNotification::class,
function ($notification, $channels, $notifiable) {
return $notifiable->routes['slack'] === Setting::getSettings()->webhook_endpoint;
}
);
}
public function testConsumableCheckoutDoesNotSendWebhookNotificationWhenSettingDisabled()
{
Notification::fake();
$this->settings->disableWebhook();
event(new CheckoutableCheckedOut(
Consumable::factory()->cardstock()->create(),
User::factory()->create(),
User::factory()->superuser()->create(),
''
));
Notification::assertNotSentTo(new AnonymousNotifiable, CheckoutConsumableNotification::class);
}
}

View file

@ -0,0 +1,109 @@
<?php
namespace Tests\Feature\Notifications;
use App\Events\CheckoutableCheckedIn;
use App\Events\CheckoutableCheckedOut;
use App\Models\Asset;
use App\Models\LicenseSeat;
use App\Models\Setting;
use App\Models\User;
use App\Notifications\CheckinLicenseSeatNotification;
use App\Notifications\CheckoutLicenseSeatNotification;
use Illuminate\Notifications\AnonymousNotifiable;
use Illuminate\Support\Facades\Notification;
use Tests\Support\InteractsWithSettings;
use Tests\TestCase;
class LicenseWebhookTest extends TestCase
{
use InteractsWithSettings;
public function targets(): array
{
return [
'License checked out to user' => [fn() => User::factory()->create()],
'License checked out to asset' => [fn() => Asset::factory()->laptopMbp()->create()],
];
}
/** @dataProvider targets */
public function testLicenseCheckoutSendsWebhookNotificationWhenSettingEnabled($checkoutTarget)
{
Notification::fake();
$this->settings->enableWebhook();
event(new CheckoutableCheckedOut(
LicenseSeat::factory()->create(),
$checkoutTarget(),
User::factory()->superuser()->create(),
''
));
Notification::assertSentTo(
new AnonymousNotifiable,
CheckoutLicenseSeatNotification::class,
function ($notification, $channels, $notifiable) {
return $notifiable->routes['slack'] === Setting::getSettings()->webhook_endpoint;
}
);
}
/** @dataProvider targets */
public function testLicenseCheckoutDoesNotSendWebhookNotificationWhenSettingDisabled($checkoutTarget)
{
Notification::fake();
$this->settings->disableWebhook();
event(new CheckoutableCheckedOut(
LicenseSeat::factory()->create(),
$checkoutTarget(),
User::factory()->superuser()->create(),
''
));
Notification::assertNotSentTo(new AnonymousNotifiable, CheckoutLicenseSeatNotification::class);
}
/** @dataProvider targets */
public function testLicenseCheckinSendsWebhookNotificationWhenSettingEnabled($checkoutTarget)
{
Notification::fake();
$this->settings->enableWebhook();
event(new CheckoutableCheckedIn(
LicenseSeat::factory()->create(),
$checkoutTarget(),
User::factory()->superuser()->create(),
''
));
Notification::assertSentTo(
new AnonymousNotifiable,
CheckinLicenseSeatNotification::class,
function ($notification, $channels, $notifiable) {
return $notifiable->routes['slack'] === Setting::getSettings()->webhook_endpoint;
}
);
}
/** @dataProvider targets */
public function testLicenseCheckinDoesNotSendWebhookNotificationWhenSettingDisabled($checkoutTarget)
{
Notification::fake();
$this->settings->disableWebhook();
event(new CheckoutableCheckedIn(
LicenseSeat::factory()->create(),
$checkoutTarget(),
User::factory()->superuser()->create(),
''
));
Notification::assertNotSentTo(new AnonymousNotifiable, CheckinLicenseSeatNotification::class);
}
}

View file

@ -0,0 +1,61 @@
<?php
namespace Tests\Feature\Users;
use App\Models\User;
use Tests\Support\InteractsWithSettings;
use Tests\TestCase;
class UpdateUserTest extends TestCase
{
use InteractsWithSettings;
public function testUsersCanBeActivated()
{
$admin = User::factory()->superuser()->create();
$user = User::factory()->create(['activated' => false]);
$this->actingAs($admin)
->put(route('users.update', $user), [
'first_name' => $user->first_name,
'username' => $user->username,
'activated' => 1,
]);
$this->assertTrue($user->refresh()->activated);
}
public function testUsersCanBeDeactivated()
{
$admin = User::factory()->superuser()->create();
$user = User::factory()->create(['activated' => true]);
$this->actingAs($admin)
->put(route('users.update', $user), [
'first_name' => $user->first_name,
'username' => $user->username,
// checkboxes that are not checked are
// not included in the request payload
// 'activated' => 0,
]);
$this->assertFalse($user->refresh()->activated);
}
public function testUsersUpdatingThemselvesDoNotDeactivateTheirAccount()
{
$admin = User::factory()->superuser()->create(['activated' => true]);
$this->actingAs($admin)
->put(route('users.update', $admin), [
'first_name' => $admin->first_name,
'username' => $admin->username,
// checkboxes that are disabled are not
// included in the request payload
// even if they are checked
// 'activated' => 0,
]);
$this->assertTrue($admin->refresh()->activated);
}
}

View file

@ -23,6 +23,24 @@ class Settings
return $this->update(['full_multiple_companies_support' => 1]);
}
public function enableWebhook(): Settings
{
return $this->update([
'webhook_botname' => 'SnipeBot5000',
'webhook_endpoint' => 'https://hooks.slack.com/services/NZ59/Q446/672N',
'webhook_channel' => '#it',
]);
}
public function disableWebhook(): Settings
{
return $this->update([
'webhook_botname' => '',
'webhook_endpoint' => '',
'webhook_channel' => '',
]);
}
/**
* @param array $attributes Attributes to modify in the application's settings.
*/