Merge pull request #7558 from turbo124/v5-stable

v5.4.0
This commit is contained in:
David Bomba 2022-06-16 17:05:19 +10:00 committed by GitHub
commit bcd3383b0b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
91 changed files with 460449 additions and 448512 deletions

1
.gitignore vendored
View File

@ -1,6 +1,7 @@
/node_modules
/public/hot
/public/storage
/public/react
/storage/*.key
/vendor
/.idea

View File

@ -1 +1 @@
5.3.100
5.4.0

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\Events\PurchaseOrder;
use App\Models\Company;
use App\Models\PurchaseOrder;
use App\Models\PurchaseOrderInvitation;
use App\Models\VendorContact;
use Illuminate\Queue\SerializesModels;
/**
* Class PurchaseOrderWasAccepted.
*/
class PurchaseOrderWasAccepted
{
use SerializesModels;
/**
* @var PurchaseOrder
*/
public $purchase_order;
public $company;
public $event_vars;
public $contact;
/**
* Create a new event instance.
*
* @param PurchaseOrder $purchase_order
* @param Company $company
* @param array $event_vars
*/
public function __construct(PurchaseOrder $purchase_order, VendorContact $contact, Company $company, array $event_vars)
{
$this->purchase_order = $purchase_order;
$this->contact = $contact;
$this->company = $company;
$this->event_vars = $event_vars;
}
}

View File

@ -13,6 +13,7 @@ namespace App\Events\PurchaseOrder;
use App\Models\Company;
use App\Models\PurchaseOrder;
use App\Models\PurchaseOrderInvitation;
use Illuminate\Queue\SerializesModels;
/**
@ -25,7 +26,7 @@ class PurchaseOrderWasViewed
/**
* @var PurchaseOrder
*/
public $purchase_order;
public $invitation;
public $company;
@ -38,9 +39,9 @@ class PurchaseOrderWasViewed
* @param Company $company
* @param array $event_vars
*/
public function __construct(PurchaseOrder $purchase_order, Company $company, array $event_vars)
public function __construct(PurchaseOrderInvitation $invitation, Company $company, array $event_vars)
{
$this->purchase_order = $purchase_order;
$this->invitation = $invitation;
$this->company = $company;
$this->event_vars = $event_vars;
}

View File

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

View File

@ -12,9 +12,11 @@
namespace App\Http\Controllers;
use App\Http\Requests\Account\CreateAccountRequest;
use App\Http\Requests\Account\UpdateAccountRequest;
use App\Jobs\Account\CreateAccount;
use App\Models\Account;
use App\Models\CompanyUser;
use App\Transformers\AccountTransformer;
use App\Transformers\CompanyUserTransformer;
use App\Utils\TruthSource;
use Illuminate\Foundation\Bus\DispatchesJobs;
@ -157,4 +159,22 @@ class AccountController extends BaseController
return $this->listResponse($ct);
}
public function update(UpdateAccountRequest $request, Account $account)
{
$fi = new \FilesystemIterator(public_path('react'), \FilesystemIterator::SKIP_DOTS);
if(iterator_count($fi) < 30)
return response()->json(['message' => 'React App Not Installed, Please install the React app before attempting to switch.'], 400);
$account->fill($request->all());
$account->save();
$this->entity_type = Account::class;
$this->entity_transformer = AccountTransformer::class;
return $this->itemResponse($account);
}
}

View File

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

View File

@ -84,6 +84,7 @@ class BaseController extends Controller
'company.products.documents',
'company.payments.paymentables',
'company.payments.documents',
'company.purchase_orders.documents',
'company.payment_terms.company',
'company.projects.documents',
'company.recurring_expenses',
@ -171,7 +172,12 @@ class BaseController extends Controller
*/
public function notFoundClient()
{
abort(404, 'Page not found in client portal.');
abort(404, 'Page not found in the client portal.');
}
public function notFoundVendor()
{
abort(404, 'Page not found in the vendor portal.');
}
/**
@ -296,6 +302,13 @@ class BaseController extends Controller
if(!$user->hasPermission('view_project'))
$query->where('projects.user_id', $user->id)->orWhere('projects.assigned_user_id', $user->id);
},
'company.purchase_orders'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at)->with('documents');
if(!$user->hasPermission('view_purchase_order'))
$query->where('purchase_orders.user_id', $user->id)->orWhere('purchase_orders.assigned_user_id', $user->id);
},
'company.quotes'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at)->with('invitations', 'documents');
@ -533,6 +546,13 @@ class BaseController extends Controller
if(!$user->hasPermission('view_project'))
$query->where('projects.user_id', $user->id)->orWhere('projects.assigned_user_id', $user->id);
},
'company.purchase_orders'=> function ($query) use ($created_at, $user) {
$query->where('created_at', '>=', $created_at)->with('documents');
if(!$user->hasPermission('view_purchase_order'))
$query->where('purchase_orders.user_id', $user->id)->orWhere('purchase_orders.assigned_user_id', $user->id);
},
'company.quotes'=> function ($query) use ($created_at, $user) {
$query->where('created_at', '>=', $created_at)->with('invitations', 'documents');
@ -780,7 +800,7 @@ class BaseController extends Controller
$this->buildCache();
if(config('ninja.react_app_enabled'))
if(Ninja::isSelfHost() && $account->set_react_as_default_ap)
return response()->view('react.index', $data)->header('X-Frame-Options', 'SAMEORIGIN', false);
else
return response()->view('index.index', $data)->header('X-Frame-Options', 'SAMEORIGIN', false);

View File

@ -154,6 +154,7 @@ class NinjaPlanController extends Controller
$recurring_invoice->auto_bill_enabled = $this->setAutoBillFlag($recurring_invoice->auto_bill);
$recurring_invoice->due_date_days = 'terms';
$recurring_invoice->next_send_date = now()->addDays(14)->format('Y-m-d');
$recurring_invoice->next_send_date_client = now()->addDays(14)->format('Y-m-d');
$recurring_invoice->save();
$r = $recurring_invoice->calc()->getRecurringInvoice();

View File

@ -135,6 +135,9 @@ class SelfUpdateController extends BaseController
nlog("Extracting zip");
//clean up old snappdf installations
$this->cleanOldSnapChromeBinaries();
// try{
// $s = new Snappdf;
// $s->getChromiumPath();
@ -190,6 +193,46 @@ class SelfUpdateController extends BaseController
}
private function cleanOldSnapChromeBinaries()
{
$current_revision = base_path('vendor/beganovich/snappdf/versions/revision.txt');
$current_revision_text = file_get_contents($current_revision);
$iterator = new \DirectoryIterator(base_path('vendor/beganovich/snappdf/versions'));
foreach ($iterator as $file)
{
if($file->isDir() && !$file->isDot() && ($current_revision_text != $file->getFileName()))
{
$directoryIterator = new \RecursiveDirectoryIterator(base_path('vendor/beganovich/snappdf/versions/'.$file->getFileName()), \RecursiveDirectoryIterator::SKIP_DOTS);
foreach (new \RecursiveIteratorIterator($directoryIterator) as $filex)
{
unlink($filex->getPathName());
}
$this->deleteDirectory(base_path('vendor/beganovich/snappdf/versions/'.$file->getFileName()));
}
}
}
private function deleteDirectory($dir) {
if (!file_exists($dir)) return true;
if (!is_dir($dir) || is_link($dir)) return unlink($dir);
foreach (scandir($dir) as $item) {
if ($item == '.' || $item == '..') continue;
if (!$this->deleteDirectory($dir . "/" . $item)) {
if (!$this->deleteDirectory($dir . "/" . $item)) return false;
};
}
return rmdir($dir);
}
private function postHookUpdate()
{
if(config('ninja.app_version') == '5.3.82')

View File

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

View File

@ -0,0 +1,144 @@
<?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);
}
session()->put('is_silent', request()->has('silent'));
if (auth()->guard('vendor')->user() && ! session()->get('is_silent') && ! $invitation->viewed_date) {
$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,229 @@
<?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\PurchaseOrderWasAccepted;
use App\Events\PurchaseOrder\PurchaseOrderWasViewed;
use App\Http\Controllers\Controller;
use App\Http\Requests\VendorPortal\PurchaseOrders\ProcessPurchaseOrdersInBulkRequest;
use App\Http\Requests\VendorPortal\PurchaseOrders\ShowPurchaseOrderRequest;
use App\Http\Requests\VendorPortal\PurchaseOrders\ShowPurchaseOrdersRequest;
use App\Jobs\Invoice\InjectSignature;
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', ['company' => auth()->user()->company, 'settings' => auth()->user()->company->settings, 'sidebar' => $this->sidebarMenu()]);
}
/**
* 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;
}
public function bulk(ProcessPurchaseOrdersInBulkRequest $request)
{
$transformed_ids = $this->transformKeys($request->purchase_orders);
if ($request->input('action') == 'download') {
return $this->downloadInvoices((array) $transformed_ids);
}
elseif ($request->input('action') == 'accept'){
return $this->acceptPurchaseOrder($request->all());
}
return redirect()
->back()
->with('message', ctrans('texts.no_action_provided'));
}
public function acceptPurchaseOrder($data)
{
$purchase_orders = PurchaseOrder::query()
->whereIn('id', $this->transformKeys($data['purchase_orders']))
->where('company_id', auth()->guard('vendor')->user()->vendor->company_id)
->whereIn('status_id', [PurchaseOrder::STATUS_DRAFT, PurchaseOrder::STATUS_SENT])
->cursor()->each(function ($purchase_order){
$purchase_order->service()
->markSent()
->applyNumber()
->setStatus(PurchaseOrder::STATUS_ACCEPTED)
->save();
if (request()->has('signature') && !is_null(request()->signature) && !empty(request()->signature)) {
InjectSignature::dispatch($purchase_order, request()->signature);
}
event(new PurchaseOrderWasAccepted($purchase_order, auth()->guard('vendor')->user(), $purchase_order->company, Ninja::eventVars()));
});
if(count($data['purchase_orders']) == 1){
$purchase_order = PurchaseOrder::whereIn('id', $this->transformKeys($data['purchase_orders']))->first();
return redirect()->route('vendor.purchase_order.show', ['purchase_order' => $purchase_order->hashed_id]);
}
else
return redirect()->route('vendor.purchase_orders.index');
}
public function downloadInvoices($ids)
{
$purchase_orders = PurchaseOrder::whereIn('id', $ids)
->where('vendor_id', auth()->guard('vendor')->user()->vendor_id)
->withTrashed()
->get();
if(count($purchase_orders) == 0)
return back()->with(['message' => ctrans('texts.no_items_selected')]);
if(count($purchase_orders) == 1){
$purchase_order = $purchase_orders->first();
$file = $purchase_order->service()->getPurchaseOrderPdf(auth()->guard('vendor')->user());
return response()->streamDownload(function () use($file) {
echo Storage::get($file);
}, basename($file), ['Content-Type' => 'application/pdf']);
}
return $this->buildZip($purchase_orders);
}
private function buildZip($purchase_orders)
{
// create new archive
$zipFile = new \PhpZip\ZipFile();
try{
foreach ($purchase_orders as $purchase_order) {
#add it to the zip
$zipFile->addFromString(basename($purchase_order->pdf_file_path()), file_get_contents($purchase_order->pdf_file_path(null, 'url', true)));
}
$filename = date('Y-m-d').'_'.str_replace(' ', '_', trans('texts.purchase_orders')).'.zip';
$filepath = sys_get_temp_dir() . '/' . $filename;
$zipFile->saveAsFile($filepath) // save the archive to a file
->close(); // close archive
return response()->download($filepath, $filename)->deleteFileAfterSend(true);
}
catch(\PhpZip\Exception\ZipException $e){
// handle exception
}
finally{
$zipFile->close();
}
}
}

View File

@ -0,0 +1,81 @@
<?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\Http\Controllers\Controller;
use App\Models\VendorContact;
use App\Utils\Traits\MakesHash;
use App\Utils\TranslationHelper;
use Illuminate\Http\Request;
class VendorContactController extends Controller
{
use MakesHash;
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;
public function edit(VendorContact $vendor_contact)
{
return $this->render('vendor_profile.edit', [
'contact' => $vendor_contact,
'vendor' => $vendor_contact->vendor,
'settings' => $vendor_contact->vendor->company->settings,
'company' => $vendor_contact->vendor->company,
'sidebar' => $this->sidebarMenu(),
'countries' => TranslationHelper::getCountries()
]);
}
public function update(VendorContact $vendor_contact)
{
$vendor_contact->fill(request()->all());
$vendor_contact->vendor->fill(request()->all());
$vendor_contact->push();
return back()->withSuccess(ctrans('texts.profile_updated_successfully'));
}
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

@ -29,7 +29,7 @@ class WePayController extends BaseController
*/
public function signup(string $token)
{
return render('gateways.wepay.signup.finished');
// return render('gateways.wepay.signup.finished');
$hash = Cache::get($token);

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,76 @@
<?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')
->whereIn('status_id', [PurchaseOrder::STATUS_SENT, PurchaseOrder::STATUS_ACCEPTED])
->where('company_id', $this->company->id)
->where('is_deleted', false);
if (in_array('sent', $this->status)) {
$local_status[] = PurchaseOrder::STATUS_SENT;
}
if (in_array('accepted', $this->status)) {
$local_status[] = PurchaseOrder::STATUS_ACCEPTED;
}
if (count($local_status) > 0) {
$query = $query->whereIn('status_id', array_unique($local_status));
}
$query = $query
->where('vendor_id', auth()->guard('vendor')->user()->vendor_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,57 @@
<?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)
{
if (auth()->guard('contact')->check()) {
auth()->guard('contact')->logout();
$request->session()->invalidate();
}
/*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,54 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Requests\Account;
use App\Http\Requests\Request;
use App\Http\ValidationRules\Account\BlackListRule;
use App\Http\ValidationRules\Account\EmailBlackListRule;
use App\Http\ValidationRules\NewUniqueUserRule;
use App\Utils\Ninja;
class UpdateAccountRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return (auth()->user()->isAdmin() || auth()->user()->isOwner()) && (int)$this->account->id === auth()->user()->account_id;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'set_react_as_default_ap' => 'required|bail|bool'
];
}
/* Only allow single field to update account table */
protected function prepareForValidation()
{
$input = $this->all();
$cleaned_input = array_intersect_key( $input, array_flip(['set_react_as_default_ap']));
$this->replace($cleaned_input);
}
}

View File

@ -39,21 +39,28 @@ class Checkout3dsRequest extends FormRequest
public function getCompany()
{
MultiDB::findAndSetDbByCompanyKey($this->company_key);
return Company::where('company_key', $this->company_key)->first();
}
public function getCompanyGateway()
{
MultiDB::findAndSetDbByCompanyKey($this->company_key);
return CompanyGateway::find($this->decodePrimaryKey($this->company_gateway_id));
}
public function getPaymentHash()
{
MultiDB::findAndSetDbByCompanyKey($this->company_key);
return PaymentHash::where('hash', $this->hash)->first();
}
public function getClient()
{
return Client::find($this->getPaymentHash()->data->client_id);
MultiDB::findAndSetDbByCompanyKey($this->company_key);
return Client::withTrashed()->find($this->getPaymentHash()->data->client_id);
}
}

