Merge remote-tracking branch 'origin/develop'

This commit is contained in:
snipe
2025-10-13 14:48:46 +01:00
40 changed files with 970 additions and 107 deletions

View File

@@ -30,10 +30,10 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@v3
uses: github/codeql-action/autobuild@v4
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@v4

View File

@@ -52,6 +52,6 @@ jobs:
# Upload the SARIF file generated in the previous step
- name: Upload SARIF results file
uses: github/codeql-action/upload-sarif@v3
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: results.sarif

View File

@@ -0,0 +1,59 @@
<?php
namespace App\Actions\Categories;
use App\Exceptions\ItemStillHasAccessories;
use App\Exceptions\ItemStillHasAssetModels;
use App\Exceptions\ItemStillHasAssets;
use App\Exceptions\ItemStillHasComponents;
use App\Exceptions\ItemStillHasConsumables;
use App\Exceptions\ItemStillHasLicenses;
use App\Models\Category;
use Illuminate\Support\Facades\Storage;
class DestroyCategoryAction
{
/**
* @throws ItemStillHasAssets
* @throws ItemStillHasAssetModels
* @throws ItemStillHasComponents
* @throws ItemStillHasAccessories
* @throws ItemStillHasLicenses
* @throws ItemStillHasConsumables
*/
static function run(Category $category): bool
{
$category->loadCount([
'assets as assets_count',
'accessories as accessories_count',
'consumables as consumables_count',
'components as components_count',
'licenses as licenses_count',
'models as models_count'
]);
if ($category->assets_count > 0) {
throw new ItemStillHasAssets($category);
}
if ($category->accessories_count > 0) {
throw new ItemStillHasAccessories($category);
}
if ($category->consumables_count > 0) {
throw new ItemStillHasConsumables($category);
}
if ($category->components_count > 0) {
throw new ItemStillHasComponents($category);
}
if ($category->licenses_count > 0) {
throw new ItemStillHasLicenses($category);
}
if ($category->models_count > 0) {
throw new ItemStillHasAssetModels($category);
}
Storage::disk('public')->delete('categories'.'/'.$category->image);
$category->delete();
return true;
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace App\Actions\Manufacturers;
use App\Exceptions\ItemStillHasAccessories;
use App\Exceptions\ItemStillHasAssets;
use App\Exceptions\ItemStillHasComponents;
use App\Exceptions\ItemStillHasConsumables;
use App\Exceptions\ItemStillHasLicenses;
use App\Models\Manufacturer;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
class DeleteManufacturerAction
{
/**
* @throws ItemStillHasAssets
* @throws ItemStillHasComponents
* @throws ItemStillHasAccessories
* @throws ItemStillHasLicenses
* @throws ItemStillHasConsumables
*/
static function run(Manufacturer $manufacturer): bool
{
$manufacturer->loadCount([
'assets as assets_count',
'accessories as accessories_count',
'consumables as consumables_count',
'components as components_count',
'licenses as licenses_count',
]);
if ($manufacturer->assets_count > 0) {
throw new ItemStillHasAssets($manufacturer);
}
if ($manufacturer->accessories_count > 0) {
throw new ItemStillHasAccessories($manufacturer);
}
if ($manufacturer->consumables_count > 0) {
throw new ItemStillHasConsumables($manufacturer);
}
if ($manufacturer->components_count > 0) {
throw new ItemStillHasComponents($manufacturer);
}
if ($manufacturer->licenses_count > 0) {
throw new ItemStillHasLicenses($manufacturer);
}
if ($manufacturer->image) {
try {
Storage::disk('public')->delete('manufacturers/'.$manufacturer->image);
} catch (\Exception $e) {
Log::info($e);
}
}
$manufacturer->delete();
//dd($manufacturer);
return true;
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace App\Actions\Suppliers;
use App\Exceptions\ItemStillHasAccessories;
use App\Exceptions\ItemStillHasComponents;
use App\Exceptions\ItemStillHasConsumables;
use App\Models\Supplier;
use App\Exceptions\ItemStillHasAssets;
use App\Exceptions\ItemStillHasMaintenances;
use App\Exceptions\ItemStillHasLicenses;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
class DestroySupplierAction
{
/**
*
* @throws ItemStillHasLicenses
* @throws ItemStillHasAssets
* @throws ItemStillHasMaintenances
* @throws ItemStillHasAccessories
* @throws ItemStillHasConsumables
* @throws ItemStillHasComponents
*/
static function run(Supplier $supplier): bool
{
$supplier->loadCount([
'maintenances as maintenances_count',
'assets as assets_count',
'licenses as licenses_count',
'accessories as accessories_count',
'consumables as consumables_count',
'components as components_count',
]);
if ($supplier->assets_count > 0) {
throw new ItemStillHasAssets($supplier);
}
if ($supplier->maintenances_count > 0) {
throw new ItemStillHasMaintenances($supplier);
}
if ($supplier->licenses_count > 0) {
throw new ItemStillHasLicenses($supplier);
}
if ($supplier->accessories_count > 0) {
throw new ItemStillHasAccessories($supplier);
}
if ($supplier->consumables_count > 0) {
throw new ItemStillHasConsumables($supplier);
}
if ($supplier->components_count > 0) {
throw new ItemStillHasComponents($supplier);
}
if ($supplier->image) {
try {
Storage::disk('public')->delete('suppliers/'.$supplier->image);
} catch (\Exception $e) {
Log::info($e->getMessage());
}
}
$supplier->delete();
return true;
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Exceptions;
use Exception;
class ItemStillHasAccessories extends ItemStillHasChildren
{
//
}

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Exceptions;
use Exception;
class ItemStillHasAssetModels extends ItemStillHasChildren
{
//
}

View File

@@ -0,0 +1,9 @@
<?php
namespace App\Exceptions;
use Exception;
class ItemStillHasAssets extends ItemStillHasChildren
{
}

View File

@@ -0,0 +1,14 @@
<?php
namespace App\Exceptions;
use Exception;
class ItemStillHasChildren extends Exception
{
//public function __construct($message, $code = 0, Exception $previous = null, $parent, $children)
//{
// trans()
//
//}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Exceptions;
use Exception;
class ItemStillHasComponents extends ItemStillHasChildren
{
//
}

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Exceptions;
use Exception;
class ItemStillHasConsumables extends ItemStillHasChildren
{
//
}

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Exceptions;
use Exception;
class ItemStillHasLicenses extends ItemStillHasChildren
{
//
}

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Exceptions;
use Exception;
class ItemStillHasMaintenances extends ItemStillHasChildren
{
//
}

View File

@@ -2,6 +2,8 @@
namespace App\Http\Controllers\Api;
use App\Actions\Categories\DestroyCategoryAction;
use App\Exceptions\ItemStillHasChildren;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Http\Transformers\CategoriesTransformer;
@@ -224,17 +226,21 @@ class CategoriesController extends Controller
* @param int $id
* @return \Illuminate\Http\Response
*/
public function destroy($id) : JsonResponse
public function destroy(Category $category): JsonResponse
{
$this->authorize('delete', Category::class);
$category = Category::withCount('assets as assets_count', 'accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'licenses as licenses_count', 'models as models_count')->findOrFail($id);
if (! $category->isDeletable()) {
try {
DestroyCategoryAction::run(category: $category);
} catch (ItemStillHasChildren $e) {
return response()->json(
Helper::formatStandardApiResponse('error', null, trans('admin/categories/message.assoc_items', ['asset_type'=>$category->category_type]))
Helper::formatStandardApiResponse('error', null, trans('general.bulk_delete_associations.general_assoc_warning', ['asset_type' => $category->category_type]))
);
} catch (\Exception $e) {
report($e);
return response()->json(
Helper::formatStandardApiResponse('error', null, trans('general.something_went_wrong'))
);
}
$category->delete();
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/categories/message.delete.success')));
}

View File

@@ -2,6 +2,13 @@
namespace App\Http\Controllers\Api;
use App\Actions\Manufacturers\DeleteManufacturerAction;
use App\Exceptions\ItemStillHasAccessories;
use App\Exceptions\ItemStillHasAssets;
use App\Exceptions\ItemStillHasChildren;
use App\Exceptions\ItemStillHasComponents;
use App\Exceptions\ItemStillHasConsumables;
use App\Exceptions\ItemStillHasLicenses;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Http\Transformers\ManufacturersTransformer;
@@ -184,19 +191,19 @@ class ManufacturersController extends Controller
* @since [v4.0]
* @param int $id
*/
public function destroy($id) : JsonResponse
public function destroy(Manufacturer $manufacturer): JsonResponse
{
$this->authorize('delete', Manufacturer::class);
$manufacturer = Manufacturer::findOrFail($id);
$this->authorize('delete', $manufacturer);
if ($manufacturer->isDeletable()) {
$manufacturer->delete();
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/manufacturers/message.delete.success')));
try {
DeleteManufacturerAction::run($manufacturer);
} catch (ItemStillHasChildren $e) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.bulk_delete_associations.general_assoc_warning', ['item' => trans('general.manufacturer')])));
} catch (\Exception $e) {
report($e);
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.something_went_wrong')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/manufacturers/message.assoc_users')));
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/manufacturers/message.delete.success')));
}
/**

View File

@@ -2,6 +2,13 @@
namespace App\Http\Controllers\Api;
use App\Actions\Suppliers\DestroySupplierAction;
use App\Exceptions\ItemStillHasAccessories;
use App\Exceptions\ItemStillHasComponents;
use App\Exceptions\ItemStillHasConsumables;
use App\Exceptions\ItemStillHasMaintenances;
use App\Exceptions\ItemStillHasAssets;
use App\Exceptions\ItemStillHasLicenses;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Http\Transformers\SelectlistTransformer;
@@ -191,27 +198,40 @@ class SuppliersController extends Controller
* @since [v4.0]
* @param int $id
*/
public function destroy($id) : JsonResponse
public function destroy(Supplier $supplier): JsonResponse
{
$this->authorize('delete', Supplier::class);
$supplier = Supplier::with('maintenances', 'assets', 'licenses')->withCount('maintenances as maintenances_count', 'assets as assets_count', 'licenses as licenses_count')->findOrFail($id);
$this->authorize('delete', $supplier);
if ($supplier->assets_count > 0) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/suppliers/message.delete.assoc_assets', ['asset_count' => (int) $supplier->assets_count])));
try {
DestroySupplierAction::run(supplier: $supplier);
} catch (ItemStillHasAssets $e) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.bulk_delete_associations.assoc_assets', [
'asset_count' => (int) $supplier->assets_count, 'item' => trans('general.supplier')
])));
} catch (ItemStillHasMaintenances $e) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.bulk_delete_associations.assoc_maintenances', [
'asset_maintenances_count' => $supplier->asset_maintenances_count, 'item' => trans('general.supplier')
])));
} catch (ItemStillHasLicenses $e) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.bulk_delete_associations.assoc_licenses', [
'licenses_count' => (int) $supplier->licenses_count, 'item' => trans('general.supplier')
])));
} catch (ItemStillHasAccessories $e) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.bulk_delete_associations.assoc_accessories', [
'accessories_count' => (int) $supplier->accessories_count, 'item' => trans('general.supplier')
])));
} catch (ItemStillHasConsumables $e) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.bulk_delete_associations.assoc_consumables', [
'consumables_count' => (int) $supplier->consumables_count, 'item' => trans('general.supplier')
])));
} catch (ItemStillHasComponents $e) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.bulk_delete_associations.assoc_components', [
'components_count' => (int) $supplier->components_count, 'item' => trans('general.supplier')
])));
} catch (\Exception $e) {
report($e);
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.something_went_wrong')));
}
if ($supplier->maintenances_count > 0) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/suppliers/message.delete.assoc_maintenances', ['maintenances_count' => $supplier->maintenances_count])));
}
if ($supplier->licenses_count > 0) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/suppliers/message.delete.assoc_licenses', ['licenses_count' => (int) $supplier->licenses_count])));
}
$supplier->delete();
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/suppliers/message.delete.success')));
}

