Adds checkout acceptances

A checkout acceptance gets generated for every item that needs to be checked out. This resource tracks the user user who can accept the item and their signature
This commit is contained in:
Till Deeke 2018-07-28 12:45:33 +02:00
parent 6b05106dcb
commit 8648d53d25
14 changed files with 296 additions and 86 deletions

View file

@ -4,29 +4,24 @@ namespace App\Events;
use App\Models\Accessory;
use App\Models\Actionlog;
use App\Models\CheckoutAcceptance;
use App\Models\Contracts\Acceptable;
use App\Models\User;
use Illuminate\Broadcasting\Channel;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class ItemDeclined
class CheckoutAccepted
{
use Dispatchable, SerializesModels;
public $item;
public $declinedBy;
public $signature;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct(Acceptable $item, User $declinedBy, string $signature)
public function __construct(CheckoutAcceptance $acceptance)
{
$this->item = $item;
$this->declinedBy = $declinedBy;
$this->signature = $signature;
$this->acceptance = $acceptance;
}
}

View file

@ -4,29 +4,24 @@ namespace App\Events;
use App\Models\Accessory;
use App\Models\Actionlog;
use App\Models\CheckoutAcceptance;
use App\Models\Contracts\Acceptable;
use App\Models\User;
use Illuminate\Broadcasting\Channel;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class ItemAccepted
class CheckoutDeclined
{
use Dispatchable, SerializesModels;
public $item;
public $acceptedBy;
public $signature;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct(Acceptable $item, User $acceptedBy, string $signature)
public function __construct(CheckoutAcceptance $acceptance)
{
$this->item = $item;
$this->acceptedBy = $acceptedBy;
$this->signature = $signature;
$this->acceptance = $acceptance;
}
}

View file

