Stubs for vendor portal

This commit is contained in:
David Bomba 2022-06-13 19:59:24 +10:00
parent 8164d40007
commit 6674424244
24 changed files with 1217 additions and 5 deletions

View File

@ -0,0 +1,145 @@
<?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\VendorPortal;
use App\Events\Credit\CreditWasViewed;
use App\Events\Invoice\InvoiceWasViewed;
use App\Events\Misc\InvitationWasViewed;
use App\Events\Quote\QuoteWasViewed;
use App\Http\Controllers\Controller;
use App\Jobs\Entity\CreateRawPdf;
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\CreditInvitation;
use App\Models\InvoiceInvitation;
use App\Models\Payment;
use App\Models\PurchaseOrderInvitation;
use App\Models\QuoteInvitation;
use App\Services\ClientPortal\InstantPayment;
use App\Utils\CurlUtils;
use App\Utils\Ninja;
use App\Utils\Traits\MakesDates;
use App\Utils\Traits\MakesHash;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Str;
/**
* Class InvitationController.
*/
class InvitationController extends Controller
{
use MakesHash;
use MakesDates;
public function purchaseOrder(string $invitation_key)
{
Auth::logout();
$invitation = PurchaseOrderInvitation::where('key', $invitation_key)
->whereHas('purchase_order', function ($query) {
$query->where('is_deleted',0);
})
->with('contact.vendor')
->first();
if(!$invitation)
return abort(404,'The resource is no longer available.');
if($invitation->contact->trashed())
$invitation->contact->restore();
$vendor_contact = $invitation->contact;
$entity = 'purchase_order';
if(empty($vendor_contact->email))
$vendor_contact->email = Str::random(15) . "@example.com"; $vendor_contact->save();
if (request()->has('vendor_hash') && request()->input('vendor_hash') == $invitation->contact->vendor->vendor_hash) {
request()->session()->invalidate();
auth()->guard('vendor')->loginUsingId($vendor_contact->id, true);
} else {
nlog("else - default - login contact");
request()->session()->invalidate();
auth()->guard('vendor')->loginUsingId($vendor_contact->id, true);
}
if (auth()->guard('vendor')->user() && ! request()->has('silent') && ! $invitation->viewed_date) {
if(!session()->get('is_silent')){
$invitation->markViewed();
event(new InvitationWasViewed($invitation->purchase_order, $invitation, $invitation->company, Ninja::eventVars()));
}
}
else{
return redirect()->route('vendor.'.$entity.'.show', [$entity => $this->encodePrimaryKey($invitation->purchase_order_id), 'silent' => session()->get('is_silent')]);
}
return redirect()->route('vendor.'.$entity.'.show', [$entity => $this->encodePrimaryKey($invitation->purchase_order_id)]);
}
// public function routerForDownload(string $entity, string $invitation_key)
// {
// set_time_limit(45);
// if(Ninja::isHosted())
// return $this->returnRawPdf($entity, $invitation_key);
// return redirect('client/'.$entity.'/'.$invitation_key.'/download_pdf');
// }
// private function returnRawPdf(string $entity, string $invitation_key)
// {
// if(!in_array($entity, ['invoice', 'credit', 'quote', 'recurring_invoice']))
// return response()->json(['message' => 'Invalid resource request']);
// $key = $entity.'_id';
// $entity_obj = 'App\Models\\'.ucfirst(Str::camel($entity)).'Invitation';
// $invitation = $entity_obj::where('key', $invitation_key)
// ->with('contact.client')
// ->firstOrFail();
// if(!$invitation)
// return response()->json(["message" => "no record found"], 400);
// $file_name = $invitation->purchase_order->numberFormatter().'.pdf';
// $file = CreateRawPdf::dispatchNow($invitation, $invitation->company->db);
// $headers = ['Content-Type' => 'application/pdf'];
// if(request()->input('inline') == 'true')
// $headers = array_merge($headers, ['Content-Disposition' => 'inline']);
// return response()->streamDownload(function () use($file) {
// echo $file;
// }, $file_name, $headers);
// }
}

View File