View File

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

View File

@ -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

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

View File

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

View File

@ -49,7 +49,9 @@ class InvitationViewedListener implements ShouldQueue
if($entity_name == 'recurringInvoice')
return;
elseif($entity_name == 'purchaseOrder')
$entity_name = 'purchase_order';
$nmo = new NinjaMailerObject;
$nmo->mailable = new NinjaMailer( (new EntityViewedObject($invitation, $entity_name))->build() );
$nmo->company = $invitation->company;
@ -60,6 +62,8 @@ class InvitationViewedListener implements ShouldQueue
$entity_viewed = "{$entity_name}_viewed";
$entity_viewed_all = "{$entity_name}_viewed_all";
$methods = $this->findUserNotificationTypes($invitation, $company_user, $entity_name, ['all_notifications', $entity_viewed, $entity_viewed_all]);
if (($key = array_search('mail', $methods)) !== false) {

View File

@ -0,0 +1,61 @@
<?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\Listeners\PurchaseOrder;
use App\Libraries\MultiDB;
use App\Models\Activity;
use App\Repositories\ActivityRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use stdClass;
class PurchaseOrderAcceptedActivity implements ShouldQueue
{
protected $activity_repo;
public $delay = 5;
/**
* Create the event listener.
*
* @param ActivityRepository $activity_repo
*/
public function __construct(ActivityRepository $activity_repo)
{
$this->activity_repo = $activity_repo;
}
/**
* Handle the event.
*
* @param object $event
* @return void
*/
public function handle($event)
{
MultiDB::setDb($event->company->db);
$fields = new stdClass;
$user_id = array_key_exists('user_id', $event->event_vars) ? $event->event_vars['user_id'] : $event->purchase_order->user_id;
$event->purchase_order->service()->markSent()->save();
$fields->user_id = $user_id;
$fields->company_id = $event->purchase_order->company_id;
$fields->activity_type_id = Activity::ACCEPT_PURCHASE_ORDER;
$fields->vendor_id = $event->purchase_order->vendor_id;
$fields->vendor_contact_id = $event->contact->id;
$fields->purchase_order_id = $event->purchase_order->id;
$this->activity_repo->save($fields, $event->purchase_order, $event->event_vars);
}
}

View File

@ -0,0 +1,79 @@
<?php
/**
* Quote Ninja (https://quoteninja.com).
*
* @link https://github.com/quoteninja/quoteninja source repository
*
* @copyright Copyright (c) 2022. Quote Ninja LLC (https://quoteninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Listeners\PurchaseOrder;
use App\Jobs\Mail\NinjaMailer;
use App\Jobs\Mail\NinjaMailerJob;
use App\Jobs\Mail\NinjaMailerObject;
use App\Libraries\MultiDB;
use App\Mail\Admin\EntityCreatedObject;
use App\Mail\Admin\PurchaseOrderAcceptedObject;
use App\Notifications\Admin\EntitySentNotification;
use App\Utils\Traits\Notifications\UserNotifies;
use Illuminate\Contracts\Queue\ShouldQueue;
class PurchaseOrderAcceptedNotification implements ShouldQueue
{
use UserNotifies;
public $delay = 5;
public function __construct()
{
}
/**
* Handle the event.
*
* @param object $event
* @return void
*/
public function handle($event)
{
MultiDB::setDb($event->company->db);
$first_notification_sent = true;
$purchase_order = $event->purchase_order;
$nmo = new NinjaMailerObject;
$nmo->mailable = new NinjaMailer( (new PurchaseOrderAcceptedObject($purchase_order, $event->company))->build() );
$nmo->company = $event->company;
$nmo->settings = $event->company->settings;
/* We loop through each user and determine whether they need to be notified */
foreach ($event->company->company_users as $company_user) {
/* The User */
$user = $company_user->user;
if(!$user)
continue;
/* Returns an array of notification methods */
$methods = $this->findUserNotificationTypes($purchase_order->invitations()->first(), $company_user, 'purchase_order', ['all_notifications', 'purchase_order_accepted', 'purchase_order_accepted_all']);
/* If one of the methods is email then we fire the EntitySentMailer */
if (($key = array_search('mail', $methods)) !== false) {
unset($methods[$key]);
$nmo->to_user = $user;
NinjaMailerJob::dispatch($nmo);
/* This prevents more than one notification being sent */
$first_notification_sent = false;
}
}
}
}

View File

