mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-05-24 02:14:21 -04:00
Stubs for vendor portal
This commit is contained in:
parent
8164d40007
commit
6674424244
145
app/Http/Controllers/VendorPortal/InvitationController.php
Normal file
145
app/Http/Controllers/VendorPortal/InvitationController.php
Normal 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);
|
||||
|
||||
// }
|
||||
|
||||
|
||||
|
||||
}
|
118
app/Http/Controllers/VendorPortal/PurchaseOrderController.php
Normal file
118
app/Http/Controllers/VendorPortal/PurchaseOrderController.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
@ -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,
|
||||
|
89
app/Http/Livewire/PurchaseOrdersTable.php
Normal file
89
app/Http/Livewire/PurchaseOrdersTable.php
Normal 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
|
||||
]);
|
||||
}
|
||||
}
|
@ -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*/
|
||||
|
51
app/Http/Middleware/VendorLocale.php
Normal file
51
app/Http/Middleware/VendorLocale.php
Normal 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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
||||
}
|
||||
}
|
@ -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:
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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'));
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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') }} © {{ 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>
|
@ -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
|
168
resources/views/portal/ninja2020/layout/vendor_app.blade.php
Normal file
168
resources/views/portal/ninja2020/layout/vendor_app.blade.php
Normal 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>
|
@ -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
|
@ -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
|
@ -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');
|
||||
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user