@ -0,0 +1,118 @@
<?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\VendorPortal;
use App\Events\Misc\InvitationWasViewed;
use App\Events\PurchaseOrder\PurchaseOrderWasViewed;
use App\Http\Controllers\Controller;
use App\Http\Requests\VendorPortal\PurchaseOrders\ShowPurchaseOrderRequest;
use App\Http\Requests\VendorPortal\PurchaseOrders\ShowPurchaseOrdersRequest;
use App\Models\PurchaseOrder;
use App\Utils\Ninja;
use App\Utils\Traits\MakesDates;
use App\Utils\Traits\MakesHash;
use Illuminate\Contracts\View\Factory;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\View\View;
class PurchaseOrderController extends Controller
{
use MakesHash, MakesDates;
public const MODULE_RECURRING_INVOICES = 1;
public const MODULE_CREDITS = 2;
public const MODULE_QUOTES = 4;
public const MODULE_TASKS = 8;
public const MODULE_EXPENSES = 16;
public const MODULE_PROJECTS = 32;
public const MODULE_VENDORS = 64;
public const MODULE_TICKETS = 128;
public const MODULE_PROPOSALS = 256;
public const MODULE_RECURRING_EXPENSES = 512;
public const MODULE_RECURRING_TASKS = 1024;
public const MODULE_RECURRING_QUOTES = 2048;
public const MODULE_INVOICES = 4096;
public const MODULE_PROFORMAL_INVOICES = 8192;
public const MODULE_PURCHASE_ORDERS = 16384;
/**
* Display list of invoices.
*
* @return Factory|View
*/
public function index(ShowPurchaseOrdersRequest $request)
{
return $this->render('purchase_orders.index');
}
/**
* Show specific invoice.
*
* @param ShowInvoiceRequest $request
* @param Invoice $invoice
*
* @return Factory|View
*/
public function show(ShowPurchaseOrderRequest $request, PurchaseOrder $purchase_order)
{
set_time_limit(0);
$invitation = $purchase_order->invitations()->where('vendor_contact_id', auth()->guard('vendor')->user()->id)->first();
if ($invitation && auth()->guard('vendor') && !session()->get('is_silent') && ! $invitation->viewed_date) {
$invitation->markViewed();
event(new InvitationWasViewed($purchase_order, $invitation, $purchase_order->company, Ninja::eventVars()));
event(new PurchaseOrderWasViewed($invitation, $invitation->company, Ninja::eventVars()));
}
$data = [
'purchase_order' => $purchase_order,
'key' => $invitation ? $invitation->key : false,
'settings' => $purchase_order->company->settings,
'sidebar' => $this->sidebarMenu(),
'company' => $purchase_order->company
];
if ($request->query('mode') === 'fullscreen') {
return render('purchase_orders.show-fullscreen', $data);
}
return $this->render('purchase_orders.show', $data);
}
private function sidebarMenu() :array
{
$enabled_modules = auth()->guard('vendor')->user()->company->enabled_modules;
$data = [];
// TODO: Enable dashboard once it's completed.
// $this->settings->enable_client_portal_dashboard
// $data[] = [ 'title' => ctrans('texts.dashboard'), 'url' => 'client.dashboard', 'icon' => 'activity'];
if (self::MODULE_PURCHASE_ORDERS & $enabled_modules) {
$data[] = ['title' => ctrans('texts.purchase_orders'), 'url' => 'vendor.purchase_orders.index', 'icon' => 'file-text'];
}
// $data[] = ['title' => ctrans('texts.documents'), 'url' => 'client.documents.index', 'icon' => 'download'];
return $data;
}
}

View File

@ -42,6 +42,7 @@ use App\Http\Middleware\TrimStrings;
use App\Http\Middleware\TrustProxies;
use App\Http\Middleware\UrlSetDb;
use App\Http\Middleware\UserVerified;
use App\Http\Middleware\VendorLocale;
use App\Http\Middleware\VerifyCsrfToken;
use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth;
use Illuminate\Auth\Middleware\Authorize;
@ -158,6 +159,7 @@ class Kernel extends HttpKernel
'api_db' => SetDb::class,
'company_key_db' => SetDbByCompanyKey::class,
'locale' => Locale::class,
'vendor_locale' => VendorLocale::class,
'contact_register' => ContactRegister::class,
'shop_token_auth' => ShopTokenAuth::class,
'phantom_secret' => PhantomSecret::class,

View File

@ -0,0 +1,89 @@
<?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\Livewire;
use App\Libraries\MultiDB;
use App\Models\Invoice;
use App\Models\PurchaseOrder;
use App\Utils\Traits\WithSorting;
use Carbon\Carbon;
use Livewire\Component;
use Livewire\WithPagination;
class PurchaseOrdersTable extends Component
{
use WithPagination, WithSorting;
public $per_page = 10;
public $status = [];
public $company;
public function mount()
{
MultiDB::setDb($this->company->db);
$this->sort_asc = false;
$this->sort_field = 'date';
}
public function render()
{
$local_status = [];
$query = PurchaseOrder::query()
->with('vendor.contacts')
->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc')
->where('company_id', $this->company->id)
->where('is_deleted', false);
// if (in_array('paid', $this->status)) {
// $local_status[] = Invoice::STATUS_PAID;
// }
// if (in_array('unpaid', $this->status)) {
// $local_status[] = Invoice::STATUS_SENT;
// $local_status[] = Invoice::STATUS_PARTIAL;
// }
// if (in_array('overdue', $this->status)) {
// $local_status[] = Invoice::STATUS_SENT;
// $local_status[] = Invoice::STATUS_PARTIAL;
// }
if (count($local_status) > 0) {
$query = $query->whereIn('status_id', array_unique($local_status));
}
// if (in_array('overdue', $this->status)) {
// $query = $query->where(function ($query) {
// $query
// ->orWhere('due_date', '<', Carbon::now())
// ->orWhere('partial_due_date', '<', Carbon::now());
// });
// }
$query = $query
->where('vendor_id', auth()->guard('vendor')->user()->client_id)
// ->where('status_id', '<>', Invoice::STATUS_DRAFT)
// ->where('status_id', '<>', Invoice::STATUS_CANCELLED)
->withTrashed()
->paginate($this->per_page);
return render('components.livewire.purchase_orders-table', [
'purchase_orders' => $query
]);
}
}

View File

@ -46,7 +46,7 @@ class SetInviteDb
if($entity == "pay")
$entity = "invoice";
if(!in_array($entity, ['invoice','quote','credit','recurring_invoice']))
if(!in_array($entity, ['invoice','quote','credit','recurring_invoice','purchase_order']))
abort(404,'I could not find this resource.');
/* Try and determine the DB from the invitation key STRING*/

View File

