Vendor Portal - Purchase Orders

This commit is contained in:
David Bomba 2022-06-14 22:18:20 +10:00
parent cbdf0a827c
commit 063d600bbd
19 changed files with 314 additions and 10 deletions

View File

@ -0,0 +1,54 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Controllers\Auth;
use App\Events\Contact\ContactLoggedIn;
use App\Http\Controllers\Controller;
use App\Http\ViewComposers\PortalComposer;
use App\Libraries\MultiDB;
use App\Models\Account;
use App\Models\ClientContact;
use App\Models\Company;
use App\Utils\Ninja;
use Auth;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Route;
class VendorContactLoginController extends Controller
{
use AuthenticatesUsers;
protected $redirectTo = '/vendor/purchase_orders';
public function __construct()
{
// $this->middleware('guest:vendor', ['except' => ['logout']]);
}
public function catch()
{
}
public function logout()
{
Auth::guard('vendor')->logout();
request()->session()->invalidate();
return redirect('/vendor');
}
}

View File

@ -84,6 +84,7 @@ class BaseController extends Controller
'company.products.documents',
'company.payments.paymentables',
'company.payments.documents',
'company.purchase_orders.documents',
'company.payment_terms.company',
'company.projects.documents',
'company.recurring_expenses',
@ -296,6 +297,13 @@ class BaseController extends Controller
if(!$user->hasPermission('view_project'))
$query->where('projects.user_id', $user->id)->orWhere('projects.assigned_user_id', $user->id);
},
'company.purchase_orders'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at)->with('documents');
if(!$user->hasPermission('view_purchase_order'))
$query->where('purchase_orders.user_id', $user->id)->orWhere('purchase_orders.assigned_user_id', $user->id);
},
'company.quotes'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at)->with('invitations', 'documents');
@ -533,6 +541,13 @@ class BaseController extends Controller
if(!$user->hasPermission('view_project'))
$query->where('projects.user_id', $user->id)->orWhere('projects.assigned_user_id', $user->id);
},
'company.purchase_orders'=> function ($query) use ($created_at, $user) {
$query->where('created_at', '>=', $created_at)->with('documents');
if(!$user->hasPermission('view_purchase_order'))
$query->where('purchase_orders.user_id', $user->id)->orWhere('purchase_orders.assigned_user_id', $user->id);
},
'company.quotes'=> function ($query) use ($created_at, $user) {
$query->where('created_at', '>=', $created_at)->with('invitations', 'documents');

View File

@ -35,6 +35,7 @@ class SubdomainController extends BaseController
'html',
'lb',
'shopify',
'beta',
];
public function __construct()

View File

@ -14,6 +14,7 @@ namespace App\Http\Controllers\VendorPortal;
use App\Events\Misc\InvitationWasViewed;
use App\Events\PurchaseOrder\PurchaseOrderWasViewed;
use App\Http\Controllers\Controller;
use App\Http\Requests\VendorPortal\PurchaseOrders\ProcessPurchaseOrdersInBulkRequest;
use App\Http\Requests\VendorPortal\PurchaseOrders\ShowPurchaseOrderRequest;
use App\Http\Requests\VendorPortal\PurchaseOrders\ShowPurchaseOrdersRequest;
use App\Models\PurchaseOrder;
@ -115,4 +116,73 @@ class PurchaseOrderController extends Controller
return $data;
}
public function bulk(ProcessPurchaseOrdersInBulkRequest $request)
{
$transformed_ids = $this->transformKeys($request->purchase_orders);
if ($request->input('action') == 'download') {
return $this->downloadInvoices((array) $transformed_ids);
}
return redirect()
->back()
->with('message', ctrans('texts.no_action_provided'));
}
public function downloadInvoices($ids)
{
$purchase_orders = PurchaseOrder::whereIn('id', $ids)
->where('vendor_id', auth()->guard('vendor')->user()->vendor_id)
->withTrashed()
->get();
if(count($purchase_orders) == 0)
return back()->with(['message' => ctrans('texts.no_items_selected')]);
if(count($purchase_orders) == 1){
$purchase_order = $purchase_orders->first();
$file = $purchase_order->service()->getPurchaseOrderPdf(auth()->guard('vendor')->user());
return response()->streamDownload(function () use($file) {
echo Storage::get($file);
}, basename($file), ['Content-Type' => 'application/pdf']);
}
return $this->buildZip($purchase_orders);
}
private function buildZip($purchase_orders)
{
// create new archive
$zipFile = new \PhpZip\ZipFile();
try{
foreach ($purchase_orders as $purchase_order) {
#add it to the zip
$zipFile->addFromString(basename($purchase_order->pdf_file_path()), file_get_contents($purchase_order->pdf_file_path(null, 'url', true)));
}
$filename = date('Y-m-d').'_'.str_replace(' ', '_', trans('texts.purchase_orders')).'.zip';
$filepath = sys_get_temp_dir() . '/' . $filename;
$zipFile->saveAsFile($filepath) // save the archive to a file
->close(); // close archive
return response()->download($filepath, $filename)->deleteFileAfterSend(true);
}
catch(\PhpZip\Exception\ZipException $e){
// handle exception
}
finally{
$zipFile->close();
}
}
}