@ -65,7 +65,12 @@ class EntityViewedObject
private function getAmount()
{
return Number::formatMoney($this->entity->amount, $this->entity->client);
if($this->entity->client)
$currency_entity = $this->entity->client;
else
$currency_entity = $this->company;
return Number::formatMoney($this->entity->amount, $currency_entity);
}
private function getSubject()
@ -82,7 +87,10 @@ class EntityViewedObject
private function getData()
{
$settings = $this->entity->client->getMergedSettings();
if($this->entity->client)
$settings = $this->entity->client->getMergedSettings();
else
$settings = $this->company->settings;
$data = [
'title' => $this->getSubject(),

View File

@ -0,0 +1,103 @@
<?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\Mail\Admin;
use App\Libraries\MultiDB;
use App\Models\Company;
use App\Models\PurchaseOrder;
use App\Utils\Ninja;
use App\Utils\Number;
use Illuminate\Support\Facades\App;
use stdClass;
class PurchaseOrderAcceptedObject
{
public $purchase_order;
public $company;
public $settings;
public function __construct(PurchaseOrder $purchase_order, Company $company)
{
$this->purchase_order = $purchase_order;
$this->company = $company;
}
public function build()
{
MultiDB::setDb($this->company->db);
if(!$this->purchase_order)
return;
App::forgetInstance('translator');
/* Init a new copy of the translator*/
$t = app('translator');
/* Set the locale*/
App::setLocale($this->company->getLocale());
/* Set customized translations _NOW_ */
$t->replace(Ninja::transformTranslations($this->company->settings));
$mail_obj = new stdClass;
$mail_obj->amount = $this->getAmount();
$mail_obj->subject = $this->getSubject();
$mail_obj->data = $this->getData();
$mail_obj->markdown = 'email.admin.generic';
$mail_obj->tag = $this->company->company_key;
return $mail_obj;
}
private function getAmount()
{
return Number::formatMoney($this->purchase_order->amount, $this->company);
}
private function getSubject()
{
return
ctrans(
"texts.notification_purchase_order_accepted_subject",
[
'vendor' => $this->purchase_order->vendor->present()->name(),
'purchase_order' => $this->purchase_order->number,
]
);
}
private function getData()
{
$settings = $this->company->settings;
$data = [
'title' => $this->getSubject(),
'message' => ctrans(
"texts.notification_purchase_order_accepted",
[
'amount' => $this->getAmount(),
'vendor' => $this->purchase_order->vendor->present()->name(),
'purchase_order' => $this->purchase_order->number,
]
),
'url' => $this->purchase_order->invitations->first()->getAdminLink(),
'button' => ctrans("texts.view_purchase_order"),
'signature' => $settings->email_signature,
'logo' => $this->company->present()->logo(),
'settings' => $settings,
'whitelabel' => $this->company->account->isPaid() ? true : false,
];
return $data;
}
}

View File

@ -11,6 +11,7 @@
namespace App\Models;
use App\Exceptions\ModelNotFoundException;
use App\Jobs\Mail\NinjaMailerJob;
use App\Jobs\Mail\NinjaMailerObject;
use App\Mail\Ninja\EmailQuotaExceeded;
@ -57,7 +58,7 @@ class Account extends BaseModel
'utm_content',
'user_agent',
'platform',
// 'set_react_as_default_ap',
'set_react_as_default_ap',
];
/**
@ -75,7 +76,8 @@ class Account extends BaseModel
'updated_at' => 'timestamp',
'created_at' => 'timestamp',
'deleted_at' => 'timestamp',
'onboarding' => 'object'
'onboarding' => 'object',
'set_react_as_default_ap' => 'bool'
];
const PLAN_FREE = 'free';
@ -88,6 +90,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 +166,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:
@ -469,4 +473,14 @@ class Account extends BaseModel
}
public function resolveRouteBinding($value, $field = null)
{
if (is_numeric($value)) {
throw new ModelNotFoundException("Record with value {$value} not found");
}
return $this
->where('id', $this->decodePrimaryKey($value))->firstOrFail();
}
}

View File

@ -116,7 +116,8 @@ class Activity extends StaticModel
const RESTORE_PURCHASE_ORDER = 134;
const EMAIL_PURCHASE_ORDER = 135;
const VIEW_PURCHASE_ORDER = 136;
const ACCEPT_PURCHASE_ORDER = 137;
protected $casts = [
'is_system' => 'boolean',
'updated_at' => 'timestamp',

View File

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

View File

@ -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,8 +101,51 @@ class PurchaseOrder extends BaseModel
const STATUS_DRAFT = 1;
const STATUS_SENT = 2;
const STATUS_PARTIAL = 3;
const STATUS_APPLIED = 4;
const STATUS_ACCEPTED = 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_ACCEPTED:
return ctrans('texts.accepted');
break;
case self::STATUS_CANCELLED:
return ctrans('texts.cancelled');
break;
// code...
break;
}
}
public static function badgeForStatus(int $status)
{
switch ($status) {
case self::STATUS_DRAFT:
return '<h5><span class="badge badge-light">'.ctrans('texts.draft').'</span></h5>';
break;
case self::STATUS_SENT:
return '<h5><span class="badge badge-primary">'.ctrans('texts.sent').'</span></h5>';
break;
case self::STATUS_ACCEPTED:
return '<h5><span class="badge badge-primary">'.ctrans('texts.accepted').'</span></h5>';
break;
case self::STATUS_CANCELLED:
return '<h5><span class="badge badge-secondary">'.ctrans('texts.cancelled').'</span></h5>';
break;
default:
// code...
break;
}
}
public function assigned_user()
{

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

@ -407,12 +407,7 @@ class BaseDriver extends AbstractPaymentDriver
$this->unWindGatewayFees($this->payment_hash);
}
if ($e instanceof CheckoutHttpException) {
$error = $e->getBody();
} else if ($e instanceof Exception) {
$error = $e->getMessage();
} else
$error = $e->getMessage();
$error = $e->getMessage();
if(!$this->payment_hash)
throw new PaymentFailed($error, $e->getCode());

View File

@ -14,18 +14,24 @@ namespace App\PaymentDrivers\CheckoutCom;
use App\Exceptions\PaymentFailed;
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
use Illuminate\Http\Request;
use App\Models\ClientGatewayToken;
use App\Models\GatewayType;
use App\Models\Payment;
use App\PaymentDrivers\CheckoutComPaymentDriver;
use App\PaymentDrivers\Common\MethodInterface;
use App\Utils\Traits\MakesHash;
use Checkout\CheckoutApiException;
use Checkout\CheckoutArgumentException;
use Checkout\CheckoutAuthorizationException;
use Checkout\Library\Exceptions\CheckoutHttpException;
use Checkout\Models\Payments\IdSource;
use Checkout\Models\Payments\Payment;
use Checkout\Models\Payments\TokenSource;
use Checkout\Payments\Four\Request\Source\RequestTokenSource;
use Checkout\Payments\Source\RequestTokenSource as SourceRequestTokenSource;
use Illuminate\Contracts\View\Factory;
use Illuminate\Http\Request;
use Illuminate\View\View;
use Checkout\Payments\PaymentRequest as PaymentsPaymentRequest;
use Checkout\Payments\Four\Request\PaymentRequest;
class CreditCard implements MethodInterface
{
@ -57,6 +63,31 @@ class CreditCard implements MethodInterface
return render('gateways.checkout.credit_card.authorize', $data);
}
public function bootRequest($token)
{
if($this->checkout->is_four_api){
$token_source = new RequestTokenSource();
$token_source->token = $token;
$request = new PaymentRequest();
$request->source = $token_source;
}
else {
$token_source = new SourceRequestTokenSource();
$token_source->token = $token;
$request = new PaymentsPaymentRequest();
$request->source = $token_source;
}
return $request;
}
/**
* Handle authorization for credit card.
*
@ -67,41 +98,54 @@ class CreditCard implements MethodInterface
{
$gateway_response = \json_decode($request->gateway_response);
$method = new TokenSource(
$gateway_response->token
);
$customerRequest = $this->checkout->getCustomer();
$request = $this->bootRequest($gateway_response->token);
$request->capture = false;
$request->reference = '$1 payment for authorization.';
$request->amount = 100;
$request->currency = $this->checkout->client->getCurrencyCode();
$request->customer = $customerRequest;
$payment = new Payment($method, 'USD');
$payment->amount = 100; // $1
$payment->reference = '$1 payment for authorization.';
$payment->capture = false;
try {
$response = $this->checkout->gateway->payments()->request($payment);
if ($response->approved && $response->status === 'Authorized') {
$response = $this->checkout->gateway->getPaymentsClient()->requestPayment($request);
if ($response['approved'] && $response['status'] === 'Authorized') {
$payment_meta = new \stdClass;
$payment_meta->exp_month = (string) $response->source['expiry_month'];
$payment_meta->exp_year = (string) $response->source['expiry_year'];
$payment_meta->brand = (string) $response->source['scheme'];
$payment_meta->last4 = (string) $response->source['last4'];
$payment_meta->exp_month = (string) $response['source']['expiry_month'];
$payment_meta->exp_year = (string) $response['source']['expiry_year'];
$payment_meta->brand = (string) $response['source']['scheme'];
$payment_meta->last4 = (string) $response['source']['last4'];
$payment_meta->type = (int) GatewayType::CREDIT_CARD;
$data = [
'payment_meta' => $payment_meta,
'token' => $response->source['id'],
'payment_method_id' => GatewayType::CREDIT_CARD,
'token' => $response['source']['id'],
'payment_method_id' => GatewayType::CREDIT_CARD,
];
$payment_method = $this->checkout->storeGatewayToken($data);
$payment_method = $this->checkout->storeGatewayToken($data,['gateway_customer_reference' => $customerRequest['id']]);
return redirect()->route('client.payment_methods.show', $payment_method->hashed_id);
}
} catch (CheckoutHttpException $exception) {
throw new PaymentFailed(
$exception->getMessage()
);
} catch (CheckoutApiException $e) {
// API error
$request_id = $e->request_id;
$http_status_code = $e->http_status_code;
$error_details = $e->error_details;
throw new PaymentFailed($e->getMessage());
} catch (CheckoutArgumentException $e) {
// Bad arguments
throw new PaymentFailed($e->getMessage());
} catch (CheckoutAuthorizationException $e) {
// Bad Invalid authorization
throw new PaymentFailed($e->getMessage());
}
}
public function paymentView($data)
@ -145,89 +189,102 @@ class CreditCard implements MethodInterface
{
$cgt = ClientGatewayToken::query()
->where('id', $this->decodePrimaryKey($request->input('token')))
->where('company_id', auth()->guard('contact')->user()->client->company->id)
->where('company_id', auth()->guard('contact')->user()->client->company_id)
->first();
if (!$cgt) {
throw new PaymentFailed(ctrans('texts.payment_token_not_found'), 401);
}
$method = new IdSource($cgt->token);
$paymentRequest = $this->checkout->bootTokenRequest($cgt->token);
return $this->completePayment($method, $request);
return $this->completePayment($paymentRequest, $request);
}
private function attemptPaymentUsingCreditCard(PaymentResponseRequest $request)
{
$checkout_response = $this->checkout->payment_hash->data->server_response;
$method = new TokenSource(
$checkout_response->token
);
$paymentRequest = $this->bootRequest($checkout_response->token);
return $this->completePayment($method, $request);
return $this->completePayment($paymentRequest, $request);
}
private function completePayment($method, PaymentResponseRequest $request)
private function completePayment($paymentRequest, PaymentResponseRequest $request)
{
$payment = new Payment($method, $this->checkout->payment_hash->data->currency);
$payment->amount = $this->checkout->payment_hash->data->value;
$payment->reference = $this->checkout->getDescription();
$payment->customer = [
'name' => $this->checkout->client->present()->name() ,
'email' => $this->checkout->client->present()->email(),
];
$paymentRequest->amount = $this->checkout->payment_hash->data->value;
$paymentRequest->reference = $this->checkout->getDescription();
$paymentRequest->customer = $this->checkout->getCustomer();
$paymentRequest->metadata = ['udf1' => "Invoice Ninja"];
$paymentRequest->currency = $this->checkout->client->getCurrencyCode();
$payment->metadata = [
'udf1' => "Invoice Ninja",
];
$this->checkout->payment_hash->data = array_merge((array)$this->checkout->payment_hash->data, ['checkout_payment_ref' => $payment]);
$this->checkout->payment_hash->data = array_merge((array)$this->checkout->payment_hash->data, ['checkout_payment_ref' => $paymentRequest]);
$this->checkout->payment_hash->save();
if ($this->checkout->client->currency()->code == 'EUR' || $this->checkout->company_gateway->getConfigField('threeds')) {
$payment->{'3ds'} = ['enabled' => true];
$payment->{'success_url'} = route('checkout.3ds_redirect', [
$paymentRequest->{'3ds'} = ['enabled' => true];
$paymentRequest->{'success_url'} = route('checkout.3ds_redirect', [
'company_key' => $this->checkout->client->company->company_key,
'company_gateway_id' => $this->checkout->company_gateway->hashed_id,
'hash' => $this->checkout->payment_hash->hash,
]);
$payment->{'failure_url'} = route('checkout.3ds_redirect', [
$paymentRequest->{'failure_url'} = route('checkout.3ds_redirect', [
'company_key' => $this->checkout->client->company->company_key,
'company_gateway_id' => $this->checkout->company_gateway->hashed_id,
'hash' => $this->checkout->payment_hash->hash,
]);
}
try {
$response = $this->checkout->gateway->payments()->request($payment);
// $response = $this->checkout->gateway->payments()->request($payment);
if ($response->status == 'Authorized') {
$response = $this->checkout->gateway->getPaymentsClient()->requestPayment($paymentRequest);
if ($response['status'] == 'Authorized') {
return $this->processSuccessfulPayment($response);
}
if ($response->status == 'Pending') {
if ($response['status'] == 'Pending') {
$this->checkout->confirmGatewayFee();
return $this->processPendingPayment($response);
}
if ($response->status == 'Declined') {
if ($response['status'] == 'Declined') {
$this->checkout->unWindGatewayFees($this->checkout->payment_hash);
// $this->checkout->sendFailureMail($response->response_summary);
//@todo - this will double up the checkout . com failed mails
// $this->checkout->clientPaymentFailureMailer($response->status);
return $this->processUnsuccessfulPayment($response);
}
} catch (CheckoutHttpException $e) {
}
catch (CheckoutApiException $e) {
// API error
$request_id = $e->request_id;
$http_status_code = $e->http_status_code;
$error_details = $e->error_details;
$this->checkout->unWindGatewayFees($this->checkout->payment_hash);
return $this->checkout->processInternallyFailedPayment($this->checkout, $e);
} catch (CheckoutArgumentException $e) {
// Bad arguments
$this->checkout->unWindGatewayFees($this->checkout->payment_hash);
return $this->checkout->processInternallyFailedPayment($this->checkout, $e);
} catch (CheckoutAuthorizationException $e) {
// Bad Invalid authorization
$this->checkout->unWindGatewayFees($this->checkout->payment_hash);
return $this->checkout->processInternallyFailedPayment($this->checkout, $e);
}
}
}

View File

@ -54,17 +54,18 @@ trait Utilities
return round($amount * 100);
}
private function processSuccessfulPayment(Payment $_payment)
private function processSuccessfulPayment($_payment)
{
if ($this->getParent()->payment_hash->data->store_card) {
$this->storeLocalPaymentMethod($_payment);
}
$data = [
'payment_method' => $_payment->source['id'],
'payment_method' => $_payment['source']['id'],
'payment_type' => 12,
'amount' => $this->getParent()->payment_hash->data->raw_value,
'transaction_reference' => $_payment->id,
'transaction_reference' => $_payment['id'],
'gateway_type_id' => GatewayType::CREDIT_CARD,
];
@ -82,15 +83,15 @@ trait Utilities
return redirect()->route('client.payments.show', ['payment' => $this->getParent()->encodePrimaryKey($payment->id)]);
}
public function processUnsuccessfulPayment(Payment $_payment, $throw_exception = true)
public function processUnsuccessfulPayment($_payment, $throw_exception = true)
{
$error_message = '';
if(property_exists($_payment, 'server_response'))
$error_message = $_payment->response_summary;
elseif(property_exists($_payment, 'status'))
$error_message = $_payment->status;
if(array_key_exists('response_summary',$_payment))
$error_message = $_payment['response_summary'];
elseif(array_key_exists('status',$_payment))
$error_message = $_payment['status'];
$this->getParent()->sendFailureMail($error_message);
@ -110,36 +111,37 @@ trait Utilities
if ($throw_exception) {
throw new PaymentFailed($_payment->status . " " . $error_message, $_payment->http_code);
throw new PaymentFailed($_payment['status'] . " " . $error_message, 500);
}
}
private function processPendingPayment(Payment $_payment)
private function processPendingPayment($_payment)
{
try {
return redirect($_payment->_links['redirect']['href']);
return redirect($_payment['_links']['redirect']['href']);
} catch (Exception $e) {
return $this->processInternallyFailedPayment($this->getParent(), $e);
return $this->getParent()->processInternallyFailedPayment($this->getParent(), $e);
}
}
private function storeLocalPaymentMethod(Payment $response)
private function storeLocalPaymentMethod($response)
{
try {
$payment_meta = new stdClass;
$payment_meta->exp_month = (string) $response->source['expiry_month'];
$payment_meta->exp_year = (string) $response->source['expiry_year'];
$payment_meta->brand = (string) $response->source['scheme'];
$payment_meta->last4 = (string) $response->source['last4'];
$payment_meta->exp_month = (string) $response['source']['expiry_month'];
$payment_meta->exp_year = (string) $response['source']['expiry_year'];
$payment_meta->brand = (string) $response['source']['scheme'];
$payment_meta->last4 = (string) $response['source']['last4'];
$payment_meta->type = (int) GatewayType::CREDIT_CARD;
$data = [
'payment_meta' => $payment_meta,
'token' => $response->source['id'],
'token' => $response['source']['id'],
'payment_method_id' => $this->getParent()->payment_hash->data->payment_method_id,
];
return $this->getParent()->storePaymentMethod($data);
return $this->getParent()->storePaymentMethod($data, ['gateway_customer_reference' => $response['customer']['id']]);
} catch (Exception $e) {
session()->flash('message', ctrans('texts.payment_method_saving_failed'));
}

View File

@ -28,10 +28,22 @@ use App\PaymentDrivers\CheckoutCom\CreditCard;
use App\PaymentDrivers\CheckoutCom\Utilities;
use App\Utils\Traits\SystemLogTrait;
use Checkout\CheckoutApi;
use Checkout\CheckoutApiException;
use Checkout\CheckoutArgumentException;
use Checkout\CheckoutAuthorizationException;
use Checkout\CheckoutDefaultSdk;
use Checkout\CheckoutFourSdk;
use Checkout\Environment;
use Checkout\Library\Exceptions\CheckoutHttpException;
use Checkout\Models\Payments\IdSource;
use Checkout\Models\Payments\Refund;
use Exception;
use Checkout\Payments\Four\Request\PaymentRequest;
use Checkout\Payments\Four\Request\Source\RequestIdSource as SourceRequestIdSource;
use Checkout\Payments\PaymentRequest as PaymentsPaymentRequest;
use Checkout\Payments\Source\RequestIdSource;
use Checkout\Common\CustomerRequest;
use Checkout\Payments\RefundRequest;
class CheckoutComPaymentDriver extends BaseDriver
{
@ -52,6 +64,8 @@ class CheckoutComPaymentDriver extends BaseDriver
/* Authorise payment methods */
public $can_authorise_credit_card = true;
public $is_four_api = false;
/**
* @var CheckoutApi;
*/
@ -109,7 +123,22 @@ class CheckoutComPaymentDriver extends BaseDriver
'sandbox' => $this->company_gateway->getConfigField('testMode'),
];
$this->gateway = new CheckoutApi($config['secret'], $config['sandbox'], $config['public']);
if(strlen($config['secret']) <= 38){
$this->is_four_api = true;
$builder = CheckoutFourSdk::staticKeys();
$builder->setPublicKey($config['public']); // optional, only required for operations related with tokens
$builder->setSecretKey($config['secret']);
$builder->setEnvironment($config['sandbox'] ? Environment::sandbox(): Environment::production());
$this->gateway = $builder->build();
}
else {
$builder = CheckoutDefaultSdk::staticKeys();
$builder->setPublicKey($config['public']); // optional, only required for operations related with tokens
$builder->setSecretKey($config['secret']);
$builder->setEnvironment($config['sandbox'] ? Environment::sandbox(): Environment::production());
$this->gateway = $builder->build();
}
return $this;
}
@ -121,9 +150,6 @@ class CheckoutComPaymentDriver extends BaseDriver
*/
public function viewForType($gateway_type_id)
{
// At the moment Checkout.com payment
// driver only supports payments using credit card.
return 'gateways.checkout.credit_card.pay';
}
@ -211,22 +237,40 @@ class CheckoutComPaymentDriver extends BaseDriver
{
$this->init();
$checkout_payment = new Refund($payment->transaction_reference);
$request = new RefundRequest();
$request->reference = "{$payment->transaction_reference} " . now();
$request->amount = $this->convertToCheckoutAmount($amount, $this->client->getCurrencyCode());
try {
$refund = $this->gateway->payments()->refund($checkout_payment);
$checkout_payment = $this->gateway->payments()->details($refund->id);
$response = ['refund_response' => $refund, 'checkout_payment_fetch' => $checkout_payment];
// or, refundPayment("payment_id") for a full refund
$response = $this->gateway->getPaymentsClient()->refundPayment($payment->transaction_reference, $request);
return [
'transaction_reference' => $refund->action_id,
'transaction_reference' => $response['action_id'],
'transaction_response' => json_encode($response),
'success' => $checkout_payment->status == 'Refunded',
'description' => $checkout_payment->status,
'code' => $checkout_payment->http_code,
'success' => true,
'description' => $response['reference'],
'code' => 202,
];
} catch (CheckoutHttpException $e) {
} catch (CheckoutApiException $e) {
// API error
$request_id = $e->request_id;
$http_status_code = $e->http_status_code;
$error_details = $e->error_details;
} catch (CheckoutArgumentException $e) {
// Bad arguments
return [
'transaction_reference' => null,
'transaction_response' => json_encode($e->getMessage()),
'success' => false,
'description' => $e->getMessage(),
'code' => $e->getCode(),
];
} catch (CheckoutAuthorizationException $e) {
// Bad Invalid authorization
return [
'transaction_reference' => null,
'transaction_response' => json_encode($e->getMessage()),
@ -235,6 +279,49 @@ class CheckoutComPaymentDriver extends BaseDriver
'code' => $e->getCode(),
];
}
}
public function getCustomer()
{
try{
$response = $this->gateway->getCustomersClient()->get($this->client->present()->email());
return $response;
}
catch(\Exception $e){
$request = new CustomerRequest();
$request->email = $this->client->present()->email();
$request->name = $this->client->present()->name();
return $request;
}
}
public function bootTokenRequest($token)
{
if($this->is_four_api){
$token_source = new SourceRequestIdSource();
$token_source->id = $token;
$request = new PaymentRequest();
$request->source = $token_source;
}
else {
$token_source = new RequestIdSource();
$token_source->id = $token;
$request = new PaymentsPaymentRequest();
$request->source = $token_source;
}
return $request;
}
public function tokenBilling(ClientGatewayToken $cgt, PaymentHash $payment_hash)
@ -244,27 +331,29 @@ class CheckoutComPaymentDriver extends BaseDriver
$this->init();
$method = new IdSource($cgt->token);
$payment = new \Checkout\Models\Payments\Payment($method, $this->client->getCurrencyCode());
$payment->amount = $this->convertToCheckoutAmount($amount, $this->client->getCurrencyCode());
$payment->reference = $invoice->number . '-' . now();
$paymentRequest = $this->bootTokenRequest($cgt->token);
$paymentRequest->amount = $this->convertToCheckoutAmount($amount, $this->client->getCurrencyCode());
$paymentRequest->reference = '#' . $invoice->number . ' - ' . now();
$paymentRequest->customer = $this->getCustomer();
$paymentRequest->metadata = ['udf1' => "Invoice Ninja"];
$paymentRequest->currency = $this->client->getCurrencyCode();
$request = new PaymentResponseRequest();
$request->setMethod('POST');
$request->request->add(['payment_hash' => $payment_hash->hash]);
try {
$response = $this->gateway->payments()->request($payment);
// $response = $this->gateway->payments()->request($payment);
$response = $this->gateway->getPaymentsClient()->requestPayment($paymentRequest);
if ($response->status == 'Authorized') {
if ($response['status'] == 'Authorized') {
$this->confirmGatewayFee($request);
$data = [
'payment_method' => $response->source['id'],
'payment_type' => PaymentType::parseCardType(strtolower($response->source['scheme'])),
'payment_method' => $response['source']['id'],
'payment_type' => PaymentType::parseCardType(strtolower($response['source']['scheme'])),
'amount' => $amount,
'transaction_reference' => $response->id,
'transaction_reference' => $response['id'],
];
$payment = $this->createPayment($data, Payment::STATUS_COMPLETED);
@ -280,10 +369,10 @@ class CheckoutComPaymentDriver extends BaseDriver
return $payment;
}
if ($response->status == 'Declined') {
if ($response['status'] == 'Declined') {
$this->unWindGatewayFees($payment_hash);
$this->sendFailureMail($response->status . " " . $response->response_summary);
$this->sendFailureMail($response['status'] . " " . $response['response_summary']);
$message = [
'server_response' => $response,
@ -300,11 +389,9 @@ class CheckoutComPaymentDriver extends BaseDriver
return false;
}
} catch (Exception | CheckoutHttpException $e) {
} catch (Exception | CheckoutApiException $e) {
$this->unWindGatewayFees($payment_hash);
$message = $e instanceof CheckoutHttpException
? $e->getBody()
: $e->getMessage();
$message = $e->getMessage();
$data = [
'status' => '',
@ -334,20 +421,21 @@ class CheckoutComPaymentDriver extends BaseDriver
public function process3dsConfirmation(Checkout3dsRequest $request)
{
$this->init();
$this->setPaymentHash($request->getPaymentHash());
try {
$payment = $this->gateway->payments()->details(
$payment = $this->gateway->getPaymentsClient()->getPaymentDetails(
$request->query('cko-session-id')
);
if ($payment->approved) {
if ($payment['approved']) {
return $this->processSuccessfulPayment($payment);
} else {
return $this->processUnsuccessfulPayment($payment);
}
} catch (CheckoutHttpException | Exception $e) {
} catch (CheckoutApiException | Exception $e) {
return $this->processInternallyFailedPayment($this, $e);
}
}

View File

@ -245,7 +245,7 @@ class GoCardlessPaymentDriver extends BaseDriver
sleep(1);
foreach ($request->events as $event) {
if ($event['action'] === 'confirmed' || $event['action'] === 'paid_out' || $event['action'] === 'paid') {
if ($event['action'] === 'confirmed' || $event['action'] === 'paid_out') {
nlog("Searching for transaction reference");

View File

@ -60,11 +60,12 @@ use App\Events\Payment\PaymentWasRefunded;
use App\Events\Payment\PaymentWasRestored;
use App\Events\Payment\PaymentWasUpdated;
use App\Events\Payment\PaymentWasVoided;
use App\Events\PurchaseOrder\PurchaseOrderWasMarkedSent;
use App\Events\PurchaseOrder\PurchaseOrderWasAccepted;
use App\Events\PurchaseOrder\PurchaseOrderWasArchived;
use App\Events\PurchaseOrder\PurchaseOrderWasCreated;
use App\Events\PurchaseOrder\PurchaseOrderWasDeleted;
use App\Events\PurchaseOrder\PurchaseOrderWasEmailed;
use App\Events\PurchaseOrder\PurchaseOrderWasMarkedSent;
use App\Events\PurchaseOrder\PurchaseOrderWasRestored;
use App\Events\PurchaseOrder\PurchaseOrderWasUpdated;
use App\Events\PurchaseOrder\PurchaseOrderWasViewed;
@ -179,6 +180,8 @@ use App\Listeners\Payment\PaymentEmailedActivity;
use App\Listeners\Payment\PaymentNotification;
use App\Listeners\Payment\PaymentRestoredActivity;
use App\Listeners\PurchaseOrder\CreatePurchaseOrderActivity;
use App\Listeners\PurchaseOrder\PurchaseOrderAcceptedActivity;
use App\Listeners\PurchaseOrder\PurchaseOrderAcceptedNotification;
use App\Listeners\PurchaseOrder\PurchaseOrderArchivedActivity;
use App\Listeners\PurchaseOrder\PurchaseOrderDeletedActivity;
use App\Listeners\PurchaseOrder\PurchaseOrderEmailActivity;
@ -471,6 +474,10 @@ class EventServiceProvider extends ServiceProvider
PurchaseOrderWasViewed::class => [
PurchaseOrderViewedActivity::class,
],
PurchaseOrderWasAccepted::class => [
PurchaseOrderAcceptedActivity::class,
PurchaseOrderAcceptedNotification::class
],
CompanyDocumentsDeleted::class => [
DeleteCompanyDocuments::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

@ -49,8 +49,6 @@ class GenerateDeliveryNote
$this->contact = $contact;
// $this->disk = 'public';
$this->disk = $disk ?? config('filesystems.default');
}

View File

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

View File

@ -164,6 +164,7 @@ class SubscriptionService
$recurring_invoice = $this->convertInvoiceToRecurring($client_contact->client_id);
$recurring_invoice->next_send_date = now()->addSeconds($this->subscription->trial_duration);
$recurring_invoice->next_send_date_client = now()->addSeconds($this->subscription->trial_duration);
$recurring_invoice->backup = 'is_trial';
if(array_key_exists('coupon', $data) && ($data['coupon'] == $this->subscription->promo_code) && $this->subscription->promo_discount > 0)
@ -620,7 +621,9 @@ class SubscriptionService
$recurring_invoice = $this->convertInvoiceToRecurring($old_recurring_invoice->client_id);
$recurring_invoice = $recurring_invoice_repo->save([], $recurring_invoice);
$recurring_invoice->next_send_date = now()->format('Y-m-d');
$recurring_invoice->next_send_date_client = now()->format('Y-m-d');
$recurring_invoice->next_send_date = $recurring_invoice->nextSendDate();
$recurring_invoice->next_send_date_client = $recurring_invoice->nextSendDateClient();
/* Start the recurring service */
$recurring_invoice->service()
@ -754,8 +757,9 @@ class SubscriptionService
$recurring_invoice->auto_bill_enabled = $this->setAutoBillFlag($recurring_invoice->auto_bill);
$recurring_invoice->due_date_days = 'terms';
$recurring_invoice->next_send_date = now()->format('Y-m-d');
$recurring_invoice->next_send_date_client = now()->format('Y-m-d');
$recurring_invoice->next_send_date = $recurring_invoice->nextSendDate();
$recurring_invoice->next_send_date_client = $recurring_invoice->nextSendDateClient();
return $recurring_invoice;
}

View File

@ -58,7 +58,10 @@ class ZeroCostProduct extends AbstractService
$recurring_invoice->next_send_date = now();
$recurring_invoice = $recurring_invoice_repo->save([], $recurring_invoice);
$recurring_invoice->next_send_date = now();
$recurring_invoice->next_send_date_client = now();
$recurring_invoice->next_send_date = $recurring_invoice->nextSendDate();
$recurring_invoice->next_send_date_client = $recurring_invoice->nextSendDateClient();
/* Start the recurring service */
$recurring_invoice->service()

View File

@ -86,7 +86,7 @@ class AccountTransformer extends EntityTransformer
'hosted_client_count' => (int) $account->hosted_client_count,
'hosted_company_count' => (int) $account->hosted_company_count,
'is_hosted' => (bool) Ninja::isHosted(),
// 'set_react_as_default_ap' => (bool) $account->set_react_as_default_ap
'set_react_as_default_ap' => (bool) $account->set_react_as_default_ap
];
}

View File

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

View File

@ -15,6 +15,7 @@ use App\Models\Client;
use App\Models\Credit;
use App\Models\Invoice;
use App\Models\Payment;
use App\Models\PurchaseOrder;
use App\Models\Quote;
/**
@ -99,7 +100,10 @@ trait UserNotifies
break;
case ($entity instanceof Credit):
return array_merge($required_permissions, ["all_notifications","all_user_notifications","credit_created_user","credit_sent_user","credit_viewed_user"]);
break;
break;
case ($entity instanceof PurchaseOrder):
return array_merge($required_permissions, ["all_notifications","all_user_notifications","purchase_order_created_user","purchase_order_sent_user","purchase_order_viewed_user"]);
break;
default:
return [];
break;
@ -122,7 +126,10 @@ trait UserNotifies
break;
case ($entity instanceof Credit):
return array_diff($required_permissions, ["all_user_notifications","credit_created_user","credit_sent_user","credit_viewed_user"]);
break;
break;
case ($entity instanceof PurchaseOrder):
return array_diff($required_permissions, ["all_user_notifications","purchase_order_created_user","purchase_order_sent_user","purchase_order_viewed_user"]);
break;
default:
// code...
break;

View File

@ -11,7 +11,12 @@
"Credit card billing",
"projects",
"tasks",
"freelancer"
"freelancer",
"quotes",
"purchase orders",
"stripe billing",
"invoices",
"subscriptions"
],
"license": "Elastic License",
"authors": [
@ -37,7 +42,7 @@
"bacon/bacon-qr-code": "^2.0",
"beganovich/snappdf": "^1.7",
"braintree/braintree_php": "^6.0",
"checkout/checkout-sdk-php": "^1.0",
"checkout/checkout-sdk-php": "^2.5",
"cleverit/ubl_invoice": "^1.3",
"coconutcraig/laravel-postmark": "^2.10",
"doctrine/dbal": "^3.0",

103
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "c9278efe297c252de6bc0b5a48540c0b",
"content-hash": "6845489fdc254427c4536e22f025ff51",
"packages": [
{
"name": "afosto/yaac",
@ -110,16 +110,16 @@
},
{
"name": "apimatic/unirest-php",
"version": "2.2.2",
"version": "2.3.0",
"source": {
"type": "git",
"url": "https://github.com/apimatic/unirest-php.git",
"reference": "a45c4c71a1ea3659b118042a67cc1b6486bcf03a"
"reference": "52e226fb3b7081dc9ef64aee876142a240a5f0f9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/apimatic/unirest-php/zipball/a45c4c71a1ea3659b118042a67cc1b6486bcf03a",
"reference": "a45c4c71a1ea3659b118042a67cc1b6486bcf03a",
"url": "https://api.github.com/repos/apimatic/unirest-php/zipball/52e226fb3b7081dc9ef64aee876142a240a5f0f9",
"reference": "52e226fb3b7081dc9ef64aee876142a240a5f0f9",
"shasum": ""
},
"require": {
@ -168,9 +168,9 @@
"support": {
"email": "opensource@apimatic.io",
"issues": "https://github.com/apimatic/unirest-php/issues",
"source": "https://github.com/apimatic/unirest-php/tree/2.2.2"
"source": "https://github.com/apimatic/unirest-php/tree/2.3.0"
},
"time": "2022-03-24T08:19:20+00:00"
"time": "2022-06-15T08:29:49+00:00"
},
{
"name": "asm/php-ansible",
@ -434,16 +434,16 @@
},
{
"name": "aws/aws-sdk-php",
"version": "3.225.2",
"version": "3.225.5",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
"reference": "f846724ad842916061127d20da4fe4e129f7d4b8"
"reference": "09b404c6b80b9c31be15fa245e647a2f9fb5e733"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/f846724ad842916061127d20da4fe4e129f7d4b8",
"reference": "f846724ad842916061127d20da4fe4e129f7d4b8",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/09b404c6b80b9c31be15fa245e647a2f9fb5e733",
"reference": "09b404c6b80b9c31be15fa245e647a2f9fb5e733",
"shasum": ""
},
"require": {
@ -451,7 +451,7 @@
"ext-json": "*",
"ext-pcre": "*",
"ext-simplexml": "*",
"guzzlehttp/guzzle": "^5.3.3 || ^6.5.6 || ^7.4.3",
"guzzlehttp/guzzle": "^5.3.3 || ^6.2.1 || ^7.0",
"guzzlehttp/promises": "^1.4.0",
"guzzlehttp/psr7": "^1.7.0 || ^2.1.1",
"mtdowling/jmespath.php": "^2.6",
@ -519,9 +519,9 @@
"support": {
"forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
"issues": "https://github.com/aws/aws-sdk-php/issues",
"source": "https://github.com/aws/aws-sdk-php/tree/3.225.2"
"source": "https://github.com/aws/aws-sdk-php/tree/3.225.5"
},
"time": "2022-06-10T19:03:26+00:00"
"time": "2022-06-15T19:35:13+00:00"
},
{
"name": "bacon/bacon-qr-code",
@ -741,28 +741,37 @@
},
{
"name": "checkout/checkout-sdk-php",
"version": "1.0.19",
"version": "2.5.1",
"source": {
"type": "git",
"url": "https://github.com/checkout/checkout-sdk-php.git",
"reference": "c2c323ea2f95f74bff3055c42801e7ce22999791"
"reference": "097b862487f7583fd0fab47a08e3dc0800f5c3e4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/checkout/checkout-sdk-php/zipball/c2c323ea2f95f74bff3055c42801e7ce22999791",
"reference": "c2c323ea2f95f74bff3055c42801e7ce22999791",
"url": "https://api.github.com/repos/checkout/checkout-sdk-php/zipball/097b862487f7583fd0fab47a08e3dc0800f5c3e4",
"reference": "097b862487f7583fd0fab47a08e3dc0800f5c3e4",
"shasum": ""
},
"require": {
"php": ">=5.4.0"
"ext-fileinfo": "*",
"ext-json": "*",
"guzzlehttp/guzzle": "^6.5 || ^7.4",
"monolog/monolog": "^1.27 || ^2.4",
"php": ">=5.6.0"
},
"require-dev": {
"phpunit/phpunit": "^6"
"mockery/mockery": "^1.3 || ^1.4",
"phpstan/phpstan": "^1.2",
"phpunit/phpunit": "^5.7 || ^9.0",
"smgladkovskiy/phpcs-git-pre-commit": "dev-master",
"squizlabs/php_codesniffer": "^3.3",
"symfony/phpunit-bridge": "^5.2 || ^6.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Checkout\\": "src/"
"Checkout\\": "lib/Checkout"
}
},
"notification-url": "https://packagist.org/downloads/",
@ -772,7 +781,7 @@
"authors": [
{
"name": "Checkout.com",
"email": "platforms@checkout.com",
"email": "integrations@checkout.com",
"homepage": "https://github.com/checkout/checkout-sdk-php/graphs/contributors"
}
],
@ -794,9 +803,9 @@
],
"support": {
"issues": "https://github.com/checkout/checkout-sdk-php/issues",
"source": "https://github.com/checkout/checkout-sdk-php/tree/1.0.19"
"source": "https://github.com/checkout/checkout-sdk-php/tree/2.5.1"
},
"time": "2021-11-19T15:08:38+00:00"
"time": "2022-06-13T00:23:23+00:00"
},
{
"name": "cleverit/ubl_invoice",
@ -2285,16 +2294,16 @@
},
{
"name": "google/apiclient-services",
"version": "v0.252.0",
"version": "v0.253.0",
"source": {
"type": "git",
"url": "https://github.com/googleapis/google-api-php-client-services.git",
"reference": "9941c959f6a1f781e49019b78f453d54554dff73"
"reference": "70c62b17f7821526cb52c6f125254dc51f256109"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/googleapis/google-api-php-client-services/zipball/9941c959f6a1f781e49019b78f453d54554dff73",
"reference": "9941c959f6a1f781e49019b78f453d54554dff73",
"url": "https://api.github.com/repos/googleapis/google-api-php-client-services/zipball/70c62b17f7821526cb52c6f125254dc51f256109",
"reference": "70c62b17f7821526cb52c6f125254dc51f256109",
"shasum": ""
},
"require": {
@ -2323,22 +2332,22 @@
],
"support": {
"issues": "https://github.com/googleapis/google-api-php-client-services/issues",
"source": "https://github.com/googleapis/google-api-php-client-services/tree/v0.252.0"
"source": "https://github.com/googleapis/google-api-php-client-services/tree/v0.253.0"
},
"time": "2022-06-06T01:20:11+00:00"
"time": "2022-06-13T01:06:12+00:00"
},
{
"name": "google/auth",
"version": "v1.21.0",
"version": "v1.21.1",
"source": {
"type": "git",
"url": "https://github.com/googleapis/google-auth-library-php.git",
"reference": "73392bad2eb6852eea9084b6bbdec752515cb849"
"reference": "aa3b9ca29258ac6347ce3c8937a2418c5d78f840"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/googleapis/google-auth-library-php/zipball/73392bad2eb6852eea9084b6bbdec752515cb849",
"reference": "73392bad2eb6852eea9084b6bbdec752515cb849",
"url": "https://api.github.com/repos/googleapis/google-auth-library-php/zipball/aa3b9ca29258ac6347ce3c8937a2418c5d78f840",
"reference": "aa3b9ca29258ac6347ce3c8937a2418c5d78f840",
"shasum": ""
},
"require": {
@ -2381,9 +2390,9 @@
"support": {
"docs": "https://googleapis.github.io/google-auth-library-php/main/",
"issues": "https://github.com/googleapis/google-auth-library-php/issues",
"source": "https://github.com/googleapis/google-auth-library-php/tree/v1.21.0"
"source": "https://github.com/googleapis/google-auth-library-php/tree/v1.21.1"
},
"time": "2022-04-13T20:35:52+00:00"
"time": "2022-05-16T19:34:15+00:00"
},
{
"name": "graham-campbell/result-type",
@ -6016,16 +6025,16 @@
},
{
"name": "paragonie/constant_time_encoding",
"version": "v2.6.1",
"version": "v2.6.3",
"source": {
"type": "git",
"url": "https://github.com/paragonie/constant_time_encoding.git",
"reference": "d6e1d5d0fb2458dfdd7018ec2f74be120353a3b9"
"reference": "58c3f47f650c94ec05a151692652a868995d2938"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/d6e1d5d0fb2458dfdd7018ec2f74be120353a3b9",
"reference": "d6e1d5d0fb2458dfdd7018ec2f74be120353a3b9",
"url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/58c3f47f650c94ec05a151692652a868995d2938",
"reference": "58c3f47f650c94ec05a151692652a868995d2938",
"shasum": ""
},
"require": {
@ -6079,7 +6088,7 @@
"issues": "https://github.com/paragonie/constant_time_encoding/issues",
"source": "https://github.com/paragonie/constant_time_encoding"
},
"time": "2022-06-11T00:43:46+00:00"
"time": "2022-06-14T06:56:20+00:00"
},
{
"name": "paragonie/random_compat",
@ -6815,16 +6824,16 @@
},
{
"name": "pragmarx/google2fa",
"version": "8.0.0",
"version": "v8.0.1",
"source": {
"type": "git",
"url": "https://github.com/antonioribeiro/google2fa.git",
"reference": "26c4c5cf30a2844ba121760fd7301f8ad240100b"
"reference": "80c3d801b31fe165f8fe99ea085e0a37834e1be3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/26c4c5cf30a2844ba121760fd7301f8ad240100b",
"reference": "26c4c5cf30a2844ba121760fd7301f8ad240100b",
"url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/80c3d801b31fe165f8fe99ea085e0a37834e1be3",
"reference": "80c3d801b31fe165f8fe99ea085e0a37834e1be3",
"shasum": ""
},
"require": {
@ -6861,9 +6870,9 @@
],
"support": {
"issues": "https://github.com/antonioribeiro/google2fa/issues",
"source": "https://github.com/antonioribeiro/google2fa/tree/8.0.0"
"source": "https://github.com/antonioribeiro/google2fa/tree/v8.0.1"
},
"time": "2020-04-05T10:47:18+00:00"
"time": "2022-06-13T21:57:56+00:00"
},
{
"name": "predis/predis",

View File

@ -14,8 +14,8 @@ return [
'require_https' => env('REQUIRE_HTTPS', true),
'app_url' => rtrim(env('APP_URL', ''), '/'),
'app_domain' => env('APP_DOMAIN', 'invoicing.co'),
'app_version' => '5.3.100',
'app_tag' => '5.3.100',
'app_version' => '5.4.0',
'app_tag' => '5.4.0',
'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', ''),

View File

@ -23,9 +23,6 @@ class SetAccountFlagForReact extends Migration
{
Illuminate\Support\Facades\Artisan::call('ninja:design-update');
// Schema::table('accounts', function (Blueprint $table) {
// $table->boolean('set_react_as_default_ap')->default(0);
// });
}
/**

View File

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddReactSwitchingFlag extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('accounts', function (Blueprint $table) {
$table->boolean('set_react_as_default_ap')->default(0);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

2
public/css/app.css vendored

File diff suppressed because one or more lines are too long

View File

@ -5,8 +5,8 @@ const CACHE_NAME = 'flutter-app-cache';
const RESOURCES = {
"icons/Icon-192.png": "bb1cf5f6982006952211c7c8404ffbed",
"icons/Icon-512.png": "0f9aff01367f0a0c69773d25ca16ef35",
"/": "94862dc60be2e82c49106de806115c42",
"main.dart.js": "bb75daec9d3cdf8374011435e63376d2",
"/": "2e739a78eec983322924f724ebfa09ba",
"main.dart.js": "fa4a0263712be1ce1df7d59ca0ede10e",
"version.json": "d72bd323e3b8e22ce5acdc247f4e6f62",
"favicon.png": "dca91c54388f52eded692718d5a98b8b",
"flutter.js": "0816e65a103ba8ba51b174eeeeb2cb67",

View File

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

View File

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

View File

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

View File

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

295831
public/main.dart.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

292909
public/main.foss.dart.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

295245
public/main.next.dart.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -4,6 +4,8 @@
"/js/clients/payments/authorize-credit-card-payment.js": "/js/clients/payments/authorize-credit-card-payment.js?id=803182f668c39d631ca5c55437876da4",
"/js/clients/payments/stripe-ach.js": "/js/clients/payments/stripe-ach.js?id=7bed15f51bca764378d9a3aa605b8664",
"/js/clients/invoices/action-selectors.js": "/js/clients/invoices/action-selectors.js?id=d4f86ddee4e8a1d6e9719010aa0fe62b",
"/js/clients/purchase_orders/action-selectors.js": "/js/clients/purchase_orders/action-selectors.js?id=160b8161599fc2429b449b0970d3ba6c",
"/js/clients/purchase_orders/accept.js": "/js/clients/purchase_orders/accept.js?id=2b5fed3ae34a6fd4db171a77ba72496e",
"/js/clients/invoices/payment.js": "/js/clients/invoices/payment.js?id=b88ad7c8881cc87df07b129c5a7c76df",
"/js/clients/payments/stripe-sofort.js": "/js/clients/payments/stripe-sofort.js?id=1c5493a4c53a5b862d07ee1818179ea9",
"/js/clients/payments/stripe-alipay.js": "/js/clients/payments/stripe-alipay.js?id=0274ab4f8d2b411f2a2fe5142301e7af",
@ -38,7 +40,7 @@
"/js/clients/payments/stripe-przelewy24.js": "/js/clients/payments/stripe-przelewy24.js?id=3d53d2f7d0291d9f92cf7414dd2d351c",
"/js/clients/payments/stripe-browserpay.js": "/js/clients/payments/stripe-browserpay.js?id=db71055862995fd6ae21becfc587a3de",
"/js/clients/payments/stripe-fpx.js": "/js/clients/payments/stripe-fpx.js?id=914a6846ad1e5584635e7430fef76875",
"/css/app.css": "/css/app.css?id=db0f69e335622f720583392dd572d264",
"/css/app.css": "/css/app.css?id=6419fb85c22d562d4ec14800980801e7",
"/css/card-js.min.css": "/css/card-js.min.css?id=62afeb675235451543ada60afcedcb7c",
"/vendor/clipboard.min.js": "/vendor/clipboard.min.js?id=15f52a1ee547f2bdd46e56747332ca2d"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

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

View File

@ -0,0 +1,79 @@
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
class ActionSelectors {
constructor() {
this.parentElement = document.querySelector('.form-check-parent');
this.parentForm = document.getElementById('bulkActions');
}
watchCheckboxes(parentElement) {
document
.querySelectorAll('.child-hidden-input')
.forEach((element) => element.remove());
document.querySelectorAll('.form-check-child').forEach((child) => {
if (parentElement.checked) {
child.checked = parentElement.checked;
this.processChildItem(
child,
document.getElementById('bulkActions')
);
} else {
child.checked = false;
document
.querySelectorAll('.child-hidden-input')
.forEach((element) => element.remove());
}
});
}
processChildItem(element, parent, options = {}) {
if (options.hasOwnProperty('single')) {
document
.querySelectorAll('.child-hidden-input')
.forEach((element) => element.remove());
}
if (element.checked === false) {
let inputs = document.querySelectorAll('input.child-hidden-input');
for (let i of inputs) {
if (i.value == element.dataset.value) i.remove();
}
return;
}
let _temp = document.createElement('INPUT');
_temp.setAttribute('name', 'purchase_orders[]');
_temp.setAttribute('value', element.dataset.value);
_temp.setAttribute('class', 'child-hidden-input');
_temp.hidden = true;
parent.append(_temp);
}
handle() {
this.parentElement.addEventListener('click', () => {
this.watchCheckboxes(this.parentElement);
});
for (let child of document.querySelectorAll('.form-check-child')) {
child.addEventListener('click', () => {
this.processChildItem(child, this.parentForm);
});
}
}
}
/** @handle **/
new ActionSelectors().handle();

View File

@ -4623,7 +4623,16 @@ $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',
'purchase_order_number_placeholder' => 'Purchase Order # :purchase_order',
'accepted' => 'Accepted',
'activity_137' => ':contact accepted purchase order :purchase_order',
'vendor_information' => 'Vendor Information',
'notification_purchase_order_accepted_subject' => 'Purchase Order :purchase_order was accepted by :vendor',
'notification_purchase_order_accepted' => 'The following vendor :vendor accepted Purchase Order :purchase_order for :amount.',
);
return $LANG;

View File

@ -23,7 +23,7 @@
}
.badge-warning {
@apply bg-yellow-500 text-yellow-500;
@apply bg-yellow-100 text-yellow-600;
}
.badge-info {

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('vendor.logout') }}"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition ease-in-out duration-150">
{{ ctrans('texts.logout') }}
</a>
</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,121 @@
<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="sent" type="checkbox" class="cursor-pointer form-checkbox" id="paid-checkbox">
<label for="paid-checkbox" class="text-sm cursor-pointer">{{ ctrans('texts.status_sent') }}</label>
</div>
<div class="mr-3">
<input wire:model="status" value="accepted" type="checkbox" class="cursor-pointer form-checkbox" id="unpaid-checkbox">
<label for="unpaid-checkbox" class="text-sm cursor-pointer">{{ ctrans('texts.accepted') }}</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, $purchase_order->company->date_format(), $purchase_order->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, $purchase_order->company) }}
</td>
<td class="px-6 py-4 text-sm leading-5 text-gray-500 whitespace-nowrap">
{{ $purchase_order->translateDate($purchase_order->due_date, $purchase_order->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_id) !!}
</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($purchase_orders && $purchase_orders->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/purchase_orders/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,11 @@
@extends('portal.ninja2020.layout.clean')
@section('meta_title', ctrans('texts.vendor'))
@component('portal.ninja2020.components.test')
@endcomponent
@section('body')
<div class="flex justify-center items-center h-screen">
<h1>Vendor Portal</h1>
</div>
@endsection

View File

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

View File

@ -0,0 +1,25 @@
@extends('portal.ninja2020.layout.vendor_app')
@section('meta_title', ctrans('texts.purchase_orders'))
@section('header')
@if($errors->any())
<div class="alert alert-failure mb-4">
@foreach($errors->all() as $error)
<p>{{ $error }}</p>
@endforeach
</div>
@endif
@endsection
@section('body')
<div class="flex items-center">
<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,59 @@
@extends('portal.ninja2020.layout.vendor_app')
@section('meta_title', ctrans('texts.view_purchase_order'))
@push('head')
<meta name="show-purchase_order-terms" content="false">
<meta name="require-purchase_order-signature" content="{{ $purchase_order->company->account->hasFeature(\App\Models\Account::FEATURE_INVOICE_SETTINGS) && $settings->require_purchase_order_signature }}">
@include('portal.ninja2020.components.no-cache')
<script src="{{ asset('vendor/signature_pad@2.3.2/signature_pad.min.js') }}"></script>
@endpush
@section('body')
@if(in_array($purchase_order->status_id, [\App\Models\PurchaseOrder::STATUS_SENT, \App\Models\PurchaseOrder::STATUS_DRAFT]))
<div class="mb-4">
@include('portal.ninja2020.purchase_orders.includes.actions', ['purchase_order' => $purchase_order])
</div>
@else
<div class="bg-white shadow sm:rounded-lg mb-4">
<div class="px-4 py-5 sm:p-6">
<div class="sm:flex sm:items-start sm:justify-between">
<div>
<h3 class="text-lg leading-6 font-medium text-gray-900">
{{ ctrans('texts.purchase_order_number_placeholder', ['purchase_order' => $purchase_order->number])}}
- {{ \App\Models\PurchaseOrder::stringStatus($purchase_order->status_id) }}
</h3>
@if($key)
<div class="btn hidden md:block" data-clipboard-text="{{url("vendor/purchase_order/{$key}")}}" aria-label="Copied!">
<div class="flex text-sm leading-6 font-medium text-gray-500">
<p class="pr-10">{{url("vendor/purchase_order/{$key}")}}</p>
<p><img class="h-5 w-5" src="{{ asset('assets/clippy.svg') }}" alt="Copy to clipboard"></p>
</div>
</div>
@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/purchase_orders/accept.js') }}"></script>
<script src="{{ asset('vendor/clipboard.min.js') }}"></script>
<script type="text/javascript">
var clipboard = new ClipboardJS('.btn');
</script>
@endsection

View File

@ -0,0 +1,177 @@
@extends('portal.ninja2020.layout.vendor_app')
@section('meta_title', ctrans('texts.vendor_information'))
@section('header')
<p class="leading-5 text-gray-500">{{ ctrans('texts.update_your_personal_info') }}</p>
@endsection
@section('body')
@if(session()->has('missing_required_fields'))
<div class="validation validation-fail">
<p class="mb-3 font-semibold">{{ ctrans('texts.before_proceeding_with_payment_warning') }}:</p>
<ul>
@foreach(session()->get('missing_required_fields') as $field)
<li class="block"> {{ ctrans("texts.{$field}") }}</li>
@endforeach
</ul>
<button onclick="window.history.back();" class="block mt-3 button button-link pl-0 ml-0 underline">{{ ctrans('texts.after_completing_go_back_to_previous_page') }}</button>
</div>
@endif
<div class="mt-2 sm:mt-6">
<div class="md:grid md:grid-cols-3 md:gap-6">
<div class="md:col-span-1">
<div class="sm:px-0">
<h3 class="text-lg font-medium leading-6 text-gray-900">{{ ctrans('texts.contact_details') }}</h3>
</div>
</div> <!-- End of left-side -->
<div class="mt-5 md:mt-0 md:col-span-2">
<form action="{{ route('vendor.profile.update', ['vendor_contact' => $contact->hashed_id]) }}" method="post" id="saveVendor">
@csrf
@method('PUT')
<div class="shadow overflow-hidden rounded">
<div class="px-4 py-5 bg-white sm:p-6">
<div class="grid grid-cols-6 gap-6">
<div class="col-span-6 sm:col-span-3">
<label for="first_name" class="input-label">@lang('texts.first_name')</label>
<input id="contact_first_name"
class="input w-full {{ in_array('contact_first_name', (array) session('missing_required_fields')) ? 'border border-red-400' : '' }}"
name="first_name" value="{{ $contact->first_name }}"/>
@error('first_name')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-3">
<label for="last_name" class="input-label">@lang('texts.last_name')</label>
<input id="contact_last_name"
class="input w-full {{ in_array('contact_last_name', (array) session('missing_required_fields')) ? 'border border-red-400' : '' }}"
name="last_name" value="{{ $contact->last_name}}"/>
@error('last_name')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-4">
<label for="email_address" class="input-label">@lang('texts.email_address')</label>
<input id="contact_email_address"
class="input w-full {{ in_array('contact_email', (array) session('missing_required_fields')) ? 'border border-red-400' : '' }}"
type="email" name="email" value="{{ $contact->email }}"/>
@error('email')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-4">
<label for="contact_phone" class="input-label">@lang('texts.phone')</label>
<input id="contact_phone" class="input w-full" name="phone"
value="{{ $contact->phone}}"/>
@error('phone')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
</div>
</div>
</div>
</div> <!-- End of main form -->
</div>
</div>
<div class="mt-10 sm:mt-6">
<div class="md:grid md:grid-cols-3 md:gap-6">
<div class="md:col-span-1">
<div class="sm:px-0">
<h3 class="text-lg font-medium leading-6 text-gray-900">{{ ctrans('texts.billing_address') }}</h3>
</div>
</div>
<div class="mt-5 md:mt-0 md:col-span-2">
<div class="px-4 py-5 bg-white sm:p-6">
<div class="grid grid-cols-6 gap-6">
<div class="col-span-6 sm:col-span-4">
<label for="address1" class="input-label">{{ ctrans('texts.address1') }}</label>
<input id="address1" class="input w-full {{ in_array('billing_address1', (array) session('missing_required_fields')) ? 'border border-red-400' : '' }}" name="address1" value="{{ $vendor->address1 }}" />
@error('address1')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-3">
<label for="address2" class="input-label">{{ ctrans('texts.address2') }}</label>
<input id="address2" class="input w-full {{ in_array('billing_address2', (array) session('missing_required_fields')) ? 'border border-red-400' : '' }}" name="address2" value="{{ $vendor->address2 }}" />
@error('address2')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-3">
<label for="city" class="input-label">{{ ctrans('texts.city') }}</label>
<input id="city" class="input w-full {{ in_array('billing_city', (array) session('missing_required_fields')) ? 'border border-red-400' : '' }}" name="city" value="{{ $vendor->city }}" />
@error('city')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-2">
<label for="state" class="input-label">{{ ctrans('texts.state') }}</label>
<input id="state" class="input w-full {{ in_array('billing_state', (array) session('missing_required_fields')) ? 'border border-red-400' : '' }}" name="state" value="{{ $vendor->state }}" />
@error('state')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-2">
<label for="postal_code" class="input-label">{{ ctrans('texts.postal_code') }}</label>
<input id="postal_code" class="input w-full {{ in_array('billing_postal_code', (array) session('missing_required_fields')) ? 'border border-red-400' : '' }}" name="postal_code" value="{{ $vendor->postal_code }}" />
@error('postal_code')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-2">
<label for="country" class="input-label">@lang('texts.country')</label>
<select id="country" class="input w-full bg-white form-select {{ in_array('billing_country', (array) session('missing_required_fields')) ? 'border border-red-400' : '' }}" value="{{ $vendor->country_id }}" name="country_id">
<option value="none"></option>
@foreach($countries as $country)
<option value="{{ $country->id }}" @if($vendor->country_id == $country->id) selected @endif>
{{ $country->iso_3166_2 }} ({{ $country->name }})
</option>
@endforeach
</select>
@error('country')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
</div>
</div>
<div class="px-4 py-3 bg-gray-50 text-right sm:px-6">
<button type="submit" class="button button-primary bg-primary">{{ ctrans('texts.save') }}</button>
</div>
</div>
</form>
</div>
</div>
@endsection

View File

@ -17,4 +17,15 @@
</body>
<!--
If you are reading this, there is a fair change that the react application has not loaded for you. There are a couple of solutions:
1. Download the release file from https://github.com/invoiceninja/invoiceninja and overwrite your current installation.
2. Switch back to the Flutter application by editing the database, you can do this with the following SQL
UPDATE accounts SET
set_react_as_default_ap = 0;
-->
</html>

View File

@ -24,6 +24,7 @@ Route::group(['middleware' => ['throttle:10,1','api_secret_check','email_db']],
});
Route::group(['middleware' => ['throttle:100,1', 'api_db', 'token_auth', 'locale'], 'prefix' => 'api/v1', 'as' => 'api.'], function () {
Route::put('accounts/{account}', 'AccountController@update')->name('account.update');
Route::post('check_subdomain', 'SubdomainController@index')->name('check_subdomain');
Route::get('ping', 'PingController@index')->name('ping');
Route::get('health_check', 'PingController@health')->name('health_check');

View File

@ -9,5 +9,35 @@
| is assigned the "api" middleware group. Enjoy building your API!
|
*/
use App\Http\Controllers\Auth\VendorContactLoginController;
use App\Http\Controllers\VendorPortal\InvitationController;
use App\Http\Controllers\VendorPortal\PurchaseOrderController;
use App\Http\Controllers\VendorPortal\VendorContactController;
use Illuminate\Support\Facades\Route;
Route::get('vendors', [VendorContactLoginController::class, 'catch'])->name('vendor.catchall')->middleware(['domain_db', 'contact_account','vendor_locale']); //catch all
Route::group(['middleware' => ['invite_db'], 'prefix' => 'vendor', 'as' => 'vendor.'], function () {
/*Invitation catches*/
Route::get('purchase_order/{invitation_key}', [InvitationController::class, 'purchaseOrder']);
// 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', [VendorContactController::class, 'edit'])->name('profile.edit');
Route::put('profile/{vendor_contact}/edit', [VendorContactController::class, 'update'])->name('profile.update');
Route::post('purchase_orders/bulk', [PurchaseOrderController::class, 'bulk'])->name('purchase_orders.bulk');
Route::get('logout', [VendorContactLoginController::class, 'logout'])->name('logout');
});
Route::fallback('BaseController@notFoundVendor');

View File

@ -85,27 +85,27 @@ class InventoryManagementTest extends TestCase
$this->assertEquals(90, $product->in_stock_quantity);
$arr = $response->json();
$invoice_hashed_id = $arr['data']['id'];
// $arr = $response->json();
// $invoice_hashed_id = $arr['data']['id'];
$invoice_item = new InvoiceItem;
$invoice_item->type_id = 1;
$invoice_item->product_key = $product->product_key;
$invoice_item->notes = $product->notes;
$invoice_item->quantity = 5;
$invoice_item->cost = 100;
// $invoice_item = new InvoiceItem;
// $invoice_item->type_id = 1;
// $invoice_item->product_key = $product->product_key;
// $invoice_item->notes = $product->notes;
// $invoice_item->quantity = 5;
// $invoice_item->cost = 100;
$line_items2[] = $invoice_item;
$invoice->line_items = $line_items2;
// $line_items2[] = $invoice_item;
// $invoice->line_items = $line_items2;
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->put('/api/v1/invoices/'.$invoice_hashed_id, $invoice->toArray())
->assertStatus(200);
// $response = $this->withHeaders([
// 'X-API-SECRET' => config('ninja.api_secret'),
// 'X-API-TOKEN' => $this->token,
// ])->put('/api/v1/invoices/'.$invoice_hashed_id, $invoice->toArray())
// ->assertStatus(200);
$product = $product->refresh();
// $product = $product->refresh();
$this->assertEquals(95, $product->in_stock_quantity);
// $this->assertEquals(95, $product->in_stock_quantity);
}
}

8
webpack.mix.js vendored
View File

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