@ -0,0 +1,51 @@
<?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\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Auth;
class VendorLocale
{
/**
* Handle an incoming request.
*
* @param Request $request
* @param Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
/*LOCALE SET */
if ($request->has('lang')) {
$locale = $request->input('lang');
App::setLocale($locale);
} elseif (auth()->guard('vendor')->user()) {
App::setLocale(auth()->guard('vendor')->user()->company->locale());
} elseif (auth()->user()) {
try{
App::setLocale(auth()->user()->company()->getLocale());
}
catch(\Exception $e){
}
} else {
App::setLocale(config('ninja.i18n.locale'));
}
return $next($request);
}
}

View File

@ -0,0 +1,29 @@
<?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\Requests\Request;
use App\Http\ViewComposers\PortalComposer;
class ShowPurchaseOrderRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return (int)auth()->guard('vendor')->user()->vendor_id === (int)$this->purchase_order->vendor_id
&& auth()->guard('vendor')->user()->company->enabled_modules & PortalComposer::MODULE_PURCHASE_ORDERS;
}
}

View File

@ -0,0 +1,29 @@
<?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\Requests\Request;
use App\Http\ViewComposers\PortalComposer;
class ShowPurchaseOrdersRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return auth()->guard('vendor')->user()->company->enabled_modules & PortalComposer::MODULE_PURCHASE_ORDERS;
}
}

View File

@ -88,6 +88,7 @@ class Account extends BaseModel
const FEATURE_TASKS = 'tasks';
const FEATURE_EXPENSES = 'expenses';
const FEATURE_QUOTES = 'quotes';
const FEATURE_PURCHASE_ORDERS = 'purchase_orders';
const FEATURE_CUSTOMIZE_INVOICE_DESIGN = 'custom_designs';
const FEATURE_DIFFERENT_DESIGNS = 'different_designs';
const FEATURE_EMAIL_TEMPLATES_REMINDERS = 'template_reminders';
@ -163,6 +164,7 @@ class Account extends BaseModel
case self::FEATURE_TASKS:
case self::FEATURE_EXPENSES:
case self::FEATURE_QUOTES:
case self::FEATURE_PURCHASE_ORDERS:
return true;
case self::FEATURE_CUSTOMIZE_INVOICE_DESIGN:

View File

