diff --git a/app/Http/Controllers/Api/LocationsController.php b/app/Http/Controllers/Api/LocationsController.php index b888493286..e1c79dfbe4 100644 --- a/app/Http/Controllers/Api/LocationsController.php +++ b/app/Http/Controllers/Api/LocationsController.php @@ -25,9 +25,27 @@ class LocationsController extends Controller { $this->authorize('view', Location::class); $allowed_columns = [ - 'id', 'name', 'address', 'address2', 'city', 'state', 'country', 'zip', 'created_at', - 'updated_at', 'manager_id', 'image', - 'assigned_assets_count', 'users_count', 'assets_count','assigned_assets_count', 'assets_count', 'rtd_assets_count', 'currency', 'ldap_ou', ]; + 'id', + 'name', + 'address', + 'address2', + 'city', + 'state', + 'country', + 'zip', + 'created_at', + 'updated_at', + 'manager_id', + 'image', + 'assigned_assets_count', + 'users_count', + 'assets_count', + 'assigned_assets_count', + 'assets_count', + 'rtd_assets_count', + 'currency', + 'ldap_ou', + ]; $locations = Location::with('parent', 'manager', 'children')->select([ 'locations.id', @@ -50,6 +68,7 @@ class LocationsController extends Controller ])->withCount('assignedAssets as assigned_assets_count') ->withCount('assets as assets_count') ->withCount('rtd_assets as rtd_assets_count') + ->withCount('children as children_count') ->withCount('users as users_count'); if ($request->filled('search')) { @@ -80,6 +99,10 @@ class LocationsController extends Controller $locations->where('locations.country', '=', $request->input('country')); } + if ($request->filled('manager_id')) { + $locations->where('locations.manager_id', '=', $request->input('manager_id')); + } + // Make sure the offset and limit are actually integers and do not exceed system limits $offset = ($request->input('offset') > $locations->count()) ? $locations->count() : app('api_offset_value'); $limit = app('api_limit_value'); diff --git a/app/Http/Controllers/Api/UsersController.php b/app/Http/Controllers/Api/UsersController.php index 3eb7783e3d..6fbaf281b0 100644 --- a/app/Http/Controllers/Api/UsersController.php +++ b/app/Http/Controllers/Api/UsersController.php @@ -274,11 +274,6 @@ class UsersController extends Controller $offset = ($request->input('offset') > $users->count()) ? $users->count() : app('api_offset_value'); $limit = app('api_limit_value'); - \Log::debug('Requested offset: '. $request->input('offset')); - \Log::debug('App offset: '. app('api_offset_value')); - \Log::debug('Actual offset: '. $offset); - \Log::debug('Limit: '. $limit); - $total = $users->count(); $users = $users->skip($offset)->take($limit)->get(); diff --git a/app/Http/Controllers/AssetModelsController.php b/app/Http/Controllers/AssetModelsController.php index 012f40e399..484a2e2f85 100755 --- a/app/Http/Controllers/AssetModelsController.php +++ b/app/Http/Controllers/AssetModelsController.php @@ -442,7 +442,6 @@ class AssetModelsController extends Controller $del_count = 0; foreach ($models as $model) { - \Log::debug($model->id); if ($model->assets_count > 0) { $del_error_count++; @@ -452,8 +451,6 @@ class AssetModelsController extends Controller } } - \Log::debug($del_count); - \Log::debug($del_error_count); if ($del_error_count == 0) { return redirect()->route('models.index') diff --git a/app/Http/Controllers/LocationsController.php b/app/Http/Controllers/LocationsController.php index 7a4b47b385..dad1d0b241 100755 --- a/app/Http/Controllers/LocationsController.php +++ b/app/Http/Controllers/LocationsController.php @@ -8,6 +8,7 @@ use App\Models\Location; use App\Models\User; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Storage; +use Illuminate\Http\Request; /** * This controller handles all actions related to Locations for @@ -238,7 +239,7 @@ class LocationsController extends Controller * @author [A. Gianotto] [] * @param int $locationId * @since [v6.0.14] - * @return View + * @return \Illuminate\Contracts\View\View */ public function getClone($locationId = null) { @@ -272,8 +273,92 @@ class LocationsController extends Controller } return redirect()->route('locations.index')->with('error', trans('admin/locations/message.does_not_exist')); + } + /** + * Returns a view that allows the user to bulk delete locations + * + * @author [A. Gianotto] [] + * @since [v6.3.1] + * @return \Illuminate\Contracts\View\View + */ + public function postBulkDelete(Request $request) + { + $locations_raw_array = $request->input('ids'); + + // Make sure some IDs have been selected + if ((is_array($locations_raw_array)) && (count($locations_raw_array) > 0)) { + $locations = Location::whereIn('id', $locations_raw_array)->get(); + + $valid_count = 0; + foreach ($locations as $location) { + if ($location->isDeletable()) { + $valid_count++; + } + } + return view('locations/bulk-delete', compact('locations'))->with('valid_count', $valid_count); + } + + return redirect()->route('models.index') + ->with('error', 'You must select at least one model to edit.'); + } + + /** + * Checks that locations can be deleted and deletes them if they can + * + * @author [A. Gianotto] [] + * @since [v6.3.1] + * @return \Illuminate\Http\RedirectResponse + */ + public function postBulkDeleteStore(Request $request) { + $locations_raw_array = $request->input('ids'); + + if ((is_array($locations_raw_array)) && (count($locations_raw_array) > 0)) { + $locations = Location::whereIn('id', $locations_raw_array)->get(); + + $success_count = 0; + $error_count = 0; + + foreach ($locations as $location) { + + // Can we delete this location? + if ($location->isDeletable()) { + $location->delete(); + $success_count++; + } else { + $error_count++; + } + } + + \Log::debug('Success count: '.$success_count); + \Log::debug('Error count: '.$error_count); + // Complete success + if ($success_count == count($locations_raw_array)) { + return redirect() + ->route('locations.index') + ->with('success', trans_choice('general.bulk.delete.success', $success_count, + ['object_type' => trans_choice('general.location_plural', $success_count), 'count' => $success_count] + )); + } + + // Partial success + if ($error_count > 0) { + return redirect() + ->route('locations.index') + ->with('warning', trans('general.bulk.partial_success', + ['success' => $success_count, 'error' => $error_count, 'object_type' => trans('general.locations')] + )); + } + } + + + // Nothing was selected - return to the index + return redirect() + ->route('locations.index') + ->with('error', trans('general.bulk.nothing_selected', + ['object_type' => trans('general.locations')] + )); } } diff --git a/app/Http/Transformers/LocationsTransformer.php b/app/Http/Transformers/LocationsTransformer.php index 635a90cbc7..513b967f42 100644 --- a/app/Http/Transformers/LocationsTransformer.php +++ b/app/Http/Transformers/LocationsTransformer.php @@ -65,6 +65,9 @@ class LocationsTransformer $permissions_array['available_actions'] = [ 'update' => Gate::allows('update', Location::class) ? true : false, 'delete' => $location->isDeletable(), + 'bulk_selectable' => [ + 'delete' => $location->isDeletable() + ], 'clone' => (Gate::allows('create', Location::class) && ($location->deleted_at == '')), ]; diff --git a/app/Models/Location.php b/app/Models/Location.php index 145d6cef9a..1122b5da15 100755 --- a/app/Models/Location.php +++ b/app/Models/Location.php @@ -106,6 +106,7 @@ class Location extends SnipeModel return Gate::allows('delete', $this) && ($this->assignedAssets()->count() === 0) && ($this->assets()->count() === 0) + && ($this->children()->count() === 0) && ($this->users()->count() === 0); } diff --git a/app/Presenters/LocationPresenter.php b/app/Presenters/LocationPresenter.php index 86e82c1220..6a9bc0b568 100644 --- a/app/Presenters/LocationPresenter.php +++ b/app/Presenters/LocationPresenter.php @@ -14,7 +14,11 @@ class LocationPresenter extends Presenter public static function dataTableLayout() { $layout = [ - + [ + 'field' => 'bulk_selectable', + 'checkbox' => true, + 'formatter' => 'checkboxEnabledFormatter', + ], [ 'field' => 'id', 'searchable' => false, diff --git a/app/Providers/SettingsServiceProvider.php b/app/Providers/SettingsServiceProvider.php index 41dd80b4fc..1c89dc2b8c 100644 --- a/app/Providers/SettingsServiceProvider.php +++ b/app/Providers/SettingsServiceProvider.php @@ -39,24 +39,12 @@ class SettingsServiceProvider extends ServiceProvider $limit = abs($int_limit); } -// \Log::debug('Max in env: '.config('app.max_results')); -// \Log::debug('Original requested limit: '.request('limit')); -// \Log::debug('Int limit: '.$int_limit); -// \Log::debug('Modified limit: '.$limit); -// \Log::debug('------------------------------'); - - return $limit; }); // Make sure the offset is actually set and is an integer \App::singleton('api_offset_value', function () { $offset = intval(request('offset')); -// \Log::debug('Original requested offset: '.request('offset')); -// \Log::debug('Modified offset: '.$offset); -// \Log::debug('------------------------------'); - - return $offset; }); diff --git a/resources/lang/en-US/general.php b/resources/lang/en-US/general.php index 0754d70468..6e1663b590 100644 --- a/resources/lang/en-US/general.php +++ b/resources/lang/en-US/general.php @@ -182,6 +182,7 @@ return [ 'lock_passwords' => 'This field value will not be saved in a demo installation.', 'feature_disabled' => 'This feature has been disabled for the demo installation.', 'location' => 'Location', + 'location_plural' => 'Location|Locations', 'locations' => 'Locations', 'logo_size' => 'Square logos look best with Logo + Text. Logo maximum display size is 50px high x 500px wide. ', 'logout' => 'Logout', @@ -443,7 +444,6 @@ return [ 'sample_value' => 'Sample Value', 'no_headers' => 'No Columns Found', 'error_in_import_file' => 'There was an error reading the CSV file: :error', - 'percent_complete' => ':percent % Complete', 'errors_importing' => 'Some Errors occurred while importing: ', 'warning' => 'WARNING: :warning', 'success_redirecting' => '"Success... Redirecting.', @@ -503,5 +503,16 @@ return [ 'or' => 'or', 'url' => 'URL', 'edit_fieldset' => 'Edit fieldset fields and options', + 'bulk' => [ + 'delete' => + [ + 'header' => 'Bulk Delete :object_type', + 'warn' => 'You are about to delete one :object_type|You are about to delete :count :object_type', + 'success' => ':object_type successfully deleted|Successfully deleted :count :object_type', + 'error' => 'Could not delete :object_type', + 'nothing_selected' => 'No :object_type selected - nothing to do', + 'partial' => 'Deleted :success_count :object_type, but :error_count :object_type could not be deleted', + ], + ], ]; diff --git a/resources/views/locations/bulk-delete.blade.php b/resources/views/locations/bulk-delete.blade.php new file mode 100644 index 0000000000..deab1e7fbd --- /dev/null +++ b/resources/views/locations/bulk-delete.blade.php @@ -0,0 +1,70 @@ +@extends('layouts/default') + +{{-- Page title --}} +@section('title') + {{ trans('general.bulk.delete.header', ['object_type' => trans_choice('general.location_plural', $valid_count)]) }} + @parent +@stop + +@section('header_right') + + {{ trans('general.back') }} +@stop + +{{-- Page content --}} +@section('content') +
+ +
+
+ {{csrf_field()}} +
+
+