@ -1,10 +1,13 @@
<?php
namespace App\Http\Controllers\Account;
use App\Events\CheckoutAccepted;
use App\Events\CheckoutDeclined;
use App\Events\ItemAccepted;
use App\Events\ItemDeclined;
use App\Http\Controllers\Controller;
use App\Models\Asset;
use App\Models\CheckoutAcceptance;
use App\Models\Company;
use App\Models\Consumable;
use App\Models\Contracts\Acceptable;
@ -16,49 +19,52 @@ use Illuminate\Support\Str;
class AcceptanceController extends Controller {
public function index(Request $request) {
return '';
$acceptances = CheckoutAcceptance::forUser(Auth::user())->pending()->get();
return view('account/accept.index', compact('acceptances'));
}
public function edit(Request $request, $type, $id) {
public function create(Request $request, $id) {
$item = $this->getItemById($type, $id);
$acceptance = CheckoutAcceptance::find($id);
if (is_null($item)) {
if (is_null($acceptance)) {
return redirect()->reoute('account.accept')->with('error', trans('admin/hardware/message.does_not_exist'));
}
if ($item->isAccepted()) {
if (! $acceptance->isPending()) {
return redirect()->route('account.accept')->with('error', trans('admin/users/message.error.asset_already_accepted'));
}
if (! $item->isCheckedOutTo(Auth::user())) {
if (! $acceptance->isCheckedOutTo(Auth::user())) {
return redirect()->route('account.accept')->with('error', trans('admin/users/message.error.incorrect_user_accepted'));
}
if (!Company::isCurrentUserHasAccess($item)) {
if (!Company::isCurrentUserHasAccess($acceptance->checkoutable)) {
return redirect()->route('account.accept')->with('error', trans('general.insufficient_permissions'));
}
return view('account/accept', compact('item'));
return view('account/accept.create', compact('acceptance'));
}
public function update(Request $request, $type, $id) {
$item = $this->getItemById($type, $id);
public function store(Request $request, $id) {
$acceptance = CheckoutAcceptance::find($id);
if (is_null($item)) {
if (is_null($acceptance)) {
return redirect()->reoute('account.accept')->with('error', trans('admin/hardware/message.does_not_exist'));
}
if ($item->isAccepted()) {
if (! $acceptance->isPending()) {
return redirect()->route('account.accept')->with('error', trans('admin/users/message.error.asset_already_accepted'));
}
}
if (! $item->isCheckedOutTo(Auth::user())) {
if (! $acceptance->isCheckedOutTo(Auth::user())) {
return redirect()->route('account.accept')->with('error', trans('admin/users/message.error.incorrect_user_accepted'));
}
if (!Company::isCurrentUserHasAccess($item)) {
if (!Company::isCurrentUserHasAccess($acceptance->checkoutable)) {
return redirect()->route('account.accept')->with('error', trans('general.insufficient_permissions'));
}
}
if (!$request->filled('asset_acceptance')) {
return redirect()->back()->with('error', trans('admin/users/message.error.accept_or_decline'));
@ -79,41 +85,30 @@ class AcceptanceController extends Controller {
if ($request->input('asset_acceptance') == 'accepted') {
$item->accept(Auth::user(), $sig_filename);
$acceptance->accepted_at = now();
$acceptance->signature_filename = $sig_filename;
$acceptance->save();
event(new ItemAccepted($item, Auth::user(), $sig_filename));
// TODO: Update state for the checkoutable
event(new CheckoutAccepted($acceptance));
$return_msg = trans('admin/users/message.accepted');
} else {
$item->decline(Auth::user(), $sig_filename);
$acceptance->declined_at = now();
$acceptance->signature_filename = $sig_filename;
$acceptance->save();
event(new ItemDeclined($item, Auth::user(), $sig_filename));
// TODO: Update state for the checkoutable
event(new CheckoutDeclined($acceptance));
$return_msg = trans('admin/users/message.declined');
}
return redirect()->to('account/accept')->with('success', $return_msg);
}
private function getItemById($type, $id) : ? Acceptable {
switch ($type) {
case 'asset':
$item = Asset::findOrFail($id);
break;
case 'consumable':
$item = Consumable::findOrFail($id);
break;
case 'license':
$item = License::findOrFail($id);
break;
default:
$item = null;
break;
}
return $item;
}
}

View file

@ -6,6 +6,8 @@ use App\Events\AccessoryCheckedIn;
use App\Events\AccessoryCheckedOut;
use App\Events\AssetCheckedIn;
use App\Events\AssetCheckedOut;
use App\Events\CheckoutAccepted;
use App\Events\CheckoutDeclined;
use App\Events\ComponentCheckedIn;
use App\Events\ComponentCheckedOut;
use App\Events\ConsumableCheckedOut;
@ -16,6 +18,7 @@ use App\Events\LicenseCheckedOut;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\Component;
use App\Models\LicenseSeat;
class LogListener
@ -65,23 +68,34 @@ class LogListener
$event->license->logCheckout($event->note, $event->checkedOutTo);
}
public function onItemAccepted(ItemAccepted $event) {
public function onCheckoutAccepted(CheckoutAccepted $event) {
$logaction = new Actionlog();
$logaction->item()->associate($event->item);
$logaction->target()->associate($event->acceptedBy);
$logaction->accept_signature = $event->signature;
$logaction->item()->associate($event->acceptance->checkoutable);
$logaction->target()->associate($event->acceptance->assignedTo);
$logaction->accept_signature = $event->acceptance->signature_filename;
$logaction->action_type = 'accepted';
// TODO: log the actual license seat that was checked out
if($event->acceptance->checkoutable instanceof LicenseSeat) {
$logaction->item()->associate($event->acceptance->checkoutable->license);
}
$logaction->save();
}
public function onItemDeclined(ItemDeclined $event) {
public function onCheckoutDeclined(CheckoutDeclined $event) {
$logaction = new Actionlog();
$logaction->item()->associate($event->item);
$logaction->target()->associate($event->declinedBy);
$logaction->accept_signature = $event->signature;
$logaction->item()->associate($event->acceptance->checkoutable);
$logaction->target()->associate($event->acceptance->assignedTo);
$logaction->accept_signature = $event->acceptance->signature_filename;
$logaction->action_type = 'declined';
// TODO: log the actual license seat that was checked out
if($event->acceptance->checkoutable instanceof LicenseSeat) {
$logaction->item()->associate($event->acceptance->checkoutable->license);
}
$logaction->save();
}
@ -102,8 +116,8 @@ class LogListener
'ConsumableCheckedOut',
'LicenseCheckedIn',
'LicenseCheckedOut',
'ItemAccepted',
'ItemDeclined',
'CheckoutAccepted',
'CheckoutDeclined',
];
foreach($list as $event) {

View file

@ -2,6 +2,7 @@
namespace App\Listeners;
use App\Models\CheckoutAcceptance;
use App\Models\Recipients\AdminRecipient;
use App\Models\Setting;
use App\Models\User;
@ -9,6 +10,7 @@ use App\Notifications\CheckoutAccessoryNotification;
use App\Notifications\CheckoutAssetNotification;
use App\Notifications\CheckoutConsumableNotification;
use App\Notifications\CheckoutLicenseNotification;
use App\Notifications\CheckoutLicenseSeatNotification;
use Illuminate\Support\Facades\Notification;
class SendingCheckOutNotificationsListener
@ -24,9 +26,20 @@ class SendingCheckOutNotificationsListener
return;
}
/**
* Make a checkout acceptance and attach it in the notification
*/
$acceptance = null;
if ($event->consumable->requireAcceptance()) {
$acceptance = new CheckoutAcceptance;
$acceptance->checkoutable()->associate($event->consumable);
$acceptance->assignedTo()->associate($event->checkedOutTo);
$acceptance->save();
}
Notification::send(
$this->getNotifiables($event),
new CheckoutConsumableNotification($event->consumable, $event->checkedOutTo, $event->checkedOutBy, $event->note)
new CheckoutConsumableNotification($event->consumable, $event->checkedOutTo, $event->checkedOutBy, $acceptance, $event->note)
);
}
@ -41,16 +54,27 @@ class SendingCheckOutNotificationsListener
return;
}
/**
* Make a checkout acceptance and attach it in the notification
*/
$acceptance = null;
if ($event->accessory->requireAcceptance()) {
$acceptance = new CheckoutAcceptance;
$acceptance->checkoutable()->associate($event->accessory);
$acceptance->assignedTo()->associate($event->checkedOutTo);
$acceptance->save();
}
Notification::send(
$this->getNotifiables($event),
new CheckoutAccessoryNotification($event->accessory, $event->checkedOutTo, $event->checkedOutBy, $event->note)
new CheckoutAccessoryNotification($event->accessory, $event->checkedOutTo, $event->checkedOutBy, $acceptance, $event->note)
);
}
/**
* Notify the user about the checked out license
*/
public function onLicenseCheckedOut($event) {
public function onLicenseSeatCheckedOut($event) {
/**
* When the item wasn't checked out to a user, we can't send notifications
*/
@ -58,9 +82,20 @@ class SendingCheckOutNotificationsListener
return;
}
/**
* Make a checkout acceptance and attach it in the notification
*/
$acceptance = null;
if ($event->licenseSeat->license->requireAcceptance()) {
$acceptance = new CheckoutAcceptance;
$acceptance->checkoutable()->associate($event->licenseSeat);
$acceptance->assignedTo()->associate($event->checkedOutTo);
$acceptance->save();
}
Notification::send(
$this->getNotifiables($event),
new CheckoutLicenseNotification($event->license, $event->checkedOutTo, $event->checkedOutBy, $event->note)
new CheckoutLicenseSeatNotification($event->licenseSeat, $event->checkedOutTo, $event->checkedOutBy, $acceptance, $event->note)
);
}
@ -75,9 +110,20 @@ class SendingCheckOutNotificationsListener
return;
}
/**
* Make a checkout acceptance and attach it in the notification
*/
$acceptance = null;
if ($event->asset->requireAcceptance()) {
$acceptance = new CheckoutAcceptance;
$acceptance->checkoutable()->associate($event->asset);
$acceptance->assignedTo()->associate($event->checkedOutTo);
$acceptance->save();
}
Notification::send(
$this->getNotifiables($event),
new CheckoutAssetNotification($event->asset, $event->checkedOutTo, $event->checkedOutBy, $event->note)
new CheckoutAssetNotification($event->asset, $event->checkedOutTo, $event->checkedOutBy, $acceptance, $event->note)
);
}
@ -123,8 +169,8 @@ class SendingCheckOutNotificationsListener
);
$events->listen(
'App\Events\LicenseCheckedOut',
'App\Listeners\SendingCheckOutNotificationsListener@onLicenseCheckedOut'
'App\Events\LicenseSeatCheckedOut',
'App\Listeners\SendingCheckOutNotificationsListener@onLicenseSeatCheckedOut'
);
$events->listen(

View file

@ -0,0 +1,50 @@
<?php
namespace App\Models;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class CheckoutAcceptance extends Model
{
use SoftDeletes;
/**
* The attributes that should be mutated to dates.
*
* @var array
*/
protected $dates = [
'accepted_at',
'declined_at',
'deleted_at'
];
public function checkoutable() {
return $this->morphTo();
}
public function assignedTo() {
return $this->belongsTo(User::class);
}
public function isPending() {
return $this->accepted_at == null && $this->declined_at == null;
}
public function isCheckedOutTo(User $user) {
return $this->assignedTo->is($user);
}
public function scopeForUser(Builder $query, User $user) {
return $query->where('assigned_to_id', $user->id);
}
public function scopePending(Builder $query) {
return $query->whereNull('accepted_at')->whereNull('declined_at');
}
}

View file

@ -20,13 +20,14 @@ class CheckoutAccessoryNotification extends Notification
/**
* Create a new notification instance.
*/
public function __construct(Accessory $accessory, $checkedOutTo, User $checkedOutBy, $note)
public function __construct(Accessory $accessory, $checkedOutTo, User $checkedOutBy, $acceptance, $note)
{
$this->item = $accessory;
$this->admin = $checkedOutBy;
$this->note = $note;
$this->target = $checkedOutTo;
$this->acceptance = $acceptance;
$this->settings = Setting::getSettings();
@ -119,6 +120,8 @@ class CheckoutAccessoryNotification extends Notification
$eula = $this->item->getEula();
$req_accept = $this->item->requireAcceptance();
$accept_url = is_null($this->acceptance) ? null : route('account.accept.item', $this->acceptance);
return (new MailMessage)->markdown('notifications.markdown.checkout-accessory',
[
'item' => $this->item,
@ -127,7 +130,7 @@ class CheckoutAccessoryNotification extends Notification
'target' => $this->target,
'eula' => $eula,
'req_accept' => $req_accept,
'accept_url' => route('account.accept.item', ['accessory', $this->item->id]),
'accept_url' => $accept_url,
])
->subject(trans('mail.Confirm_accessory_delivery'));

View file

@ -20,13 +20,14 @@ class CheckoutAssetNotification extends Notification
*
* @param $params
*/
public function __construct(Asset $asset, $checkedOutTo, User $checkedOutBy, $note)
public function __construct(Asset $asset, $checkedOutTo, User $checkedOutBy, $acceptance, $note)
{
$this->item = $asset;
$this->admin = $checkedOutBy;
$this->note = $note;
$this->target = $checkedOutTo;
$this->acceptance = $acceptance;
$this->settings = Setting::getSettings();
@ -140,6 +141,8 @@ class CheckoutAssetNotification extends Notification
$fields = $this->item->model->fieldset->fields;
}
$accept_url = is_null($this->acceptance) ? null : route('account.accept.item', $this->acceptance);
$message = (new MailMessage)->markdown('notifications.markdown.checkout-asset',
[
'item' => $this->item,
@ -149,7 +152,7 @@ class CheckoutAssetNotification extends Notification
'fields' => $fields,
'eula' => $eula,
'req_accept' => $req_accept,
'accept_url' => route('account.accept.item', ['asset', $this->item->id]),
'accept_url' => $accept_url,
'last_checkout' => $this->last_checkout,
'expected_checkin' => $this->expected_checkin,
])

View file

@ -26,13 +26,14 @@ class CheckoutConsumableNotification extends Notification
*
* @param $params
*/
public function __construct(Consumable $consumable, $checkedOutTo, User $checkedOutBy, $note)
public function __construct(Consumable $consumable, $checkedOutTo, User $checkedOutBy, $acceptance, $note)
{
$this->item = $consumable;
$this->admin = $checkedOutBy;
$this->note = $note;
$this->target = $checkedOutTo;
$this->acceptance = $acceptance;
$this->settings = Setting::getSettings();
@ -121,6 +122,8 @@ class CheckoutConsumableNotification extends Notification
$eula = $this->item->getEula();
$req_accept = $this->item->requireAcceptance();
$accept_url = is_null($this->acceptance) ? null : route('account.accept.item', $this->acceptance);
return (new MailMessage)->markdown('notifications.markdown.checkout-consumable',
[
'item' => $this->item,
@ -129,7 +132,7 @@ class CheckoutConsumableNotification extends Notification
'target' => $this->target,
'eula' => $eula,
'req_accept' => $req_accept,
'accept_url' => route('account.accept.item', ['consumable', $this->item->id]),
'accept_url' => $accept_url,
])
->subject(trans('mail.Confirm_consumable_delivery'));

View file

@ -0,0 +1,41 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateCheckoutAcceptancesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('checkout_acceptances', function (Blueprint $table) {
$table->increments('id');
$table->morphs('checkoutable');
$table->integer('assigned_to_id')->unsigned();
$table->string('signature_filename');
$table->timestamp('accepted_at')->nullable();
$table->timestamp('declined_at')->nullable();
$table->timestamps();
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('checkout_acceptances');
}
}

View file

@ -2,7 +2,7 @@
{{-- Page title --}}
@section('title')
Accept {{ $item->present()->name() }}
Accept {{ $acceptance->checkoutable->present()->name() }}
@parent
@stop
@ -52,10 +52,10 @@
</label>
</div>
@if ($item->getEula())
@if ($acceptance->checkoutable->getEula())
<div class="col-md-12" style="padding-top: 20px">
<div id="eula_div">
{!! $item->getEula() !!}
{!! $acceptance->checkoutable->getEula() !!}
</div>
</div>
@endif

View file

@ -0,0 +1,61 @@
@extends('layouts/default')
{{-- Page title --}}
@section('title')
Accept assets {{ $user->present()->fullName() }}
@parent
@stop
{{-- Account page content --}}
@section('content')
<div class="row">
<div class="col-md-12">
<div class="box box-default">
<div class="box-body">
<!-- checked out Accessories table -->
<div class="table-responsive">
<table
data-cookie-id-table="pendingAcceptances"
data-pagination="true"
data-id-table="pendingAcceptances"
data-search="true"
data-side-pagination="client"
data-show-columns="true"
data-show-export="true"
data-show-refresh="true"
data-sort-order="asc"
id="pendingAcceptances"
class="table table-striped snipe-table"
data-export-options='{
"fileName": "my-pending-acceptances-{{ date('Y-m-d') }}",
"ignoreColumn": ["actions","image","change","checkbox","checkincheckout","icon"]
}'>
<thead>
<tr>
<th>Name</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach ($acceptances as $acceptance)
<tr>
<td>{{ $acceptance->checkoutable->present()->name }}</td>
<td><a href="{{ route('account.accept.item', $acceptance) }}" class="btn btn-default btn-sm">Accept/Decline</a></td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div> <!-- .box-body-->
</div><!--.box.box-default-->
</div> <!-- .col-md-12-->
</div> <!-- .row-->
@stop
@section('moar_scripts')
@include ('partials.bootstrap-table')
@stop

View file

@ -317,7 +317,11 @@
<i class="fa fa-check fa-disk fa-fw"></i>
Requested Assets
</a></li>
<li {!! (Request::is('account/accept') ? ' class="active"' : '') !!}>
<a href="{{ route('account.accept') }}">
<i class="fa fa-check fa-disk fa-fw"></i>
Accept Assets
</a></li>

View file

@ -278,10 +278,10 @@ Route::group([ 'prefix' => 'account', 'middleware' => ['auth']], function () {
Route::get('accept', 'Account\AcceptanceController@index')
->name('account.accept');
Route::get('accept/{type}/{id}', 'Account\AcceptanceController@edit')
Route::get('accept/{id}', 'Account\AcceptanceController@create')
->name('account.accept.item');
Route::post('accept/{type}/{id}', 'Account\AcceptanceController@update');
Route::post('accept/{id}', 'Account\AcceptanceController@store');
});