@ -18,6 +18,7 @@ use App\Jobs\Entity\CreateEntityPdf;
use App\Jobs\Vendor\CreatePurchaseOrderPdf;
use App\Services\PurchaseOrder\PurchaseOrderService;
use App\Utils\Ninja;
use App\Utils\Traits\MakesDates;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Storage;
@ -26,6 +27,7 @@ class PurchaseOrder extends BaseModel
{
use Filterable;
use SoftDeletes;
use MakesDates;
protected $fillable = [
'number',
@ -99,9 +101,28 @@ class PurchaseOrder extends BaseModel
const STATUS_DRAFT = 1;
const STATUS_SENT = 2;
const STATUS_PARTIAL = 3;
const STATUS_APPLIED = 4;
const STATUS_APPROVED = 3;
const STATUS_CANCELLED = 4;
public static function stringStatus(int $status)
{
switch ($status) {
case self::STATUS_DRAFT:
return ctrans('texts.draft');
break;
case self::STATUS_SENT:
return ctrans('texts.sent');
break;
case self::STATUS_APPROVED:
return ctrans('texts.approved');
break;
case self::STATUS_CANCELLED:
return ctrans('texts.cancelled');
break;
// code...
break;
}
}
public function assigned_user()
{
return $this->belongsTo(User::class, 'assigned_user_id', 'id')->withTrashed();

View File

@ -73,6 +73,24 @@ class VendorContact extends Authenticatable implements HasLocalePreference
'vendor_id',
];
public function avatar()
{
if ($this->avatar) {
return $this->avatar;
}
return asset('images/svg/user.svg');
}
public function setAvatarAttribute($value)
{
if (! filter_var($value, FILTER_VALIDATE_URL) && $value) {
$this->attributes['avatar'] = url('/').$value;
} else {
$this->attributes['avatar'] = $value;
}
}
public function getEntityType()
{
return self::class;

View File

@ -50,6 +50,8 @@ class RouteServiceProvider extends ServiceProvider
$this->mapContactApiRoutes();
$this->mapVendorsApiRoutes();
$this->mapClientApiRoutes();
$this->mapShopApiRoutes();
@ -125,7 +127,7 @@ class RouteServiceProvider extends ServiceProvider
protected function mapVendorsApiRoutes()
{
Route::prefix('')
->middleware('vendor')
->middleware('client')
->namespace($this->namespace)
->group(base_path('routes/vendor.php'));
}

View File

@ -4623,7 +4623,10 @@ $LANG = array(
'purchase_order_message' => 'To view your purchase order for :amount, click the link below.',
'view_purchase_order' => 'View Purchase Order',
'purchase_orders_backup_subject' => 'Your purchase orders are ready for download',
'notification_purchase_order_viewed_subject' => 'Purchase Order :invoice was viewed by :client',
'notification_purchase_order_viewed' => 'The following vendor :client viewed Purchase Order :invoice for :amount.',
'purchase_order_date' => 'Purchase Order Date',
'purchase_orders' => 'Purchase Orders',
);
return $LANG;

View File

@ -0,0 +1,76 @@
<div class="hidden md:flex md:flex-shrink-0">
<div class="flex flex-col w-64">
<div class="flex items-center h-16 flex-shrink-0 px-4 bg-white border-r justify-center z-10">
<a href="{{ route('vendor.dashboard') }}">
<img class="h-10 w-auto" src="{!! auth()->guard('vendor')->user()->company->present()->logo($settings) !!}"
alt="{{ auth()->guard('vendor')->user()->company->present()->name() }} logo"/>
</a>
</div>
<div class="h-0 flex-1 flex flex-col overflow-y-auto z-0 border-r">
<nav class="flex-1 pb-4 pt-0 bg-white">
@foreach($sidebar as $row)
<a class="group flex items-center p-4 text-sm leading-5 font-medium hover:font-semibold focus:outline-none focus:bg-primary-darken transition ease-in-out duration-150 {{ isActive($row['url'], true) ? 'bg-primary text-white' : 'text-gray-900' }}"
href="{{ route($row['url']) }}">
@if(isActive($row['url'], true))
<img src="{{ asset('images/svg/' . $row['icon'] . '.svg') }}"
class="w-5 h-5 fill-current text-white mr-3" alt=""/>
@else
<img src="{{ asset('images/svg/dark/' . $row['icon'] . '.svg') }}"
class="w-5 h-5 fill-current text-white mr-3" alt=""/>
@endif
<span>{{ $row['title'] }}</span>
</a>
@endforeach
</nav>
@if(!auth()->guard('vendor')->user()->user->account->isPaid())
<div class="flex-shrink-0 flex bg-white p-4 justify-center">
<div class="flex items-center">
<a target="_blank" href="https://www.facebook.com/invoiceninja/">
<svg class="text-gray-900 hover:text-gray-300 mr-4" xmlns="http://www.w3.org/2000/svg"
width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"></path>
</svg>
</a>
<a target="_blank" href="https://twitter.com/invoiceninja">
<svg class="text-gray-900 hover:text-gray-300 mr-4" xmlns="http://www.w3.org/2000/svg"
width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path
d="M23 3a10.9 10.9 0 0 1-3.14 1.53 4.48 4.48 0 0 0-7.86 3v1A10.66 10.66 0 0 1 3 4s-4 9 5 13a11.64 11.64 0 0 1-7 2c9 5 20 0 20-11.5a4.5 4.5 0 0 0-.08-.83A7.72 7.72 0 0 0 23 3z"></path>
</svg>
</a>
<a target="_blank" href="https://github.com/invoiceninja/invoiceninja">
<svg class="text-gray-900 hover:text-gray-300 mr-4" xmlns="http://www.w3.org/2000/svg"
width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path
d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path>
</svg>
</a>
<a target="_blank" href="https://www.invoiceninja.com/contact">
<svg class="text-gray-900 hover:text-gray-300 mr-4" xmlns="http://www.w3.org/2000/svg"
width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path
d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path>
<polyline points="22,6 12,13 2,6"></polyline>
</svg>
</a>
<a target="_blank" href="https://www.youtube.com/channel/UCXAHcBvhW05PDtWYIq7WDFA">
<svg class="text-gray-900 hover:text-gray-300 mr-4" xmlns="http://www.w3.org/2000/svg"
width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path
d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"></path>
</svg>
</a>
</div>
</div>
@endif
</div>
<div class="flex-shrink-0 w-14"></div>
</div>
</div>

View File

@ -0,0 +1,42 @@
<div class="relative z-10 flex-shrink-0 flex h-16 bg-white shadow" xmlns:x-transition="http://www.w3.org/1999/xhtml">
<button @click.stop="sidebarOpen = true" class="px-4 border-r border-gray-200 text-gray-500 focus:outline-none focus:bg-gray-100 focus:text-gray-600 md:hidden">
<svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h7" />
</svg>
</button>
<div class="flex-1 px-3 md:px-8 flex justify-between items-center">
<span class="text-xl text-gray-900" data-ref="meta-title">@yield('meta_title')</span>
<div class="flex items-center md:ml-6 md:mr-2">
<div @click.away="open = false" class="ml-3 relative" x-data="{ open: false }">
<div>
<button data-ref="client-profile-dropdown" @click="open = !open"
class="max-w-xs flex items-center text-sm rounded-full focus:outline-none focus:ring">
<img class="h-8 w-8 rounded-full" src="{{ auth()->guard('vendor')->user()->avatar() }}" alt=""/>
<span class="ml-2 hidden sm:block">{{ auth()->guard('vendor')->user()->present()->name() }}</span>
</button>
</div>
<div x-show="open" style="display:none;" x-transition:enter="transition ease-out duration-100"
x-transition:enter-start="transform opacity-0 scale-95"
x-transition:enter-end="transform opacity-100 scale-100"
x-transition:leave="transition ease-in duration-75"
x-transition:leave-start="transform opacity-100 scale-100"
x-transition:leave-end="transform opacity-0 scale-95"
class="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg">
<div class="py-1 rounded-md bg-white ring-1 ring-black ring-opacity-5">
<a data-ref="client-profile-dropdown-settings"
href="{{ route('vendor.profile.edit', ['vendor_contact' => auth()->guard('vendor')->user()->hashed_id]) }}"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition ease-in-out duration-150">
{{ ctrans('texts.profile') }}
</a>
<a href="{{ route('client.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>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,45 @@
<div
class="main_layout h-screen flex overflow-hidden bg-gray-100"
x-data="{ sidebarOpen: false }"
@keydown.window.escape="sidebarOpen = false"
id="main-sidebar">
@if($settings && $settings->enable_client_portal)
<!-- Off-canvas menu for mobile -->
@include('portal.ninja2020.components.general.sidebar.vendor_mobile', ['sidebar' => $sidebar])
<!-- Static sidebar for desktop -->
@unless(request()->query('sidebar') === 'hidden')
@include('portal.ninja2020.components.general.sidebar.vendor_desktop', ['sidebar' => $sidebar])
@endunless
@endif
<div class="flex flex-col w-0 flex-1 overflow-hidden">
@if($settings && $settings->enable_client_portal)
@include('portal.ninja2020.components.general.sidebar.vendor_header', ['sidebar' => $sidebar])
@endif
<main
class="flex-1 relative z-0 overflow-y-auto pt-6 focus:outline-none"
tabindex="0" x-data
x-init="$el.focus()">
<div class="mx-auto px-4 sm:px-6 md:px-8">
@yield('header')
</div>
<div class="mx-auto px-4 sm:px-6 md:px-8">
<div class="pt-4 py-6">
@includeWhen(session()->has('success'), 'portal.ninja2020.components.general.messages.success')
{{ $slot }}
</div>
</div>
</main>
@include('portal.ninja2020.components.general.vendor_footer')
</div>
</div>
<script>
</script>

View File

@ -0,0 +1,65 @@
<div class="md:hidden">
<div @click="sidebarOpen = false" class="fixed inset-0 z-30 bg-gray-600 opacity-0 pointer-events-none transition-opacity ease-linear duration-300" :class="{'opacity-75 pointer-events-auto': sidebarOpen, 'opacity-0 pointer-events-none': !sidebarOpen}"></div>
<div class="fixed inset-y-0 left-0 flex flex-col z-40 max-w-xs w-full pt-5 pb-4 bg-white transform ease-in-out duration-300 -translate-x-full" :class="{'translate-x-0': sidebarOpen, '-translate-x-full': !sidebarOpen}">
<div class="absolute top-0 right-0 -mr-14 p-1">
<button x-show="sidebarOpen" @click="sidebarOpen = false" class="flex items-center justify-center h-12 w-12 rounded-full focus:outline-none focus:bg-gray-600">
<svg class="h-6 w-6 text-white" stroke="currentColor" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="flex-shrink-0 flex items-center px-4">
<img class="h-8 w-auto" src="{!! auth()->guard('vendor')->user()->company->present()->logo($settings) !!}" alt="{{ auth()->guard('vendor')->user()->company->present()->name() }} logo" />
</div>
<div class="mt-5 flex-1 h-0 overflow-y-auto">
<nav class="flex-1 pb-4 pt-0 bg-white">
@foreach($sidebar as $row)
<a class="group flex items-center p-4 text-sm leading-5 font-medium hover:font-semibold focus:outline-none focus:font-semibold transition ease-in-out duration-150 {{ isActive($row['url'], true) ? 'bg-primary text-white' : 'text-gray-900' }}" href="{{ route($row['url']) }}">
@if(isActive($row['url'], true))
<img src="{{ asset('images/svg/' . $row['icon'] . '.svg') }}"
class="w-5 h-5 fill-current mr-3" alt=""/>
@else
<img src="{{ asset('images/svg/dark/' . $row['icon'] . '.svg') }}"
class="w-5 h-5 fill-current mr-3" alt=""/>
@endif
<span>{{ $row['title'] }}</span>
</a>
@endforeach
</nav>
@if(!auth()->guard('vendor')->user()->user->account->isPaid())
<div class="flex-shrink-0 flex bg-white p-4 justify-center">
<div class="flex items-center">
<a target="_blank" href="https://www.facebook.com/invoiceninja/">
<svg class="text-gray-900 hover:text-gray-300 mr-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"></path>
</svg>
</a>
<a target="_blank" href="https://twitter.com/invoiceninja">
<svg class="text-gray-900 hover:text-gray-300 mr-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M23 3a10.9 10.9 0 0 1-3.14 1.53 4.48 4.48 0 0 0-7.86 3v1A10.66 10.66 0 0 1 3 4s-4 9 5 13a11.64 11.64 0 0 1-7 2c9 5 20 0 20-11.5a4.5 4.5 0 0 0-.08-.83A7.72 7.72 0 0 0 23 3z"></path>
</svg>
</a>
<a target="_blank" href="https://github.com/invoiceninja/invoiceninja">
<svg class="text-gray-900 hover:text-gray-300 mr-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path>
</svg> </a>
<a target="_blank" href="https://www.invoiceninja.com/contact">
<svg class="text-gray-900 hover:text-gray-300 mr-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path>
<polyline points="22,6 12,13 2,6"></polyline>
</svg>
</a>
<a target="_blank" href="https://www.youtube.com/channel/UCXAHcBvhW05PDtWYIq7WDFA">
<svg class="text-gray-900 hover:text-gray-300 mr-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"></path>
</svg>
</a>
</div>
</div>
@endif
</div>
<div class="flex-shrink-0 w-14"></div>
</div>
</div>

View File

@ -0,0 +1,48 @@
<footer class="bg-white px-4 py-5 shadow px-4 sm:px-6 md:px-8 flex justify-center border-t border-gray-200 justify-between items-center" x-data="{ privacy: false, tos: false }">
<section>
@if(auth()->guard('vendor')->user() && auth()->guard('vendor')->user()->user->account->isPaid())
<span class="text-xs md:text-sm text-gray-700">{{ ctrans('texts.footer_label', ['company' => auth()->guard('vendor')->user()->vendor->company->present()->name(), 'year' => date('Y')]) }}</span>
@else
<span href="https://invoiceninja.com" target="_blank" class="text-xs md:text-sm text-gray-700">
{{ ctrans('texts.copyright') }} &copy; {{ date('Y') }}
<a class="text-primary hover:underline" href="https://invoiceninja.com" target="_blank">Invoice Ninja</a>.
</span>
@endif
<div class="flex items-center">
@if(strlen($settings->client_portal_privacy_policy) > 1)
<a x-on:click="privacy = true; tos = false" href="#" class="hover:underline text-sm primary-color flex items-center mr-2">{{ __('texts.privacy_policy')}}</a>
@endif
@if(strlen($settings->client_portal_privacy_policy) > 1 && strlen($settings->client_portal_terms) > 1)
<!-- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-minus">
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg> Long dash between items. -->
@endif
@if(strlen($settings->client_portal_terms) > 1)
<a x-on:click="privacy = false; tos = true" href="#" class="hover:underline text-sm primary-color flex items-center mr-2">{{ __('texts.terms')}}</a>
@endif
</div>
</section>
@if(auth()->guard('vendor')->user()->user && !auth()->guard('vendor')->user()->user->account->isPaid())
<a href="https://invoiceninja.com" target="_blank">
<img class="h-8" src="{{ asset('images/invoiceninja-black-logo-2.png') }}" alt="Invoice Ninja Logo">
</a>
@endif
@if(strlen($settings->client_portal_privacy_policy) > 1)
@component('portal.ninja2020.components.general.pop-up', ['title' => __('texts.privacy_policy') ,'show_property' => 'privacy'])
{!! $settings->client_portal_privacy_policy !!}
@endcomponent
@endif
@if(strlen($settings->client_portal_terms) > 1)
@component('portal.ninja2020.components.general.pop-up', ['title' => __('texts.terms') ,'show_property' => 'tos'])
{!! $settings->client_portal_terms !!}
@endcomponent
@endif
<div class="bg-gray-200 hidden"></div>
</footer>

View File

@ -0,0 +1,125 @@
<div>
<div class="flex items-center justify-between">
<div class="flex items-center">
<span class="hidden mr-2 text-sm md:block">{{ ctrans('texts.per_page') }}</span>
<select wire:model="per_page" class="py-1 text-sm form-select">
<option>5</option>
<option selected>10</option>
<option>15</option>
<option>20</option>
</select>
</div>
<div class="flex items-center">
<div class="mr-3">
<input wire:model="status" value="paid" type="checkbox" class="cursor-pointer form-checkbox" id="paid-checkbox">
<label for="paid-checkbox" class="text-sm cursor-pointer">{{ ctrans('texts.status_paid') }}</label>
</div>
<div class="mr-3">
<input wire:model="status" value="unpaid" type="checkbox" class="cursor-pointer form-checkbox" id="unpaid-checkbox">
<label for="unpaid-checkbox" class="text-sm cursor-pointer">{{ ctrans('texts.status_unpaid') }}</label>
</div>
<div class="mr-3">
<input wire:model="status" value="overdue" type="checkbox" class="cursor-pointer form-checkbox" id="overdue-checkbox">
<label for="overdue-checkbox" class="text-sm cursor-pointer">{{ ctrans('texts.past_due') }}</label>
</div>
</div>
</div>
<div class="py-2 -my-2 overflow-x-auto sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8">
<div class="inline-block min-w-full overflow-hidden align-middle rounded">
<table class="min-w-full mt-4 border border-gray-200 rounded shadow invoices-table">
<thead>
<tr>
<th class="px-6 py-3 text-xs font-medium leading-4 tracking-wider text-left text-white uppercase border-b border-gray-200 bg-primary">
<label>
<input type="checkbox" class="form-check form-check-parent">
</label>
</th>
<th class="px-6 py-3 text-xs font-medium leading-4 tracking-wider text-left text-white uppercase border-b border-gray-200 bg-primary">
<span role="button" wire:click="sortBy('number')" class="cursor-pointer">
{{ ctrans('texts.purchase_order_number_short') }}
</span>
</th>
<th class="px-6 py-3 text-xs font-medium leading-4 tracking-wider text-left text-white uppercase border-b border-gray-200 bg-primary">
<span role="button" wire:click="sortBy('date')" class="cursor-pointer">
{{ ctrans('texts.purchase_order_date') }}
</span>
</th>
<th class="px-6 py-3 text-xs font-medium leading-4 tracking-wider text-left text-white uppercase border-b border-gray-200 bg-primary">
<span role="button" wire:click="sortBy('amount')" class="cursor-pointer">
{{ ctrans('texts.amount') }}
</span>
</th>
<th class="px-6 py-3 text-xs font-medium leading-4 tracking-wider text-left text-white uppercase border-b border-gray-200 bg-primary">
<span role="button" wire:click="sortBy('balance')" class="cursor-pointer">
{{ ctrans('texts.balance') }}
</span>
</th>
<th class="px-6 py-3 text-xs font-medium leading-4 tracking-wider text-left text-white uppercase border-b border-gray-200 bg-primary">
<span role="button" wire:click="sortBy('due_date')" class="cursor-pointer">
{{ ctrans('texts.due_date') }}
</span>
</th>
<th class="px-6 py-3 text-xs font-medium leading-4 tracking-wider text-left text-white uppercase border-b border-gray-200 bg-primary">
<span role="button" wire:click="sortBy('status_id')" class="cursor-pointer">
{{ ctrans('texts.status') }}
</span>
</th>
<th class="px-white-3 border-b border-gray-200 bg-primary"></th>
</tr>
</thead>
<tbody>
@forelse($purchase_orders as $purchase_order)
<tr class="bg-white group hover:bg-gray-100">
<td class="px-6 py-4 text-sm font-medium leading-5 text-gray-900 whitespace-nowrap">
<label>
<input type="checkbox" class="form-check form-check-child" data-value="{{ $purchase_order->hashed_id }}">
</label>
</td>
<td class="px-6 py-4 text-sm leading-5 text-gray-500 whitespace-nowrap">
{{ $purchase_order->number }}
</td>
<td class="px-6 py-4 text-sm leading-5 text-gray-500 whitespace-nowrap">
{{ $purchase_order->translateDate($purchase_order->date, $invoice->company->date_format(), $invoice->company->locale()) }}
</td>
<td class="px-6 py-4 text-sm leading-5 text-gray-500 whitespace-nowrap">
{{ App\Utils\Number::formatMoney($purchase_order->amount, $purchase_order->company) }}
</td>
<td class="px-6 py-4 text-sm leading-5 text-gray-500 whitespace-nowrap">
{{ App\Utils\Number::formatMoney($purchase_order->balance, $invoice->company) }}
</td>
<td class="px-6 py-4 text-sm leading-5 text-gray-500 whitespace-nowrap">
{{ $purchase_order->translateDate($purchase_order->due_date, $invoice->company->date_format(), $purchase_order->company->locale()) }}
</td>
<td class="px-6 py-4 text-sm leading-5 text-gray-500 whitespace-nowrap">
{!! App\Models\PurchaseOrder::badgeForStatus($purchase_order->status) !!}
</td>
<td class="flex items-center justify-end px-6 py-4 text-sm font-medium leading-5 whitespace-nowrap">
<a href="{{ route('vendor.purchase_order.show', $purchase_order->hashed_id) }}" class="button-link text-primary">
{{ ctrans('texts.view') }}
</a>
</td>
</tr>
@empty
<tr class="bg-white group hover:bg-gray-100">
<td class="px-6 py-4 text-sm leading-5 text-gray-500 whitespace-nowrap" colspan="100%">
{{ ctrans('texts.no_results') }}
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
<div class="flex justify-center mt-6 mb-6 md:justify-between">
@if($invoices && $invoices->total() > 0)
<span class="hidden text-sm text-gray-700 md:block mr-2">
{{ ctrans('texts.showing_x_of', ['first' => $purchase_orders->firstItem(), 'last' => $purchase_orders->lastItem(), 'total' => $purchase_orders->total()]) }}
</span>
@endif
{{ $purchase_orders->links('portal/ninja2020/vendor/pagination') }}
</div>
</div>
@push('footer')
<script src="{{ asset('js/clients/invoices/action-selectors.js') }}"></script>
@endpush

View File

@ -0,0 +1,168 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<!-- Error: {{ session('error') }} -->
@if (config('services.analytics.tracking_id'))
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-122229484-1"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', '{{ config('services.analytics.tracking_id') }}', {'anonymize_ip': true});
function trackEvent(category, action) {
ga('send', 'event', category, action, this.src);
}
</script>
<script>
Vue.config.devtools = true;
</script>
@else
<script>
function gtag() {
}
</script>
@endif
<!-- Title -->
@if(isset($company->account) && !$company->account->isPaid())
<title>@yield('meta_title', '') Invoice Ninja</title>
@elseif(isset($company) && !is_null($company))
<title>@yield('meta_title', '') {{ $company->present()->name() }}</title>
@else
<title>@yield('meta_title', '')</title>
@endif
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="@yield('meta_description')"/>
<!-- CSRF Token -->
<meta name="csrf-token" content="{{ csrf_token() }}">
<!-- Scripts -->
<script src="{{ mix('js/app.js') }}" defer></script>
<script src="{{ asset('vendor/alpinejs@2.8.2/alpine.js') }}" defer></script>
<!-- Fonts -->
<link rel="dns-prefetch" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css?family=Open+Sans&display=swap" rel="stylesheet" type="text/css">
<!-- Styles -->
<link href="{{ mix('css/app.css') }}" rel="stylesheet">
@if(auth()->guard('vendor')->user() && !auth()->guard('vendor')->user()->user->account->isPaid())
<link href="{{ asset('favicon.png') }}" rel="shortcut icon" type="image/png">
@endif
<link rel="canonical" href="{{ config('ninja.site_url') }}/{{ request()->path() }}"/>
@if((bool) \App\Utils\Ninja::isSelfHost())
<style>
{!! $settings->portal_custom_css !!}
</style>
@endif
@livewireStyles
{{-- Feel free to push anything to header using @push('header') --}}
@stack('head')
@if((isset($company) && $company->account->isPaid() && !empty($settings->portal_custom_head)) || ((bool) \App\Utils\Ninja::isSelfHost() && !empty($settings->portal_custom_head)))
<div class="py-1 text-sm text-center text-white bg-primary">
{!! $settings->portal_custom_head !!}
</div>
@endif
<link rel="stylesheet" type="text/css" href="{{ asset('vendor/cookieconsent@3/cookieconsent.min.css') }}" />
</head>
@include('portal.ninja2020.components.primary-color')
<body class="antialiased">
@if(session()->has('message'))
<div class="py-1 text-sm text-center text-white bg-primary disposable-alert">
{{ session('message') }}
</div>
@endif
@component('portal.ninja2020.components.general.sidebar.vendor_main', ['settings' => $settings, 'sidebar' => $sidebar])
@yield('body')
@endcomponent
@livewireScripts
<script src="{{ asset('vendor/cookieconsent@3/cookieconsent.min.js') }}" data-cfasync="false"></script>
<script>
window.addEventListener("load", function(){
if (! window.cookieconsent) {
return;
}
window.cookieconsent.initialise({
"palette": {
"popup": {
"background": "#000"
},
"button": {
"background": "#f1d600"
},
},
"content": {
"href": "{{ config('ninja.privacy_policy_url.hosted') }}",
"message": "This website uses cookies to ensure you get the best experience on our website.",
"dismiss": "Got it!",
"link": "Learn more",
}
})}
);
</script>
@if($company && $company->google_analytics_key)
<script>
(function (i, s, o, g, r, a, m) {
i['GoogleAnalyticsObject'] = r;
i[r] = i[r] || function () {
(i[r].q = i[r].q || []).push(arguments)
}, i[r].l = 1 * new Date();
a = s.createElement(o),
m = s.getElementsByTagName(o)[0];
a.async = 1;
a.src = g;
m.parentNode.insertBefore(a, m)
})(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga');
ga('create', '{{ $company->google_analytics_key }}', 'auto');
ga('set', 'anonymizeIp', true);
ga('send', 'pageview');
function trackEvent(category, action) {
ga('send', 'event', category, action, this.src);
}
</script>
@endif
</body>
<footer>
@yield('footer')
@stack('footer')
@if((bool) \App\Utils\Ninja::isSelfHost() && !empty($settings->portal_custom_footer))
<div class="py-1 text-sm text-center text-white bg-primary">
{!! $settings->portal_custom_footer !!}
</div>
@endif
</footer>
@if((bool) \App\Utils\Ninja::isSelfHost())
<script>
{!! $settings->portal_custom_js !!}
</script>
@endif
</html>

View File

@ -0,0 +1,25 @@
@extends('portal.ninja2020.layout.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">
<form action="{{ route('vendor.purchase_orders.bulk') }}" method="post" id="bulkActions">
@csrf
<button type="submit" onclick="setTimeout(() => this.disabled = true, 0); setTimeout(() => this.disabled = false, 5000); return true;" class="button button-primary bg-primary" name="action" value="download">{{ ctrans('texts.download') }}</button>
</form>
</div>
<div class="flex flex-col mt-4">
@livewire('purchase_orders-table', ['company' => $company])
</div>
@endsection

View File

@ -0,0 +1,89 @@
@extends('portal.ninja2020.layout.vendor_app')
@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 }}">
@include('portal.ninja2020.components.no-cache')
<script src="{{ asset('vendor/signature_pad@2.3.2/signature_pad.min.js') }}"></script>
@endpush
@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>
@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])}}
- {{ \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="flex text-sm leading-6 font-medium text-gray-500">
<p class="pr-10">{{url("client/invoice/{$key}")}}</p>
<p><img class="h-5 w-5" src="{{ asset('assets/clippy.svg') }}" alt="Copy to clipboard"></p>
</div>
</div>
@endif
</div>
</div>
</div>
</div>
@endif
@include('portal.ninja2020.components.entity-documents', ['entity' => $purchase_order])
@include('portal.ninja2020.components.pdf-viewer', ['entity' => $purchase_order])
@include('portal.ninja2020.invoices.includes.terms', ['entities' => [$purchase_order], 'entity_type' => ctrans('texts.purchase_order')])
@include('portal.ninja2020.invoices.includes.signature')
@endsection
@section('footer')
<script src="{{ asset('js/clients/invoices/payment.js') }}"></script>
<script src="{{ asset('vendor/clipboard.min.js') }}"></script>
<script type="text/javascript">
var clipboard = new ClipboardJS('.btn');
</script>
@endsection

View File

@ -9,5 +9,25 @@
| is assigned the "api" middleware group. Enjoy building your API!
|
*/
use App\Http\Controllers\VendorPortal\InvitationController;
use App\Http\Controllers\VendorPortal\PurchaseOrderController;
use Illuminate\Support\Facades\Route;
Route::group(['middleware' => ['invite_db'], 'prefix' => 'vendor', 'as' => 'vendor.'], function () {
/*Invitation catches*/
Route::get('purchase_order/{invitation_key}', [InvitationController::class, 'purchaseOrder']);
// Route::get('purchase_order/{invitation_key}/download_pdf', 'PurchaseOrderController@downloadPdf')->name('recurring_invoice.download_invitation_key');
// Route::get('purchase_order/{invitation_key}/download', 'ClientPortal\InvitationController@routerForDownload');
});
Route::group(['middleware' => ['auth:vendor', 'vendor_locale', 'domain_db'], 'prefix' => 'vendor', 'as' => 'vendor.'], function () {
Route::get('dashboard', [PurchaseOrderController::class, 'index'])->name('dashboard');
Route::get('purchase_orders', [PurchaseOrderController::class, 'index'])->name('purchase_orders.index');
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');
});