mirror of
https://github.com/snipe/snipe-it.git
synced 2024-12-24 13:14:07 -08:00
Merge pull request #15114 from snipe/checkout_multiple_accessories
Checkout multiple of an accessory in one checkout
This commit is contained in:
commit
4465aef991
|
@ -4,12 +4,12 @@ namespace App\Http\Controllers\Accessories;
|
|||
|
||||
use App\Events\CheckoutableCheckedOut;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\AccessoryCheckoutRequest;
|
||||
use App\Models\Accessory;
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use \Illuminate\Contracts\View\View;
|
||||
use \Illuminate\Http\RedirectResponse;
|
||||
|
||||
|
@ -57,44 +57,29 @@ class AccessoryCheckoutController extends Controller
|
|||
*
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @param Request $request
|
||||
* @param int $accessoryId
|
||||
* @param int $accessory
|
||||
*/
|
||||
public function store(Request $request, $accessoryId) : RedirectResponse
|
||||
public function store(AccessoryCheckoutRequest $request, Accessory $accessory) : RedirectResponse
|
||||
{
|
||||
// Check if the accessory exists
|
||||
if (is_null($accessory = Accessory::withCount('users as users_count')->find($accessoryId))) {
|
||||
// Redirect to the accessory management page with error
|
||||
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.user_not_found'));
|
||||
}
|
||||
|
||||
$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);
|
||||
|
||||
if (!$user = User::find($request->input('assigned_to'))) {
|
||||
return redirect()->route('accessories.checkout.show', $accessory->id)->with('error', trans('admin/accessories/message.checkout.user_does_not_exist'));
|
||||
for ($i = 0; $i < $accessory->checkout_qty; $i++) {
|
||||
$accessory->users()->attach($accessory->id, [
|
||||
'accessory_id' => $accessory->id,
|
||||
'created_at' => Carbon::now(),
|
||||
'user_id' => Auth::id(),
|
||||
'assigned_to' => $request->input('assigned_to'),
|
||||
'note' => $request->input('note'),
|
||||
]);
|
||||
}
|
||||
|
||||
// Make sure there is at least one available to checkout
|
||||
if ($accessory->numRemaining() <= 0){
|
||||
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.checkout.unavailable'));
|
||||
}
|
||||
|
||||
|
||||
// Update the accessory data
|
||||
$accessory->assigned_to = e($request->input('assigned_to'));
|
||||
|
||||
$accessory->users()->attach($accessory->id, [
|
||||
'accessory_id' => $accessory->id,
|
||||
'created_at' => Carbon::now(),
|
||||
'user_id' => Auth::id(),
|
||||
'assigned_to' => $request->get('assigned_to'),
|
||||
'note' => $request->input('note'),
|
||||
]);
|
||||
|
||||
DB::table('accessories_users')->where('assigned_to', '=', $accessory->assigned_to)->where('accessory_id', '=', $accessory->id)->first();
|
||||
|
||||
event(new CheckoutableCheckedOut($accessory, $user, auth()->user(), $request->input('note')));
|
||||
|
||||
// Redirect to the new accessory page
|
||||
return redirect()->route('accessories.index')->with('success', trans('admin/accessories/message.checkout.success'));
|
||||
return redirect()->route('accessories.index')
|
||||
->with('success', trans('admin/accessories/message.checkout.success'));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,8 @@ namespace App\Http\Controllers\Api;
|
|||
use App\Events\CheckoutableCheckedOut;
|
||||
use App\Helpers\Helper;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\AccessoryCheckoutRequest;
|
||||
use App\Http\Requests\StoreAccessoryRequest;
|
||||
use App\Http\Transformers\AccessoriesTransformer;
|
||||
use App\Http\Transformers\SelectlistTransformer;
|
||||
use App\Models\Accessory;
|
||||
|
@ -121,12 +123,12 @@ class AccessoriesController extends Controller
|
|||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*
|
||||
* @param \App\Http\Requests\ImageUploadRequest $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @since [v4.0]
|
||||
* @param \App\Http\Requests\ImageUploadRequest $request
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function store(ImageUploadRequest $request)
|
||||
public function store(StoreAccessoryRequest $request)
|
||||
{
|
||||
$this->authorize('create', Accessory::class);
|
||||
$accessory = new Accessory;
|
||||
|
@ -144,10 +146,10 @@ class AccessoriesController extends Controller
|
|||
/**
|
||||
* Display the specified resource.
|
||||
*
|
||||
* @param int $id
|
||||
* @return array
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @since [v4.0]
|
||||
* @param int $id
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function show($id)
|
||||
{
|
||||
|
@ -161,10 +163,10 @@ class AccessoriesController extends Controller
|
|||
/**
|
||||
* Display the specified resource.
|
||||
*
|
||||
* @param int $id
|
||||
* @return array
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @since [v4.0]
|
||||
* @param int $id
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function accessory_detail($id)
|
||||
{
|
||||
|
@ -273,43 +275,31 @@ class AccessoriesController extends Controller
|
|||
* If Slack is enabled and/or asset acceptance is enabled, it will also
|
||||
* trigger a Slack message and send an email.
|
||||
*
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @param int $accessoryId
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
*/
|
||||
public function checkout(Request $request, $accessoryId)
|
||||
public function checkout(AccessoryCheckoutRequest $request, Accessory $accessory)
|
||||
{
|
||||
// Check if the accessory exists
|
||||
if (is_null($accessory = Accessory::withCount('users as users_count')->find($accessoryId))) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/accessories/message.does_not_exist')));
|
||||
}
|
||||
|
||||
$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);
|
||||
|
||||
|
||||
if ($accessory->numRemaining() > 0) {
|
||||
|
||||
if (! $user = User::find($request->input('assigned_to'))) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/accessories/message.checkout.user_does_not_exist')));
|
||||
}
|
||||
|
||||
// Update the accessory data
|
||||
$accessory->assigned_to = $request->input('assigned_to');
|
||||
|
||||
for ($i = 0; $i < $accessory->checkout_qty; $i++) {
|
||||
$accessory->users()->attach($accessory->id, [
|
||||
'accessory_id' => $accessory->id,
|
||||
'created_at' => Carbon::now(),
|
||||
'user_id' => Auth::id(),
|
||||
'assigned_to' => $request->get('assigned_to'),
|
||||
'note' => $request->get('note'),
|
||||
'assigned_to' => $request->input('assigned_to'),
|
||||
'note' => $request->input('note'),
|
||||
]);
|
||||
|
||||
event(new CheckoutableCheckedOut($accessory, $user, auth()->user(), $request->input('note')));
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/accessories/message.checkout.success')));
|
||||
}
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, 'No accessories remaining'));
|
||||
// Set this value to be able to pass the qty through to the event
|
||||
event(new CheckoutableCheckedOut($accessory, $user, auth()->user(), $request->input('note')));
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/accessories/message.checkout.success')));
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -43,10 +43,12 @@ class Kernel extends HttpKernel
|
|||
\Laravel\Passport\Http\Middleware\CreateFreshApiToken::class,
|
||||
\App\Http\Middleware\AssetCountForSidebar::class,
|
||||
\Illuminate\Session\Middleware\AuthenticateSession::class,
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
],
|
||||
|
||||
'api' => [
|
||||
'auth:api',
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
],
|
||||
];
|
||||
|
||||
|
|
79
app/Http/Requests/AccessoryCheckoutRequest.php
Normal file
79
app/Http/Requests/AccessoryCheckoutRequest.php
Normal file
|
@ -0,0 +1,79 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Models\Accessory;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
|
||||
class AccessoryCheckoutRequest extends ImageUploadRequest
|
||||
{
|
||||
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return Gate::allows('checkout', new Accessory);
|
||||
}
|
||||
|
||||
public function prepareForValidation(): void
|
||||
{
|
||||
|
||||
if ($this->accessory) {
|
||||
|
||||
$this->diff = ($this->accessory->numRemaining() - $this->checkout_qty);
|
||||
$this->merge([
|
||||
'checkout_qty' => $this->checkout_qty ?? 1,
|
||||
'number_remaining_after_checkout' => (int) ($this->accessory->numRemaining() - $this->checkout_qty),
|
||||
'number_currently_remaining' => (int) $this->accessory->numRemaining(),
|
||||
'checkout_difference' => (int) $this->diff,
|
||||
]);
|
||||
|
||||
\Log::debug('---------------------------------------------');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
|
||||
return array_merge(
|
||||
[
|
||||
'assigned_to' => [
|
||||
'required',
|
||||
'integer',
|
||||
'exists:users,id,deleted_at,NULL',
|
||||
'not_array'
|
||||
],
|
||||
|
||||
'number_remaining_after_checkout' => [
|
||||
'min:0',
|
||||
'required',
|
||||
'integer',
|
||||
],
|
||||
|
||||
'checkout_qty' => [
|
||||
'integer',
|
||||
'lte:number_currently_remaining',
|
||||
'min:1',
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
$messages = [
|
||||
'checkout_qty.lte' => trans_choice('admin/accessories/message.checkout.checkout_qty.lte', $this->number_currently_remaining, [
|
||||
'number_currently_remaining' => $this->number_currently_remaining,
|
||||
'checkout_qty' => $this->checkout_qty,
|
||||
]),
|
||||
];
|
||||
return $messages;
|
||||
}
|
||||
}
|
56
app/Http/Requests/StoreAccessoryRequest.php
Normal file
56
app/Http/Requests/StoreAccessoryRequest.php
Normal file
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Models\Accessory;
|
||||
use App\Models\Category;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
|
||||
class StoreAccessoryRequest extends ImageUploadRequest
|
||||
{
|
||||
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return Gate::allows('create', new Accessory);
|
||||
}
|
||||
|
||||
public function prepareForValidation(): void
|
||||
{
|
||||
|
||||
if ($this->category_id) {
|
||||
if ($category = Category::find($this->category_id)) {
|
||||
$this->merge([
|
||||
'category_type' => $category->category_type ?? null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return array_merge(
|
||||
['category_type' => 'in:accessory'],
|
||||
parent::rules(),
|
||||
);
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
$messages = ['category_type.in' => trans('admin/accessories/message.invalid_category_type')];
|
||||
return $messages;
|
||||
}
|
||||
|
||||
public function response(array $errors)
|
||||
{
|
||||
return $this->redirector->back()->withInput()->withErrors($errors, $this->errorBag);
|
||||
}
|
||||
}
|
|
@ -63,7 +63,7 @@ class Accessory extends SnipeModel
|
|||
'company_id' => 'integer|nullable',
|
||||
'min_amt' => 'integer|min:0|nullable',
|
||||
'purchase_cost' => 'numeric|nullable|gte:0',
|
||||
'purchase_date' => 'date_format:Y-m-d|nullable',
|
||||
'purchase_date' => 'date_format:Y-m-d|nullable',
|
||||
];
|
||||
|
||||
|
||||
|
@ -329,11 +329,24 @@ class Accessory extends SnipeModel
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check how many items within an accessory are checked out
|
||||
*
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @since [v5.0]
|
||||
* @return int
|
||||
*/
|
||||
public function numCheckedOut()
|
||||
{
|
||||
return $this->users_count ?? $this->users()->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
|
||||
* on the eloquent query in the controller, otherwise $this->users_count will be null and
|
||||
* bad things happen.
|
||||
*
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
|
@ -342,11 +355,11 @@ class Accessory extends SnipeModel
|
|||
*/
|
||||
public function numRemaining()
|
||||
{
|
||||
$checkedout = $this->users_count;
|
||||
$checkedout = $this->numCheckedOut();
|
||||
$total = $this->qty;
|
||||
$remaining = $total - $checkedout;
|
||||
|
||||
return (int) $remaining;
|
||||
return $remaining;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -30,6 +30,7 @@ class CheckoutAccessoryNotification extends Notification
|
|||
$this->item = $accessory;
|
||||
$this->admin = $checkedOutBy;
|
||||
$this->note = $note;
|
||||
$this->checkout_qty = $accessory->checkout_qty;
|
||||
$this->target = $checkedOutTo;
|
||||
$this->acceptance = $acceptance;
|
||||
$this->settings = Setting::getSettings();
|
||||
|
@ -107,7 +108,7 @@ class CheckoutAccessoryNotification extends Notification
|
|||
->from($botname)
|
||||
->to($channel)
|
||||
->attachment(function ($attachment) use ($item, $note, $admin, $fields) {
|
||||
$attachment->title(htmlspecialchars_decode($item->present()->name), $item->present()->viewUrl())
|
||||
$attachment->title(htmlspecialchars_decode($this->checkout_qty.' x '.$item->present()->name), $item->present()->viewUrl())
|
||||
->fields($fields)
|
||||
->content($note);
|
||||
});
|
||||
|
@ -127,6 +128,7 @@ class CheckoutAccessoryNotification extends Notification
|
|||
->addStartGroupToSection('activityText')
|
||||
->fact(htmlspecialchars_decode($item->present()->name), '', 'activityTitle')
|
||||
->fact(trans('mail.assigned_to'), $target->present()->name)
|
||||
->fact(trans('general.qty'), $this->checkout_qty)
|
||||
->fact(trans('mail.checkedout_from'), $item->location->name ? $item->location->name : '')
|
||||
->fact(trans('mail.Accessory_Checkout_Notification') . " by ", $admin->present()->fullName())
|
||||
->fact(trans('admin/consumables/general.remaining'), $item->numRemaining())
|
||||
|
@ -184,6 +186,7 @@ class CheckoutAccessoryNotification extends Notification
|
|||
'eula' => $eula,
|
||||
'req_accept' => $req_accept,
|
||||
'accept_url' => $accept_url,
|
||||
'checkout_qty' => $this->checkout_qty,
|
||||
])
|
||||
->subject(trans('mail.Confirm_accessory_delivery'));
|
||||
}
|
||||
|
|
|
@ -26,7 +26,11 @@ return array(
|
|||
'error' => 'Accessory was not checked out, please try again',
|
||||
'success' => 'Accessory checked out successfully.',
|
||||
'unavailable' => 'Accessory is not available for checkout. Check quantity available',
|
||||
'user_does_not_exist' => 'That user is invalid. Please try again.'
|
||||
'user_does_not_exist' => 'That user is invalid. Please try again.',
|
||||
'checkout_qty' => array(
|
||||
'lte' => 'There is currently only one available accessory of this type, and you are trying to check out :checkout_qty. Please adjust the checkout quantity or the total stock of this accessory and try again.|There are :number_currently_remaining total available accessories, and you are trying to check out :checkout_qty. Please adjust the checkout quantity or the total stock of this accessory and try again.',
|
||||
),
|
||||
|
||||
),
|
||||
|
||||
'checkin' => array(
|
||||
|
|
|
@ -13,87 +13,148 @@ return [
|
|||
|
|
||||
*/
|
||||
|
||||
'accepted' => 'The :attribute must be accepted.',
|
||||
'active_url' => 'The :attribute is not a valid URL.',
|
||||
'after' => 'The :attribute must be a date after :date.',
|
||||
'after_or_equal' => 'The :attribute must be a date after or equal to :date.',
|
||||
'alpha' => 'The :attribute may only contain letters.',
|
||||
'alpha_dash' => 'The :attribute may only contain letters, numbers, and dashes.',
|
||||
'alpha_num' => 'The :attribute may only contain letters and numbers.',
|
||||
'array' => 'The :attribute must be an array.',
|
||||
'before' => 'The :attribute must be a date before :date.',
|
||||
'before_or_equal' => 'The :attribute must be a date before or equal to :date.',
|
||||
'between' => [
|
||||
'numeric' => 'The :attribute must be between :min - :max.',
|
||||
'file' => 'The :attribute must be between :min - :max kilobytes.',
|
||||
'string' => 'The :attribute must be between :min - :max characters.',
|
||||
'array' => 'The :attribute must have between :min and :max items.',
|
||||
'accepted' => 'The :attribute field must be accepted.',
|
||||
'accepted_if' => 'The :attribute field must be accepted when :other is :value.',
|
||||
'active_url' => 'The :attribute field must be a valid URL.',
|
||||
'after' => 'The :attribute field must be a date after :date.',
|
||||
'after_or_equal' => 'The :attribute field must be a date after or equal to :date.',
|
||||
'alpha' => 'The :attribute field must only contain letters.',
|
||||
'alpha_dash' => 'The :attribute field must only contain letters, numbers, dashes, and underscores.',
|
||||
'alpha_num' => 'The :attribute field must only contain letters and numbers.',
|
||||
'array' => 'The :attribute field must be an array.',
|
||||
'ascii' => 'The :attribute field must only contain single-byte alphanumeric characters and symbols.',
|
||||
'before' => 'The :attribute field must be a date before :date.',
|
||||
'before_or_equal' => 'The :attribute field must be a date before or equal to :date.',
|
||||
'between' => [
|
||||
'array' => 'The :attribute field must have between :min and :max items.',
|
||||
'file' => 'The :attribute field must be between :min and :max kilobytes.',
|
||||
'numeric' => 'The :attribute field must be between :min and :max.',
|
||||
'string' => 'The :attribute field must be between :min and :max characters.',
|
||||
],
|
||||
'boolean' => 'The :attribute must be true or false.',
|
||||
'confirmed' => 'The :attribute confirmation does not match.',
|
||||
'date' => 'The :attribute is not a valid date.',
|
||||
'date_format' => 'The :attribute does not match the format :format.',
|
||||
'different' => 'The :attribute and :other must be different.',
|
||||
'digits' => 'The :attribute must be :digits digits.',
|
||||
'digits_between' => 'The :attribute must be between :min and :max digits.',
|
||||
'dimensions' => 'The :attribute has invalid image dimensions.',
|
||||
'distinct' => 'The :attribute field has a duplicate value.',
|
||||
'email' => 'The :attribute format is invalid.',
|
||||
'exists' => 'The selected :attribute is invalid.',
|
||||
'file' => 'The :attribute must be a file.',
|
||||
'filled' => 'The :attribute field must have a value.',
|
||||
'image' => 'The :attribute must be an image.',
|
||||
'boolean' => 'The :attribute field must be true or false.',
|
||||
'can' => 'The :attribute field contains an unauthorized value.',
|
||||
'confirmed' => 'The :attribute field confirmation does not match.',
|
||||
'contains' => 'The :attribute field is missing a required value.',
|
||||
'current_password' => 'The password is incorrect.',
|
||||
'date' => 'The :attribute field must be a valid date.',
|
||||
'date_equals' => 'The :attribute field must be a date equal to :date.',
|
||||
'date_format' => 'The :attribute field must match the format :format.',
|
||||
'decimal' => 'The :attribute field must have :decimal decimal places.',
|
||||
'declined' => 'The :attribute field must be declined.',
|
||||
'declined_if' => 'The :attribute field must be declined when :other is :value.',
|
||||
'different' => 'The :attribute field and :other must be different.',
|
||||
'digits' => 'The :attribute field must be :digits digits.',
|
||||
'digits_between' => 'The :attribute field must be between :min and :max digits.',
|
||||
'dimensions' => 'The :attribute field has invalid image dimensions.',
|
||||
'distinct' => 'The :attribute field has a duplicate value.',
|
||||
'doesnt_end_with' => 'The :attribute field must not end with one of the following: :values.',
|
||||
'doesnt_start_with' => 'The :attribute field must not start with one of the following: :values.',
|
||||
'email' => 'The :attribute field must be a valid email address.',
|
||||
'ends_with' => 'The :attribute field must end with one of the following: :values.',
|
||||
'enum' => 'The selected :attribute is invalid.',
|
||||
'exists' => 'The selected :attribute is invalid.',
|
||||
'extensions' => 'The :attribute field must have one of the following extensions: :values.',
|
||||
'file' => 'The :attribute field must be a file.',
|
||||
'filled' => 'The :attribute field must have a value.',
|
||||
'gt' => [
|
||||
'array' => 'The :attribute field must have more than :value items.',
|
||||
'file' => 'The :attribute field must be greater than :value kilobytes.',
|
||||
'numeric' => 'The :attribute field must be greater than :value.',
|
||||
'string' => 'The :attribute field must be greater than :value characters.',
|
||||
],
|
||||
'gte' => [
|
||||
'array' => 'The :attribute field must have :value items or more.',
|
||||
'file' => 'The :attribute field must be greater than or equal to :value kilobytes.',
|
||||
'numeric' => 'The :attribute field must be greater than or equal to :value.',
|
||||
'string' => 'The :attribute field must be greater than or equal to :value characters.',
|
||||
],
|
||||
'hex_color' => 'The :attribute field must be a valid hexadecimal color.',
|
||||
'image' => 'The :attribute field must be an image.',
|
||||
'import_field_empty' => 'The value for :fieldname cannot be null.',
|
||||
'in' => 'The selected :attribute is invalid.',
|
||||
'in_array' => 'The :attribute field does not exist in :other.',
|
||||
'integer' => 'The :attribute must be an integer.',
|
||||
'ip' => 'The :attribute must be a valid IP address.',
|
||||
'ipv4' => 'The :attribute must be a valid IPv4 address.',
|
||||
'ipv6' => 'The :attribute must be a valid IPv6 address.',
|
||||
'is_unique_department' => 'The :attribute must be unique to this Company Location',
|
||||
'json' => 'The :attribute must be a valid JSON string.',
|
||||
'max' => [
|
||||
'numeric' => 'The :attribute may not be greater than :max.',
|
||||
'file' => 'The :attribute may not be greater than :max kilobytes.',
|
||||
'string' => 'The :attribute may not be greater than :max characters.',
|
||||
'array' => 'The :attribute may not have more than :max items.',
|
||||
'in' => 'The selected :attribute is invalid.',
|
||||
'in_array' => 'The :attribute field must exist in :other.',
|
||||
'integer' => 'The :attribute field must be an integer.',
|
||||
'ip' => 'The :attribute field must be a valid IP address.',
|
||||
'ipv4' => 'The :attribute field must be a valid IPv4 address.',
|
||||
'ipv6' => 'The :attribute field must be a valid IPv6 address.',
|
||||
'json' => 'The :attribute field must be a valid JSON string.',
|
||||
'list' => 'The :attribute field must be a list.',
|
||||
'lowercase' => 'The :attribute field must be lowercase.',
|
||||
'lt' => [
|
||||
'array' => 'The :attribute field must have less than :value items.',
|
||||
'file' => 'The :attribute field must be less than :value kilobytes.',
|
||||
'numeric' => 'The :attribute field must be less than :value.',
|
||||
'string' => 'The :attribute field must be less than :value characters.',
|
||||
],
|
||||
'mimes' => 'The :attribute must be a file of type: :values.',
|
||||
'mimetypes' => 'The :attribute must be a file of type: :values.',
|
||||
'min' => [
|
||||
'numeric' => 'The :attribute must be at least :min.',
|
||||
'file' => 'The :attribute must be at least :min kilobytes.',
|
||||
'string' => 'The :attribute must be at least :min characters.',
|
||||
'array' => 'The :attribute must have at least :min items.',
|
||||
'lte' => [
|
||||
'array' => 'The :attribute field must not have more than :value items.',
|
||||
'file' => 'The :attribute field must be less than or equal to :value kilobytes.',
|
||||
'numeric' => 'The :attribute field must be less than or equal to :value.',
|
||||
'string' => 'The :attribute field must be less than or equal to :value characters.',
|
||||
],
|
||||
'starts_with' => 'The :attribute must start with one of the following: :values.',
|
||||
'ends_with' => 'The :attribute must end with one of the following: :values.',
|
||||
|
||||
'not_in' => 'The selected :attribute is invalid.',
|
||||
'numeric' => 'The :attribute must be a number.',
|
||||
'present' => 'The :attribute field must be present.',
|
||||
'valid_regex' => 'That is not a valid regex. ',
|
||||
'regex' => 'The :attribute format is invalid.',
|
||||
'required' => 'The :attribute field is required.',
|
||||
'required_if' => 'The :attribute field is required when :other is :value.',
|
||||
'required_unless' => 'The :attribute field is required unless :other is in :values.',
|
||||
'required_with' => 'The :attribute field is required when :values is present.',
|
||||
'required_with_all' => 'The :attribute field is required when :values is present.',
|
||||
'required_without' => 'The :attribute field is required when :values is not present.',
|
||||
'mac_address' => 'The :attribute field must be a valid MAC address.',
|
||||
'max' => [
|
||||
'array' => 'The :attribute field must not have more than :max items.',
|
||||
'file' => 'The :attribute field must not be greater than :max kilobytes.',
|
||||
'numeric' => 'The :attribute field must not be greater than :max.',
|
||||
'string' => 'The :attribute field must not be greater than :max characters.',
|
||||
],
|
||||
'max_digits' => 'The :attribute field must not have more than :max digits.',
|
||||
'mimes' => 'The :attribute field must be a file of type: :values.',
|
||||
'mimetypes' => 'The :attribute field must be a file of type: :values.',
|
||||
'min' => [
|
||||
'array' => 'The :attribute field must have at least :min items.',
|
||||
'file' => 'The :attribute field must be at least :min kilobytes.',
|
||||
'numeric' => 'The :attribute field must be at least :min.',
|
||||
'string' => 'The :attribute field must be at least :min characters.',
|
||||
],
|
||||
'min_digits' => 'The :attribute field must have at least :min digits.',
|
||||
'missing' => 'The :attribute field must be missing.',
|
||||
'missing_if' => 'The :attribute field must be missing when :other is :value.',
|
||||
'missing_unless' => 'The :attribute field must be missing unless :other is :value.',
|
||||
'missing_with' => 'The :attribute field must be missing when :values is present.',
|
||||
'missing_with_all' => 'The :attribute field must be missing when :values are present.',
|
||||
'multiple_of' => 'The :attribute field must be a multiple of :value.',
|
||||
'not_in' => 'The selected :attribute is invalid.',
|
||||
'not_regex' => 'The :attribute field format is invalid.',
|
||||
'numeric' => 'The :attribute field must be a number.',
|
||||
'password' => [
|
||||
'letters' => 'The :attribute field must contain at least one letter.',
|
||||
'mixed' => 'The :attribute field must contain at least one uppercase and one lowercase letter.',
|
||||
'numbers' => 'The :attribute field must contain at least one number.',
|
||||
'symbols' => 'The :attribute field must contain at least one symbol.',
|
||||
'uncompromised' => 'The given :attribute has appeared in a data leak. Please choose a different :attribute.',
|
||||
],
|
||||
'present' => 'The :attribute field must be present.',
|
||||
'present_if' => 'The :attribute field must be present when :other is :value.',
|
||||
'present_unless' => 'The :attribute field must be present unless :other is :value.',
|
||||
'present_with' => 'The :attribute field must be present when :values is present.',
|
||||
'present_with_all' => 'The :attribute field must be present when :values are present.',
|
||||
'prohibited' => 'The :attribute field is prohibited.',
|
||||
'prohibited_if' => 'The :attribute field is prohibited when :other is :value.',
|
||||
'prohibited_unless' => 'The :attribute field is prohibited unless :other is in :values.',
|
||||
'prohibits' => 'The :attribute field prohibits :other from being present.',
|
||||
'regex' => 'The :attribute field format is invalid.',
|
||||
'required' => 'The :attribute field is required.',
|
||||
'required_array_keys' => 'The :attribute field must contain entries for: :values.',
|
||||
'required_if' => 'The :attribute field is required when :other is :value.',
|
||||
'required_if_accepted' => 'The :attribute field is required when :other is accepted.',
|
||||
'required_if_declined' => 'The :attribute field is required when :other is declined.',
|
||||
'required_unless' => 'The :attribute field is required unless :other is in :values.',
|
||||
'required_with' => 'The :attribute field is required when :values is present.',
|
||||
'required_with_all' => 'The :attribute field is required when :values are present.',
|
||||
'required_without' => 'The :attribute field is required when :values is not present.',
|
||||
'required_without_all' => 'The :attribute field is required when none of :values are present.',
|
||||
'same' => 'The :attribute and :other must match.',
|
||||
'size' => [
|
||||
'numeric' => 'The :attribute must be :size.',
|
||||
'file' => 'The :attribute must be :size kilobytes.',
|
||||
'string' => 'The :attribute must be :size characters.',
|
||||
'array' => 'The :attribute must contain :size items.',
|
||||
'same' => 'The :attribute field must match :other.',
|
||||
'size' => [
|
||||
'array' => 'The :attribute field must contain :size items.',
|
||||
'file' => 'The :attribute field must be :size kilobytes.',
|
||||
'numeric' => 'The :attribute field must be :size.',
|
||||
'string' => 'The :attribute field must be :size characters.',
|
||||
],
|
||||
'starts_with' => 'The :attribute field must start with one of the following: :values.',
|
||||
'string' => 'The :attribute must be a string.',
|
||||
'timezone' => 'The :attribute must be a valid zone.',
|
||||
'two_column_unique_undeleted' => 'The :attribute must be unique across :table1 and :table2. ',
|
||||
'unique' => 'The :attribute has already been taken.',
|
||||
'uploaded' => 'The :attribute failed to upload.',
|
||||
'url' => 'The :attribute format is invalid.',
|
||||
'unique_undeleted' => 'The :attribute must be unique.',
|
||||
'non_circular' => 'The :attribute must not create a circular reference.',
|
||||
'not_array' => ':attribute cannot be an array.',
|
||||
|
@ -102,12 +163,13 @@ return [
|
|||
'numbers' => 'Password must contain at least one number.',
|
||||
'case_diff' => 'Password must use mixed case.',
|
||||
'symbols' => 'Password must contain symbols.',
|
||||
'gte' => [
|
||||
'numeric' => 'Value cannot be negative'
|
||||
],
|
||||
'checkboxes' => ':attribute contains invalid options.',
|
||||
'radio_buttons' => ':attribute is invalid.',
|
||||
|
||||
'timezone' => 'The :attribute field must be a valid timezone.',
|
||||
'unique' => 'The :attribute has already been taken.',
|
||||
'uploaded' => 'The :attribute failed to upload.',
|
||||
'uppercase' => 'The :attribute field must be uppercase.',
|
||||
'url' => 'The :attribute field must be a valid URL.',
|
||||
'ulid' => 'The :attribute field must be a valid ULID.',
|
||||
'uuid' => 'The :attribute field must be a valid UUID.',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
@ -129,7 +191,7 @@ return [
|
|||
|
||||
// date_format validation with slightly less stupid messages. It duplicates a lot, but it gets the job done :(
|
||||
// We use this because the default error message for date_format is reflects php Y-m-d, which non-PHP
|
||||
// people won't know how to format.
|
||||
// people won't know how to format.
|
||||
'purchase_date.date_format' => 'The :attribute must be a valid date in YYYY-MM-DD format',
|
||||
'last_audit_date.date_format' => 'The :attribute must be a valid date in YYYY-MM-DD hh:mm:ss format',
|
||||
'expiration_date.date_format' => 'The :attribute must be a valid date in YYYY-MM-DD format',
|
||||
|
@ -137,9 +199,10 @@ return [
|
|||
'expected_checkin.date_format' => 'The :attribute must be a valid date in YYYY-MM-DD format',
|
||||
'start_date.date_format' => 'The :attribute must be a valid date in YYYY-MM-DD format',
|
||||
'end_date.date_format' => 'The :attribute must be a valid date in YYYY-MM-DD format',
|
||||
|
||||
],
|
||||
|
||||
'checkboxes' => ':attribute contains invalid options.',
|
||||
'radio_buttons' => ':attribute is invalid.',
|
||||
'invalid_value_in_field' => 'Invalid value included in this field',
|
||||
],
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Custom Validation Attributes
|
||||
|
@ -158,5 +221,5 @@ return [
|
|||
| Generic Validation Messages
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'invalid_value_in_field' => 'Invalid value included in this field',
|
||||
|
||||
];
|
||||
|
|
|
@ -49,9 +49,35 @@
|
|||
</div>
|
||||
@endif
|
||||
|
||||
<!-- total -->
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">{{ trans('admin/components/general.total') }}</label>
|
||||
<div class="col-md-6">
|
||||
<p class="form-control-static">{{ $accessory->qty }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- remaining -->
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">{{ trans('admin/components/general.remaining') }}</label>
|
||||
<div class="col-md-6">
|
||||
<p class="form-control-static">{{ $accessory->numRemaining() }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- User -->
|
||||
|
||||
@include ('partials.forms.edit.user-select', ['translated_name' => trans('general.select_user'), 'fieldname' => 'assigned_to'])
|
||||
@include ('partials.forms.edit.user-select', ['translated_name' => trans('general.select_user'), 'fieldname' => 'assigned_to', 'required'=> 'true'])
|
||||
|
||||
<!-- Checkout QTY -->
|
||||
<div class="form-group {{ $errors->has('checkout_qty') ? 'error' : '' }} ">
|
||||
<label for="checkout_qty" class="col-md-3 control-label">{{ trans('general.qty') }}</label>
|
||||
<div class="col-md-7 col-sm-12 required">
|
||||
<div class="col-md-2" style="padding-left:0px">
|
||||
<input class="form-control" type="number" name="checkout_qty" id="checkout_qty" value="{{ old('checkout_qty', 1) }}" min="1" max="{{ $accessory->numRemaining() }}" />
|
||||
</div>
|
||||
</div>
|
||||
{!! $errors->first('checkout_qty', '<div class="col-md-8 col-md-offset-3"><span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span></div>') !!}
|
||||
</div>
|
||||
|
||||
|
||||
@if ($accessory->requireAcceptance() || $accessory->getEula() || ($snipeSettings->webhook_endpoint!=''))
|
||||
|
|
|
@ -20,6 +20,9 @@
|
|||
@if (isset($item->model_no))
|
||||
| **{{ trans('general.model_no') }}** | {{ $item->model_no }} |
|
||||
@endif
|
||||
@if (isset($checkout_qty))
|
||||
| **{{ trans('general.qty') }}** | {{ $checkout_qty }} |
|
||||
@endif
|
||||
@if ($note)
|
||||
| **{{ trans('mail.additional_notes') }}** | {{ $note }} |
|
||||
@endif
|
||||
|
|
|
@ -13,7 +13,7 @@ Route::group(['prefix' => 'accessories', 'middleware' => ['auth']], function ()
|
|||
)->name('accessories.checkout.show');
|
||||
|
||||
Route::post(
|
||||
'{accessoryID}/checkout',
|
||||
'{accessory}/checkout',
|
||||
[Accessories\AccessoryCheckoutController::class, 'store']
|
||||
)->name('accessories.checkout.store');
|
||||
|
||||
|
|
|
@ -33,20 +33,110 @@ class AccessoryCheckoutTest extends TestCase
|
|||
->postJson(route('api.accessories.checkout', Accessory::factory()->withoutItemsRemaining()->create()), [
|
||||
'assigned_to' => User::factory()->create()->id,
|
||||
])
|
||||
->assertStatusMessageIs('error');
|
||||
->assertOk()
|
||||
->assertStatusMessageIs('error')
|
||||
->assertJson(
|
||||
[
|
||||
'status' => 'error',
|
||||
'messages' =>
|
||||
[
|
||||
'checkout_qty' =>
|
||||
[
|
||||
trans_choice('admin/accessories/message.checkout.checkout_qty.lte', 0,
|
||||
[
|
||||
'number_currently_remaining' => 0,
|
||||
'checkout_qty' => 1,
|
||||
'number_remaining_after_checkout' => 0
|
||||
])
|
||||
],
|
||||
|
||||
],
|
||||
'payload' => null,
|
||||
])
|
||||
->assertStatus(200)
|
||||
->json();
|
||||
}
|
||||
|
||||
public function testAccessoryCanBeCheckedOut()
|
||||
public function testAccessoryCanBeCheckedOutWithoutQty()
|
||||
{
|
||||
$accessory = Accessory::factory()->create();
|
||||
$user = User::factory()->create();
|
||||
$admin = User::factory()->checkoutAccessories()->create();
|
||||
|
||||
$this->actingAsForApi($admin)
|
||||
->postJson(route('api.accessories.checkout', $accessory), [
|
||||
'assigned_to' => $user->id,
|
||||
])
|
||||
->assertOk()
|
||||
->assertStatusMessageIs('success')
|
||||
->assertStatus(200)
|
||||
->assertJson(['messages' => trans('admin/accessories/message.checkout.success')])
|
||||
->json();
|
||||
|
||||
$this->assertTrue($accessory->users->contains($user));
|
||||
|
||||
$this->assertEquals(
|
||||
1,
|
||||
Actionlog::where([
|
||||
'action_type' => 'checkout',
|
||||
'target_id' => $user->id,
|
||||
'target_type' => User::class,
|
||||
'item_id' => $accessory->id,
|
||||
'item_type' => Accessory::class,
|
||||
'user_id' => $admin->id,
|
||||
])->count(),'Log entry either does not exist or there are more than expected'
|
||||
);
|
||||
}
|
||||
|
||||
public function testAccessoryCanBeCheckedOutWithQty()
|
||||
{
|
||||
$accessory = Accessory::factory()->create(['qty' => 20]);
|
||||
$user = User::factory()->create();
|
||||
$admin = User::factory()->checkoutAccessories()->create();
|
||||
|
||||
$this->actingAsForApi($admin)
|
||||
->postJson(route('api.accessories.checkout', $accessory), [
|
||||
'assigned_to' => $user->id,
|
||||
'checkout_qty' => 2,
|
||||
])
|
||||
->assertOk()
|
||||
->assertStatusMessageIs('success')
|
||||
->assertStatus(200)
|
||||
->assertJson(['messages' => trans('admin/accessories/message.checkout.success')])
|
||||
->json();
|
||||
|
||||
$this->assertTrue($accessory->users->contains($user));
|
||||
|
||||
$this->assertEquals(
|
||||
1,
|
||||
Actionlog::where([
|
||||
'action_type' => 'checkout',
|
||||
'target_id' => $user->id,
|
||||
'target_type' => User::class,
|
||||
'item_id' => $accessory->id,
|
||||
'item_type' => Accessory::class,
|
||||
'user_id' => $admin->id,
|
||||
])->count(),
|
||||
'Log entry either does not exist or there are more than expected'
|
||||
);
|
||||
}
|
||||
|
||||
public function testAccessoryCannotBeCheckedOutToInvalidUser()
|
||||
{
|
||||
$accessory = Accessory::factory()->create();
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAsForApi(User::factory()->checkoutAccessories()->create())
|
||||
->postJson(route('api.accessories.checkout', $accessory), [
|
||||
'assigned_to' => $user->id,
|
||||
]);
|
||||
'assigned_to' => 'invalid-user-id',
|
||||
'note' => 'oh hi there',
|
||||
])
|
||||
->assertOk()
|
||||
->assertStatusMessageIs('error')
|
||||
->assertStatus(200)
|
||||
->json();
|
||||
|
||||
$this->assertTrue($accessory->users->contains($user));
|
||||
$this->assertFalse($accessory->users->contains($user));
|
||||
}
|
||||
|
||||
public function testUserSentNotificationUponCheckout()
|
||||
|
|
|
@ -20,23 +20,36 @@ class AccessoryCheckoutTest extends TestCase
|
|||
|
||||
public function testValidationWhenCheckingOutAccessory()
|
||||
{
|
||||
$this->actingAs(User::factory()->checkoutAccessories()->create())
|
||||
->post(route('accessories.checkout.store', Accessory::factory()->create()), [
|
||||
$accessory = Accessory::factory()->create();
|
||||
$response = $this->actingAs(User::factory()->superuser()->create())
|
||||
->from(route('accessories.checkout.show', $accessory))
|
||||
->post(route('accessories.checkout.store', $accessory), [
|
||||
// missing assigned_to
|
||||
])
|
||||
->assertSessionHas('error');
|
||||
->assertStatus(302)
|
||||
->assertSessionHas('errors')
|
||||
->assertRedirect(route('accessories.checkout.store', $accessory));
|
||||
|
||||
$this->followRedirects($response)->assertSee(trans('general.error'));
|
||||
}
|
||||
|
||||
public function testAccessoryMustBeAvailableWhenCheckingOut()
|
||||
public function testAccessoryMustHaveAvailableItemsForCheckoutWhenCheckingOut()
|
||||
{
|
||||
$this->actingAs(User::factory()->checkoutAccessories()->create())
|
||||
->post(route('accessories.checkout.store', Accessory::factory()->withoutItemsRemaining()->create()), [
|
||||
|
||||
$accessory = Accessory::factory()->withoutItemsRemaining()->create();
|
||||
$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,
|
||||
])
|
||||
->assertSessionHas('error');
|
||||
->assertStatus(302)
|
||||
->assertSessionHas('errors')
|
||||
->assertRedirect(route('accessories.checkout.store', $accessory));
|
||||
$response->assertInvalid(['checkout_qty']);
|
||||
$this->followRedirects($response)->assertSee(trans('general.error'));
|
||||
}
|
||||
|
||||
public function testAccessoryCanBeCheckedOut()
|
||||
public function testAccessoryCanBeCheckedOutWithoutQuantity()
|
||||
{
|
||||
$accessory = Accessory::factory()->create();
|
||||
$user = User::factory()->create();
|
||||
|
@ -44,9 +57,44 @@ class AccessoryCheckoutTest extends TestCase
|
|||
$this->actingAs(User::factory()->checkoutAccessories()->create())
|
||||
->post(route('accessories.checkout.store', $accessory), [
|
||||
'assigned_to' => $user->id,
|
||||
'note' => 'oh hi there',
|
||||
]);
|
||||
|
||||
$this->assertTrue($accessory->users->contains($user));
|
||||
|
||||
$this->assertDatabaseHas('action_logs', [
|
||||
'action_type' => 'checkout',
|
||||
'target_id' => $user->id,
|
||||
'target_type' => User::class,
|
||||
'item_id' => $accessory->id,
|
||||
'item_type' => Accessory::class,
|
||||
'note' => 'oh hi there',
|
||||
]);
|
||||
}
|
||||
|
||||
public function testAccessoryCanBeCheckedOutWithQuantity()
|
||||
{
|
||||
$accessory = Accessory::factory()->create(['qty'=>5]);
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs(User::factory()->checkoutAccessories()->create())
|
||||
->from(route('accessories.checkout.show', $accessory))
|
||||
->post(route('accessories.checkout.store', $accessory), [
|
||||
'assigned_to' => $user->id,
|
||||
'checkout_qty' => 3,
|
||||
'note' => 'oh hi there',
|
||||
]);
|
||||
|
||||
$this->assertTrue($accessory->users->contains($user));
|
||||
|
||||
$this->assertDatabaseHas('action_logs', [
|
||||
'action_type' => 'checkout',
|
||||
'target_id' => $user->id,
|
||||
'target_type' => User::class,
|
||||
'item_id' => $accessory->id,
|
||||
'item_type' => Accessory::class,
|
||||
'note' => 'oh hi there',
|
||||
]);
|
||||
}
|
||||
|
||||
public function testUserSentNotificationUponCheckout()
|
||||
|
@ -57,6 +105,7 @@ class AccessoryCheckoutTest extends TestCase
|
|||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs(User::factory()->checkoutAccessories()->create())
|
||||
->from(route('accessories.checkout.show', $accessory))
|
||||
->post(route('accessories.checkout.store', $accessory), [
|
||||
'assigned_to' => $user->id,
|
||||
]);
|
||||
|
@ -71,6 +120,7 @@ class AccessoryCheckoutTest extends TestCase
|
|||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($actor)
|
||||
->from(route('accessories.checkout.show', $accessory))
|
||||
->post(route('accessories.checkout.store', $accessory), [
|
||||
'assigned_to' => $user->id,
|
||||
'note' => 'oh hi there',
|
||||
|
|
Loading…
Reference in a new issue