View File

@ -0,0 +1,31 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Requests\VendorPortal\PurchaseOrders;
use App\Http\ViewComposers\PortalComposer;
use Illuminate\Foundation\Http\FormRequest;
class ProcessPurchaseOrdersInBulkRequest extends FormRequest
{
public function authorize()
{
return auth()->guard('vendor')->user()->vendor->company->enabled_modules & PortalComposer::MODULE_PURCHASE_ORDERS;
}
public function rules()
{
return [
'purchase_orders' => ['array'],
];
}
}

View File

@ -70,7 +70,7 @@ class CreatePurchaseOrderPdf implements ShouldQueue
*
* @param $invitation
*/
public function __construct($invitation, $disk = 'public')
public function __construct($invitation, $disk = null)
{
$this->invitation = $invitation;
$this->company = $invitation->company;
@ -83,7 +83,7 @@ class CreatePurchaseOrderPdf implements ShouldQueue
$this->vendor = $invitation->contact->vendor;
$this->vendor->load('company');
$this->disk = Ninja::isHosted() ? config('filesystems.default') : $disk;
$this->disk = $disk ?? config('filesystems.default');
}

View File

@ -14,6 +14,7 @@ namespace App\Models;
use App\DataMapper\CompanySettings;
use App\Models\Language;
use App\Models\Presenters\CompanyPresenter;
use App\Models\PurchaseOrder;
use App\Models\User;
use App\Services\Notification\NotificationService;
use App\Utils\Ninja;
@ -192,6 +193,11 @@ class Company extends BaseModel
return $this->hasMany(Subscription::class)->withTrashed();
}
public function purchase_orders()
{
return $this->hasMany(PurchaseOrder::class)->withTrashed();
}
public function task_statuses()
{
return $this->hasMany(TaskStatus::class)->withTrashed();

View File

@ -101,7 +101,7 @@ class PurchaseOrder extends BaseModel
const STATUS_DRAFT = 1;
const STATUS_SENT = 2;
const STATUS_APPROVED = 3;
const STATUS_ACCEPTED = 3;
const STATUS_CANCELLED = 4;
public static function stringStatus(int $status)
@ -113,8 +113,8 @@ class PurchaseOrder extends BaseModel
case self::STATUS_SENT:
return ctrans('texts.sent');
break;
case self::STATUS_APPROVED:
return ctrans('texts.approved');
case self::STATUS_ACCEPTED:
return ctrans('texts.accepted');
break;
case self::STATUS_CANCELLED:
return ctrans('texts.cancelled');
@ -134,8 +134,8 @@ class PurchaseOrder extends BaseModel
case self::STATUS_SENT:
return '<h5><span class="badge badge-primary">'.ctrans('texts.sent').'</span></h5>';
break;
case self::STATUS_APPROVED:
return '<h5><span class="badge badge-primary">'.ctrans('texts.approved').'</span></h5>';
case self::STATUS_ACCEPTED:
return '<h5><span class="badge badge-primary">'.ctrans('texts.accepted').'</span></h5>';
break;
case self::STATUS_CANCELLED:
return '<h5><span class="badge badge-secondary">'.ctrans('texts.cancelled').'</span></h5>';

View File

@ -341,7 +341,7 @@ class InvoiceService
if(Storage::disk(config('filesystems.default'))->exists($this->invoice->client->invoice_filepath($invitation) . $this->invoice->numberFormatter().'.pdf'))
Storage::disk(config('filesystems.default'))->delete($this->invoice->client->invoice_filepath($invitation) . $this->invoice->numberFormatter().'.pdf');
if(Ninja::isHosted() && Storage::disk(config('filesystems.default'))->exists($this->invoice->client->invoice_filepath($invitation) . $this->invoice->numberFormatter().'.pdf')) {
if(Ninja::isHosted() && Storage::disk('public')->exists($this->invoice->client->invoice_filepath($invitation) . $this->invoice->numberFormatter().'.pdf')) {
Storage::disk('public')->delete($this->invoice->client->invoice_filepath($invitation) . $this->invoice->numberFormatter().'.pdf');
}

View File

@ -29,6 +29,7 @@ use App\Models\Payment;
use App\Models\PaymentTerm;
use App\Models\Product;
use App\Models\Project;
use App\Models\PurchaseOrder;
use App\Models\Quote;
use App\Models\RecurringExpense;
use App\Models\RecurringInvoice;
@ -39,6 +40,7 @@ use App\Models\TaskStatus;
use App\Models\TaxRate;
use App\Models\User;
use App\Models\Webhook;
use App\Transformers\PurchaseOrderTransformer;
use App\Transformers\RecurringExpenseTransformer;
use App\Utils\Traits\MakesHash;
use stdClass;
@ -95,6 +97,7 @@ class CompanyTransformer extends EntityTransformer
'task_statuses',
'subscriptions',
'recurring_expenses',
'purchase_orders',
];
/**
@ -391,4 +394,11 @@ class CompanyTransformer extends EntityTransformer
return $this->includeCollection($company->subscriptions, $transformer, Subscription::class);
}
public function includePurchaseOrders(Company $company)
{
$transformer = new PurchaseOrderTransformer($this->serializer);
return $this->includeCollection($company->purchase_orders, $transformer, PurchaseOrder::class);
}
}

View File

@ -0,0 +1,2 @@
/*! For license information please see action-selectors.js.LICENSE.txt */
(()=>{function e(e,n){var r="undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(!r){if(Array.isArray(e)||(r=function(e,n){if(!e)return;if("string"==typeof e)return t(e,n);var r=Object.prototype.toString.call(e).slice(8,-1);"Object"===r&&e.constructor&&(r=e.constructor.name);if("Map"===r||"Set"===r)return Array.from(e);if("Arguments"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r))return t(e,n)}(e))||n&&e&&"number"==typeof e.length){r&&(e=r);var o=0,c=function(){};return{s:c,n:function(){return o>=e.length?{done:!0}:{done:!1,value:e[o++]}},e:function(e){throw e},f:c}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var i,a=!0,l=!1;return{s:function(){r=r.call(e)},n:function(){var e=r.next();return a=e.done,e},e:function(e){l=!0,i=e},f:function(){try{a||null==r.return||r.return()}finally{if(l)throw i}}}}function t(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n<t;n++)r[n]=e[n];return r}function n(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}(new(function(){function t(){!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,t),this.parentElement=document.querySelector(".form-check-parent"),this.parentForm=document.getElementById("bulkActions")}var r,o,c;return r=t,o=[{key:"watchCheckboxes",value:function(e){var t=this;document.querySelectorAll(".child-hidden-input").forEach((function(e){return e.remove()})),document.querySelectorAll(".form-check-child").forEach((function(n){e.checked?(n.checked=e.checked,t.processChildItem(n,document.getElementById("bulkActions"))):(n.checked=!1,document.querySelectorAll(".child-hidden-input").forEach((function(e){return e.remove()})))}))}},{key:"processChildItem",value:function(t,n){var r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};if(r.hasOwnProperty("single")&&document.querySelectorAll(".child-hidden-input").forEach((function(e){return e.remove()})),!1!==t.checked){var o=document.createElement("INPUT");o.setAttribute("name","purchase_orders[]"),o.setAttribute("value",t.dataset.value),o.setAttribute("class","child-hidden-input"),o.hidden=!0,n.append(o)}else{var c,i=document.querySelectorAll("input.child-hidden-input"),a=e(i);try{for(a.s();!(c=a.n()).done;){var l=c.value;l.value==t.dataset.value&&l.remove()}}catch(e){a.e(e)}finally{a.f()}}}},{key:"handle",value:function(){var t=this;this.parentElement.addEventListener("click",(function(){t.watchCheckboxes(t.parentElement)}));var n,r=e(document.querySelectorAll(".form-check-child"));try{var o=function(){var e=n.value;e.addEventListener("click",(function(){t.processChildItem(e,t.parentForm)}))};for(r.s();!(n=r.n()).done;)o()}catch(e){r.e(e)}finally{r.f()}}}],o&&n(r.prototype,o),c&&n(r,c),Object.defineProperty(r,"prototype",{writable:!1}),t}())).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

@ -4,6 +4,7 @@
"/js/clients/payments/authorize-credit-card-payment.js": "/js/clients/payments/authorize-credit-card-payment.js?id=803182f668c39d631ca5c55437876da4",
"/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/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,79 @@
/**
* 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 ActionSelectors {
constructor() {
this.parentElement = document.querySelector('.form-check-parent');
this.parentForm = document.getElementById('bulkActions');
}
watchCheckboxes(parentElement) {
document
.querySelectorAll('.child-hidden-input')
.forEach((element) => element.remove());
document.querySelectorAll('.form-check-child').forEach((child) => {
if (parentElement.checked) {
child.checked = parentElement.checked;
this.processChildItem(
child,
document.getElementById('bulkActions')
);
} else {
child.checked = false;
document
.querySelectorAll('.child-hidden-input')
.forEach((element) => element.remove());
}
});
}
processChildItem(element, parent, options = {}) {
if (options.hasOwnProperty('single')) {
document
.querySelectorAll('.child-hidden-input')
.forEach((element) => element.remove());
}
if (element.checked === false) {
let inputs = document.querySelectorAll('input.child-hidden-input');
for (let i of inputs) {
if (i.value == element.dataset.value) i.remove();
}
return;
}
let _temp = document.createElement('INPUT');
_temp.setAttribute('name', 'purchase_orders[]');
_temp.setAttribute('value', element.dataset.value);
_temp.setAttribute('class', 'child-hidden-input');
_temp.hidden = true;
parent.append(_temp);
}
handle() {
this.parentElement.addEventListener('click', () => {
this.watchCheckboxes(this.parentElement);
});
for (let child of document.querySelectorAll('.form-check-child')) {
child.addEventListener('click', () => {
this.processChildItem(child, this.parentForm);
});
}
}
}
/** @handle **/
new ActionSelectors().handle();

View File

@ -4628,7 +4628,7 @@ $LANG = array(
'purchase_order_date' => 'Purchase Order Date',
'purchase_orders' => 'Purchase Orders',
'purchase_order_number_placeholder' => 'Purchase Order # :purchase_order',
'accepted' => 'Accepted',
);
return $LANG;

View File

@ -121,5 +121,5 @@
</div>
@push('footer')
<script src="{{ asset('js/clients/invoices/action-selectors.js') }}"></script>
<script src="{{ asset('js/clients/purchase_orders/action-selectors.js') }}"></script>
@endpush

View File

@ -0,0 +1,18 @@
@extends('portal.ninja2020.layout.vendor_app')
@section('meta_title', ctrans('texts.purchase_orders'))
@section('header')
@if($errors->any())
<div class="alert alert-failure mb-4">
@foreach($errors->all() as $error)
<p>{{ $error }}</p>
@endforeach
</div>
@endif
@endsection
@section('body')
<div class="flex items-center">
<h1>Vendor Portal</h1>
</div>
@endsection

View File

@ -9,10 +9,13 @@
| is assigned the "api" middleware group. Enjoy building your API!
|
*/
use App\Http\Controllers\Auth\VendorContactLoginController;
use App\Http\Controllers\VendorPortal\InvitationController;
use App\Http\Controllers\VendorPortal\PurchaseOrderController;
use Illuminate\Support\Facades\Route;
Route::get('vendor', [VendorContactLoginController::class, 'catch'])->name('vendor.catchall')->middleware(['domain_db', 'contact_account','vendor_locale']); //catch all
Route::group(['middleware' => ['invite_db'], 'prefix' => 'vendor', 'as' => 'vendor.'], function () {
/*Invitation catches*/
Route::get('purchase_order/{invitation_key}', [InvitationController::class, 'purchaseOrder']);
@ -30,5 +33,6 @@ Route::group(['middleware' => ['auth:vendor', 'vendor_locale', 'domain_db'], 'pr
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');
});

4
webpack.mix.js vendored
View File

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