View File

@@ -0,0 +1,59 @@
<?php
namespace App\Http\Controllers;
use App\Actions\Categories\DestroyCategoryAction;
use App\Exceptions\ItemStillHasAccessories;
use App\Exceptions\ItemStillHasAssetModels;
use App\Exceptions\ItemStillHasAssets;
use App\Exceptions\ItemStillHasComponents;
use App\Exceptions\ItemStillHasConsumables;
use App\Exceptions\ItemStillHasLicenses;
use App\Models\Category;
use Illuminate\Http\Request;
class BulkCategoriesController extends Controller
{
public function destroy(Request $request)
{
$this->authorize('delete', Category::class);
$errors = [];
$success_count = 0;
foreach ($request->ids as $id) {
$category = Category::find($id);
if (is_null($category)) {
$errors[] = trans('admin/categories/message.does_not_exist');
continue;
}
try {
DestroyCategoryAction::run(category: $category);
$success_count++;
} catch (ItemStillHasAccessories $e) {
$errors[] = trans('general.bulk_delete_associations.assoc_assets_no_count', ['item_name' => $category->name, 'item' => trans('general.category')]);
} catch (ItemStillHasAssetModels) {
$errors[] = trans('general.bulk_delete_associations.assoc_asset_models_no_count', ['item_name' => $category->name, 'item' => trans('general.category')]);
} catch (ItemStillHasAssets) {
$errors[] = trans('general.bulk_delete_associations.assoc_assets_no_count', ['item_name' => $category->name, 'item' => trans('general.category')]);
} catch (ItemStillHasComponents) {
$errors[] = trans('general.bulk_delete_associations.assoc_components_no_count', ['item_name' => $category->name, 'item' => trans('general.category')]);
} catch (ItemStillHasConsumables) {
$errors[] = trans('general.bulk_delete_associations.assoc_consumables_no_count', ['item_name' => $category->name, 'item' => trans('general.category')]);
} catch (ItemStillHasLicenses) {
$errors[] = trans('general.bulk_delete_associations.assoc_licenses_no_count', ['item_name' => $category->name, 'item' => trans('general.category')]);;
} catch (\Exception $e) {
report($e);
$errors[] = trans('general.something_went_wrong');
}
}
if (count($errors) > 0) {
if ($success_count > 0) {
return redirect()->route('categories.index')->with('success', trans_choice('admin/categories/message.delete.partial_success', $success_count, ['count' => $success_count]))->with('multi_error_messages', $errors);
}
return redirect()->route('categories.index')->with('multi_error_messages', $errors);
} else {
return redirect()->route('categories.index')->with('success', trans('admin/categories/message.delete.bulk_success'));
}
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace App\Http\Controllers;
use App\Actions\Manufacturers\DeleteManufacturerAction;
use App\Exceptions\ItemStillHasAccessories;
use App\Exceptions\ItemStillHasAssetModels;
use App\Exceptions\ItemStillHasAssets;
use App\Exceptions\ItemStillHasChildren;
use App\Exceptions\ItemStillHasComponents;
use App\Exceptions\ItemStillHasConsumables;
use App\Exceptions\ItemStillHasLicenses;
use App\Models\Manufacturer;
use Illuminate\Http\Request;
class BulkManufacturersController extends Controller
{
public function destroy(Request $request)
{
$this->authorize('delete', Manufacturer::class);
$errors = [];
$success_count = 0;
foreach ($request->ids as $id) {
$manufacturer = Manufacturer::find($id);
if (is_null($manufacturer)) {
$errors[] = trans('admin/manufacturers/message.does_not_exist');
continue;
}
try {
DeleteManufacturerAction::run(manufacturer: $manufacturer);
$success_count++;
} catch (ItemStillHasAssets $e) {
$errors[] = trans('general.bulk_delete_associations.assoc_assets_no_count', ['item_name' => $manufacturer->name, 'item' => trans('general.manufacturer')]);
} catch (ItemStillHasAccessories $e) {
$errors[] = trans('general.bulk_delete_associations.assoc_accessories_no_count', ['item_name' => $manufacturer->name, 'item' => trans('general.manufacturer')]);
} catch (ItemStillHasConsumables $e) {
$errors[] = trans('general.bulk_delete_associations.assoc_consumables_no_count', ['item_name' => $manufacturer->name, 'item' => trans('general.manufacturer')]);
} catch (ItemStillHasComponents $e) {
$errors[] = trans('general.bulk_delete_associations.assoc_components_no_count', ['item_name' => $manufacturer->name, 'item' => trans('general.manufacturer')]);
} catch (ItemStillHasLicenses $e) {
$errors[] = trans('general.bulk_delete_associations.assoc_licenses_no_count', ['item_name' => $manufacturer->name, 'item' => trans('general.manufacturer')]);;
} catch (\Exception $e) {
report($e);
$errors[] = trans('general.something_went_wrong');
}
}
if (count($errors) > 0) {
if ($success_count > 0) {
return redirect()->route('manufacturers.index')->with('success', trans_choice('admin/manufacturers/message.delete.partial_success', $success_count, ['count' => $success_count]))->with('multi_error_messages', $errors);
}
return redirect()->route('manufacturers.index')->with('multi_error_messages', $errors);
} else {
return redirect()->route('manufacturers.index')->with('success', trans('admin/manufacturers/message.delete.bulk_success'));
}
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace App\Http\Controllers;
use App\Actions\Suppliers\DestroySupplierAction;
use App\Exceptions\ItemStillHasAccessories;
use App\Exceptions\ItemStillHasComponents;
use App\Exceptions\ItemStillHasConsumables;
use App\Exceptions\ItemStillHasMaintenances;
use App\Exceptions\ItemStillHasAssets;
use App\Exceptions\ItemStillHasLicenses;
use App\Models\Supplier;
use Illuminate\Http\Request;
class BulkSuppliersController extends Controller
{
public function destroy(Request $request)
{
$this->authorize('delete', Supplier::class);
$errors = [];
$success_count = 0;
foreach ($request->ids as $id) {
$supplier = Supplier::find($id);
if (is_null($supplier)) {
$errors[] = trans('admin/suppliers/message.delete.not_found');
continue;
}
try {
DestroySupplierAction::run(supplier: $supplier);
} catch (ItemStillHasAssets $e) {
$errors[] = trans('general.bulk_delete_associations.assoc_assets', ['asset_count' => (int) $supplier->assets_count, 'item' => trans('general.supplier'), 'item_name' => $supplier->name]);
} catch (ItemStillHasMaintenances $e) {
$errors[] = trans('general.bulk_delete_associations.assoc_maintenances', ['asset_maintenances_count' => $supplier->asset_maintenances_count, 'item' => trans('general.supplier'), 'item_name' => $supplier->name]);
} catch (ItemStillHasLicenses $e) {
$errors[] = trans('general.bulk_delete_associations.assoc_licenses', ['licenses_count' => (int) $supplier->licenses_count, 'item' => trans('general.supplier'), 'item_name' => $supplier->name]);
} catch (ItemStillHasAccessories $e) {
$errors[] = trans('general.bulk_delete_associations.assoc_accessories', ['accessories_count' => (int) $supplier->accessories_count, 'item' => trans('general.supplier'), 'item_name' => $supplier->name]);
} catch (ItemStillHasConsumables $e) {
$errors[] = trans('general.bulk_delete_associations.assoc_consumables', ['consumables_count' => (int) $supplier->consumables_count, 'item' => trans('general.supplier'), 'item_name' => $supplier->name]);
} catch (ItemStillHasComponents $e) {
$errors[] = trans('general.bulk_delete_associations.assoc_components', ['components_count' => (int) $supplier->components_count, 'item' => trans('general.supplier'), 'item_name' => $supplier->name]);
} catch (\Exception $e) {
report($e);
$errors[] = trans('general.something_went_wrong');
}
}
if (count($errors) > 0) {
if ($success_count > 0) {
return redirect()->route('suppliers.index')->with('success', trans_choice('admin/suppliers/message.delete.partial_success', $success_count, ['count' => $success_count]))->with('multi_error_messages', $errors);
}
return redirect()->route('suppliers.index')->with('multi_error_messages', $errors);
} else {
return redirect()->route('suppliers.index')->with('success', trans('admin/suppliers/message.delete.bulk_success'));
}
}
}

View File

@@ -2,6 +2,14 @@
namespace App\Http\Controllers;
use App\Actions\Categories\DestroyCategoryAction;
use App\Exceptions\ItemStillHasAccessories;
use App\Exceptions\ItemStillHasAssetModels;
use App\Exceptions\ItemStillHasAssets;
use App\Exceptions\ItemStillHasChildren;
use App\Exceptions\ItemStillHasComponents;
use App\Exceptions\ItemStillHasConsumables;
use App\Exceptions\ItemStillHasLicenses;
use App\Helpers\Helper;
use App\Http\Requests\ImageUploadRequest;
use App\Models\Category;
@@ -143,20 +151,18 @@ class CategoriesController extends Controller
* @since [v1.0]
* @param int $categoryId
*/
public function destroy($categoryId) : RedirectResponse
public function destroy(Category $category): RedirectResponse
{
$this->authorize('delete', Category::class);
// Check if the category exists
if (is_null($category = Category::withCount('assets as assets_count', 'accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'licenses as licenses_count', 'models as models_count')->findOrFail($categoryId))) {
return redirect()->route('categories.index')->with('error', trans('admin/categories/message.not_found'));
try {
DestroyCategoryAction::run($category);
} catch (ItemStillHasChildren $e) {
return redirect()->route('categories.index')->with('error', trans('general.bulk_delete_associations.general_assoc_warning', ['item' => trans('general.category')]));
} catch (\Exception $e) {
report($e);
return redirect()->route('categories.index')->with('error', trans('admin/categories/message.delete.error'));
}
if (! $category->isDeletable()) {
return redirect()->route('categories.index')->with('error', trans('admin/categories/message.assoc_items', ['asset_type'=> $category->category_type]));
}
Storage::disk('public')->delete('categories'.'/'.$category->image);
$category->delete();
return redirect()->route('categories.index')->with('success', trans('admin/categories/message.delete.success'));
}

View File

@@ -2,6 +2,14 @@
namespace App\Http\Controllers;
use App\Actions\Manufacturers\DeleteManufacturerAction;
use App\Exceptions\ItemStillHasAccessories;
use App\Exceptions\ItemStillHasAssets;
use App\Exceptions\ItemStillHasChildren;
use App\Exceptions\ItemStillHasComponents;
use App\Exceptions\ItemStillHasConsumables;
use App\Exceptions\ItemStillHasLicenses;
use App\Helpers\Helper;
use App\Http\Requests\ImageUploadRequest;
use App\Models\Actionlog;
use App\Models\Manufacturer;
@@ -157,32 +165,18 @@ class ManufacturersController extends Controller
* @param int $manufacturerId
* @since [v1.0]
*/
public function destroy($manufacturerId) : RedirectResponse
public function destroy(Manufacturer $manufacturer): RedirectResponse
{
$this->authorize('delete', Manufacturer::class);
if (is_null($manufacturer = Manufacturer::withTrashed()->withCount('models as models_count')->find($manufacturerId))) {
return redirect()->route('manufacturers.index')->with('error', trans('admin/manufacturers/message.not_found'));
$this->authorize('delete', $manufacturer);
try {
DeleteManufacturerAction::run($manufacturer);
} catch (ItemStillHasChildren $e) {
return redirect()->route('manufacturers.index')->with('error', trans('general.bulk_delete_associations.general_assoc_warning', ['item' => trans('general.manufacturer')]));
} catch (\Exception $e) {
report($e);
return redirect()->route('manufacturers.index')->with('error', trans('general.something_went_wrong'));
}
if (! $manufacturer->isDeletable()) {
return redirect()->route('manufacturers.index')->with('error', trans('admin/manufacturers/message.assoc_users'));
}
if ($manufacturer->image) {
try {
Storage::disk('public')->delete('manufacturers/'.$manufacturer->image);
} catch (\Exception $e) {
Log::info($e);
}
}
// Soft delete the manufacturer if active, permanent delete if is already deleted
if ($manufacturer->deleted_at === null) {
$manufacturer->delete();
} else {
$manufacturer->forceDelete();
}
// Redirect to the manufacturers management page
return redirect()->route('manufacturers.index')->with('success', trans('admin/manufacturers/message.delete.success'));
}

View File

@@ -2,10 +2,18 @@
namespace App\Http\Controllers;
use App\Actions\Suppliers\DestroySupplierAction;
use App\Exceptions\ItemStillHasAccessories;
use App\Exceptions\ItemStillHasComponents;
use App\Exceptions\ItemStillHasConsumables;
use App\Exceptions\ItemStillHasMaintenances;
use App\Exceptions\ItemStillHasAssets;
use App\Exceptions\ItemStillHasLicenses;
use App\Http\Requests\ImageUploadRequest;
use App\Models\Supplier;
use Illuminate\Http\RedirectResponse;
use \Illuminate\Contracts\View\View;
use Illuminate\Support\MessageBag;
/**
* This controller handles all actions related to Suppliers for
@@ -118,30 +126,41 @@ class SuppliersController extends Controller
*
* @param int $supplierId
*/
public function destroy($supplierId) : RedirectResponse
public function destroy(Supplier $supplier): RedirectResponse
{
$this->authorize('delete', Supplier::class);
if (is_null($supplier = Supplier::with('maintenances', 'assets', 'licenses')->withCount('maintenances as maintenances_count', 'assets as assets_count', 'licenses as licenses_count')->find($supplierId))) {
return redirect()->route('suppliers.index')->with('error', trans('admin/suppliers/message.not_found'));
try {
DestroySupplierAction::run(supplier: $supplier);
} catch (ItemStillHasAssets $e) {
return redirect()->route('suppliers.index')->with('error', trans('general.bulk_delete_associations.assoc_assets', [
'asset_count' => (int) $supplier->assets_count, 'item' => trans('general.supplier')
]));
} catch (ItemStillHasMaintenances $e) {
return redirect()->route('suppliers.index')->with('error', trans('general.bulk_delete_associations.assoc_maintenances', [
'asset_maintenances_count' => $supplier->asset_maintenances_count, 'item' => trans('general.supplier')
]));
} catch (ItemStillHasLicenses $e) {
return redirect()->route('suppliers.index')->with('error', trans('general.bulk_delete_associations.assoc_licenses', [
'licenses_count' => (int) $supplier->licenses_count, 'item' => trans('general.supplier')
]));
} catch (ItemStillHasAccessories $e) {
return redirect()->route('suppliers.index')->with('error', trans('general.bulk_delete_associations.assoc_accessories', [
'accessories_count' => (int) $supplier->accessories_count, 'item' => trans('general.supplier')
]));
} catch (ItemStillHasConsumables $e) {
return redirect()->route('suppliers.index')->with('error', trans('general.bulk_delete_associations.assoc_consumables', [
'consumables_count' => (int) $supplier->consumables_count, 'item' => trans('general.supplier')
]));
} catch (ItemStillHasComponents $e) {
return redirect()->route('suppliers.index')->with('error', trans('general.bulk_delete_associations.assoc_components', [
'components_count' => (int) $supplier->components_count, 'item' => trans('general.supplier')
]));
} catch (\Exception $e) {
report($e);
return redirect()->route('suppliers.index')->with('error', trans('admin/suppliers/message.delete.error'));
}
if ($supplier->assets_count > 0) {
return redirect()->route('suppliers.index')->with('error', trans('admin/suppliers/message.delete.assoc_assets', ['asset_count' => (int) $supplier->assets_count]));
}
if ($supplier->maintenances_count > 0) {
return redirect()->route('suppliers.index')->with('error', trans('admin/suppliers/message.delete.assoc_maintenances', ['maintenances_count' => $supplier->maintenances_count]));
}
if ($supplier->licenses_count > 0) {
return redirect()->route('suppliers.index')->with('error', trans('admin/suppliers/message.delete.assoc_licenses', ['licenses_count' => (int) $supplier->licenses_count]));
}
$supplier->delete();
return redirect()->route('suppliers.index')->with('success',
trans('admin/suppliers/message.delete.success')
);
return redirect()->route('suppliers.index')->with('success', trans('admin/suppliers/message.delete.success'));
}
/**
@@ -154,6 +173,5 @@ class SuppliersController extends Controller
{
$this->authorize('view', Supplier::class);
return view('suppliers/view', compact('supplier'));
}
}

View File

@@ -14,6 +14,11 @@ class CategoryPresenter extends Presenter
public static function dataTableLayout()
{
$layout = [
[
'field' => 'checkbox',
'checkbox' => true,
'titleTooltip' => trans('general.select_all_none'),
],
[
'field' => 'id',
'searchable' => false,

View File

@@ -14,7 +14,11 @@ class ManufacturerPresenter extends Presenter
public static function dataTableLayout()
{
$layout = [
[
'field' => 'checkbox',
'checkbox' => true,
'titleTooltip' => trans('general.select_all_none'),
],
[
'field' => 'id',
'searchable' => false,

View File

@@ -13,6 +13,11 @@ class SupplierPresenter extends Presenter
public static function dataTableLayout()
{
$layout = [
[
'field' => 'checkbox',
'checkbox' => true,
'titleTooltip' => trans('general.select_all_none'),
],
[
'field' => 'id',
'searchable' => false,

View File

@@ -18,9 +18,11 @@ return array(
),
'delete' => array(
'confirm' => 'Are you sure you wish to delete this category?',
'error' => 'There was an issue deleting the category. Please try again.',
'success' => 'The category was deleted successfully.'
'confirm' => 'Are you sure you wish to delete this category?',
'error' => 'There was an issue deleting the category. Please try again.',
'success' => 'Category was deleted successfully.',
'bulk_success' => 'Categories were deleted successfully.',
'partial_success' => 'Category deleted successfully. See additional information below. | :count categories were deleted successfully. See additional information below.',
)
);

View File

@@ -22,9 +22,11 @@ return array(
),
'delete' => array(
'confirm' => 'Are you sure you wish to delete this manufacturer?',
'confirm' => 'Are you sure you wish to delete this manufacturer?',
'error' => 'There was an issue deleting the manufacturer. Please try again.',
'success' => 'The Manufacturer was deleted successfully.'
'success' => 'Manufacturer deleted successfully.',
'bulk_success' => 'Manufacturers deleted successfully.',
'partial_success' => 'Manufacturer deleted successfully. See additional information below. | :count manufacturers were deleted successfully. See additional information below.',
)
);

View File

@@ -20,9 +20,9 @@ return array(
'confirm' => 'Are you sure you wish to delete this supplier?',
'error' => 'There was an issue deleting the supplier. Please try again.',
'success' => 'Supplier was deleted successfully.',
'assoc_assets' => 'This supplier is currently associated with :asset_count asset(s) and cannot be deleted. Please update your assets to no longer reference this supplier and try again. ',
'assoc_licenses' => 'This supplier is currently associated with :licenses_count licences(s) and cannot be deleted. Please update your licenses to no longer reference this supplier and try again. ',
'assoc_maintenances' => 'This supplier is currently associated with :maintenances_count asset maintenances(s) and cannot be deleted. Please update your asset maintenances to no longer reference this supplier and try again. ',
'not_found' => 'Supplier not found.',
'bulk_success' => 'Suppliers were deleted successfully.',
'partial_success' => 'Supplier deleted successfully. See additional information below. | :count suppliers were deleted successfully. See additional information below.',
)
);

View File

@@ -1,6 +1,7 @@
<?php
return [
'show_all' => 'Show All',
'2FA_reset' => '2FA reset',
'accessories' => 'Accessories',
'activated' => 'Activated',
@@ -629,6 +630,24 @@ return [
'notes' => 'Add a note',
],
'bulk_delete_associations' => [
'general_assoc_warning' => ':item_name still has associated items. Please remove them before deleting this :item.',
'assoc_assets' => ':item_name is currently associated with :asset_count asset(s) and cannot be deleted. Please update your assets to no longer reference this :item and try again.',
'asset_models' => ':item_name is currently associated with :asset_count asset(s) and cannot be deleted. Please update your asset models to no longer reference this :item and try again.',
'assoc_maintenances' => ':item_name is currently associated with :maintenance_count maintenance(s) and cannot be deleted. Please update your maintenances to no longer reference this :item and try again.',
'assoc_accessories' => ':item_name is currently associated with :accessory_count accessory(ies) and cannot be deleted. Please update your accessories to no longer reference this :item and try again.',
'assoc_consumables' => ':item_name is currently associated with :consumable_count consumable(s) and cannot be deleted. Please update your consumables to no longer reference this :item and try again.',
'assoc_components' => ':item_name is currently associated with :component_count component(s) and cannot be deleted. Please update your components to no longer reference this :item and try again.',
'assoc_licenses' => ':item_name is currently associated with :license_count license(s) and cannot be deleted. Please update your licenses to no longer reference this :item and try again.',
'assoc_assets_no_count' => ':item_name is currently associated with other assets and cannot be deleted. Please update your assets to no longer reference this :item and try again.',
'asset_models_no_count' => ':item_name is currently associated with other asset models and cannot be deleted. Please update your assets to no longer reference this :item and try again.',
'assoc_maintenances_no_count' => ':item_name is currently associated with other maintenances and cannot be deleted. Please update your maintenances to no longer reference this :item and try again.',
'assoc_accessories_no_count' => ':item_name is currently associated with other accessories and cannot be deleted. Please update your accessories to no longer reference this :item and try again.',
'assoc_consumables_no_count' => ':item_name is currently associated with other consumables and cannot be deleted. Please update your consumables to no longer reference this :item and try again.',
'assoc_components_no_count' => ':item_name is currently associated with other components and cannot be deleted. Please update your components to no longer reference this :item and try again.',
'assoc_licenses_no_count' => ':item_name is currently associated with other licenses and cannot be deleted. Please update your licenses to no longer reference this :item and try again.',
],
'breadcrumb_button_actions' => [
'edit_item' => 'Edit :name',
'checkout_item' => 'Checkout :name',

View File

@@ -13,13 +13,31 @@
<div class="col-md-12">
<div class="box box-default">
<div class="box-body">
<table
data-columns="{{ \App\Presenters\CategoryPresenter::dataTableLayout() }}"
<x-tables.bulk-actions
id_divname='categoriesBulkEditToolbar'
action_route="{{route('categories.bulk.delete')}}"
id_formname="categoriesBulkForm"
id_button="bulkCategoryEditButton"
model_name="category"
>
@can('delete', App\Models\Category::class)
<option>Delete</option>
@endcan
</x-tables.bulk-actions>
<table
data-columns="{{ \App\Presenters\CategoryPresenter::dataTableLayout() }}"
data-cookie-id-table="categoryTable"
data-id-table="categoryTable"
data-side-pagination="server"
data-sort-order="asc"
id="categoryTable"
{{-- begin stuff for bulk dropdown --}}
data-toolbar="#categoriesBulkEditToolbar"
data-bulk-button-id="#bulkCategoryEditButton"
data-bulk-form-id="#categoriesBulkForm"
{{-- end stuff for bulk dropdown --}}
data-buttons="categoryButtons"
class="table table-striped snipe-table"
data-url="{{ route('api.categories.index') }}"

View File

@@ -0,0 +1,35 @@
@props([
'id_divname',
'id_formname',
'id_button',
'action_route',
'action_method',
'model_name' => 'asset',
])
<div id="{{ $id_divname }}" style="min-width:400px">
<form
method="POST"
action="{{ $action_route }}"
accept-charset="UTF-8"
class="form-inline"
id="{{ $id_formname }}"
>
@csrf
{{-- The sort and order will only be used if the cookie is actually empty (like on first-use)--}}
<input name="sort" type="hidden" value="{{`$model_name.id`}}">
<input name="order" type="hidden" value="asc">
<label for="bulk_actions">
<span class="sr-only">
{{ trans('button.bulk_actions') }}
</span>
</label>
<select name="bulk_actions" class="form-control select2" aria-label="bulk_actions" style="min-width: 350px;">
{{ $slot }}
</select>
<button class="btn btn-primary" id="{{ $id_button }}"
disabled>{{ trans('button.go') }}</button>
</form>
</div>

View File

@@ -30,7 +30,17 @@
</form>
@else
<x-tables.bulk-actions
id_divname='manufacturersBulkEditToolbar'
action_route="{{route('manufacturers.bulk.delete')}}"
id_formname="manufacturersBulkForm"
id_button="bulkManufacturerEditButton"
model_name="manufacturer"
>
@can('delete', App\Models\Manufacturer::class)
<option>{{trans('general.delete')}}</option>
@endcan
</x-tables.bulk-actions>
<table
data-columns="{{ \App\Presenters\ManufacturerPresenter::dataTableLayout() }}"
@@ -40,6 +50,11 @@
data-side-pagination="server"
data-sort-order="asc"
id="manufacturersTable"
{{-- begin stuff for bulk dropdown --}}
data-toolbar="#manufacturersBulkEditToolbar"
data-bulk-button-id="#bulkManufacturerEditButton"
data-bulk-form-id="#manufacturersBulkForm"
{{-- end stuff for bulk dropdown --}}
data-buttons="manufacturerButtons"
class="table table-striped snipe-table"
data-url="{{route('api.manufacturers.index', ['deleted' => (request('deleted')=='true') ? 'true' : 'false' ]) }}"
@@ -49,8 +64,7 @@
}'>
</table>
@endif
@endif
</div><!-- /.box-body -->
</div><!-- /.box -->
</div>

View File

@@ -147,6 +147,29 @@
</div>
@endif
@if ($messages = session()->get('multi_error_messages'))
<div class="col-md-12">
<div class="alert alert alert-warning fade in">
<button type="button" class="close" data-dismiss="alert">&times;</button>
<i class="fas fa-exclamation-triangle faa-pulse animated"></i>
<strong>{{ trans('general.notification_error') }}: </strong>
<ul>
@foreach(array_splice($messages, 0,3) as $key => $message)
<li>{{ $message }}</li>
@endforeach
</ul>
<details>
<summary>{{ trans('general.show_all') }}</summary>
<ul>
@foreach(array_splice($messages, 3) as $key => $message)
<li>{{ $message }}</li>
@endforeach
</ul>
</details>
</div>
</div>
@endif
@if ($message = session()->get('warning'))
<div class="col-md-12">

View File

@@ -12,15 +12,35 @@
<div class="row">
<div class="col-md-12">
<div class="box box-default">
<div class="box box-default">
<div class="box-body">
<table
<div class="row">
<div class="col-md-12">
<x-tables.bulk-actions
id_divname='suppliersBulkEditToolbar'
action_route="{{route('suppliers.bulk.delete')}}"
id_formname="suppliersBulkForm"
id_button="bulkSupplierEditButton"
model_name="supplier"
>
@can('delete', App\Models\Supplier::class)
<option>Delete</option>
@endcan
</x-tables.bulk-actions>
<table
data-columns="{{ \App\Presenters\SupplierPresenter::dataTableLayout() }}"
data-cookie-id-table="suppliersTable"
data-id-table="suppliersTable"
data-side-pagination="server"
data-sort-order="asc"
id="suppliersTable"
{{-- begin stuff for bulk dropdown --}}
data-toolbar="#suppliersBulkEditToolbar"
data-bulk-button-id="#bulkSupplierEditButton"
data-bulk-form-id="#suppliersBulkForm"
{{-- end stuff for bulk dropdown --}}
data-advanced-search="false"
data-buttons="supplierButtons"
class="table table-striped snipe-table"
@@ -30,7 +50,9 @@
"ignoreColumn": ["actions","image","change","checkbox","checkincheckout","icon"]
}'>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -5,6 +5,9 @@ use App\Http\Controllers\ActionlogController;
use App\Http\Controllers\Auth\ForgotPasswordController;
use App\Http\Controllers\Auth\LoginController;
use App\Http\Controllers\Auth\ResetPasswordController;
use App\Http\Controllers\BulkCategoriesController;
use App\Http\Controllers\BulkManufacturersController;
use App\Http\Controllers\BulkSuppliersController;
use App\Http\Controllers\CategoriesController;
use App\Http\Controllers\CompaniesController;
use App\Http\Controllers\DashboardController;
@@ -43,6 +46,8 @@ Route::group(['middleware' => 'auth'], function () {
Route::resource('categories', CategoriesController::class, [
'parameters' => ['category' => 'category_id'],
]);
Route::post('categories/bulk/delete', [BulkCategoriesController::class, 'destroy'])->name('categories.bulk.delete');
/*
* Labels
@@ -71,11 +76,15 @@ Route::group(['middleware' => 'auth'], function () {
Route::resource('manufacturers', ManufacturersController::class);
Route::post('manufacturers/bulk/delete', [BulkManufacturersController::class, 'destroy'])->name('manufacturers.bulk.delete');
/*
* Suppliers
*/
Route::resource('suppliers', SuppliersController::class);
Route::post('suppliers/bulk/delete', [BulkSuppliersController::class, 'destroy'])->name('suppliers.bulk.delete');
/*
* Depreciations
*/

View File

@@ -0,0 +1,54 @@
<?php
namespace Tests\Feature\Categories\Ui;
use App\Models\AssetModel;
use App\Models\Category;
use App\Models\User;
use Tests\Concerns\TestsPermissionsRequirement;
use Tests\TestCase;
class BulkDeleteCategoriesTest extends TestCase implements TestsPermissionsRequirement
{
public function testRequiresPermission()
{
$this->actingAs(User::factory()->create())
->post(route('categories.bulk.delete'), [
'ids' => [1, 2, 3]
])
->assertForbidden();
}
public function test_category_cannot_be_bulk_deleted_if_models_still_associated()
{
$category = Category::factory()->create();
AssetModel::factory()->create(['category_id' => $category->id]);
$this->actingAs(User::factory()->deleteCategories()->create())
->post(route('categories.bulk.delete'), [
'ids' => [$category->id]
]);
$this->assertModelExists($category);
$this->assertNotSoftDeleted($category);
}
public function test_category_can_be_bulk_deleted_if_no_models_associated()
{
$category1 = Category::factory()->create();
$category2 = Category::factory()->create();
$category3 = Category::factory()->create();
$this->actingAs(User::factory()->deleteCategories()->create())
->post(route('categories.bulk.delete'), [
'ids' => [$category1->id, $category2->id, $category3->id]
])
->assertRedirect(route('categories.index'));
$this->assertSoftDeleted($category1);
$this->assertSoftDeleted($category2);
$this->assertSoftDeleted($category3);
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace Tests\Feature\Manufacturers\Ui;
use App\Models\Accessory;
use App\Models\Manufacturer;
use App\Models\User;
use Tests\Concerns\TestsPermissionsRequirement;
use Tests\TestCase;
class BulkDeleteManufacturersTest extends TestCase implements TestsPermissionsRequirement
{
public function testRequiresPermission()
{
$this->actingAs(User::factory()->create())
->post(route('manufacturers.bulk.delete'), [
'ids' => [1, 2, 3]
])
->assertForbidden();
}
public function test_manufacturer_cannot_be_bulk_deleted_if_models_still_associated()
{
//TODO: better test for specific messages
$manufacturer = Manufacturer::factory()->create();
Accessory::factory()->for($manufacturer)->create();
$this->actingAs(User::factory()->deleteManufacturers()->create())
->post(route('manufacturers.bulk.delete'), [
'ids' => [$manufacturer->id]
]);
$this->assertModelExists($manufacturer);
$this->assertNotSoftDeleted($manufacturer);
}
public function test_manufacturers_can_be_bulk_deleted()
{
$manufacturer1 = Manufacturer::factory()->create();
$manufacturer2 = Manufacturer::factory()->create();
$manufacturer3 = Manufacturer::factory()->create();
$this->actingAs(User::factory()->deleteManufacturers()->create())
->post(route('manufacturers.bulk.delete'), [
'ids' => [$manufacturer1->id, $manufacturer2->id, $manufacturer3->id]
])
->assertRedirect(route('manufacturers.index'));
$this->assertSoftDeleted($manufacturer1);
$this->assertSoftDeleted($manufacturer2);
$this->assertSoftDeleted($manufacturer3);
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace Tests\Feature\Manufacturers\Ui;
use App\Models\Accessory;
use App\Models\Category;
use App\Models\Manufacturer;
use App\Models\User;
use Tests\Concerns\TestsPermissionsRequirement;
use Tests\TestCase;
class DeleteManufacturersTest extends TestCase implements TestsPermissionsRequirement
{
public function testRequiresPermission()
{
$this->actingAs(User::factory()->create())
->delete(route('categories.destroy', Category::factory()->create()))
->assertForbidden();
}
public function test_manufacturer_cannot_be_deleted_if_models_still_associated()
{
$manufacturer = Manufacturer::factory()->create();
Accessory::factory()->for($manufacturer)->create();
$this->actingAs(User::factory()->deleteManufacturers()->create())
->delete(route('manufacturers.destroy', $manufacturer));
$this->assertNotSoftDeleted($manufacturer);
}
public function test_manufacturer_can_be_deleted()
{
$manufacturer = Manufacturer::factory()->create();
$this->assertDatabaseHas('manufacturers', ['id' => $manufacturer->id]);
$this->actingAs(User::factory()->deleteManufacturers()->create())
->delete(route('manufacturers.destroy', $manufacturer))
->assertRedirect(route('manufacturers.index'));
$this->assertSoftDeleted($manufacturer);
}
}

View File

@@ -33,6 +33,7 @@ class DeleteSuppliersTest extends TestCase implements TestsPermissionsRequiremen
$actor->deleteJson(route('api.suppliers.destroy', $supplierWithMaintenance))->assertStatusMessageIs('error');
$actor->deleteJson(route('api.suppliers.destroy', $supplierWithLicense))->assertStatusMessageIs('error');
$this->assertNotSoftDeleted($supplierWithAsset);
$this->assertNotSoftDeleted($supplierWithMaintenance);
$this->assertNotSoftDeleted($supplierWithLicense);

View File

@@ -0,0 +1,52 @@
<?php
namespace Tests\Feature\Suppliers\Ui;
use App\Models\Asset;
use App\Models\Supplier;
use App\Models\User;
use Tests\Concerns\TestsPermissionsRequirement;
use Tests\TestCase;
class BulkDeleteSuppliersTest extends TestCase implements TestsPermissionsRequirement
{
public function testRequiresPermission()
{
$this->actingAs(User::factory()->create())
->post(route('suppliers.bulk.delete'), [
'ids' => [1, 2, 3]
])
->assertForbidden();
}
public function test_suppliers_cannot_be_bulk_deleted_if_models_still_associated()
{
$supplier = Supplier::factory()->create();
Asset::factory()->create(['supplier_id' => $supplier->id]);
$this->actingAs(User::factory()->deleteSuppliers()->create())
->post(route('suppliers.bulk.delete'), [
'ids' => [$supplier->id]
]);
$this->assertModelExists($supplier);
$this->assertNotSoftDeleted($supplier);
}
public function test_supplier_can_be_bulk_deleted()
{
$supplier1 = Supplier::factory()->create();
$supplier2 = Supplier::factory()->create();
$supplier3 = Supplier::factory()->create();
$this->actingAs(User::factory()->deleteSuppliers()->create())
->post(route('suppliers.bulk.delete'), [
'ids' => [$supplier1->id, $supplier2->id, $supplier3->id]
])
->assertRedirect(route('suppliers.index'));
$this->assertSoftDeleted($supplier1);
$this->assertSoftDeleted($supplier2);
$this->assertSoftDeleted($supplier3);
}
}