Accept a purchase order

This commit is contained in:
David Bomba 2022-06-15 15:20:00 +10:00
parent 45a6daf347
commit 1e30bf4bdc
12 changed files with 206 additions and 46 deletions

View File

@ -222,6 +222,9 @@ class Handler extends ExceptionHandler
case 'user':
$login = 'login';
break;
case 'vendor':
$login = 'vendor.catchall';
break;
default:
$login = 'default';
break;

View File

@ -172,7 +172,12 @@ class BaseController extends Controller
*/
public function notFoundClient()
{
abort(404, 'Page not found in client portal.');
abort(404, 'Page not found in the client portal.');
}
public function notFoundVendor()
{
abort(404, 'Page not found in the vendor portal.');
}
/**

View File

@ -118,17 +118,38 @@ class PurchaseOrderController extends Controller
public function bulk(ProcessPurchaseOrdersInBulkRequest $request)
{
$transformed_ids = $this->transformKeys($request->purchase_orders);
if ($request->input('action') == 'download') {
return $this->downloadInvoices((array) $transformed_ids);
}
elseif ($request->input('action') == 'accept'){
return $this->acceptPurchaseOrder($request->all());
}
return redirect()
->back()
->with('message', ctrans('texts.no_action_provided'));
}
public function acceptPurchaseOrder($data)
{
$purchase_orders = PurchaseOrder::query()
->whereIn('id', $this->transformKeys($data['purchase_orders']))
->where('company_id', auth()->guard('vendor')->user()->vendor->company_id)
->whereIn('status_id', [PurchaseOrder::STATUS_DRAFT, PurchaseOrder::STATUS_SENT]);
$purchase_orders->update(['status_id' => PurchaseOrder::STATUS_ACCEPTED]);
if($purchase_orders->count() == 1)
return redirect()->route('vendor.purchase_order.show', ['purchase_order' => $purchase_orders->first()->hashed_id]);
else
return redirect()->route('vendor.purchase_orders.index');
}
public function downloadInvoices($ids)
{

View File

@ -0,0 +1,2 @@
/*! For license information please see accept.js.LICENSE.txt */
(()=>{function e(e,t){for(var n=0;n<t.length;n++){var a=t[n];a.enumerable=a.enumerable||!1,a.configurable=!0,"value"in a&&(a.writable=!0),Object.defineProperty(e,a.key,a)}}var t=function(){function t(e,n){!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,t),this.shouldDisplaySignature=e,this.shouldDisplayTerms=n,this.termsAccepted=!1}var n,a,r;return n=t,(a=[{key:"submitForm",value:function(){document.getElementById("approve-form").submit()}},{key:"displaySignature",value:function(){document.getElementById("displaySignatureModal").removeAttribute("style");var e=new SignaturePad(document.getElementById("signature-pad"),{penColor:"rgb(0, 0, 0)"});this.signaturePad=e}},{key:"displayTerms",value:function(){document.getElementById("displayTermsModal").removeAttribute("style")}},{key:"handle",value:function(){var e=this;document.getElementById("approve-button").addEventListener("click",(function(){e.shouldDisplaySignature&&e.shouldDisplayTerms&&(e.displaySignature(),document.getElementById("signature-next-step").addEventListener("click",(function(){e.displayTerms(),document.getElementById("accept-terms-button").addEventListener("click",(function(){document.querySelector('input[name="signature"').value=e.signaturePad.toDataURL(),e.termsAccepted=!0,e.submitForm()}))}))),e.shouldDisplaySignature&&!e.shouldDisplayTerms&&(e.displaySignature(),document.getElementById("signature-next-step").addEventListener("click",(function(){document.querySelector('input[name="signature"').value=e.signaturePad.toDataURL(),e.submitForm()}))),!e.shouldDisplaySignature&&e.shouldDisplayTerms&&(e.displayTerms(),document.getElementById("accept-terms-button").addEventListener("click",(function(){e.termsAccepted=!0,e.submitForm()}))),e.shouldDisplaySignature||e.shouldDisplayTerms||e.submitForm()}))}}])&&e(n.prototype,a),r&&e(n,r),Object.defineProperty(n,"prototype",{writable:!1}),t}(),n=document.querySelector('meta[name="require-purchase_order-signature"]').content,a=document.querySelector('meta[name="show-purchase_order-terms"]').content;new t(Boolean(+n),Boolean(+a)).handle()})();

View File

@ -0,0 +1,9 @@
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/

View File

@ -5,6 +5,7 @@
"/js/clients/payments/stripe-ach.js": "/js/clients/payments/stripe-ach.js?id=7bed15f51bca764378d9a3aa605b8664",
"/js/clients/invoices/action-selectors.js": "/js/clients/invoices/action-selectors.js?id=d4f86ddee4e8a1d6e9719010aa0fe62b",
"/js/clients/purchase_orders/action-selectors.js": "/js/clients/purchase_orders/action-selectors.js?id=160b8161599fc2429b449b0970d3ba6c",
"/js/clients/purchase_orders/accept.js": "/js/clients/purchase_orders/accept.js?id=2b5fed3ae34a6fd4db171a77ba72496e",
"/js/clients/invoices/payment.js": "/js/clients/invoices/payment.js?id=b88ad7c8881cc87df07b129c5a7c76df",
"/js/clients/payments/stripe-sofort.js": "/js/clients/payments/stripe-sofort.js?id=1c5493a4c53a5b862d07ee1818179ea9",
"/js/clients/payments/stripe-alipay.js": "/js/clients/payments/stripe-alipay.js?id=0274ab4f8d2b411f2a2fe5142301e7af",

