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': case 'user':
$login = 'login'; $login = 'login';
break; break;
case 'vendor':
$login = 'vendor.catchall';
break;
default: default:
$login = 'default'; $login = 'default';
break; break;

View File

@ -172,7 +172,12 @@ class BaseController extends Controller
*/ */
public function notFoundClient() 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) public function bulk(ProcessPurchaseOrdersInBulkRequest $request)
{ {
$transformed_ids = $this->transformKeys($request->purchase_orders); $transformed_ids = $this->transformKeys($request->purchase_orders);
if ($request->input('action') == 'download') { if ($request->input('action') == 'download') {
return $this->downloadInvoices((array) $transformed_ids); return $this->downloadInvoices((array) $transformed_ids);
} }
elseif ($request->input('action') == 'accept'){
return $this->acceptPurchaseOrder($request->all());
}
return redirect() return redirect()
->back() ->back()
->with('message', ctrans('texts.no_action_provided')); ->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) 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/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/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/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/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-sofort.js": "/js/clients/payments/stripe-sofort.js?id=1c5493a4c53a5b862d07ee1818179ea9",
"/js/clients/payments/stripe-alipay.js": "/js/clients/payments/stripe-alipay.js?id=0274ab4f8d2b411f2a2fe5142301e7af", "/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') }} {{ ctrans('texts.profile') }}
</a> </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"> class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition ease-in-out duration-150">
{{ ctrans('texts.logout') }} {{ ctrans('texts.logout') }}
</a> </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')) @section('meta_title', ctrans('texts.view_purchase_order'))
@push('head') @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') @include('portal.ninja2020.components.no-cache')
<script src="{{ asset('vendor/signature_pad@2.3.2/signature_pad.min.js') }}"></script> <script src="{{ asset('vendor/signature_pad@2.3.2/signature_pad.min.js') }}"></script>
@ -11,56 +12,24 @@
@section('body') @section('body')
@if($purchase_order) @if(in_array($purchase_order->status_id, [\App\Models\PurchaseOrder::STATUS_SENT, \App\Models\PurchaseOrder::STATUS_DRAFT]))
<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"> <div class="mb-4">
@csrf @include('portal.ninja2020.purchase_orders.includes.actions', ['purchase_order' => $purchase_order])
<input type="hidden" name="signature"> </div>
<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>
@else @else
<div class="bg-white shadow sm:rounded-lg mb-4"> <div class="bg-white shadow sm:rounded-lg mb-4">
<div class="px-4 py-5 sm:p-6"> <div class="px-4 py-5 sm:p-6">
<div class="sm:flex sm:items-start sm:justify-between"> <div class="sm:flex sm:items-start sm:justify-between">
<div> <div>
<h3 class="text-lg leading-6 font-medium text-gray-900"> <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) }} - {{ \App\Models\PurchaseOrder::stringStatus($purchase_order->status_id) }}
</h3> </h3>
@if($key) @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"> <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> <p><img class="h-5 w-5" src="{{ asset('assets/clippy.svg') }}" alt="Copy to clipboard"></p>
</div> </div>
</div> </div>
@ -78,7 +47,7 @@
@endsection @endsection
@section('footer') @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 src="{{ asset('vendor/clipboard.min.js') }}"></script>
<script type="text/javascript"> <script type="text/javascript">
@ -87,3 +56,4 @@
</script> </script>
@endsection @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('purchase_orders/{purchase_order}', [PurchaseOrderController::class, 'show'])->name('purchase_order.show');
Route::get('profile/{vendor_contact}/edit', [PurchaseOrderController::class, 'index'])->name('profile.edit'); Route::get('profile/{vendor_contact}/edit', [PurchaseOrderController::class, 'index'])->name('profile.edit');
Route::post('invoices/payment', [PurchaseOrderController::class, 'bulk'])->name('purchase_orders.bulk'); Route::post('purchase_orders/bulk', [PurchaseOrderController::class, 'bulk'])->name('purchase_orders.bulk');
Route::get('logout', [VendorContactLoginController::class, 'logout'])->name('logout'); 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", "resources/js/clients/purchase_orders/action-selectors.js",
"public/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( .js(
"resources/js/clients/invoices/payment.js", "resources/js/clients/invoices/payment.js",
"public/js/clients/invoices/payment.js" "public/js/clients/invoices/payment.js"