added options to checkout accessoires to locations and assets

Added #14979: add checkout functionality to accessoires
This commit is contained in:
arne-kroeger 2024-07-29 10:54:53 +02:00
parent c3a296c19f
commit b18baf74d2
21 changed files with 347 additions and 120 deletions

View file

@ -92,7 +92,7 @@ class SQLStreamer {
$parser->line_aware_piping(); // <----- THIS is doing the heavy lifting!
$check_tables = ['settings' => null, 'migrations' => null /* 'assets' => null */]; //TODO - move to statics?
//can't use 'users' because the 'accessories_users' table?
//can't use 'users' because the 'accessories_checkout' table?
// can't use 'assets' because 'ver1_components_assets'
foreach($check_tables as $check_table => $_ignore) {
foreach ($parser->tablenames as $tablename => $_count) {

View file

@ -721,7 +721,7 @@ class Helper
{
$alert_threshold = \App\Models\Setting::getSettings()->alert_threshold;
$consumables = Consumable::withCount('consumableAssignments as consumable_assignments_count')->whereNotNull('min_amt')->get();
$accessories = Accessory::withCount('users as users_count')->whereNotNull('min_amt')->get();
$accessories = Accessory::withCount('checkouts as checkouts_count')->whereNotNull('min_amt')->get();
$components = Component::whereNotNull('min_amt')->get();
$asset_models = AssetModel::where('min_amt', '>', 0)->get();
$licenses = License::where('min_amt', '>', 0)->get();
@ -749,7 +749,7 @@ class Helper
}
foreach ($accessories as $accessory) {
$avail = $accessory->qty - $accessory->users_count;
$avail = $accessory->qty - $accessory->checkouts_count;
if ($avail < ($accessory->min_amt) + $alert_threshold) {
if ($accessory->qty > 0) {
$percent = number_format((($avail / $accessory->qty) * 100), 0);

View file

@ -144,12 +144,12 @@ class AccessoriesController extends Controller
*/
public function update(ImageUploadRequest $request, $accessoryId = null) : RedirectResponse
{
if ($accessory = Accessory::withCount('users as users_count')->find($accessoryId)) {
if ($accessory = Accessory::withCount('checkouts as checkouts_count')->find($accessoryId)) {
$this->authorize($accessory);
$validator = Validator::make($request->all(), [
"qty" => "required|numeric|min:$accessory->users_count"
"qty" => "required|numeric|min:$accessory->checkouts_count"
]);
if ($validator->fails()) {
@ -233,7 +233,7 @@ class AccessoriesController extends Controller
*/
public function show($accessoryID = null) : View | RedirectResponse
{
$accessory = Accessory::withCount('users as users_count')->find($accessoryID);
$accessory = Accessory::withCount('checkouts as checkouts_count')->find($accessoryID);
$this->authorize('view', $accessory);
if (isset($accessory->id)) {
return view('accessories/view', compact('accessory'));

View file

@ -6,6 +6,7 @@ use App\Events\CheckoutableCheckedIn;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Models\Accessory;
use App\Models\AccessoryCheckout;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
@ -24,7 +25,7 @@ class AccessoryCheckinController extends Controller
*/
public function create($accessoryUserId = null, $backto = null) : View | RedirectResponse
{
if (is_null($accessory_user = DB::table('accessories_users')->find($accessoryUserId))) {
if (is_null($accessory_user = DB::table('accessories_checkout')->find($accessoryUserId))) {
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.not_found'));
}
@ -39,16 +40,16 @@ class AccessoryCheckinController extends Controller
*
* @uses Accessory::checkin_email() to determine if an email can and should be sent
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param null $accessoryUserId
* @param null $accessoryCheckoutId
* @param string $backto
*/
public function store(Request $request, $accessoryUserId = null, $backto = null) : RedirectResponse
public function store(Request $request, $accessoryCheckoutId = null, $backto = null) : RedirectResponse
{
if (is_null($accessory_user = DB::table('accessories_users')->find($accessoryUserId))) {
if (is_null($accessory_checkout = AccessoryCheckout::find($accessoryCheckoutId))) {
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.does_not_exist'));
}
$accessory = Accessory::find($accessory_user->accessory_id);
$accessory = Accessory::find($accessory_checkout->accessory_id);
$this->authorize('checkin', $accessory);
@ -59,10 +60,8 @@ class AccessoryCheckinController extends Controller
}
// Was the accessory updated?
if (DB::table('accessories_users')->where('id', '=', $accessory_user->id)->delete()) {
$return_to = e($accessory_user->assigned_to);
event(new CheckoutableCheckedIn($accessory, User::find($return_to), auth()->user(), $request->input('note'), $checkin_at));
if ($accessory_checkout->delete()) {
event(new CheckoutableCheckedIn($accessory, $accessory_checkout->assignedTo, auth()->user(), $request->input('note'), $checkin_at));
session()->put(['redirect_option' => $request->get('redirect_option')]);

View file

@ -4,9 +4,11 @@ namespace App\Http\Controllers\Accessories;
use App\Events\CheckoutableCheckedOut;
use App\Helpers\Helper;
use App\Http\Controllers\CheckInOutRequest;
use App\Http\Controllers\Controller;
use App\Http\Requests\AccessoryCheckoutRequest;
use App\Models\Accessory;
use App\Models\AccessoryCheckout;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Http\Request;
@ -16,6 +18,9 @@ use \Illuminate\Http\RedirectResponse;
class AccessoryCheckoutController extends Controller
{
use CheckInOutRequest;
/**
* Return the form to checkout an Accessory to a user.
*
@ -25,7 +30,7 @@ class AccessoryCheckoutController extends Controller
public function create($id) : View | RedirectResponse
{
if ($accessory = Accessory::withCount('users as users_count')->find($id)) {
if ($accessory = Accessory::withCount('checkouts as checkouts_count')->find($id)) {
$this->authorize('checkout', $accessory);
@ -58,30 +63,32 @@ class AccessoryCheckoutController extends Controller
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param Request $request
* @param int $accessory
* @param Accessory $accessory
*/
public function store(AccessoryCheckoutRequest $request, Accessory $accessory) : RedirectResponse
{
$this->authorize('checkout', $accessory);
$accessory->assigned_to = $request->input('assigned_to');
$user = User::find($request->input('assigned_to'));
$accessory->checkout_qty = $request->input('checkout_qty', 1);
$target = $this->determineCheckoutTarget();
$accessory->checkout_qty = $request->input('checkout_qty', 1);
for ($i = 0; $i < $accessory->checkout_qty; $i++) {
$accessory->users()->attach($accessory->id, [
AccessoryCheckout::create([
'accessory_id' => $accessory->id,
'created_at' => Carbon::now(),
'user_id' => Auth::id(),
'assigned_to' => $request->input('assigned_to'),
'assigned_to' => $target->id,
'assigned_type' => $target::class,
'note' => $request->input('note'),
]);
}
event(new CheckoutableCheckedOut($accessory, $user, auth()->user(), $request->input('note')));
event(new CheckoutableCheckedOut($accessory, $target, auth()->user(), $request->input('note')));
// Set this as user since we only allow checkout to user for this item type
$request->request->add(['checkout_to_type' => 'user']);
$request->request->add(['assigned_user' => $user->id]);
$request->request->add(['checkout_to_type' => request('checkout_to_type')]);
$request->request->add(['assigned_user' => $target->id]);
session()->put(['redirect_option' => $request->get('redirect_option'), 'checkout_to_type' => $request->get('checkout_to_type')]);

View file

@ -4,6 +4,7 @@ namespace App\Http\Controllers\Api;
use App\Events\CheckoutableCheckedOut;
use App\Helpers\Helper;
use App\Http\Controllers\CheckInOutRequest;
use App\Http\Controllers\Controller;
use App\Http\Requests\AccessoryCheckoutRequest;
use App\Http\Requests\StoreAccessoryRequest;
@ -17,10 +18,12 @@ use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Http\Request;
use App\Http\Requests\ImageUploadRequest;
use App\Models\AccessoryCheckout;
class AccessoriesController extends Controller
{
use CheckInOutRequest;
/**
* Display a listing of the resource.
*
@ -48,13 +51,13 @@ class AccessoriesController extends Controller
'min_amt',
'company_id',
'notes',
'users_count',
'checkouts_count',
'qty',
];
$accessories = Accessory::select('accessories.*')->with('category', 'company', 'manufacturer', 'users', 'location', 'supplier')
->withCount('users as users_count');
$accessories = Accessory::select('accessories.*')->with('category', 'company', 'manufacturer', 'checkouts', 'location', 'supplier')
->withCount('checkouts as checkouts_count');
if ($request->filled('search')) {
$accessories = $accessories->TextSearch($request->input('search'));
@ -154,7 +157,7 @@ class AccessoriesController extends Controller
public function show($id)
{
$this->authorize('view', Accessory::class);
$accessory = Accessory::withCount('users as users_count')->findOrFail($id);
$accessory = Accessory::withCount('checkouts as checkouts_count')->findOrFail($id);
return (new AccessoriesTransformer)->transformAccessory($accessory);
}
@ -197,28 +200,23 @@ class AccessoriesController extends Controller
$offset = request('offset', 0);
$limit = request('limit', 50);
$accessory_users = $accessory->users;
$total = $accessory_users->count();
$accessory_checkouts = $accessory->checkouts;
$total = $accessory_checkouts->count();
if ($total < $offset) {
$offset = 0;
}
$accessory_users = $accessory->users()->skip($offset)->take($limit)->get();
$accessory_checkouts = $accessory->checkouts()->skip($offset)->take($limit)->get();
if ($request->filled('search')) {
$accessory_users = $accessory->users()
->where(function ($query) use ($request) {
$search_str = '%' . $request->input('search') . '%';
$query->where('first_name', 'like', $search_str)
->orWhere('last_name', 'like', $search_str)
->orWhere('note', 'like', $search_str);
})
$accessory_checkouts = $accessory->checkouts()->TextSearch($request->input('search'))
->get();
$total = $accessory_users->count();
$total = $accessory_checkouts->count();
}
return (new AccessoriesTransformer)->transformCheckedoutAccessory($accessory, $accessory_users, $total);
return (new AccessoriesTransformer)->transformCheckedoutAccessory($accessory, $accessory_checkouts, $total);
}
@ -282,22 +280,22 @@ class AccessoriesController extends Controller
public function checkout(AccessoryCheckoutRequest $request, Accessory $accessory)
{
$this->authorize('checkout', $accessory);
$accessory->assigned_to = $request->input('assigned_to');
$user = User::find($request->input('assigned_to'));
$target = $this->determineCheckoutTarget();
$accessory->checkout_qty = $request->input('checkout_qty', 1);
for ($i = 0; $i < $accessory->checkout_qty; $i++) {
$accessory->users()->attach($accessory->id, [
AccessoryCheckout::create([
'accessory_id' => $accessory->id,
'created_at' => Carbon::now(),
'user_id' => Auth::id(),
'assigned_to' => $request->input('assigned_to'),
'assigned_to' => $target->id,
'assigned_type' => $target::class,
'note' => $request->input('note'),
]);
}
// Set this value to be able to pass the qty through to the event
event(new CheckoutableCheckedOut($accessory, $user, auth()->user(), $request->input('note')));
event(new CheckoutableCheckedOut($accessory, $target, auth()->user(), $request->input('note')));
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/accessories/message.checkout.success')));
@ -316,19 +314,19 @@ class AccessoriesController extends Controller
*/
public function checkin(Request $request, $accessoryUserId = null)
{
if (is_null($accessory_user = DB::table('accessories_users')->find($accessoryUserId))) {
if (is_null($accessory_checkout = DB::table('accessories_checkout')->find($accessoryUserId))) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/accessories/message.does_not_exist')));
}
$accessory = Accessory::find($accessory_user->accessory_id);
$accessory = Accessory::find($accessory_checkout->accessory_id);
$this->authorize('checkin', $accessory);
$logaction = $accessory->logCheckin(User::find($accessory_user->assigned_to), $request->input('note'));
$logaction = $accessory->logCheckin(User::find($accessory_checkout->assigned_to), $request->input('note'));
// Was the accessory updated?
if (DB::table('accessories_users')->where('id', '=', $accessory_user->id)->delete()) {
if (! is_null($accessory_user->assigned_to)) {
$user = User::find($accessory_user->assigned_to);
if (DB::table('accessories_checkout')->where('id', '=', $accessory_checkout->id)->delete()) {
if (! is_null($accessory_checkout->assigned_to)) {
$user = User::find($accessory_checkout->assigned_to);
}
$data['log_id'] = $logaction->id;

View file

@ -219,7 +219,7 @@ class BulkUsersController extends Controller
$users = User::whereIn('id', $user_raw_array)->get();
$assets = Asset::whereIn('assigned_to', $user_raw_array)->where('assigned_type', \App\Models\User::class)->get();
$accessories = DB::table('accessories_users')->whereIn('assigned_to', $user_raw_array)->get();
$accessories = DB::table('accessories_checkout')->whereIn('assigned_to', $user_raw_array)->get();
$licenses = DB::table('license_seats')->whereIn('assigned_to', $user_raw_array)->get();
$consumables = DB::table('consumables_users')->whereIn('assigned_to', $user_raw_array)->get();

View file

@ -44,13 +44,10 @@ class AccessoryCheckoutRequest extends ImageUploadRequest
return array_merge(
[
'assigned_to' => [
'required',
'integer',
'exists:users,id,deleted_at,NULL',
'not_array'
],
'assigned_user' => 'required_without_all:assigned_asset,assigned_location',
'assigned_asset' => 'required_without_all:assigned_user,assigned_location',
'assigned_location' => 'required_without_all:assigned_user,assigned_asset',
'number_remaining_after_checkout' => [
'min:0',
'required',

View file

@ -39,7 +39,7 @@ class AccessoriesTransformer
'order_number' => ($accessory->order_number) ? e($accessory->order_number) : null,
'min_qty' => ($accessory->min_amt) ? (int) $accessory->min_amt : null,
'remaining_qty' => (int) $accessory->numRemaining(),
'users_count' => $accessory->users_count,
'checkouts_count' => $accessory->checkouts_count,
'created_at' => Helper::getFormattedDateObject($accessory->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($accessory->updated_at, 'datetime'),
@ -66,27 +66,42 @@ class AccessoriesTransformer
return $array;
}
public function transformCheckedoutAccessory($accessory, $accessory_users, $total)
public function transformCheckedoutAccessory($accessory, $accessory_checkouts, $total)
{
$array = [];
foreach ($accessory_users as $user) {
foreach ($accessory_checkouts as $checkout) {
$array[] = [
'assigned_pivot_id' => $user->pivot->id,
'id' => (int) $user->id,
'username' => e($user->username),
'name' => e($user->getFullNameAttribute()),
'first_name'=> e($user->first_name),
'last_name'=> e($user->last_name),
'employee_number' => e($user->employee_num),
'checkout_notes' => e($user->pivot->note),
'last_checkout' => Helper::getFormattedDateObject($user->pivot->created_at, 'datetime'),
'type' => 'user',
'id' => $checkout->id,
'assigned_to' => $this->transformAssignedTo($checkout),
'checkout_notes' => e($checkout->note),
'last_checkout' => Helper::getFormattedDateObject($checkout->created_at, 'datetime'),
'available_actions' => ['checkin' => true],
];
}
return (new DatatablesTransformer)->transformDatatables($array, $total);
}
public function transformAssignedTo($accessoryCheckout)
{
if ($accessoryCheckout->checkedOutToUser()) {
return [
'id' => (int) $accessoryCheckout->assigned->id,
'username' => e($accessoryCheckout->assigned->username),
'name' => e($accessoryCheckout->assigned->getFullNameAttribute()),
'first_name'=> e($accessoryCheckout->assigned->first_name),
'last_name'=> ($accessoryCheckout->assigned->last_name) ? e($accessoryCheckout->assigned->last_name) : null,
'email'=> ($accessoryCheckout->assigned->email) ? e($accessoryCheckout->assigned->email) : null,
'employee_number' => ($accessoryCheckout->assigned->employee_num) ? e($accessoryCheckout->assigned->employee_num) : null,
'type' => 'user',
];
}
return $accessoryCheckout->assigned ? [
'id' => $accessoryCheckout->assigned->id,
'name' => e($accessoryCheckout->assigned->display_name),
'type' => $accessoryCheckout->assignedType(),
] : null;
}
}

View file

@ -205,11 +205,11 @@ class ActionlogsTransformer
public function transformCheckedoutActionlog (Collection $accessories_users, $total)
public function transformCheckedoutActionlog (Collection $accessories_checkout, $total)
{
$array = array();
foreach ($accessories_users as $user) {
foreach ($accessories_checkout as $user) {
$array[] = (new UsersTransformer)->transformUser($user);
}
return (new DatatablesTransformer)->transformDatatables($array, $total);

View file

@ -253,9 +253,10 @@ class Accessory extends SnipeModel
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function users()
public function checkouts()
{
return $this->belongsToMany(\App\Models\User::class, 'accessories_users', 'accessory_id', 'assigned_to')->withPivot('id', 'created_at', 'note')->withTrashed();
return $this->hasMany(\App\Models\AccessoryCheckout::class, 'accessory_id')
->with('assignedTo');
}
/**
@ -267,7 +268,9 @@ class Accessory extends SnipeModel
*/
public function hasUsers()
{
return $this->belongsToMany(\App\Models\User::class, 'accessories_users', 'accessory_id', 'assigned_to')->count();
return $this->hasMany(\App\Models\AccessoryCheckout::class, 'accessory_id')
->where('assigned_type', User::class)
->count();
}
/**
@ -338,15 +341,15 @@ class Accessory extends SnipeModel
*/
public function numCheckedOut()
{
return $this->users_count ?? $this->users()->count();
return $this->checkouts_count ?? $this->checkouts()->count();
}
/**
* Check how many items of an accessory remain.
*
* In order to use this model method, you MUST call withCount('users as users_count')
* on the eloquent query in the controller, otherwise $this->users_count will be null and
* In order to use this model method, you MUST call withCount('checkouts as checkouts_count')
* on the eloquent query in the controller, otherwise $this->checkouts_count will be null and
* bad things happen.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
@ -370,12 +373,12 @@ class Accessory extends SnipeModel
*/
public function declinedCheckout(User $declinedBy, $signature)
{
if (is_null($accessory_user = \DB::table('accessories_users')->where('assigned_to', $declinedBy->id)->where('accessory_id', $this->id)->latest('created_at'))) {
if (is_null($accessory_checkout = AccessoryCheckout::userAssigned()->where('assigned_to', $declinedBy->id)->where('accessory_id', $this->id)->latest('created_at'))) {
// Redirect to the accessory management page with error
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.does_not_exist'));
}
$accessory_user->limit(1)->delete();
$accessory_checkout->limit(1)->delete();
}
/**

148
app/Models/AccessoryCheckout.php Executable file
View file

@ -0,0 +1,148 @@
<?php
namespace App\Models;
use App\Helpers\Helper;
use App\Models\Traits\Acceptable;
use App\Models\Traits\Searchable;
use App\Presenters\Presentable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Storage;
use Watson\Validating\ValidatingTrait;
/**
* Model for Accessories.
*
* @version v1.0
*/
class AccessoryCheckout extends Model
{
use Searchable;
protected $fillable = ['user_id', 'accessory_id', 'assigned_to', 'assigned_type', 'note'];
protected $table = 'accessories_checkout';
/**
* Establishes the accessory checkout -> accessory relationship
*
* @author [A. Kroeger]
* @since [v7.0.9]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function accessory()
{
return $this->hasOne(\App\Models\Accessory::class, 'accessory_id');
}
/**
* Establishes the accessory checkout -> user relationship
*
* @author [A. Kroeger]
* @since [v7.0.9]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function user()
{
return $this->hasOne(\App\Models\User::class, 'user_id');
}
/**
* Get the target this asset is checked out to
*
* @author [A. Kroeger]
* @since [v7.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function assignedTo()
{
return $this->morphTo('assigned', 'assigned_type', 'assigned_to')->withTrashed();
}
/**
* Gets the lowercased name of the type of target the asset is assigned to
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.0]
* @return string
*/
public function assignedType()
{
return $this->assigned_type ? strtolower(class_basename($this->assigned_type)) : null;
}
/**
* Determines whether the accessory is checked out to a user
*
* Even though we allow allow for checkout to things beyond users
* this method is an easy way of seeing if we are checked out to a user.
*
* @author [A. Kroeger]
* @since [v7.0]
*/
public function checkedOutToUser(): bool
{
return $this->assignedType() === Asset::USER;
}
public function scopeUserAssigned(Builder $query): void
{
$query->where('assigned_type', '=', User::class);
}
/**
* Run additional, advanced searches.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param array $terms The search terms
* @return \Illuminate\Database\Eloquent\Builder
*/
public function advancedTextSearch(Builder $query, array $terms)
{
$userQuery = User::where(function ($query) use ($terms) {
foreach ($terms as $term) {
$search_str = '%' . $term . '%';
$query->where('first_name', 'like', $search_str)
->orWhere('last_name', 'like', $search_str)
->orWhere('note', 'like', $search_str);
}
})->select('id');
$locationQuery = Location::where(function ($query) use ($terms) {
foreach ($terms as $term) {
$search_str = '%' . $term . '%';
$query->where('name', 'like', $search_str);
}
})->select('id');
$assetQuery = Asset::where(function ($query) use ($terms) {
foreach ($terms as $term) {
$search_str = '%' . $term . '%';
$query->where('name', 'like', $search_str);
}
})->select('id');
$query->where(function ($query) use ($userQuery) {
$query->where('assigned_type', User::class)
->whereIn('assigned_to', $userQuery);
})->orWhere(function($query) use ($locationQuery) {
$query->where('assigned_type', Location::class)
->whereIn('assigned_to', $locationQuery);
})->orWhere(function($query) use ($assetQuery) {
$query->where('assigned_type', Asset::class)
->whereIn('assigned_to', $assetQuery);
})->orWhere(function($query) use ($terms) {
foreach ($terms as $term) {
$search_str = '%' . $term . '%';
$query->where('note', 'like', $search_str);
}
});
return $query;
}
}

View file

@ -331,7 +331,7 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
*/
public function accessories()
{
return $this->belongsToMany(\App\Models\Accessory::class, 'accessories_users', 'assigned_to', 'accessory_id')
return $this->belongsToMany(\App\Models\Accessory::class, 'accessories_checkout', 'assigned_to', 'accessory_id')
->withPivot('id', 'created_at', 'note')->withTrashed()->orderBy('accessory_id');
}

View file

@ -90,7 +90,7 @@ class AccessoryPresenter extends Presenter
'visible' => false,
'title' => trans('admin/accessories/general.remaining'),
],[
'field' => 'users_count',
'field' => 'checkouts_count',
'searchable' => false,
'sortable' => true,
'visible' => true,

View file

@ -3,6 +3,7 @@
namespace Database\Factories;
use App\Models\Accessory;
use App\Models\AccessoryCheckout;
use App\Models\Category;
use App\Models\Location;
use App\Models\Manufacturer;
@ -125,11 +126,12 @@ class AccessoryFactory extends Factory
})->afterCreating(function ($accessory) {
$user = User::factory()->create();
$accessory->users()->attach($accessory->id, [
$accessory->checkouts()->create([
'accessory_id' => $accessory->id,
'created_at' => now(),
'created_at' => Carbon::now(),
'user_id' => $user->id,
'assigned_to' => $user->id,
'assigned_type' => User::class,
'note' => '',
]);
});
@ -145,11 +147,12 @@ class AccessoryFactory extends Factory
public function checkedOutToUser(User $user = null)
{
return $this->afterCreating(function (Accessory $accessory) use ($user) {
$accessory->users()->attach($accessory->id, [
$accessory->checkouts()->create([
'accessory_id' => $accessory->id,
'created_at' => Carbon::now(),
'user_id' => 1,
'assigned_to' => $user->id ?? User::factory()->create()->id,
'assigned_type' => User::class,
]);
});
}

View file

@ -0,0 +1,39 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use App\Models\Setting;
use App\Models\User;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
if (Schema::hasTable('accessories_users')) {
Schema::rename('accessories_users', 'accessories_checkout');
Schema::table('accessories_checkout', function (Blueprint $table) {
if (!Schema::hasColumn('accessories_checkout', 'assigned_type')) {
$table->string('assigned_type')->nullable();
}
});
}
DB::update('update '.DB::getTablePrefix().'accessories_checkout set assigned_type = \'App\\\\Models\\\\User\' where assigned_type is null', []);
}
/**
* Reverse the migrations.
*/
public function down(): void
{
if (Schema::hasTable('accessories_checkout')) {
Schema::rename('accessories_checkout', 'accessories_users');
}
}
};

View file

@ -66,7 +66,14 @@
</div>
<!-- User -->
@include ('partials.forms.edit.user-select', ['translated_name' => trans('general.select_user'), 'fieldname' => 'assigned_to', 'required'=> 'true'])
@include ('partials.forms.checkout-selector', ['user_select' => 'true','asset_select' => 'true', 'location_select' => 'true'])
@include ('partials.forms.edit.user-select', ['translated_name' => trans('general.select_user'), 'fieldname' => 'assigned_user', 'required'=> 'true'])
<!-- We have to pass unselect here so that we don't default to the asset that's being checked out. We want that asset to be pre-selected everywhere else. -->
@include ('partials.forms.edit.asset-select', ['translated_name' => trans('general.asset'), 'fieldname' => 'assigned_asset', 'unselect' => 'true', 'style' => 'display:none;', 'required'=>'true'])
@include ('partials.forms.edit.location-select', ['translated_name' => trans('general.location'), 'fieldname' => 'assigned_location', 'style' => 'display:none;', 'required'=>'true'])
<!-- Checkout QTY -->
<div class="form-group {{ $errors->has('checkout_qty') ? 'error' : '' }} ">

View file

@ -68,9 +68,9 @@
<div class="row">
<div class="col-md-12">
<table
data-cookie-id-table="usersTable"
data-cookie-id-table="checkoutsTable"
data-pagination="true"
data-id-table="usersTable"
data-id-table="checkoutsTable"
data-search="true"
data-side-pagination="server"
data-show-columns="true"
@ -78,16 +78,16 @@
data-show-export="true"
data-show-refresh="true"
data-sort-order="asc"
id="usersTable"
id="checkoutsTable"
class="table table-striped snipe-table"
data-url="{{ route('api.accessories.checkedout', $accessory->id) }}"
data-export-options='{
"fileName": "export-accessories-{{ str_slug($accessory->name) }}-users-{{ date('Y-m-d') }}",
"fileName": "export-accessories-{{ str_slug($accessory->name) }}-checkouts-{{ date('Y-m-d') }}",
"ignoreColumn": ["actions","image","change","checkbox","checkincheckout","icon"]
}'>
<thead>
<tr>
<th data-searchable="false" data-formatter="usersLinkFormatter" data-sortable="false" data-field="name">{{ trans('general.user') }}</th>
<th data-searchable="false" data-formatter="polymorphicItemFormatter" data-sortable="false" data-field="assigned_to">{{ trans('general.checked_out_to') }}</th>
<th data-searchable="false" data-sortable="false" data-field="checkout_notes">{{ trans('general.notes') }}</th>
<th data-searchable="false" data-formatter="dateDisplayFormatter" data-sortable="false" data-field="last_checkout">{{ trans('admin/hardware/table.checkout_date') }}</th>
<th data-searchable="false" data-sortable="false" data-field="actions" data-formatter="accessoriesInOutFormatter">{{ trans('table.actions') }}</th>
@ -314,7 +314,7 @@
<strong>{{ trans('general.checked_out') }}</strong>
</div>
<div class="col-md-9">
{{ $accessory->users_count }}
{{ $accessory->checkouts_count }}
</div>
</div>
</div>
@ -337,7 +337,7 @@
@endcan
@can('delete', $accessory)
@if ($accessory->users_count == 0)
@if ($accessory->checkouts_count == 0)
<div class="text-center" style="padding-top:5px;">
<button class="btn btn-block btn-danger delete-asset" style="padding-top:5px;" data-toggle="modal" data-title="{{ trans('general.delete') }}" data-content="{{ trans('general.delete_confirm_no_undo', ['item' => $accessory->name]) }}" data-target="#dataConfirmModal">
{{ trans('general.delete') }}

View file

@ -17,7 +17,7 @@ class AccessoryCheckinTest extends TestCase
$accessory = Accessory::factory()->checkedOutToUser()->create();
$this->actingAs(User::factory()->create())
->post(route('accessories.checkin.store', $accessory->users->first()->pivot->id))
->post(route('accessories.checkin.store', $accessory->checkouts->first()->id))
->assertForbidden();
}
@ -28,12 +28,12 @@ class AccessoryCheckinTest extends TestCase
$user = User::factory()->create();
$accessory = Accessory::factory()->checkedOutToUser($user)->create();
$this->assertTrue($accessory->users->contains($user));
$this->assertTrue($accessory->checkouts()->where('assigned_type', User::class)->where('assigned_to', $user->id)->count() > 0);
$this->actingAs(User::factory()->checkinAccessories()->create())
->post(route('accessories.checkin.store', $accessory->users->first()->pivot->id));
->post(route('accessories.checkin.store', $accessory->checkouts->first()->id));
$this->assertFalse($accessory->fresh()->users->contains($user));
$this->assertFalse($accessory->fresh()->checkouts()->where('assigned_type', User::class)->where('assigned_to', $user->id)->count() > 0);
Event::assertDispatched(CheckoutableCheckedIn::class, 1);
}

View file

@ -22,7 +22,7 @@ class AccessoryCheckoutTest extends TestCase
{
$this->actingAsForApi(User::factory()->checkoutAccessories()->create())
->postJson(route('api.accessories.checkout', Accessory::factory()->create()), [
// missing assigned_to
// missing assigned_user, assigned_location, assigned_asset
])
->assertStatusMessageIs('error');
}
@ -31,7 +31,8 @@ class AccessoryCheckoutTest extends TestCase
{
$this->actingAsForApi(User::factory()->checkoutAccessories()->create())
->postJson(route('api.accessories.checkout', Accessory::factory()->withoutItemsRemaining()->create()), [
'assigned_to' => User::factory()->create()->id,
'assigned_user' => User::factory()->create()->id,
'checkout_to_type' => 'user'
])
->assertOk()
->assertStatusMessageIs('error')
@ -65,7 +66,8 @@ class AccessoryCheckoutTest extends TestCase
$this->actingAsForApi($admin)
->postJson(route('api.accessories.checkout', $accessory), [
'assigned_to' => $user->id,
'assigned_user' => $user->id,
'checkout_to_type' => 'user'
])
->assertOk()
->assertStatusMessageIs('success')
@ -73,7 +75,7 @@ class AccessoryCheckoutTest extends TestCase
->assertJson(['messages' => trans('admin/accessories/message.checkout.success')])
->json();
$this->assertTrue($accessory->users->contains($user));
$this->assertTrue($accessory->checkouts()->where('assigned_type', User::class)->where('assigned_to', $user->id)->count() > 0);
$this->assertEquals(
1,
@ -96,7 +98,8 @@ class AccessoryCheckoutTest extends TestCase
$this->actingAsForApi($admin)
->postJson(route('api.accessories.checkout', $accessory), [
'assigned_to' => $user->id,
'assigned_user' => $user->id,
'checkout_to_type' => 'user',
'checkout_qty' => 2,
])
->assertOk()
@ -105,7 +108,7 @@ class AccessoryCheckoutTest extends TestCase
->assertJson(['messages' => trans('admin/accessories/message.checkout.success')])
->json();
$this->assertTrue($accessory->users->contains($user));
$this->assertTrue($accessory->checkouts()->where('assigned_type', User::class)->where('assigned_to', $user->id)->count() > 0);
$this->assertEquals(
1,
@ -128,7 +131,8 @@ class AccessoryCheckoutTest extends TestCase
$this->actingAsForApi(User::factory()->checkoutAccessories()->create())
->postJson(route('api.accessories.checkout', $accessory), [
'assigned_to' => 'invalid-user-id',
'assigned_user' => 'invalid-user-id',
'checkout_to_type' => 'user',
'note' => 'oh hi there',
])
->assertOk()
@ -136,7 +140,7 @@ class AccessoryCheckoutTest extends TestCase
->assertStatus(200)
->json();
$this->assertFalse($accessory->users->contains($user));
$this->assertFalse($accessory->checkouts()->where('assigned_type', User::class)->where('assigned_to', $user->id)->count() > 0);
}
public function testUserSentNotificationUponCheckout()
@ -148,7 +152,8 @@ class AccessoryCheckoutTest extends TestCase
$this->actingAsForApi(User::factory()->checkoutAccessories()->create())
->postJson(route('api.accessories.checkout', $accessory), [
'assigned_to' => $user->id,
'assigned_user' => $user->id,
'checkout_to_type' => 'user',
]);
Notification::assertSentTo($user, CheckoutAccessoryNotification::class);
@ -162,7 +167,8 @@ class AccessoryCheckoutTest extends TestCase
$this->actingAsForApi($actor)
->postJson(route('api.accessories.checkout', $accessory), [
'assigned_to' => $user->id,
'assigned_user' => $user->id,
'checkout_to_type' => 'user',
'note' => 'oh hi there',
]);

View file

@ -40,7 +40,8 @@ class AccessoryCheckoutTest extends TestCase
$response = $this->actingAs(User::factory()->viewAccessories()->checkoutAccessories()->create())
->from(route('accessories.checkout.show', $accessory))
->post(route('accessories.checkout.store', $accessory), [
'assigned_to' => User::factory()->create()->id,
'assigned_user' => User::factory()->create()->id,
'checkout_to_type' => 'user',
])
->assertStatus(302)
->assertSessionHas('errors')
@ -56,11 +57,12 @@ class AccessoryCheckoutTest extends TestCase
$this->actingAs(User::factory()->checkoutAccessories()->create())
->post(route('accessories.checkout.store', $accessory), [
'assigned_to' => $user->id,
'assigned_user' => $user->id,
'checkout_to_type' => 'user',
'note' => 'oh hi there',
]);
$this->assertTrue($accessory->users->contains($user));
$this->assertTrue($accessory->checkouts()->where('assigned_type', User::class)->where('assigned_to', $user->id)->count() > 0);
$this->assertDatabaseHas('action_logs', [
'action_type' => 'checkout',
@ -80,12 +82,13 @@ class AccessoryCheckoutTest extends TestCase
$this->actingAs(User::factory()->checkoutAccessories()->create())
->from(route('accessories.checkout.show', $accessory))
->post(route('accessories.checkout.store', $accessory), [
'assigned_to' => $user->id,
'assigned_user' => $user->id,
'checkout_to_type' => 'user',
'checkout_qty' => 3,
'note' => 'oh hi there',
]);
$this->assertTrue($accessory->users->contains($user));
$this->assertTrue($accessory->checkouts()->where('assigned_type', User::class)->where('assigned_to', $user->id)->count() > 0);
$this->assertDatabaseHas('action_logs', [
'action_type' => 'checkout',
@ -107,7 +110,8 @@ class AccessoryCheckoutTest extends TestCase
$this->actingAs(User::factory()->checkoutAccessories()->create())
->from(route('accessories.checkout.show', $accessory))
->post(route('accessories.checkout.store', $accessory), [
'assigned_to' => $user->id,
'assigned_user' => $user->id,
'checkout_to_type' => 'user',
]);
Notification::assertSentTo($user, CheckoutAccessoryNotification::class);
@ -122,7 +126,8 @@ class AccessoryCheckoutTest extends TestCase
$this->actingAs($actor)
->from(route('accessories.checkout.show', $accessory))
->post(route('accessories.checkout.store', $accessory), [
'assigned_to' => $user->id,
'assigned_user' => $user->id,
'checkout_to_type' => 'user',
'note' => 'oh hi there',
]);