View File

@ -0,0 +1,103 @@
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
class Accept {
constructor(displaySignature, displayTerms) {
this.shouldDisplaySignature = displaySignature;
this.shouldDisplayTerms = displayTerms;
this.termsAccepted = false;
}
submitForm() {
document.getElementById('approve-form').submit();
}
displaySignature() {
let displaySignatureModal = document.getElementById(
'displaySignatureModal'
);
displaySignatureModal.removeAttribute('style');
const signaturePad = new SignaturePad(
document.getElementById('signature-pad'),
{
penColor: 'rgb(0, 0, 0)',
}
);
this.signaturePad = signaturePad;
}
displayTerms() {
let displayTermsModal = document.getElementById("displayTermsModal");
displayTermsModal.removeAttribute("style");
}
handle() {
document
.getElementById('approve-button')
.addEventListener('click', () => {
if (this.shouldDisplaySignature && this.shouldDisplayTerms) {
this.displaySignature();
document
.getElementById('signature-next-step')
.addEventListener('click', () => {
this.displayTerms();
document
.getElementById('accept-terms-button')
.addEventListener('click', () => {
document.querySelector(
'input[name="signature"'
).value = this.signaturePad.toDataURL();
this.termsAccepted = true;
this.submitForm();
});
});
}
if (this.shouldDisplaySignature && !this.shouldDisplayTerms) {
this.displaySignature();
document
.getElementById('signature-next-step')
.addEventListener('click', () => {
document.querySelector(
'input[name="signature"'
).value = this.signaturePad.toDataURL();
this.submitForm();
});
}
if (!this.shouldDisplaySignature && this.shouldDisplayTerms) {
this.displayTerms();
document
.getElementById('accept-terms-button')
.addEventListener('click', () => {
this.termsAccepted = true;
this.submitForm();
});
}
if (!this.shouldDisplaySignature && !this.shouldDisplayTerms) {
this.submitForm();
}
});
}
}
const signature = document.querySelector('meta[name="require-purchase_order-signature"]')
.content;
const terms = document.querySelector('meta[name="show-purchase_order-terms"]').content;
new Accept(Boolean(+signature), Boolean(+terms)).handle();

View File

@ -30,7 +30,7 @@
{{ ctrans('texts.profile') }}
</a>
<a href="{{ route('client.logout') }}"
<a href="{{ route('vendor.logout') }}"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition ease-in-out duration-150">
{{ ctrans('texts.logout') }}
</a>

View File

@ -0,0 +1,40 @@
<form action="{{ route('vendor.purchase_orders.bulk') }}" method="post" id="approve-form" />
@csrf
<input type="hidden" name="action" value="accept">
<input type="hidden" name="process" value="true">
<input type="hidden" name="purchase_orders[]" value="{{ $purchase_order->hashed_id }}">
<input type="hidden" name="signature">
<div class="bg-white shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<div class="sm:flex sm:items-start sm:justify-between">
<div>
<h3 class="text-lg leading-6 font-medium text-gray-900">
{{ ctrans('texts.approve') }}
</h3>
<div class="btn hidden md:block" data-clipboard-text="{{url("vendor/purchase_order/{$key}")}}" aria-label="Copied!">
<div class="flex text-sm leading-6 font-medium text-gray-500">
<p class="mr-2">{{url("vendor/purchase_order/{$key}")}}</p>
<p><img class="h-5 w-5" src="{{ asset('assets/clippy.svg') }}" alt="Copy to clipboard"></p>
</div>
</div>
</div>
<div class="mt-5 sm:mt-0 sm:ml-6 sm:flex-shrink-0 sm:flex sm:items-center">
@yield('quote-not-approved-right-side')
<div class="inline-flex rounded-md shadow-sm">
<button onclick="setTimeout(() => this.disabled = true, 0); return true;" type="button"
class="button button-primary bg-primary"
id="approve-button">{{ ctrans('texts.accept') }}</button>
</div>
</div>
</div>
</div>
</div>
</form>

View File