{{ trans_choice('general.bulk.delete.warn', $valid_count, ['count' => $valid_count,'object_type' => trans_choice('general.location_plural', $valid_count)]) }}

+
+ +
+ + + + + + + + + @foreach ($locations as $location) + assets_count > 0 ) ? ' class="danger"' : '') !!}> + + + + + @endforeach + +
+ + {{ trans('general.name') }}
+ isDeletable()) ? ' checked="checked"' : ' disabled') !!}> + {{ $location->name }}
+
+ + +
+
+
+
+@stop +@section('moar_scripts') + +@stop diff --git a/resources/views/locations/index.blade.php b/resources/views/locations/index.blade.php index 4133f1a2f7..2d020cb026 100755 --- a/resources/views/locations/index.blade.php +++ b/resources/views/locations/index.blade.php @@ -20,11 +20,17 @@
+ @include('partials.locations-bulk-actions') + function notesFormatter(value) { if (value) { - return value.replace(/(?:\r\n|\r|\n)/g, '
');; + return value.replace(/(?:\r\n|\r|\n)/g, '
'); } } + // Check if checkbox should be selectable + // Selectability is determined by the API field "selectable" which is set at the Presenter/API Transformer + // However since different bulk actions have different requirements, we have to walk through the available_actions object + // to determine whether to disable it + function checkboxEnabledFormatter (value, row) { + + // add some stuff to get the value of the select2 option here? + + if ((row.available_actions) && (row.available_actions.bulk_selectable) && (row.available_actions.bulk_selectable.delete !== true)) { + console.log('value for ID ' + row.id + ' is NOT true:' + row.available_actions.bulk_selectable.delete); + return { + disabled:true, + //checked: false, <-- not sure this will work the way we want? + } + } + console.log('value for ID ' + row.id + ' IS true:' + row.available_actions.bulk_selectable.delete); + } + // We need a special formatter for license seats, since they don't work exactly the same // Checkouts need the license ID, checkins need the specific seat ID 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))) { + if ((row.available_actions.checkout === true) && (row.user_can_checkout === true) && ((!row.asset_id) && (!row.assigned_to))) { return '{{ trans('general.checkout') }}'; } else { return '{{ trans('general.checkin') }}'; diff --git a/resources/views/partials/locations-bulk-actions.blade.php b/resources/views/partials/locations-bulk-actions.blade.php new file mode 100644 index 0000000000..228a68dfba --- /dev/null +++ b/resources/views/partials/locations-bulk-actions.blade.php @@ -0,0 +1,20 @@ +@can('delete', \App\Models\Location::class) +
+ {{ Form::open([ + 'method' => 'POST', + 'route' => ['locations.bulkdelete.show'], + 'class' => 'form-inline', + 'id' => 'locationsBulkForm']) }} + +
+ + + +
+ + {{ Form::close() }} +
+@endcan + diff --git a/resources/views/partials/models-bulk-actions.blade.php b/resources/views/partials/models-bulk-actions.blade.php index d8a3f202c7..8fca921ae1 100644 --- a/resources/views/partials/models-bulk-actions.blade.php +++ b/resources/views/partials/models-bulk-actions.blade.php @@ -6,7 +6,7 @@ 'id' => 'modelsBulkForm']) }} @if (request('status')!='deleted') - @can('delete', \App\Models\User::class) + @can('delete', \App\Models\AssetModel::class)
- - - - - - - - @foreach ($user->managedLocations as $location) - - - - - @endforeach - -
{{ trans('general.name') }}{{ trans('general.date') }}
{!! $location->present()->nameUrl() !!}{{ $location->created_at }}
+ + @include('partials.locations-bulk-actions') + + + +
+
diff --git a/routes/web.php b/routes/web.php index 635cdbcb94..88bba08f81 100644 --- a/routes/web.php +++ b/routes/web.php @@ -54,7 +54,18 @@ Route::group(['middleware' => 'auth'], function () { */ Route::group(['prefix' => 'locations', 'middleware' => ['auth']], function () { - + + Route::post( + 'bulkdelete', + [LocationsController::class, 'postBulkDelete'] + )->name('locations.bulkdelete.show'); + + Route::post( + 'bulkedit', + [LocationsController::class, 'postBulkDeleteStore'] + )->name('locations.bulkdelete.store'); + + Route::get('{locationId}/clone', [LocationsController::class, 'getClone'] )->name('clone/location'); @@ -68,6 +79,7 @@ Route::group(['middleware' => 'auth'], function () { '{locationId}/printallassigned', [LocationsController::class, 'print_all_assigned'] )->name('locations.print_all_assigned'); + }); Route::resource('locations', LocationsController::class, [