@ -2,7 +2,8 @@
@section('meta_title', ctrans('texts.view_purchase_order'))
@push('head')
<meta name="require-invoice-signature" content="{{ $purchase_order->vendor->user->account->hasFeature(\App\Models\Account::FEATURE_INVOICE_SETTINGS) && $settings->require_purchase_order_signature }}">
<meta name="show-purchase_order-terms" content="false">
<meta name="require-purchase_order-signature" content="{{ $purchase_order->company->account->hasFeature(\App\Models\Account::FEATURE_INVOICE_SETTINGS) && $settings->require_purchase_order_signature }}">
@include('portal.ninja2020.components.no-cache')
<script src="{{ asset('vendor/signature_pad@2.3.2/signature_pad.min.js') }}"></script>
@ -11,56 +12,24 @@
@section('body')
@if($purchase_order)
<form action="{{ ($settings->client_portal_allow_under_payment || $settings->client_portal_allow_over_payment) ? route('client.invoices.bulk') : route('client.payments.process') }}" method="post" id="payment-form">
@csrf
<input type="hidden" name="signature">
<div class="bg-white shadow sm:rounded-lg mb-4" translate>
<div class="px-4 py-5 sm:p-6">
<div class="sm:flex sm:items-start sm:justify-between">
<div>
<h3 class="text-lg leading-6 font-medium text-gray-900">
{{ ctrans('texts.purchase_order_number_placeholder', ['purchase_order' => $purchase_order->number])}}
- {{ ctrans('texts.unpaid') }}
</h3>
@if($key)
<div class="btn hidden md:block" data-clipboard-text="{{url("vendor/purchase_order/{$key}")}}" aria-label="Copied!">
<div class="flex text-sm leading-6 font-medium text-gray-500">
<p class="mr-2">{{url("vendor/purchase_order/{$key}")}}</p>
<p><img class="h-5 w-5" src="{{ asset('assets/clippy.svg') }}" alt="Copy to clipboard"></p>
</div>
</div>
@endif
</div>
<div class="mt-5 sm:mt-0 sm:ml-6 flex justify-end">
<div class="inline-flex rounded-md shadow-sm">
<input type="hidden" name="purchase_orders[]" value="{{ $purchase_order->hashed_id }}">
<input type="hidden" name="action" value="payment">
</div>
</div>
</div>
</div>
</div>
</form>
@if(in_array($purchase_order->status_id, [\App\Models\PurchaseOrder::STATUS_SENT, \App\Models\PurchaseOrder::STATUS_DRAFT]))
<div class="mb-4">
@include('portal.ninja2020.purchase_orders.includes.actions', ['purchase_order' => $purchase_order])
</div>
@else
<div class="bg-white shadow sm:rounded-lg mb-4">
<div class="px-4 py-5 sm:p-6">
<div class="sm:flex sm:items-start sm:justify-between">
<div>
<h3 class="text-lg leading-6 font-medium text-gray-900">
{{ ctrans('texts.invoice_number_placeholder', ['invoice' => $purchase_order->number])}}
{{ ctrans('texts.purchase_order_number_placeholder', ['purchase_order' => $purchase_order->number])}}
- {{ \App\Models\PurchaseOrder::stringStatus($purchase_order->status_id) }}
</h3>
@if($key)
<div class="btn hidden md:block" data-clipboard-text="{{url("client/invoice/{$key}")}}" aria-label="Copied!">
<div class="btn hidden md:block" data-clipboard-text="{{url("vendor/purchase_order/{$key}")}}" aria-label="Copied!">
<div class="flex text-sm leading-6 font-medium text-gray-500">
<p class="pr-10">{{url("client/invoice/{$key}")}}</p>
<p class="pr-10">{{url("vendor/purchase_order/{$key}")}}</p>
<p><img class="h-5 w-5" src="{{ asset('assets/clippy.svg') }}" alt="Copy to clipboard"></p>
</div>
</div>
@ -78,7 +47,7 @@
@endsection
@section('footer')
<script src="{{ asset('js/clients/invoices/payment.js') }}"></script>
<script src="{{ asset('js/clients/purchase_orders/accept.js') }}"></script>
<script src="{{ asset('vendor/clipboard.min.js') }}"></script>
<script type="text/javascript">
@ -87,3 +56,4 @@
</script>
@endsection

View File

@ -32,7 +32,9 @@ Route::group(['middleware' => ['auth:vendor', 'vendor_locale', 'domain_db'], 'pr
Route::get('purchase_orders/{purchase_order}', [PurchaseOrderController::class, 'show'])->name('purchase_order.show');
Route::get('profile/{vendor_contact}/edit', [PurchaseOrderController::class, 'index'])->name('profile.edit');
Route::post('invoices/payment', [PurchaseOrderController::class, 'bulk'])->name('purchase_orders.bulk');
Route::get('logout', [VendorContactLoginController::class, 'logout'])->name('logout');
Route::post('purchase_orders/bulk', [PurchaseOrderController::class, 'bulk'])->name('purchase_orders.bulk');
Route::post('logout', [VendorContactLoginController::class, 'logout'])->name('logout');
});
});
Route::fallback('BaseController@notFoundVendor');

4
webpack.mix.js vendored
View File

@ -22,6 +22,10 @@ mix.js("resources/js/app.js", "public/js")
"resources/js/clients/purchase_orders/action-selectors.js",
"public/js/clients/purchase_orders/action-selectors.js"
)
.js(
"resources/js/clients/purchase_orders/accept.js",
"public/js/clients/purchase_orders/accept.js"
)
.js(
"resources/js/clients/invoices/payment.js",
"public/js/clients/invoices/payment.js"