diff --git a/.gitignore b/.gitignore
index 7f98460740a8..8dc8e56c2d70 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,3 +30,4 @@ storage/migrations
# Ignore Tailwind & Javascript build file >2mb without PurgeCSS (development-only)
public/css/app.css
public/js/app.js
+public/js/clients/*
diff --git a/app/Helpers/ClientPortal.php b/app/Helpers/ClientPortal.php
new file mode 100644
index 000000000000..c3a8600e6f82
--- /dev/null
+++ b/app/Helpers/ClientPortal.php
@@ -0,0 +1,51 @@
+render('auth.passwords.request', [
'title' => 'Client Password Reset',
'passwordEmailRoute' => 'client.password.email'
]);
diff --git a/app/Http/Controllers/Auth/ContactLoginController.php b/app/Http/Controllers/Auth/ContactLoginController.php
index 0c4aeaf82e68..43a4bde989df 100644
--- a/app/Http/Controllers/Auth/ContactLoginController.php
+++ b/app/Http/Controllers/Auth/ContactLoginController.php
@@ -28,12 +28,12 @@ class ContactLoginController extends Controller
{
$this->middleware('guest:contact', ['except' => ['logout']]);
}
-
+
public function showLoginForm()
{
- return view('portal.default.auth.login');
+ return $this->render('auth.login');
}
-
+
public function login(Request $request)
{
@@ -65,10 +65,10 @@ class ContactLoginController extends Controller
if (session()->get('url.intended')) {
return redirect(session()->get('url.intended'));
}
-
+
return redirect(route('client.dashboard'));
}
-
+
public function logout()
{
Auth::guard('contact')->logout();
diff --git a/app/Http/Controllers/Auth/ContactResetPasswordController.php b/app/Http/Controllers/Auth/ContactResetPasswordController.php
index 8ba1d67948a3..4ac6eeae6728 100644
--- a/app/Http/Controllers/Auth/ContactResetPasswordController.php
+++ b/app/Http/Controllers/Auth/ContactResetPasswordController.php
@@ -60,7 +60,7 @@ class ContactResetPasswordController extends Controller
*/
public function showResetForm(Request $request, $token = null)
{
- return view('portal.default.auth.passwords.reset')->with(
+ return $this->render('auth.passwords.reset')->with(
['token' => $token, 'email' => $request->email]
);
}
diff --git a/app/Http/Controllers/ClientPortal/CreditController.php b/app/Http/Controllers/ClientPortal/CreditController.php
new file mode 100644
index 000000000000..618039a4771e
--- /dev/null
+++ b/app/Http/Controllers/ClientPortal/CreditController.php
@@ -0,0 +1,32 @@
+user()->company->credits()->paginate(10);
+
+ return $this->render('credits.index', [
+ 'credits' => $credits,
+ ]);
+ }
+
+ public function show(ShowCreditRequest $request, Credit $credit)
+ {
+ return $this->render('credits.show', [
+ 'credit' => $credit,
+ ]);
+ }
+}
diff --git a/app/Http/Controllers/ClientPortal/DashboardController.php b/app/Http/Controllers/ClientPortal/DashboardController.php
index 208dd7679587..19ff8f57aa1c 100644
--- a/app/Http/Controllers/ClientPortal/DashboardController.php
+++ b/app/Http/Controllers/ClientPortal/DashboardController.php
@@ -16,80 +16,11 @@ use App\Http\Controllers\Controller;
class DashboardController extends Controller
{
-
/**
- * Display a listing of the resource.
- *
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function index()
{
- return view('dashboard.index');
- }
-
- /**
- * Show the form for creating a new resource.
- *
- * @return \Illuminate\Http\Response
- */
- public function create()
- {
- //
- }
-
- /**
- * Store a newly created resource in storage.
- *
- * @param \Illuminate\Http\Request $request
- * @return \Illuminate\Http\Response
- */
- public function store(Request $request)
- {
- //
- }
-
- /**
- * Display the specified resource.
- *
- * @param int $id
- * @return \Illuminate\Http\Response
- */
- public function show($id)
- {
- //
- }
-
- /**
- * Show the form for editing the specified resource.
- *
- * @param int $id
- * @return \Illuminate\Http\Response
- */
- public function edit($id)
- {
- //
- }
-
- /**
- * Update the specified resource in storage.
- *
- * @param \Illuminate\Http\Request $request
- * @param int $id
- * @return \Illuminate\Http\Response
- */
- public function update(Request $request, $id)
- {
- //
- }
-
- /**
- * Remove the specified resource from storage.
- *
- * @param int $id
- * @return \Illuminate\Http\Response
- */
- public function destroy($id)
- {
- //
+ return $this->render('dashboard.index');
}
}
diff --git a/app/Http/Controllers/ClientPortal/InvoiceController.php b/app/Http/Controllers/ClientPortal/InvoiceController.php
index 48572b54a00f..95b96ea83d48 100644
--- a/app/Http/Controllers/ClientPortal/InvoiceController.php
+++ b/app/Http/Controllers/ClientPortal/InvoiceController.php
@@ -13,21 +13,18 @@ namespace App\Http\Controllers\ClientPortal;
use App\Filters\InvoiceFilters;
use App\Http\Controllers\Controller;
+use App\Http\Requests\ClientPortal\ProcessInvoicesInBulkRequest;
use App\Http\Requests\ClientPortal\ShowInvoiceRequest;
+use App\Http\Requests\Request;
use App\Jobs\Entity\ActionEntity;
use App\Models\Invoice;
-use App\Repositories\BaseRepository;
use App\Utils\Number;
use App\Utils\Traits\MakesDates;
use App\Utils\Traits\MakesHash;
-use Illuminate\Http\Request;
-use Illuminate\Support\Facades\File;
-use Illuminate\Support\Facades\Log;
-use Illuminate\Support\Facades\Storage;
-use Yajra\DataTables\Facades\DataTables;
use Yajra\DataTables\Html\Builder;
use ZipStream\Option\Archive;
use ZipStream\ZipStream;
+use function GuzzleHttp\Promise\all;
/**
* Class InvoiceController
@@ -38,42 +35,20 @@ class InvoiceController extends Controller
{
use MakesHash;
use MakesDates;
+
/**
* Show the list of Invoices
*
- * @param \App\Filters\InvoiceFilters $filters The filters
+ * @param \App\Filters\InvoiceFilters $filters The filters
*
- * @return \Illuminate\Http\Response
+ * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
+ * @throws \Exception
*/
public function index(InvoiceFilters $filters, Builder $builder)
{
- $invoices = Invoice::filter($filters)->with('client', 'client.country');
+ $invoices = auth()->user()->client->company->invoices()->paginate(10);
- if (request()->ajax()) {
- return DataTables::of($invoices)->addColumn('action', function ($invoice) {
- return $this->buildClientButtons($invoice);
- })
- ->addColumn('checkbox', function ($invoice) {
- return ' ';
- })
- ->editColumn('status_id', function ($invoice) {
- return Invoice::badgeForStatus($invoice->status);
- })->editColumn('date', function ($invoice) {
- return $this->formatDate($invoice->date, $invoice->client->date_format());
- })->editColumn('due_date', function ($invoice) {
- return $this->formatDate($invoice->due_date, $invoice->client->date_format());
- })->editColumn('balance', function ($invoice) {
- return Number::formatMoney($invoice->balance, $invoice->client);
- })->editColumn('amount', function ($invoice) {
- return Number::formatMoney($invoice->amount, $invoice->client);
- })
- ->rawColumns(['checkbox', 'action', 'status_id'])
- ->make(true);
- }
-
- $data['html'] = $builder;
-
- return view('portal.default.invoices.index', $data);
+ return $this->render('invoices.index', ['invoices' => $invoices]);
}
private function buildClientButtons($invoice)
@@ -96,31 +71,34 @@ class InvoiceController extends Controller
*
* @param \App\Models\Invoice $invoice The invoice
*
- * @return \Illuminate\Http\Response
+ * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function show(ShowInvoiceRequest $request, Invoice $invoice)
{
$data = [
'invoice' => $invoice,
];
-
- return view('portal.default.invoices.show', $data);
+
+ return $this->render('invoices.show', $data);
}
/**
* Pay one or more invoices
*
- * @return View
+ * @param ProcessInvoicesInBulkRequest $request
+ * @return mixed
*/
- public function bulk()
+ public function bulk(ProcessInvoicesInBulkRequest $request)
{
- $transformed_ids = $this->transformKeys(explode(",", request()->input('hashed_ids')));
+ $transformed_ids = $this->transformKeys($request->invoices);
if (request()->input('action') == 'payment') {
- return $this->makePayment($transformed_ids);
+ return $this->makePayment((array)$transformed_ids);
} elseif (request()->input('action') == 'download') {
- return $this->downloadInvoicePDF($transformed_ids);
+ return $this->downloadInvoicePDF((array)$transformed_ids);
}
+
+ return redirect()->back();
}
@@ -159,7 +137,7 @@ class InvoiceController extends Controller
'total' => $total,
];
- return view('portal.default.invoices.payment', $data);
+ return $this->render('invoices.payment', $data);
}
private function downloadInvoicePDF(array $ids)
diff --git a/app/Http/Controllers/ClientPortal/PaymentController.php b/app/Http/Controllers/ClientPortal/PaymentController.php
index cf94d8e79f65..8b5bd8b56f70 100644
--- a/app/Http/Controllers/ClientPortal/PaymentController.php
+++ b/app/Http/Controllers/ClientPortal/PaymentController.php
@@ -38,54 +38,35 @@ class PaymentController extends Controller
/**
* Show the list of Invoices
*
- * @param \App\Filters\InvoiceFilters $filters The filters
+ * @param PaymentFilters $filters The filters
*
- * @return \Illuminate\Http\Response
+ * @param Builder $builder
+ * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function index(PaymentFilters $filters, Builder $builder)
{
//$payments = Payment::filter($filters);
- $payments = Payment::with('type', 'client');
+ $payments = Payment::with('type', 'client')->paginate(10);
- if (request()->ajax()) {
- return DataTables::of($payments)->addColumn('action', function ($payment) {
- return ' '.ctrans('texts.view').' ';
- })->editColumn('type_id', function ($payment) {
- return $payment->type->name;
- })
- ->editColumn('status_id', function ($payment) {
- return Payment::badgeForStatus($payment->status_id);
- })
- ->editColumn('date', function ($payment) {
- //return $payment->date;
- return $payment->formatDate($payment->date, $payment->client->date_format());
- })
- ->editColumn('amount', function ($payment) {
- return Number::formatMoney($payment->amount, $payment->client);
- })
- ->rawColumns(['action', 'status_id','type_id'])
- ->make(true);
- }
-
- $data['html'] = $builder;
-
- return view('portal.default.payments.index', $data);
+ return $this->render('payments.index', [
+ 'payments' => $payments,
+ ]);
}
/**
* Display the specified resource.
*
- * @param \App\Models\Invoice $invoice The invoice
- *
- * @return \Illuminate\Http\Response
+ * @param Request $request
+ * @param Payment $payment
+ * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function show(Request $request, Payment $payment)
{
$payment->load('invoices');
- $data['payment'] = $payment;
-
- return view('portal.default.payments.show', $data);
+ return $this->render('payments.show', [
+ 'payment' => $payment,
+ ]);
}
/**
@@ -111,7 +92,7 @@ class PaymentController extends Controller
if ($invoices->count() == 0) {
return back()->with(['warning' => 'No payable invoices selected']);
}
-
+
$invoices->map(function ($invoice) {
$invoice->balance = Number::formatMoney($invoice->balance, $invoice->client);
$invoice->due_date = $this->formatDate($invoice->due_date, $invoice->client->date_format());
@@ -127,7 +108,7 @@ class PaymentController extends Controller
//if there is a gateway fee, now is the time to calculate it
//and add it to the invoice
-
+
$data = [
'invoices' => $invoices,
'amount' => $amount,
@@ -137,8 +118,8 @@ class PaymentController extends Controller
'payment_method_id' => $payment_method_id,
'hashed_ids' => explode(",", request()->input('hashed_ids')),
];
-
-
+
+
return $gateway->driver(auth()->user()->client)->processPaymentView($data);
}
diff --git a/app/Http/Controllers/ClientPortal/PaymentMethodController.php b/app/Http/Controllers/ClientPortal/PaymentMethodController.php
index fb837de64e1f..3432732b98a6 100644
--- a/app/Http/Controllers/ClientPortal/PaymentMethodController.php
+++ b/app/Http/Controllers/ClientPortal/PaymentMethodController.php
@@ -16,9 +16,7 @@ use App\Http\Controllers\Controller;
use App\Models\ClientGatewayToken;
use App\Utils\Traits\MakesDates;
use Illuminate\Http\Request;
-use Illuminate\Http\Response;
use Illuminate\Support\Facades\Log;
-use Yajra\DataTables\Facades\DataTables;
use Yajra\DataTables\Html\Builder;
class PaymentMethodController extends Controller
@@ -28,49 +26,18 @@ class PaymentMethodController extends Controller
/**
* Display a listing of the resource.
*
- * @return \Illuminate\Http\Response
+ * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
+ * @throws \Exception
*/
public function index(Builder $builder)
{
- $payment_methods = ClientGatewayToken::whereClientId(auth()->user()->client->id);
- $payment_methods->with('gateway_type');
+ $payment_methods = ClientGatewayToken::with('gateway_type')
+ ->whereClientId(auth()->user()->client->id)
+ ->paginate(10);
- if (request()->ajax()) {
- return DataTables::of($payment_methods)->addColumn('action', function ($payment_method) {
- return ' ' . ctrans('texts.view') . ' ';
- })
- ->editColumn('gateway_type_id', function ($payment_method) {
- return ctrans("texts.{$payment_method->gateway_type->alias}");
- })->editColumn('created_at', function ($payment_method) {
- return $this->formatDateTimestamp($payment_method->created_at, auth()->user()->client->date_format());
- })->editColumn('is_default', function ($payment_method) {
- return $payment_method->is_default ? ctrans('texts.default') : '';
- })->editColumn('meta', function ($payment_method) {
- if (isset($payment_method->meta->exp_month) && isset($payment_method->meta->exp_year)) {
- return "{$payment_method->meta->exp_month}/{$payment_method->meta->exp_year}";
- } else {
- return "";
- }
- })->addColumn('last4', function ($payment_method) {
- if (isset($payment_method->meta->last4)) {
- return $payment_method->meta->last4;
- } else {
- return "";
- }
- })->addColumn('brand', function ($payment_method) {
- if (isset($payment_method->meta->brand)) {
- return $payment_method->meta->brand;
- } else {
- return "";
- }
- })
- ->rawColumns(['action', 'status_id', 'last4', 'brand'])
- ->make(true);
- }
-
- $data['html'] = $builder;
-
- return view('portal.default.payment_methods.index', $data);
+ return $this->render('payment_methods.index', [
+ 'payment_methods' => $payment_methods,
+ ]);
}
/**
@@ -112,7 +79,9 @@ class PaymentMethodController extends Controller
*/
public function show(ClientGatewayToken $payment_method)
{
- return view('portal.default.payment_methods.show', compact('payment_method'));
+ return $this->render('payment_methods.show', [
+ 'payment_method' => $payment_method,
+ ]);
}
/**
@@ -154,6 +123,8 @@ class PaymentMethodController extends Controller
return back();
}
- return redirect()->route('client.payment_methods.index');
+ return redirect()
+ ->route('client.payment_methods.index')
+ ->withSuccess('Payment method has been successfully removed.');
}
}
diff --git a/app/Http/Controllers/ClientPortal/ProfileController.php b/app/Http/Controllers/ClientPortal/ProfileController.php
index 2e80fa42c63e..f33f6a96717c 100644
--- a/app/Http/Controllers/ClientPortal/ProfileController.php
+++ b/app/Http/Controllers/ClientPortal/ProfileController.php
@@ -22,59 +22,39 @@ use Illuminate\Support\Facades\Log;
class ProfileController extends Controller
{
-
- /**
- * Display the specified resource.
- *
- * @param int $id
- * @return \Illuminate\Http\Response
- */
- public function show($id)
- {
- //
- }
-
/**
* Show the form for editing the specified resource.
*
- * @param int $id
- * @return \Illuminate\Http\Response
+ * @param ClientContact $client_contact
+ * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function edit(ClientContact $client_contact)
{
- /* Dropzone configuration */
- $data = [
- 'params' => [
- 'is_avatar' => true,
- ],
- 'url' => '/client/document',
- 'multi_upload' => false,
- ];
-
- return view('portal.default.profile.index', $data);
+ return $this->render('profile.index');
}
/**
* Update the specified resource in storage.
*
- * @param \Illuminate\Http\Request $request
- * @param int $id
- * @return \Illuminate\Http\Response
+ * @param UpdateContactRequest $request
+ * @param ClientContact $client_contact
+ * @return \Illuminate\Http\RedirectResponse
*/
public function update(UpdateContactRequest $request, ClientContact $client_contact)
{
$client_contact->fill($request->all());
- //update password if needed
- if ($request->input('password')) {
- $client_contact->password = Hash::make($request->input('password'));
+ if ($request->has('password')) {
+ $client_contact->password = encrypt($request->password);
}
$client_contact->save();
// auth()->user()->fresh();
- return back();
+ return back()->withSuccess(
+ ctrans('texts.profile_updated_successfully')
+ );
}
public function updateClient(UpdateClientRequest $request, ClientContact $client_contact)
@@ -93,6 +73,8 @@ class ProfileController extends Controller
$client->fill($request->all());
$client->save();
- return back();
+ return back()->withSuccess(
+ ctrans('texts.profile_updated_successfully')
+ );
}
}
diff --git a/app/Http/Controllers/ClientPortal/QuoteController.php b/app/Http/Controllers/ClientPortal/QuoteController.php
new file mode 100644
index 000000000000..549d8f71fee6
--- /dev/null
+++ b/app/Http/Controllers/ClientPortal/QuoteController.php
@@ -0,0 +1,114 @@
+user()->company->quotes()->paginate(10);
+
+ return $this->render('quotes.index', [
+ 'quotes' => $quotes,
+ ]);
+ }
+
+
+ /**
+ * Display the specified resource.
+ *
+ * @param ShowQuoteRequest $request
+ * @param Quote $quote
+ * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
+ */
+ public function show(ShowQuoteRequest $request, Quote $quote)
+ {
+ return $this->render('quotes.show', [
+ 'quote' => $quote,
+ ]);
+ }
+
+ public function bulk(ProcessQuotesInBulkRequest $request)
+ {
+ $transformed_ids = $this->transformKeys($request->quotes);
+
+ if ($request->action == 'download') {
+ return $this->downloadQuotePdf((array)$transformed_ids);
+ }
+
+ if ($request->action = 'approve') {
+ return $this->approve((array)$transformed_ids, $request->has('process'));
+ }
+
+ return back();
+ }
+
+ protected function downloadQuotePdf(array $ids)
+ {
+ $quotes = Quote::whereIn('id', $ids)
+ ->whereClientId(auth()->user()->client->id)
+ ->get();
+
+ if (!$quotes || $quotes->count() == 0) {
+ return;
+ }
+
+ if ($quotes->count() == 1) {
+ return response()->download(public_path($quotes->first()->pdf_file_path()));
+ }
+
+ # enable output of HTTP headers
+ $options = new Archive();
+ $options->setSendHttpHeaders(true);
+
+ # create a new zipstream object
+ $zip = new ZipStream(date('Y-m-d') . '_' . str_replace(' ', '_', trans('texts.invoices')) . ".zip", $options);
+
+ foreach ($quotes as $quote) {
+ $zip->addFileFromPath(basename($quote->pdf_file_path()), public_path($quote->pdf_file_path()));
+ }
+
+ # finish the zip stream
+ $zip->finish();
+ }
+
+ protected function approve(array $ids, $process = false)
+ {
+ $quotes = Quote::whereIn('id', $ids)
+ ->whereClientId(auth()->user()->client->id)
+ ->get();
+
+ if (!$quotes || $quotes->count() == 0) {
+ return redirect()->route('client.quotes.index');
+ }
+
+ if ($process) {
+
+ foreach ($quotes as $quote) {
+ $quote->service()->approve()->save();
+ }
+
+ return route('client.quotes.index')->withSuccess('Quote(s) approved successfully.');
+ }
+
+ return $this->render('quotes.approve', [
+ 'quotes' => $quotes,
+ ]);
+ }
+}
diff --git a/app/Http/Controllers/ClientPortal/RecurringInvoiceController.php b/app/Http/Controllers/ClientPortal/RecurringInvoiceController.php
index 52407639e1e5..4ffbf270b2c5 100644
--- a/app/Http/Controllers/ClientPortal/RecurringInvoiceController.php
+++ b/app/Http/Controllers/ClientPortal/RecurringInvoiceController.php
@@ -35,75 +35,48 @@ class RecurringInvoiceController extends Controller
{
use MakesHash;
use MakesDates;
+
/**
- * Show the list of Invoices
+ * Show the list of recurring invoices.
*
- * @param \App\Filters\InvoiceFilters $filters The filters
- *
- * @return \Illuminate\Http\Response
+ * @param Builder $builder
+ * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function index(Builder $builder)
{
$invoices = RecurringInvoice::whereClientId(auth()->user()->client->id)
- ->whereIn('status_id', [RecurringInvoice::STATUS_PENDING, RecurringInvoice::STATUS_ACTIVE, RecurringInvoice::STATUS_COMPLETED])
- ->orderBy('status_id', 'asc')
- ->with('client')
- ->get();
+ ->whereIn('status_id', [RecurringInvoice::STATUS_PENDING, RecurringInvoice::STATUS_ACTIVE, RecurringInvoice::STATUS_COMPLETED])
+ ->orderBy('status_id', 'asc')
+ ->with('client')
+ ->paginate(10);
- if (request()->ajax()) {
- return DataTables::of($invoices)->addColumn('action', function ($invoice) {
- return ' '.ctrans('texts.view').' ';
- })->addColumn('frequency_id', function ($invoice) {
- return RecurringInvoice::frequencyForKey($invoice->frequency_id);
- })
- ->editColumn('status_id', function ($invoice) {
- return RecurringInvoice::badgeForStatus($invoice->status);
- })
- ->editColumn('start_date', function ($invoice) {
- return $this->formatDate($invoice->date, $invoice->client->date_format());
- })
- ->editColumn('next_send_date', function ($invoice) {
- return $this->formatDate($invoice->next_send_date, $invoice->client->date_format());
- })
- ->editColumn('amount', function ($invoice) {
- return Number::formatMoney($invoice->amount, $invoice->client);
- })
- ->rawColumns(['action', 'status_id'])
- ->make(true);
- }
-
- $data['html'] = $builder;
-
- return view('portal.default.recurring_invoices.index', $data);
+ return $this->render('recurring_invoices.index', [
+ 'invoices' => $invoices,
+ ]);
}
/**
- * Display the specified resource.
+ * Display the recurring invoice.
*
- * @param \App\Models\Invoice $invoice The invoice
- *
- * @return \Illuminate\Http\Response
+ * @param ShowRecurringInvoiceRequest $request
+ * @param RecurringInvoice $recurring_invoice
+ * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function show(ShowRecurringInvoiceRequest $request, RecurringInvoice $recurring_invoice)
{
- $data = [
+ return $this->render('recurring_invoices.show', [
'invoice' => $recurring_invoice->load('invoices'),
- ];
-
- return view('portal.default.recurring_invoices.show', $data);
+ ]);
}
-
public function requestCancellation(Request $request, RecurringInvoice $recurring_invoice)
{
- $data = [
- 'invoice' => $recurring_invoice
- ];
-
//todo double check the user is able to request a cancellation
//can add locale specific by chaining ->locale();
$recurring_invoice->user->notify(new ClientContactRequestCancellation($recurring_invoice, auth()->user()));
- return view('portal.default.recurring_invoices.request_cancellation', $data);
+ return $this->render('recurring_invoices.cancellation.index', [
+ 'invoice' => $recurring_invoice,
+ ]);
}
}
diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php
index 5a68ef46b35d..2d5031252030 100644
--- a/app/Http/Controllers/Controller.php
+++ b/app/Http/Controllers/Controller.php
@@ -22,6 +22,8 @@ class Controller extends BaseController
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
/**
+ * Proxy method for rendering views.
+ *
* @param string $path
* @param array $options
*
@@ -29,15 +31,6 @@ class Controller extends BaseController
*/
public function render(string $path, array $options = [])
{
- $theme = array_key_exists('theme', $options) ? $options['theme'] : 'ninja2020';
-
- if (array_key_exists('root', $options)) {
- return view(
- sprintf('%s.%s.%s', $options['root'], $theme, $path),
- $options
- );
- }
-
- return view("portal.$theme.$path", $options);
+ return render($path, $options);
}
}
diff --git a/app/Http/Requests/ClientPortal/ProcessInvoicesInBulkRequest.php b/app/Http/Requests/ClientPortal/ProcessInvoicesInBulkRequest.php
new file mode 100644
index 000000000000..9a41f91069da
--- /dev/null
+++ b/app/Http/Requests/ClientPortal/ProcessInvoicesInBulkRequest.php
@@ -0,0 +1,30 @@
+ ['array'],
+ ];
+ }
+}
diff --git a/app/Http/Requests/ClientPortal/ProcessQuotesInBulkRequest.php b/app/Http/Requests/ClientPortal/ProcessQuotesInBulkRequest.php
new file mode 100644
index 000000000000..f99e60880f73
--- /dev/null
+++ b/app/Http/Requests/ClientPortal/ProcessQuotesInBulkRequest.php
@@ -0,0 +1,30 @@
+ ['array'],
+ ];
+ }
+}
diff --git a/app/Http/Requests/ClientPortal/ShowCreditRequest.php b/app/Http/Requests/ClientPortal/ShowCreditRequest.php
new file mode 100644
index 000000000000..0de22c7e9ef9
--- /dev/null
+++ b/app/Http/Requests/ClientPortal/ShowCreditRequest.php
@@ -0,0 +1,30 @@
+user()->client->id === $this->quote->client_id;
+ }
+
+ /**
+ * Get the validation rules that apply to the request.
+ *
+ * @return array
+ */
+ public function rules()
+ {
+ return [
+ //
+ ];
+ }
+}
diff --git a/app/Http/Requests/ClientPortal/UpdateClientRequest.php b/app/Http/Requests/ClientPortal/UpdateClientRequest.php
index de0b8c9c7497..069d93c98979 100644
--- a/app/Http/Requests/ClientPortal/UpdateClientRequest.php
+++ b/app/Http/Requests/ClientPortal/UpdateClientRequest.php
@@ -17,7 +17,7 @@ use App\Utils\Traits\MakesHash;
class UpdateClientRequest extends Request
{
use MakesHash;
-
+
/**
* Determine if the user is authorized to make this request.
*
@@ -32,7 +32,7 @@ class UpdateClientRequest extends Request
public function rules()
{
return [
- 'name' => 'required',
+ 'name' => 'sometimes|required',
'file' => 'sometimes|nullable|max:100000|mimes:png,svg,jpeg,gif,jpg,bmp'
];
}
diff --git a/app/Http/ViewComposers/PortalComposer.php b/app/Http/ViewComposers/PortalComposer.php
index 1ccce4925bb7..812fb958a1fe 100644
--- a/app/Http/ViewComposers/PortalComposer.php
+++ b/app/Http/ViewComposers/PortalComposer.php
@@ -58,11 +58,13 @@ class PortalComposer
{
$data = [];
- $data[] = [ 'title' => ctrans('texts.dashboard'), 'url' => 'client.dashboard', 'icon' => 'fa fa-tachometer fa-fw fa-2x'];
- $data[] = [ 'title' => ctrans('texts.invoices'), 'url' => 'client.invoices.index', 'icon' => 'fa fa-file-pdf-o fa-fw fa-2x'];
- $data[] = [ 'title' => ctrans('texts.recurring_invoices'), 'url' => 'client.recurring_invoices.index', 'icon' => 'fa fa-files-o fa-fw fa-2x'];
- $data[] = [ 'title' => ctrans('texts.payments'), 'url' => 'client.payments.index', 'icon' => 'fa fa-credit-card fa-fw fa-2x'];
- $data[] = [ 'title' => ctrans('texts.payment_methods'), 'url' => 'client.payment_methods.index', 'icon' => 'fa fa-cc-stripe fa-fw fa-2x'];
+ $data[] = [ 'title' => ctrans('texts.dashboard'), 'url' => 'client.dashboard', 'icon' => 'activity'];
+ $data[] = [ 'title' => ctrans('texts.invoices'), 'url' => 'client.invoices.index', 'icon' => 'file-text'];
+ $data[] = [ 'title' => ctrans('texts.recurring_invoices'), 'url' => 'client.recurring_invoices.index', 'icon' => 'file'];
+ $data[] = [ 'title' => ctrans('texts.payments'), 'url' => 'client.payments.index', 'icon' => 'credit-card'];
+ $data[] = [ 'title' => ctrans('texts.payment_methods'), 'url' => 'client.payment_methods.index', 'icon' => 'shield'];
+ $data[] = [ 'title' => ctrans('texts.quotes'), 'url' => 'client.quotes.index', 'icon' => 'align-left'];
+ $data[] = [ 'title' => ctrans('texts.credits'), 'url' => 'client.credits.index', 'icon' => 'credit-card'];
return $data;
}
diff --git a/app/Models/ClientContact.php b/app/Models/ClientContact.php
index 0388b1775033..bdc9bda15ce9 100644
--- a/app/Models/ClientContact.php
+++ b/app/Models/ClientContact.php
@@ -45,7 +45,7 @@ class ClientContact extends Authenticatable implements HasLocalePreference
protected $dates = [
'deleted_at'
];
-
+
protected $appends = [
'hashed_id'
];
@@ -85,12 +85,12 @@ class ClientContact extends Authenticatable implements HasLocalePreference
'email',
'is_primary',
];
-
+
public function getHashedIdAttribute()
{
return $this->encodePrimaryKey($this->id);
}
-
+
/**/
public function getRouteKeyName()
{
@@ -139,7 +139,7 @@ class ClientContact extends Authenticatable implements HasLocalePreference
public function preferredLocale()
{
$languages = Cache::get('languages');
-
+
return $languages->filter(function ($item) {
return $item->id == $this->client->getSetting('language_id');
})->first()->locale;
@@ -162,4 +162,15 @@ class ClientContact extends Authenticatable implements HasLocalePreference
->withTrashed()
->where('id', $this->decodePrimaryKey($value))->firstOrFail();
}
+
+ /**
+ * @return mixed|string
+ */
+ public function avatar()
+ {
+ if($this->avatar)
+ return $this->avatar;
+
+ return asset('images/svg/user.svg');
+ }
}
diff --git a/app/Models/ClientGatewayToken.php b/app/Models/ClientGatewayToken.php
index ec823beb0501..25eb936e17fb 100644
--- a/app/Models/ClientGatewayToken.php
+++ b/app/Models/ClientGatewayToken.php
@@ -16,9 +16,12 @@ use App\Models\Company;
use App\Models\CompanyGateway;
use App\Models\GatewayType;
use App\Models\User;
+use App\Utils\Traits\MakesDates;
class ClientGatewayToken extends BaseModel
{
+ use MakesDates;
+
protected $casts = [
'meta' => 'object',
'updated_at' => 'timestamp',
@@ -50,7 +53,7 @@ class ClientGatewayToken extends BaseModel
{
return $this->hasOne(User::class)->withTrashed();
}
-
+
/**
* Retrieve the model for a bound value.
*
diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php
index 8560e8cc3ebe..f2debf5f29e4 100644
--- a/app/Models/Invoice.php
+++ b/app/Models/Invoice.php
@@ -114,6 +114,10 @@ class Invoice extends BaseModel
'status'
];
+ protected $dates = [
+ 'date',
+ ];
+
const STATUS_DRAFT = 1;
const STATUS_SENT = 2;
const STATUS_PARTIAL = 3;
diff --git a/app/Models/Quote.php b/app/Models/Quote.php
index 6fed6f116acb..3875a5d4b802 100644
--- a/app/Models/Quote.php
+++ b/app/Models/Quote.php
@@ -17,6 +17,7 @@ use App\Jobs\Invoice\CreateInvoicePdf;
use App\Jobs\Quote\CreateQuotePdf;
use App\Models\Filterable;
use App\Services\Quote\QuoteService;
+use App\Utils\Traits\MakesDates;
use App\Utils\Traits\MakesHash;
use App\Utils\Traits\MakesInvoiceValues;
use App\Utils\Traits\MakesReminders;
@@ -29,6 +30,7 @@ use Laracasts\Presenter\PresentableTrait;
class Quote extends BaseModel
{
use MakesHash;
+ use MakesDates;
use Filterable;
use SoftDeletes;
use MakesReminders;
@@ -150,7 +152,8 @@ class Quote extends BaseModel
{
$storage_path = 'storage/' . $this->client->quote_filepath() . $this->number . '.pdf';
- if (Storage::exists($storage_path)) {
+ if (Storage::exists($storage_path))
+ {
return $storage_path;
}
@@ -162,4 +165,43 @@ class Quote extends BaseModel
return $storage_path;
}
+
+ /**
+ * @param int $status
+ * @return string
+ */
+ public static function badgeForStatus(int $status)
+ {
+ switch ($status) {
+ case Quote::STATUS_DRAFT:
+ return '
' . ctrans('texts.draft') . ' ';
+ break;
+ case Quote::STATUS_SENT:
+ return '' . ctrans('texts.sent') . ' ';
+ break;
+ case Quote::STATUS_APPROVED:
+ return '' . ctrans('texts.approved') . ' ';
+ break;
+ case Quote::STATUS_EXPIRED:
+ return '' . ctrans('texts.expired') . ' ';
+ break;
+ default:
+ # code...
+ break;
+ }
+ }
+
+ /**
+ * Check if the quote has been approved.
+ *
+ * @return bool
+ */
+ public function isApproved()
+ {
+ if($this->status_id === $this::STATUS_APPROVED) {
+ return true;
+ }
+
+ return false;
+ }
}
diff --git a/app/PaymentDrivers/StripePaymentDriver.php b/app/PaymentDrivers/StripePaymentDriver.php
index f6dea8a96e46..aa760fce9f5a 100644
--- a/app/PaymentDrivers/StripePaymentDriver.php
+++ b/app/PaymentDrivers/StripePaymentDriver.php
@@ -71,7 +71,7 @@ class StripePaymentDriver extends BasePaymentDriver
GatewayType::CREDIT_CARD,
//GatewayType::TOKEN,
];
-
+
if ($this->company_gateway->getSofortEnabled() && $this->invitation && $this->client() && isset($this->client()->country) && in_array($this->client()->country, ['AUT', 'BEL', 'DEU', 'ITA', 'NLD', 'ESP'])) {
$types[] = GatewayType::SOFORT;
}
@@ -83,19 +83,19 @@ class StripePaymentDriver extends BasePaymentDriver
if ($this->company_gateway->getSepaEnabled()) {
$types[] = GatewayType::SEPA;
}
-
+
if ($this->company_gateway->getBitcoinEnabled()) {
$types[] = GatewayType::CRYPTO;
}
-
+
if ($this->company_gateway->getAlipayEnabled()) {
$types[] = GatewayType::ALIPAY;
}
-
+
if ($this->company_gateway->getApplePayEnabled()) {
$types[] = GatewayType::APPLE_PAY;
}
-
+
return $types;
}
@@ -104,55 +104,48 @@ class StripePaymentDriver extends BasePaymentDriver
{
switch ($gateway_type_id) {
case GatewayType::CREDIT_CARD:
- return 'portal.default.gateways.stripe.credit_card';
- break;
case GatewayType::TOKEN:
- return 'portal.default.gateways.stripe.credit_card';
+ return 'gateways.stripe.credit_card';
break;
case GatewayType::SOFORT:
- return 'portal.default.gateways.stripe.sofort';
+ return 'gateways.stripe.sofort';
break;
case GatewayType::BANK_TRANSFER:
- return 'portal.default.gateways.stripe.ach';
+ return 'gateways.stripe.ach';
break;
case GatewayType::SEPA:
- return 'portal.default.gateways.stripe.sepa';
+ return 'gateways.stripe.sepa';
break;
case GatewayType::CRYPTO:
- return 'portal.default.gateways.stripe.other';
- break;
case GatewayType::ALIPAY:
- return 'portal.default.gateways.stripe.other';
- break;
case GatewayType::APPLE_PAY:
- return 'portal.default.gateways.stripe.other';
+ return 'gateways.stripe.other';
break;
default:
- # code...
break;
}
}
/**
+ * Authorises a credit card for future use.
*
- * Authorises a credit card for future use
* @param array $data Array of variables needed for the view
- *
- * @return view The gateway specific partial to be rendered
- *
+ * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function authorizeCreditCardView(array $data)
{
$intent['intent'] = $this->getSetupIntent();
- return view('portal.default.gateways.stripe.add_credit_card', array_merge($data, $intent));
+ return render('gateways.stripe.add_credit_card', array_merge($data, $intent));
}
/**
- * Processes the gateway response for credti card authorization
- * @param Request $request The returning request object
+ * Processes the gateway response for credit card authorization.
+ *
+ * @param Request $request The returning request object
* @return view Returns the user to payment methods screen.
+ * @throws \Stripe\Exception\ApiErrorException
*/
public function authorizeCreditCardResponse($request)
{
@@ -202,18 +195,11 @@ class StripePaymentDriver extends BasePaymentDriver
}
/**
- * Processes the payment with this gateway
+ * Process the payment with gateway.
*
- * @var invoices
- * @var amount
- * @var fee
- * @var amount_with_fee
- * @var token
- * @var payment_method_id
- * @var hashed_ids
- *
- * @param array $data variables required to build payment page
- * @return view Gateway and payment method specific view
+ * @param array $data
+ * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View|void
+ * @throws \Exception
*/
public function processPaymentView(array $data)
{
@@ -237,7 +223,7 @@ class StripePaymentDriver extends BasePaymentDriver
$data['gateway'] = $this;
- return view($this->viewForType($data['payment_method_id']), $data);
+ return render($this->viewForType($data['payment_method_id']), $data);
}
/**
@@ -292,7 +278,7 @@ class StripePaymentDriver extends BasePaymentDriver
* requires_payment_method
*
*/
-
+
if ($this->getContact()) {
$client_contact = $this->getContact();
} else {
@@ -322,7 +308,7 @@ class StripePaymentDriver extends BasePaymentDriver
if ($save_card == 'true') {
$stripe_payment_method->attach(['customer' => $customer]);
-
+
$cgt = new ClientGatewayToken;
$cgt->company_id = $this->client->company->id;
$cgt->client_id = $this->client->id;
@@ -340,7 +326,7 @@ class StripePaymentDriver extends BasePaymentDriver
$cgt->save();
}
}
-
+
//todo need to fix this to support payment types other than credit card.... sepa etc etc
if (!$payment_type) {
$payment_type = PaymentType::CREDIT_CARD_OTHER;
@@ -363,7 +349,7 @@ class StripePaymentDriver extends BasePaymentDriver
$payment->service()->UpdateInvoicePayment();
//UpdateInvoicePayment::dispatchNow($payment, $payment->company);
-
+
SystemLogger::dispatch(
[
'server_response' => $payment_intent,
diff --git a/config/breadcrumbs.php b/config/breadcrumbs.php
index 08902ab83d6a..742db9deb271 100644
--- a/config/breadcrumbs.php
+++ b/config/breadcrumbs.php
@@ -22,7 +22,7 @@ return [
|
*/
- 'view' => 'breadcrumbs::bootstrap4',
+ 'view' => 'portal.ninja2020.components.breadcrumbs',
/*
|--------------------------------------------------------------------------
diff --git a/public/images/svg/activity.svg b/public/images/svg/activity.svg
new file mode 100644
index 000000000000..ad2574938f00
--- /dev/null
+++ b/public/images/svg/activity.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/images/svg/align-left.svg b/public/images/svg/align-left.svg
new file mode 100644
index 000000000000..97a69a8eaa6d
--- /dev/null
+++ b/public/images/svg/align-left.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/images/svg/credit-card.svg b/public/images/svg/credit-card.svg
new file mode 100644
index 000000000000..e89a77fb1ffc
--- /dev/null
+++ b/public/images/svg/credit-card.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/images/svg/file-text.svg b/public/images/svg/file-text.svg
new file mode 100644
index 000000000000..d988a9438ad4
--- /dev/null
+++ b/public/images/svg/file-text.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/images/svg/file.svg b/public/images/svg/file.svg
new file mode 100644
index 000000000000..53e959a2e043
--- /dev/null
+++ b/public/images/svg/file.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/images/svg/shield.svg b/public/images/svg/shield.svg
new file mode 100644
index 000000000000..469f892d3cce
--- /dev/null
+++ b/public/images/svg/shield.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/images/svg/user.svg b/public/images/svg/user.svg
new file mode 100644
index 000000000000..4941bd2fa9dd
--- /dev/null
+++ b/public/images/svg/user.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/resources/js/clients/invoices/action-selectors.js b/resources/js/clients/invoices/action-selectors.js
new file mode 100644
index 000000000000..d4d7c95fa0f8
--- /dev/null
+++ b/resources/js/clients/invoices/action-selectors.js
@@ -0,0 +1,69 @@
+/**
+ * Invoice Ninja (https://invoiceninja.com)
+ *
+ * @link https://github.com/invoiceninja/invoiceninja source repository
+ *
+ * @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
+ *
+ * @license https://opensource.org/licenses/AAL
+ */
+
+class ActionSelectors {
+ constructor() {
+ this.parentElement = document.querySelector(".form-check-parent");
+ this.parentForm = document.getElementById("bulkActions");
+ }
+
+ watchCheckboxes(parentElement) {
+ 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());
+ }
+
+ let _temp = document.createElement("INPUT");
+
+ _temp.setAttribute("name", "invoices[]");
+ _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(".pay-now-button")) {
+ child.addEventListener("click", () => {
+ this.processChildItem(child, this.parentForm, { single: true });
+ document.querySelector('button[value="payment"]').click();
+ });
+ }
+
+ for (let child of document.querySelectorAll(".form-check-child")) {
+ child.addEventListener("click", () => {
+ this.processChildItem(child, this.parentForm);
+ });
+ }
+ }
+}
+
+/** @handle **/
+new ActionSelectors().handle();
diff --git a/resources/js/clients/invoices/payment.js b/resources/js/clients/invoices/payment.js
new file mode 100644
index 000000000000..432d2ebe6587
--- /dev/null
+++ b/resources/js/clients/invoices/payment.js
@@ -0,0 +1,91 @@
+/**
+ * Invoice Ninja (https://invoiceninja.com)
+ *
+ * @link https://github.com/invoiceninja/invoiceninja source repository
+ *
+ * @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
+ *
+ * @license https://opensource.org/licenses/AAL
+ */
+
+class Payment {
+ constructor(displayTerms, displaySignature) {
+ this.shouldDisplayTerms = displayTerms;
+ this.shouldDisplaySignature = displaySignature;
+ this.termsAccepted = false;
+ }
+
+ handleMethodSelect(element) {
+
+ 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.displaySignature();
+
+ document.getElementById('signature-next-step').addEventListener('click', () => {
+ this.submitForm();
+ });
+ }
+
+ if (this.shouldDisplaySignature && this.shouldDisplayTerms) {
+ this.displaySignature();
+
+ document.getElementById('signature-next-step').addEventListener('click', () => {
+ this.displayTerms();
+
+ document.getElementById('accept-terms-button').addEventListener('click', () => {
+ this.termsAccepted = true;
+ this.submitForm();
+ });
+ });
+ }
+
+ if (!this.shouldDisplaySignature && !this.shouldDisplayTerms) {
+ this.submitForm();
+ }
+ }
+
+ submitForm() {
+ document.getElementById('payment-form').submit();
+ }
+
+ displayTerms() {
+ let displayTermsModal = document.getElementById('displayTermsModal');
+ displayTermsModal.removeAttribute('style');
+ }
+
+ displaySignature() {
+ let displaySignatureModal = document.getElementById('displaySignatureModal');
+ displaySignatureModal.removeAttribute('style');
+
+ const signaturePad = new SignaturePad(document.getElementById('signature-pad'), {
+ backgroundColor: 'rgb(240,240,240)',
+ penColor: 'rgb(0, 0, 0)'
+ });
+ }
+
+ handle() {
+ document.querySelectorAll('.dropdown-gateway-button').forEach((element) => {
+ element.addEventListener('click', () => this.handleMethodSelect(element));
+ });
+ }
+}
+
+const signature = document.querySelector(
+ 'meta[name="require-invoice-signature"]'
+).content;
+
+const terms = document.querySelector(
+ 'meta[name="show-invoice-terms"]'
+).content;
+
+new Payment(Boolean(+signature), Boolean(+terms)).handle();
+
+
diff --git a/resources/js/clients/payment_methods/authorize-stripe-card.js b/resources/js/clients/payment_methods/authorize-stripe-card.js
new file mode 100644
index 000000000000..fbb3c1da0013
--- /dev/null
+++ b/resources/js/clients/payment_methods/authorize-stripe-card.js
@@ -0,0 +1,90 @@
+/**
+ * Invoice Ninja (https://invoiceninja.com)
+ *
+ * @link https://github.com/invoiceninja/invoiceninja source repository
+ *
+ * @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
+ *
+ * @license https://opensource.org/licenses/AAL
+ */
+
+class AuthorizeStripeCard {
+ constructor(key) {
+ this.key = key;
+ this.cardHolderName = document.getElementById("cardholder-name");
+ this.cardButton = document.getElementById("card-button");
+ this.clientSecret = this.cardButton.dataset.secret;
+ }
+
+ setupStripe() {
+ this.stripe = Stripe(this.key);
+ this.elements = this.stripe.elements();
+
+ return this;
+ }
+
+ createElement() {
+ this.cardElement = this.elements.create("card");
+
+ return this;
+ }
+
+ mountCardElement() {
+ this.cardElement.mount("#card-element");
+
+ return this;
+ }
+
+ handleStripe(stripe, cardHolderName) {
+ stripe
+ .handleCardSetup(this.clientSecret, this.cardElement, {
+ payment_method_data: {
+ billing_details: { name: cardHolderName.value }
+ }
+ })
+ .then(result => {
+ if (result.error) {
+ return this.handleFailure(result);
+ }
+ return this.handleSuccess(result);
+ });
+ }
+
+ handleFailure(result) {
+ let errors = document.getElementById("errors");
+
+ errors.textContent = "";
+ errors.textContent = result.error.message;
+ errors.hidden = false;
+ }
+
+ handleSuccess(result) {
+ document.getElementById("gateway_response").value = JSON.stringify(
+ result.setupIntent
+ );
+ document.getElementById("is_default").value = document.getElementById(
+ "proxy_is_default"
+ ).checked;
+
+ document.getElementById("server_response").submit();
+ }
+
+ handle() {
+ this.setupStripe()
+ .createElement()
+ .mountCardElement();
+
+ this.cardButton.addEventListener("click", () => {
+ this.handleStripe(this.stripe, this.cardHolderName);
+ });
+
+ return this;
+ }
+}
+
+const publishableKey = document.querySelector(
+ 'meta[name="stripe-publishable-key"]'
+).content;
+
+/** @handle */
+new AuthorizeStripeCard(publishableKey).handle();
diff --git a/resources/js/clients/quotes/action-selectors.js b/resources/js/clients/quotes/action-selectors.js
new file mode 100644
index 000000000000..751bad043125
--- /dev/null
+++ b/resources/js/clients/quotes/action-selectors.js
@@ -0,0 +1,62 @@
+/**
+ * Invoice Ninja (https://invoiceninja.com)
+ *
+ * @link https://github.com/invoiceninja/invoiceninja source repository
+ *
+ * @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
+ *
+ * @license https://opensource.org/licenses/AAL
+ */
+
+class ActionSelectors {
+ constructor() {
+ this.parentElement = document.querySelector(".form-check-parent");
+ this.parentForm = document.getElementById("bulkActions");
+ }
+
+ watchCheckboxes(parentElement) {
+ 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());
+ }
+
+ let _temp = document.createElement("INPUT");
+
+ _temp.setAttribute("name", "quotes[]");
+ _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();
diff --git a/resources/js/clients/quotes/approve.js b/resources/js/clients/quotes/approve.js
new file mode 100644
index 000000000000..90cbe471ce52
--- /dev/null
+++ b/resources/js/clients/quotes/approve.js
@@ -0,0 +1,51 @@
+/**
+ * Invoice Ninja (https://invoiceninja.com)
+ *
+ * @link https://github.com/invoiceninja/invoiceninja source repository
+ *
+ * @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
+ *
+ * @license https://opensource.org/licenses/AAL
+ */
+
+class Approve {
+ constructor(displaySignature) {
+ this.shouldDisplaySignature = displaySignature;
+ }
+
+ submitForm() {
+ document.getElementById('approve-form').submit();
+ }
+
+ displaySignature() {
+ let displaySignatureModal = document.getElementById('displaySignatureModal');
+ displaySignatureModal.removeAttribute('style');
+
+ const signaturePad = new SignaturePad(document.getElementById('signature-pad'), {
+ backgroundColor: 'rgb(240,240,240)',
+ penColor: 'rgb(0, 0, 0)'
+ });
+ }
+
+ handle() {
+ document.getElementById('approve-button').addEventListener('click', () => {
+ if (this.shouldDisplaySignature) {
+ this.displaySignature();
+
+ document.getElementById('signature-next-step').addEventListener('click', () => {
+ this.submitForm();
+ });
+ }
+
+ if (!this.shouldDisplaySignature) this.submitForm();
+ })
+ }
+}
+
+const signature = document.querySelector(
+ 'meta[name="require-quote-signature"]'
+).content;
+
+new Approve(Boolean(+signature)).handle();
+
+
diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php
index e1d2c748f339..894ebb4ef247 100644
--- a/resources/lang/en/texts.php
+++ b/resources/lang/en/texts.php
@@ -2,6 +2,7 @@
return [
'continue' => 'Continue',
+ 'back' => 'Back',
'complete' => 'Complete',
'next' => 'Next',
'next_step' => 'Next step',
@@ -504,6 +505,7 @@ return [
'api_tokens' => 'API Tokens',
'users_and_tokens' => 'Users & Tokens',
'account_login' => 'Account Login',
+ 'account_login_text' => 'Welcome back! Glad to see you.',
'recover_password' => 'Recover your password',
'forgot_password' => 'Forgot your password?',
'email_address' => 'Email address',
@@ -711,6 +713,7 @@ return [
'reminder_subject' => 'Reminder: Invoice :invoice from :account',
'reset' => 'Reset',
'invoice_not_found' => 'The requested invoice is not available',
+ 'request_cancellation' => 'Request cancellation',
'referral_program' => 'Referral Program',
'referral_code' => 'Referral URL',
'last_sent_on' => 'Sent Last: :date',
@@ -2196,6 +2199,7 @@ return [
'create_expense_category' => 'Create category',
'pro_plan_reports' => ':link to enable reports by joining the Pro Plan',
'mark_ready' => 'Mark Ready',
+ 'profile_updated_successfully' => 'The profile has been updated successfully.',
'limits' => 'Limits',
'fees' => 'Fees',
@@ -2705,6 +2709,7 @@ return [
'amount_greater_than_balance' => 'The amount is greater than the invoice balance, a credit will be created with the remaining amount.',
'custom_fields_tip' => 'Use Label|Option1,Option2
to show a select box.',
'client_information' => 'Client Information',
+ 'client_information_text' => 'Use a permanent address where you can receive mail.',
'updated_client_details' => 'Successfully updated client details',
'auto' => 'Auto',
'tax_amount' => 'Tax Amount',
diff --git a/resources/sass/app.scss b/resources/sass/app.scss
index 88ab33ef0e20..d46045ccc484 100644
--- a/resources/sass/app.scss
+++ b/resources/sass/app.scss
@@ -7,6 +7,11 @@
@import 'components/validation';
@import 'components/inputs';
@import 'components/alerts';
+@import 'components/badge';
+
+.active-page {
+ @apply bg-blue-900 #{!important};
+}
// ..
@tailwind utilities;
diff --git a/resources/sass/components/badge.scss b/resources/sass/components/badge.scss
new file mode 100644
index 000000000000..4c76a4d2ad91
--- /dev/null
+++ b/resources/sass/components/badge.scss
@@ -0,0 +1,31 @@
+.badge {
+ @apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium leading-4;
+}
+
+.badge-light {
+ @apply bg-gray-100 text-gray-800;
+}
+
+.badge-primary {
+ @apply bg-blue-200 text-blue-500;
+}
+
+.badge-danger {
+ @apply bg-red-100 text-red-500;
+}
+
+.badge-success {
+ @apply bg-green-100 text-green-500;
+}
+
+.badge-secondary {
+ @apply bg-gray-800 text-gray-200;
+}
+
+.badge-warning {
+ @apply bg-orange-100 text-orange-500;
+}
+
+.badge-info {
+ @apply bg-blue-100 text-blue-500;
+}
diff --git a/resources/sass/components/buttons.scss b/resources/sass/components/buttons.scss
index 78129e5914d4..88b8e378adbb 100644
--- a/resources/sass/components/buttons.scss
+++ b/resources/sass/components/buttons.scss
@@ -1,5 +1,5 @@
.button {
- @apply rounded py-3 px-4;
+ @apply rounded py-3 px-4 text-sm leading-4 transition duration-150 ease-in-out font-semibold;
}
.button-primary {
@@ -13,3 +13,31 @@
.button-block {
@apply block w-full;
}
+
+.button-danger {
+ @apply bg-red-500 text-white;
+
+ &:hover {
+ @apply bg-red-600;
+ }
+}
+
+.button-secondary {
+ @apply bg-gray-100;
+
+ &:hover {
+ @apply bg-gray-200;
+ }
+}
+
+.button-link {
+ @apply text-blue-600;
+
+ &:hover {
+ @apply text-blue-700 underline;
+ }
+
+ &:focus {
+ @apply outline-none underline;
+ }
+}
diff --git a/resources/sass/components/inputs.scss b/resources/sass/components/inputs.scss
index ea940a5bc3c0..016bbd2e605f 100644
--- a/resources/sass/components/inputs.scss
+++ b/resources/sass/components/inputs.scss
@@ -1,5 +1,5 @@
.input {
- @apply items-center border border-gray-300 rounded mt-2 w-full py-3 px-4;
+ @apply items-center border border-gray-300 rounded mt-2 w-full py-2 px-4 text-sm;
&:focus {
@apply outline-none border-blue-500;
@@ -9,3 +9,7 @@
.input-label {
@apply text-sm text-gray-600;
}
+
+.input-slim {
+ @apply py-2;
+}
diff --git a/resources/views/portal/default/invoices/payment.blade.php b/resources/views/portal/default/invoices/payment.blade.php
index e3f78e9ddd66..4e21ebd358a8 100644
--- a/resources/views/portal/default/invoices/payment.blade.php
+++ b/resources/views/portal/default/invoices/payment.blade.php
@@ -164,12 +164,12 @@ $('#terms_accepted').on('click', function(e){
//push to payment
-
+
});
$("#modal_pay_now_button").on('click', function(e){
-
+
//disable to prevent firing twice
$("#modal_pay_now_button").attr("disabled", true);
@@ -187,11 +187,11 @@ $("#modal_pay_now_button").on('click', function(e){
function getSignature()
{
- //check in signature is required
+ //check in signature is required
$("#signature").jSignature({ 'UndoButton': true, }).bind('change', function(e) {
if( $("#signature").jSignature('getData', 'native').length >= 1) {
-
+
$("#modal_pay_now_button").removeAttr("disabled");
} else {
@@ -214,7 +214,7 @@ $("#modal_pay_now_button").on('click', function(e){
//var data = false;
}
-
+
@endpush
@section('footer')
diff --git a/resources/views/portal/ninja2020/auth/login.blade.php b/resources/views/portal/ninja2020/auth/login.blade.php
new file mode 100644
index 000000000000..092a65e0b548
--- /dev/null
+++ b/resources/views/portal/ninja2020/auth/login.blade.php
@@ -0,0 +1,59 @@
+@extends('portal.ninja2020.layout.clean')
+@section('meta_title', ctrans('texts.login'))
+
+@section('body')
+
+
+
+
+
+
+
+@endsection
diff --git a/resources/views/portal/ninja2020/auth/passwords/request.blade.php b/resources/views/portal/ninja2020/auth/passwords/request.blade.php
new file mode 100644
index 000000000000..e7d9a1636605
--- /dev/null
+++ b/resources/views/portal/ninja2020/auth/passwords/request.blade.php
@@ -0,0 +1,44 @@
+@extends('portal.ninja2020.layout.clean')
+@section('meta_title', $title)
+
+@section('body')
+
+
+
+
+
+
+
+
{{ ctrans('texts.password_recovery') }}
+
{{ ctrans('texts.reset_password_text') }}
+ @if(session('status'))
+
+ {{ session('status') }}
+
+ @endif
+
+
+
+
+
+
+@endsection
diff --git a/resources/views/portal/ninja2020/auth/passwords/reset.blade.php b/resources/views/portal/ninja2020/auth/passwords/reset.blade.php
new file mode 100644
index 000000000000..a8284ffd6c01
--- /dev/null
+++ b/resources/views/portal/ninja2020/auth/passwords/reset.blade.php
@@ -0,0 +1,67 @@
+@extends('portal.ninja2020.layout.clean')
+@section('meta_title', ctrans('texts.password_recovery'))
+
+@section('body')
+
+
+
+
+
+
+
+
{{ ctrans('texts.password_recovery') }}
+
{{ ctrans('texts.reset_password_text') }}
+ @if(session('status'))
+
+ {{ session('status') }}
+
+ @endif
+
+
+
+
+
+
+@endsection
diff --git a/resources/views/portal/ninja2020/components/breadcrumbs.blade.php b/resources/views/portal/ninja2020/components/breadcrumbs.blade.php
new file mode 100644
index 000000000000..f13095694ef2
--- /dev/null
+++ b/resources/views/portal/ninja2020/components/breadcrumbs.blade.php
@@ -0,0 +1,33 @@
+
+
+
+
+
+ {{ ctrans('texts.back') }}
+
+
+
+@if (count($breadcrumbs))
+
+
+ @foreach ($breadcrumbs as $breadcrumb)
+
+ @if ($breadcrumb->url && !$loop->last)
+ {{ $breadcrumb->title }}
+
+
+
+ @else
+ {{ $breadcrumb->title }}
+ @endif
+
+ @endforeach
+
+
+@endif
diff --git a/resources/views/portal/ninja2020/components/general/messages/success.blade.php b/resources/views/portal/ninja2020/components/general/messages/success.blade.php
new file mode 100644
index 000000000000..f90521114846
--- /dev/null
+++ b/resources/views/portal/ninja2020/components/general/messages/success.blade.php
@@ -0,0 +1,4 @@
+
+ {{ session('success') }}
+
+
diff --git a/resources/views/portal/ninja2020/components/general/sidebar/desktop.blade.php b/resources/views/portal/ninja2020/components/general/sidebar/desktop.blade.php
new file mode 100644
index 000000000000..ef6183732bd4
--- /dev/null
+++ b/resources/views/portal/ninja2020/components/general/sidebar/desktop.blade.php
@@ -0,0 +1,23 @@
+
+
diff --git a/resources/views/portal/ninja2020/components/general/sidebar/header.blade.php b/resources/views/portal/ninja2020/components/general/sidebar/header.blade.php
new file mode 100644
index 000000000000..59cce6cd2168
--- /dev/null
+++ b/resources/views/portal/ninja2020/components/general/sidebar/header.blade.php
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
{{ ctrans('texts.search') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ auth()->user()->present()->name() }}
+
+
+
+
+
+
+
diff --git a/resources/views/portal/ninja2020/components/general/sidebar/main.blade.php b/resources/views/portal/ninja2020/components/general/sidebar/main.blade.php
new file mode 100644
index 000000000000..c92cb944e200
--- /dev/null
+++ b/resources/views/portal/ninja2020/components/general/sidebar/main.blade.php
@@ -0,0 +1,31 @@
+
+
+
+@include('portal.ninja2020.components.general.sidebar.mobile')
+
+
+ @include('portal.ninja2020.components.general.sidebar.desktop')
+
+
+ @include('portal.ninja2020.components.general.sidebar.header')
+
+
+
+ @yield('header')
+
+
+
+
+ @includeWhen(session()->has('success'), 'portal.ninja2020.components.general.messages.success')
+ {{ $slot }}
+
+
+
+
+
diff --git a/resources/views/portal/ninja2020/components/general/sidebar/mobile.blade.php b/resources/views/portal/ninja2020/components/general/sidebar/mobile.blade.php
new file mode 100644
index 000000000000..fde52895ca43
--- /dev/null
+++ b/resources/views/portal/ninja2020/components/general/sidebar/mobile.blade.php
@@ -0,0 +1,34 @@
+
diff --git a/resources/views/portal/ninja2020/credits/index.blade.php b/resources/views/portal/ninja2020/credits/index.blade.php
new file mode 100644
index 000000000000..e8c1aa7e8e19
--- /dev/null
+++ b/resources/views/portal/ninja2020/credits/index.blade.php
@@ -0,0 +1,87 @@
+@extends('portal.ninja2020.layout.app')
+@section('meta_title', ctrans('texts.credits'))
+
+@section('header')
+ {{ Breadcrumbs::render('credits') }}
+
+ @if($errors->any())
+
+ @foreach($errors->all() as $error)
+
{{ $error }}
+ @endforeach
+
+ @endif
+
+
+
+
+
+
+ {{ ctrans('texts.credits') }}
+
+
+
+ {{ ctrans('texts.list_of_credits') }}
+
+
+
+
+
+
+@endsection
+
+@section('body')
+
+
+
+
+
+
+
+ {{ ctrans('texts.amount') }}
+
+
+ {{ ctrans('texts.balance') }}
+
+
+ {{ ctrans('texts.credit_date') }}
+
+
+ {{ ctrans('texts.public_notes') }}
+
+
+
+
+
+ @foreach($credits as $credit)
+
+
+ {{ App\Utils\Number::formatMoney($credit->amount, $credit->client) }}
+
+
+ {{ App\Utils\Number::formatMoney($credit->balance, $credit->client) }}
+
+
+ {{ $credit->formatDate($credit->date, $credit->client->date_format()) }}
+
+
+ {{ empty($credit->public_notes) ? '/' : $credit->public_notes }}
+
+
+
+ @lang('texts.view')
+
+
+
+ @endforeach
+
+
+
+
+
+ {{ $credits->links('portal.ninja2020.vendor.pagination') }}
+
+
+@endsection
diff --git a/resources/views/portal/ninja2020/credits/show.blade.php b/resources/views/portal/ninja2020/credits/show.blade.php
new file mode 100644
index 000000000000..f4889ca70e85
--- /dev/null
+++ b/resources/views/portal/ninja2020/credits/show.blade.php
@@ -0,0 +1,57 @@
+@extends('portal.ninja2020.layout.app')
+@section('meta_title', ctrans('texts.credit'))
+
+@section('header')
+ {{ Breadcrumbs::render('credits.show', $credit) }}
+@endsection
+
+@section('body')
+
+
+
+
+ {{ ctrans('texts.credit') }}
+
+
+
+
+
+
+
+
+
+ {{ ctrans('texts.amount') }}
+
+
+ {{ App\Utils\Number::formatMoney($credit->amount, $credit->client) }}
+
+
+
+
+ {{ ctrans('texts.balance') }}
+
+
+ {{ App\Utils\Number::formatMoney($credit->balance, $credit->client) }}
+
+
+
+
+ {{ ctrans('texts.credit_date') }}
+
+
+ {{ $credit->formatDate($credit->date, $credit->client->date_format()) }}
+
+
+
+
+ {{ ctrans('texts.public_notes') }}
+
+
+ {{ $credit->public_notes }}
+
+
+
+
+
+
+@endsection
diff --git a/resources/views/portal/ninja2020/dashboard/index.blade.php b/resources/views/portal/ninja2020/dashboard/index.blade.php
index 04624fbd2abe..737c4d520323 100644
--- a/resources/views/portal/ninja2020/dashboard/index.blade.php
+++ b/resources/views/portal/ninja2020/dashboard/index.blade.php
@@ -1,7 +1,29 @@
-@extends('portal.ninja2020.layout.clean')
+@extends('portal.ninja2020.layout.app')
+@section('meta_title', ctrans('texts.dashboard'))
-@section('body')
-
-
Hello world
+@section('header')
+ {{ Breadcrumbs::render('dashboard') }}
+
+
+
+
+
+
+ {{ ctrans('texts.dashboard') }}
+
+
+
+ {{ ctrans('texts.quick_overview_statistics') }}
+
+
+
+
+
@endsection
+
+@section('body')
+ Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet esse magnam nam numquam omnis optio, pariatur
+ perferendis quae quaerat quam, quas quos repellat sapiente sit soluta, tenetur totam ut vel veritatis voluptatibus?
+ Aut, dolor illo? Asperiores eum eveniet quae sed?
+@endsection
diff --git a/resources/views/portal/ninja2020/gateways/stripe/add_credit_card.blade.php b/resources/views/portal/ninja2020/gateways/stripe/add_credit_card.blade.php
new file mode 100644
index 000000000000..e6dda2e0ec20
--- /dev/null
+++ b/resources/views/portal/ninja2020/gateways/stripe/add_credit_card.blade.php
@@ -0,0 +1,79 @@
+@extends('portal.ninja2020.layout.app')
+@section('meta_title', ctrans('texts.add_credit_card'))
+
+@push('head')
+
+@endpush
+
+@section('header')
+ {{ Breadcrumbs::render('payment_methods.add_credit_card') }}
+@endsection
+
+@section('body')
+
+
+
+
+
+
+
+
+ {{ ctrans('texts.add_credit_card') }}
+
+
+ {{ ctrans('texts.authorize_for_future_use') }}
+
+
+
+
+
+
+ {{ ctrans('texts.name') }}
+
+
+
+
+
+
+ {{ ctrans('texts.credit_card') }}
+
+
+
+
+
+
+
+ {{ ctrans('texts.save_as_default') }}
+
+
+
+
+
+
+
+ {{ ctrans('texts.save') }}
+
+
+
+
+
+
+
+
+@endsection
+
+@push('footer')
+
+
+@endpush
diff --git a/resources/views/portal/ninja2020/invoices/includes/signature.blade.php b/resources/views/portal/ninja2020/invoices/includes/signature.blade.php
new file mode 100644
index 000000000000..3da52cec874d
--- /dev/null
+++ b/resources/views/portal/ninja2020/invoices/includes/signature.blade.php
@@ -0,0 +1,44 @@
+
diff --git a/resources/views/portal/ninja2020/invoices/includes/terms.blade.php b/resources/views/portal/ninja2020/invoices/includes/terms.blade.php
new file mode 100644
index 000000000000..3a7f44539ed8
--- /dev/null
+++ b/resources/views/portal/ninja2020/invoices/includes/terms.blade.php
@@ -0,0 +1,49 @@
+
diff --git a/resources/views/portal/ninja2020/invoices/index.blade.php b/resources/views/portal/ninja2020/invoices/index.blade.php
new file mode 100644
index 000000000000..21539be06bf9
--- /dev/null
+++ b/resources/views/portal/ninja2020/invoices/index.blade.php
@@ -0,0 +1,125 @@
+@extends('portal.ninja2020.layout.app')
+@section('meta_title', ctrans('texts.invoices'))
+
+@section('header')
+ {{ Breadcrumbs::render('invoices') }}
+
+ @if($errors->any())
+
+ @foreach($errors->all() as $error)
+
{{ $error }}
+ @endforeach
+
+ @endif
+
+
+
+
+
+
+ {{ ctrans('texts.invoices') }}
+
+
+
+ {{ ctrans('texts.list_of_invoices') }}
+
+
+
+
+
+
+@endsection
+
+@section('body')
+
+ {{ ctrans('texts.with_selected') }}
+
+
+
+
+
+ {{ $invoices->links('portal.ninja2020.vendor.pagination') }}
+
+
+@endsection
+
+@push('footer')
+
+@endpush
diff --git a/resources/views/portal/ninja2020/invoices/payment.blade.php b/resources/views/portal/ninja2020/invoices/payment.blade.php
new file mode 100644
index 000000000000..666874e8e564
--- /dev/null
+++ b/resources/views/portal/ninja2020/invoices/payment.blade.php
@@ -0,0 +1,113 @@
+@extends('portal.ninja2020.layout.app')
+@section('meta_title', ctrans('texts.pay_now'))
+
+@push('head')
+
+
+
+@endpush
+
+@section('body')
+
+
+
+
+
+
+
+
+
+
+ {{ ctrans('texts.pay_now') }}
+
+
+
+
+
+
+
+
+
+
+ @foreach($invoices as $invoice)
+
+
+
+
+
+
+ {{ ctrans('texts.invoice_number') }}
+
+
+ {{ $invoice->number }}
+
+
+
+
+ {{ ctrans('texts.due_date') }}
+
+
+ {{ $invoice->due_date }}
+
+
+
+
+ {{ ctrans('texts.additional_info') }}
+
+
+ @if($invoice->po_number)
+ {{ $invoice->po_number }}
+ @elseif($invoice->public_notes)
+ {{ $invoice->public_notes }}
+ @else
+ {{ $invoice->invoice_date}}
+ @endif
+
+
+
+
+
+ @endforeach
+
+
+
+
+ @include('portal.ninja2020.invoices.includes.terms')
+ @include('portal.ninja2020.invoices.includes.signature')
+@endsection
+
+@push('footer')
+
+@endpush
diff --git a/resources/views/portal/ninja2020/invoices/show.blade.php b/resources/views/portal/ninja2020/invoices/show.blade.php
new file mode 100644
index 000000000000..19f44e5bda54
--- /dev/null
+++ b/resources/views/portal/ninja2020/invoices/show.blade.php
@@ -0,0 +1,43 @@
+@extends('portal.ninja2020.layout.app')
+@section('meta_title', ctrans('texts.view_invoice'))
+
+@section('header')
+ {{ Breadcrumbs::render('invoices.show', $invoice) }}
+@endsection
+
+@section('body')
+
+ @if($invoice->isPayable())
+
+ @endif
+
+
+
+@endsection
diff --git a/resources/views/portal/ninja2020/layout/app.blade.php b/resources/views/portal/ninja2020/layout/app.blade.php
new file mode 100644
index 000000000000..1747d8adf504
--- /dev/null
+++ b/resources/views/portal/ninja2020/layout/app.blade.php
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
+ @if (config('services.analytics.tracking_id'))
+
+
+
+ @else
+
+ @endif
+
+
+
@yield('meta_title', 'Invoice Ninja') — {{ config('app.name') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{--
--}}
+
+
+
+ {{-- Feel free to push anything to header using @push('header') --}}
+ @stack('head')
+
+
+
+
+ @component('portal.ninja2020.components.general.sidebar.main')
+ @yield('body')
+ @endcomponent
+
+
+
+ @yield('footer')
+ @stack('footer')
+
+
+
diff --git a/resources/views/portal/ninja2020/layout/clean.blade.php b/resources/views/portal/ninja2020/layout/clean.blade.php
index c9066d529ee3..97b3237b5a83 100644
--- a/resources/views/portal/ninja2020/layout/clean.blade.php
+++ b/resources/views/portal/ninja2020/layout/clean.blade.php
@@ -44,6 +44,7 @@
+
diff --git a/resources/views/portal/ninja2020/payment_methods/create.blade.php b/resources/views/portal/ninja2020/payment_methods/create.blade.php
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/resources/views/portal/ninja2020/payment_methods/includes/modals/removal.blade.php b/resources/views/portal/ninja2020/payment_methods/includes/modals/removal.blade.php
new file mode 100644
index 000000000000..0248d1209d91
--- /dev/null
+++ b/resources/views/portal/ninja2020/payment_methods/includes/modals/removal.blade.php
@@ -0,0 +1,52 @@
+
diff --git a/resources/views/portal/ninja2020/payment_methods/index.blade.php b/resources/views/portal/ninja2020/payment_methods/index.blade.php
new file mode 100644
index 000000000000..d9eea112fc14
--- /dev/null
+++ b/resources/views/portal/ninja2020/payment_methods/index.blade.php
@@ -0,0 +1,109 @@
+@extends('portal.ninja2020.layout.app')
+@section('meta_title', ctrans('texts.payment_methods'))
+
+@section('header')
+ {{ Breadcrumbs::render('payment_methods') }}
+
+
+
+
+
+
+ {{ ctrans('texts.payment_methods') }}
+
+
+
+ {{ ctrans('texts.list_of_payment_methods') }}
+
+
+
+
+
+
+
+
+@endsection
+
+@section('body')
+
+
+
+
+
+
+
+ {{ ctrans('texts.created_at') }}
+
+
+ {{ ctrans('texts.payment_type_id') }}
+
+
+ {{ ctrans('texts.type') }}
+
+
+ {{ ctrans('texts.expires') }}
+
+
+ {{ ctrans('texts.card_number') }}
+
+
+ {{ ctrans('texts.default') }}
+
+
+
+
+
+ @foreach($payment_methods as $payment_method)
+
+
+ {{ $payment_method->formatDateTimestamp($payment_method->created_at, auth()->user()->client->date_format()) }}
+
+
+ {{ ctrans("texts.{$payment_method->gateway_type->alias}") }}
+
+
+ {{ ucfirst(optional($payment_method->meta)->brand) }}
+
+
+ @if(isset($payment_method->meta->exp_month) && isset($payment_method->meta->exp_year))
+ {{ $payment_method->meta->exp_month}} / {{ $payment_method->meta->exp_year }}
+ @endif
+
+
+ @isset($payment_method->meta->last4)
+ **** {{ $payment_method->meta->last4 }}
+ @endisset
+
+
+ @if($payment_method->is_default)
+
+
+
+ @endif
+
+
+
+ @lang('texts.view')
+
+
+
+ @endforeach
+
+
+
+
+
+ {{ $payment_methods->links('portal.ninja2020.vendor.pagination') }}
+
+
+@endsection
diff --git a/resources/views/portal/ninja2020/payment_methods/show.blade.php b/resources/views/portal/ninja2020/payment_methods/show.blade.php
new file mode 100644
index 000000000000..be3e5c3d18f1
--- /dev/null
+++ b/resources/views/portal/ninja2020/payment_methods/show.blade.php
@@ -0,0 +1,100 @@
+@extends('portal.ninja2020.layout.app')
+@section('meta_title', ucfirst($payment_method->gateway_type->name))
+
+@section('header')
+ {{ Breadcrumbs::render('payment_methods.show', $payment_method) }}
+@endsection
+
+@section('body')
+
+
+
+
+ {{ ctrans("texts.{$payment_method->gateway_type->alias}") }}
+
+
+
+ {{ ctrans('texts.details_of_method') }}
+
+
+
+
+
+
+ {{ ctrans('texts.payment_type') }}
+
+
+ {{ ucfirst($payment_method->gateway_type->name) }}
+
+
+
+
+ {{ ctrans('texts.type') }}
+
+
+ {{ ucfirst($payment_method->meta->brand) }}
+
+
+
+
+ {{ ctrans('texts.card_number') }}
+
+
+ **** {{ ucfirst($payment_method->meta->last4) }}
+
+
+
+
+ {{ ctrans('texts.date_created') }}
+
+
+ {{ $payment_method->formatDateTimestamp($payment_method->created_at, auth()->user()->client->date_format()) }}
+
+
+
+
+ {{ ctrans('texts.default') }}
+
+
+ {{ $payment_method->is_default ? ctrans('texts.yes') : ctrans('texts.no') }}
+
+
+ @isset($payment_method->meta->exp_month)
+
+
+ {{ ctrans('texts.expires') }}
+
+
+ {{ $payment_method->meta->exp_month }} / {{ $payment_method->meta->exp_year }}
+
+
+ @endisset
+
+
+
+
+
+
+
+
+ Remove
+
+
+
+ Permanently remove this payment method.
+
+
+
+
+
+
+ Remove payment method
+
+ @include('portal.ninja2020.payment_methods.includes.modals.removal')
+
+
+
+
+
+
+@endsection
diff --git a/resources/views/portal/ninja2020/payments/index.blade.php b/resources/views/portal/ninja2020/payments/index.blade.php
new file mode 100644
index 000000000000..c0790d8d2bfb
--- /dev/null
+++ b/resources/views/portal/ninja2020/payments/index.blade.php
@@ -0,0 +1,85 @@
+@extends('portal.ninja2020.layout.app')
+@section('meta_title', ctrans('texts.payments'))
+
+@section('header')
+ {{ Breadcrumbs::render('payments') }}
+
+
+
+
+
+
+ {{ ctrans('texts.payments') }}
+
+
+
+ {{ ctrans('texts.List of your payments.') }}
+
+
+
+
+
+
+@endsection
+
+@section('body')
+
+
+
+
+
+
+
+ {{ ctrans('texts.payment_date') }}
+
+
+ {{ ctrans('texts.payment_type_id') }}
+
+
+ {{ ctrans('texts.amount') }}
+
+
+ {{ ctrans('texts.transaction_reference') }}
+
+
+ {{ ctrans('texts.status') }}
+
+
+
+
+
+ @foreach($payments as $payment)
+ hashed_id) }}'">
+
+ {{ $payment->formatDate($payment->date, $payment->client->date_format()) }}
+
+
+ {{ $payment->type->name }}
+
+
+ {{ \App\Utils\Number::formatMoney($payment->amount, $payment->client) }}
+
+
+ {{ $payment->transaction_reference }}
+
+
+ {!! \App\Models\Payment::badgeForStatus($payment->status_id) !!}
+
+
+
+ @lang('texts.view')
+
+
+
+ @endforeach
+
+
+
+
+
+ {{ $payments->links('portal.ninja2020.vendor.pagination') }}
+
+
+@endsection
diff --git a/resources/views/portal/ninja2020/payments/show.blade.php b/resources/views/portal/ninja2020/payments/show.blade.php
new file mode 100644
index 000000000000..8bc4dc4ab105
--- /dev/null
+++ b/resources/views/portal/ninja2020/payments/show.blade.php
@@ -0,0 +1,92 @@
+@extends('portal.ninja2020.layout.app')
+@section('meta_title', ctrans('texts.payment'))
+
+@section('header')
+ {{ Breadcrumbs::render('payments.show', $payment) }}
+@endsection
+
+@section('body')
+
+
+
+
+ {{ ctrans('texts.payment') }}
+
+
+ {{ ctrans('texts.Details of the payment.') }}
+
+
+
+
+
+
+ {{ ctrans('texts.payment_date') }}
+
+
+ {{ $payment->clientPaymentDate() }}
+
+
+
+
+ {{ ctrans('texts.transaction_reference') }}
+
+
+ {{ $payment->transaction_reference }}
+
+
+
+
+ {{ ctrans('texts.method') }}
+
+
+ {{ $payment->type->name }}
+
+
+
+
+ {{ ctrans('texts.amount') }}
+
+
+ {{ $payment->formattedAmount() }}
+
+
+
+
+ {{ ctrans('texts.status') }}
+
+
+ {!! \App\Models\Payment::badgeForStatus($payment->status_id) !!}
+
+
+
+
+
+
+
+
+ {{ ctrans('texts.invoices') }}
+
+
+ {{ ctrans('texts.List of invoices affected by payment.') }}
+
+
+
+
+ @foreach($payment->invoices as $invoice)
+
+
+ {{ ctrans('texts.invoice_number') }}
+
+
+
+ @endforeach
+
+
+
+
+@endsection
diff --git a/resources/views/portal/ninja2020/profile/index.blade.php b/resources/views/portal/ninja2020/profile/index.blade.php
new file mode 100644
index 000000000000..8c5fc03a1b96
--- /dev/null
+++ b/resources/views/portal/ninja2020/profile/index.blade.php
@@ -0,0 +1,356 @@
+@extends('portal.ninja2020.layout.app')
+
+@section('meta_title', ctrans('texts.client_information'))
+
+@section('header')
+
{{ ctrans('texts.Update your personal information.') }}
+@endsection
+
+@section('body')
+
+
+
+
+
+
+
{{ ctrans('texts.profile') }}
+
+ @lang('texts.client_information_text')
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ ctrans('texts.name_website_logo') }}
+
+ {{ ctrans('texts. Make sure you use full link to your site.') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ ctrans('texts.personal_address') }}
+
+ {{ ctrans('texts.your_personal_address') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ ctrans('texts.shipping_address') }}
+
+ {{ ctrans('texts.your_shipping_address') }}
+
+
+
+
+
+
+
+@endsection
diff --git a/resources/views/portal/ninja2020/quotes/approve.blade.php b/resources/views/portal/ninja2020/quotes/approve.blade.php
new file mode 100644
index 000000000000..2e5c22644b54
--- /dev/null
+++ b/resources/views/portal/ninja2020/quotes/approve.blade.php
@@ -0,0 +1,90 @@
+@extends('portal.ninja2020.layout.app')
+@section('meta_title', ctrans('texts.approve'))
+
+@push('head')
+
+
+@endpush
+
+@section('header')
+ {{ Breadcrumbs::render('quotes.approve') }}
+@endsection
+
+@section('body')
+
+
+
+
+
+
+
+
+
+
+ {{ ctrans('texts.approve') }}
+
+
+
+
+
+
+ @foreach($quotes as $quote)
+
+
+
+
+
+
+ {{ ctrans('texts.quote_number') }}
+
+
+ {{ $quote->number }}
+
+
+
+
+ {{ ctrans('texts.quote_date') }}
+
+
+ {{ $quote->formatDate($quote->date, $quote->client->date_format()) }}
+
+
+
+
+ {{ ctrans('texts.balance') }}
+
+
+ {{ App\Utils\Number::formatMoney($quote->balance, $quote->client) }}
+
+
+
+
+
+ @endforeach
+
+
+
+
+ @include('portal.ninja2020.invoices.includes.signature')
+@endsection
+
+@push('footer')
+
+@endpush
diff --git a/resources/views/portal/ninja2020/quotes/includes/signature.blade.php b/resources/views/portal/ninja2020/quotes/includes/signature.blade.php
new file mode 100644
index 000000000000..3da52cec874d
--- /dev/null
+++ b/resources/views/portal/ninja2020/quotes/includes/signature.blade.php
@@ -0,0 +1,44 @@
+
diff --git a/resources/views/portal/ninja2020/quotes/index.blade.php b/resources/views/portal/ninja2020/quotes/index.blade.php
new file mode 100644
index 000000000000..e83547a73336
--- /dev/null
+++ b/resources/views/portal/ninja2020/quotes/index.blade.php
@@ -0,0 +1,117 @@
+@extends('portal.ninja2020.layout.app')
+@section('meta_title', ctrans('texts.quotes'))
+
+@section('header')
+ {{ Breadcrumbs::render('quotes') }}
+
+ @if($errors->any())
+
+ @foreach($errors->all() as $error)
+
{{ $error }}
+ @endforeach
+
+ @endif
+
+
+
+
+
+
+ {{ ctrans('texts.quotes') }}
+
+
+
+ {{ ctrans('texts.list_of_quotes') }}
+
+
+
+
+
+
+@endsection
+
+@section('body')
+
+ {{ ctrans('texts.with_selected') }}
+
+
+
+
+
+ {{ $quotes->links('portal.ninja2020.vendor.pagination') }}
+
+
+@endsection
+
+@push('footer')
+
+@endpush
diff --git a/resources/views/portal/ninja2020/quotes/show.blade.php b/resources/views/portal/ninja2020/quotes/show.blade.php
new file mode 100644
index 000000000000..94caa1a3b53d
--- /dev/null
+++ b/resources/views/portal/ninja2020/quotes/show.blade.php
@@ -0,0 +1,44 @@
+@extends('portal.ninja2020.layout.app')
+@section('meta_title', ctrans('texts.view_quote'))
+
+@section('header')
+ {{ Breadcrumbs::render('quotes.show', $quote) }}
+@endsection
+
+@section('body')
+
+ @if(!$quote->isApproved())
+
+ @endif
+
+
+
+@endsection
diff --git a/resources/views/portal/ninja2020/recurring_invoices/cancellation/index.blade.php b/resources/views/portal/ninja2020/recurring_invoices/cancellation/index.blade.php
new file mode 100644
index 000000000000..46209cfc0f5c
--- /dev/null
+++ b/resources/views/portal/ninja2020/recurring_invoices/cancellation/index.blade.php
@@ -0,0 +1,74 @@
+@extends('portal.ninja2020.layout.app')
+
+@section('header')
+ {{ Breadcrumbs::render('recurring_invoices.request_cancellation', $invoice) }}
+@stop
+
+@section('body')
+
+
+
+
+ {{ ctrans('texts.recurring_invoices') }}
+
+
+ Details of the recurring invoice.
+
+
+
+
+
+
+ {{ ctrans('texts.start_date') }}
+
+
+ {{ $invoice->formatDate($invoice->start_date, $invoice->client->date_format()) }}
+
+
+
+
+ {{ ctrans('texts.next_send_date') }}
+
+
+ {{ $invoice->formatDate($invoice->next_send_date, $invoice->client->date_format()) }}
+
+
+
+
+ {{ ctrans('texts.frequency') }}
+
+
+ {{ \App\Models\RecurringInvoice::frequencyForKey($invoice->frequency_id) }}
+
+
+
+
+ {{ ctrans('texts.cycles_remaining') }}
+
+
+ {{ $invoice->remaining_cycles }}
+
+
+
+
+ {{ ctrans('texts.amount') }}
+
+
+ {{ \App\Utils\Number::formatMoney($invoice->amount, $invoice->client) }}
+
+
+
+
+
+
+
+
+
+ Cancellation pending, we'll be in touch!
+
+
+
+
+
+
+@endsection
diff --git a/resources/views/portal/ninja2020/recurring_invoices/includes/modals/cancellation.blade.php b/resources/views/portal/ninja2020/recurring_invoices/includes/modals/cancellation.blade.php
new file mode 100644
index 000000000000..8e075c6c8a1d
--- /dev/null
+++ b/resources/views/portal/ninja2020/recurring_invoices/includes/modals/cancellation.blade.php
@@ -0,0 +1,49 @@
+
diff --git a/resources/views/portal/ninja2020/recurring_invoices/index.blade.php b/resources/views/portal/ninja2020/recurring_invoices/index.blade.php
new file mode 100644
index 000000000000..1c8565be480f
--- /dev/null
+++ b/resources/views/portal/ninja2020/recurring_invoices/index.blade.php
@@ -0,0 +1,85 @@
+@extends('portal.ninja2020.layout.app')
+@section('meta_title', ctrans('texts.recurring_invoices'))
+
+@section('header')
+ {{ Breadcrumbs::render('recurring_invoices') }}
+
+
+
+
+
+
+ {{ ctrans('texts.recurring_invoices') }}
+
+
+
+ {{ ctrans('texts.list_of_recurring_invoices') }}
+
+
+
+
+
+
+@endsection
+
+@section('body')
+
+
+
+
+
+
+
+ {{ ctrans('texts.frequency') }}
+
+
+ {{ ctrans('texts.start_date') }}
+
+
+ {{ ctrans('texts.next_send_date') }}
+
+
+ {{ ctrans('texts.cycles_remaining') }}
+
+
+ {{ ctrans('texts.amount') }}
+
+
+
+
+
+ @foreach($invoices as $invoice)
+
+
+ {{ \App\Models\RecurringInvoice::frequencyForKey($invoice->frequency_id) }}
+
+
+ {{ $invoice->formatDate($invoice->date, $invoice->client->date_format()) }}
+
+
+ {{ $invoice->formatDate($invoice->next_send_date, $invoice->client->date_format()) }}
+
+
+ {{ $invoice->remaining_cycles }}
+
+
+ {{ \App\Utils\Number::formatMoney($invoice->amount, $invoice->client) }}
+
+
+
+ @lang('texts.view')
+
+
+
+ @endforeach
+
+
+
+
+
+ {{ $invoices->links('portal.ninja2020.vendor.pagination') }}
+
+
+@endsection
diff --git a/resources/views/portal/ninja2020/recurring_invoices/show.blade.php b/resources/views/portal/ninja2020/recurring_invoices/show.blade.php
new file mode 100644
index 000000000000..dfc3ce54b881
--- /dev/null
+++ b/resources/views/portal/ninja2020/recurring_invoices/show.blade.php
@@ -0,0 +1,87 @@
+@extends('portal.ninja2020.layout.app')
+@section('meta_title', ctrans('texts.recurring_invoice'))
+
+@section('header')
+ {{ Breadcrumbs::render('recurring_invoices.show', $invoice) }}
+@endsection
+
+@section('body')
+
+
+
+
+ {{ ctrans('texts.recurring_invoices') }}
+
+
+ {{ ctrans('texts.details_of_recurring_invoice') }}.
+
+
+
+
+
+
+ {{ ctrans('texts.start_date') }}
+
+
+ {{ $invoice->formatDate($invoice->start_date, $invoice->client->date_format()) }}
+
+
+
+
+ {{ ctrans('texts.next_send_date') }}
+
+
+ {{ $invoice->formatDate($invoice->next_send_date, $invoice->client->date_format()) }}
+
+
+
+
+ {{ ctrans('texts.frequency') }}
+
+
+ {{ \App\Models\RecurringInvoice::frequencyForKey($invoice->frequency_id) }}
+
+
+
+
+ {{ ctrans('texts.cycles_remaining') }}
+
+
+ {{ $invoice->remaining_cycles }}
+
+
+
+
+ {{ ctrans('texts.amount') }}
+
+
+ {{ \App\Utils\Number::formatMoney($invoice->amount, $invoice->client) }}
+
+
+
+
+
+
+
+
+
+ {{ ctrans('texts.cancellation') }}
+
+
+
+ {{ ctrans('texts.In case you want to stop the recurring invoice, please click the request the
+ cancellation.') }}
+
+
+
+
+
+ Request Cancellation
+ @include('portal.ninja2020.recurring_invoices.includes.modals.cancellation')
+
+
+
+
+
+
+@endsection
diff --git a/resources/views/portal/ninja2020/vendor/pagination.blade.php b/resources/views/portal/ninja2020/vendor/pagination.blade.php
new file mode 100644
index 000000000000..12b4a24f9b61
--- /dev/null
+++ b/resources/views/portal/ninja2020/vendor/pagination.blade.php
@@ -0,0 +1,67 @@
+
+
+
+ @foreach ($elements as $element)
+ @if (is_string($element))
+
+ ...
+
+ @endif
+
+ @if (is_array($element))
+ @foreach ($element as $page => $url)
+ @if ($page == $paginator->currentPage())
+
+ {{ $page }}
+
+ @else
+
+ {{ $page }}
+
+ @endif
+ @endforeach
+ @endif
+
+ @endforeach
+
+
+ @if ($paginator->hasMorePages())
+
+ @else
+
+ @endif
+
diff --git a/routes/breadcrumbs.php b/routes/breadcrumbs.php
index 0a94a65e1f54..8b22d20c290c 100644
--- a/routes/breadcrumbs.php
+++ b/routes/breadcrumbs.php
@@ -2,7 +2,91 @@
// Dashboard
Breadcrumbs::for('dashboard', function ($trail) {
- $trail->push(trans('texts.dashboard'), route('dashboard.index'));
+ $trail->push(trans('texts.dashboard'), route('client.dashboard'));
+});
+
+// Invoices
+Breadcrumbs::for('invoices', function ($trail) {
+ $trail->push(ctrans('texts.invoices'), route('client.invoices.index'));
+});
+
+// Invoices > Show invoice
+Breadcrumbs::for('invoices.show', function ($trail, $invoice) {
+ $trail->parent('invoices');
+ $trail->push(sprintf('%s: %s', ctrans('texts.invoice'), $invoice->number), route('client.invoices.index', $invoice->hashed_id));
+});
+
+// Recurring invoices
+Breadcrumbs::for('recurring_invoices', function ($trail) {
+ $trail->push(ctrans('texts.recurring_invoices'), route('client.recurring_invoices.index'));
+});
+
+// Recurring invoices > Show recurring invoice
+Breadcrumbs::for('recurring_invoices.show', function ($trail, $invoice) {
+ $trail->parent('recurring_invoices');
+ $trail->push(sprintf('%s: %s', ctrans('texts.recurring_invoice'), $invoice->hashed_id), route('client.recurring_invoices.index', $invoice->hashed_id));
+});
+
+// Recurring invoices > Show recurring invoice
+Breadcrumbs::for('recurring_invoices.request_cancellation', function ($trail, $invoice) {
+ $trail->parent('recurring_invoices.show', $invoice);
+ $trail->push(ctrans('texts.request_cancellation'), route('client.recurring_invoices.request_cancellation', $invoice->hashed_id));
+});
+
+// Payments
+Breadcrumbs::for('payments', function ($trail) {
+ $trail->push(ctrans('texts.payments'), route('client.payments.index'));
+});
+
+// Payments > Show payment
+Breadcrumbs::for('payments.show', function ($trail, $invoice) {
+ $trail->parent('payments');
+ $trail->push(sprintf('%s: %s', ctrans('texts.payment'), $invoice->hashed_id), route('client.payments.index', $invoice->hashed_id));
+});
+
+// Payment methods
+Breadcrumbs::for('payment_methods', function ($trail) {
+ $trail->push(ctrans('texts.payment_methods'), route('client.payment_methods.index'));
+});
+
+// Payment methods > Show payment method
+Breadcrumbs::for('payment_methods.show', function ($trail, $invoice) {
+ $trail->parent('payment_methods');
+ $trail->push(sprintf('%s: %s', ctrans('texts.payment_methods'), $invoice->hashed_id), route('client.payment_methods.index', $invoice->hashed_id));
+});
+
+// Payment methods > Create method
+Breadcrumbs::for('payment_methods.add_credit_card', function ($trail) {
+ $trail->parent('payment_methods');
+ $trail->push(ctrans('texts.add_credit_card'));
+});
+
+// Quotes
+Breadcrumbs::for('quotes', function ($trail) {
+ $trail->push(ctrans('texts.quotes'), route('client.quotes.index'));
+});
+
+// Quotes > Show quote
+Breadcrumbs::for('quotes.show', function ($trail, $quote) {
+ $trail->parent('quotes');
+ $trail->push(sprintf('%s: %s', ctrans('texts.quotes'), $quote->hashed_id), route('client.quotes.index', $quote->hashed_id));
+});
+
+// Quotes > Approve
+Breadcrumbs::for('quotes.approve', function ($trail) {
+ $trail->parent('quotes');
+ $trail->push(ctrans('texts.approve'));
+});
+
+// Quotes
+Breadcrumbs::for('credits', function ($trail) {
+ $trail->push(ctrans('texts.credits'), route('client.credits.index'));
+});
+
+// Quotes > Show quote
+Breadcrumbs::for('credits.show', function ($trail, $credit) {
+ $trail->parent('credits');
+ $trail->push(sprintf('%s: %s', ctrans('texts.credits'), $credit->hashed_id), route('client.credits.index', $credit->hashed_id));
});
// Dashboard > Client
diff --git a/routes/client.php b/routes/client.php
index 146807f4f3e7..cbb1dc9aaf09 100644
--- a/routes/client.php
+++ b/routes/client.php
@@ -1,5 +1,7 @@
name('client.login'); //catch all
Route::get('client/login', 'Auth\ContactLoginController@showLoginForm')->name('client.login')->middleware('locale');
@@ -12,46 +14,54 @@ Route::post('client/password/reset', 'Auth\ContactResetPasswordController@reset'
//todo implement domain DB
Route::group(['middleware' => ['auth:contact','locale'], 'prefix' => 'client', 'as' => 'client.'], function () {
- Route::get('dashboard', 'ClientPortal\DashboardController@index')->name('dashboard'); // name = (dashboard. index / create / show / update / destroy / edit
- Route::get('invoices', 'ClientPortal\InvoiceController@index')->name('invoices.index')->middleware('portal_enabled');
- Route::post('invoices/payment', 'ClientPortal\InvoiceController@bulk')->name('invoices.bulk');
- Route::get('invoices/{invoice}', 'ClientPortal\InvoiceController@show')->name('invoice.show');
- Route::get('invoices/{invoice_invitation}', 'ClientPortal\InvoiceController@show')->name('invoice.show_invitation');
+ Route::get('dashboard', 'ClientPortal\DashboardController@index')->name('dashboard'); // name = (dashboard. index / create / show / update / destroy / edit
- Route::get('recurring_invoices', 'ClientPortal\RecurringInvoiceController@index')->name('recurring_invoices.index')->middleware('portal_enabled');
- Route::get('recurring_invoices/{recurring_invoice}', 'ClientPortal\RecurringInvoiceController@show')->name('recurring_invoices.show');
- Route::get('recurring_invoices/{recurring_invoice}/request_cancellation', 'ClientPortal\RecurringInvoiceController@requestCancellation')->name('recurring_invoices.request_cancellation');
-
- Route::get('payments', 'ClientPortal\PaymentController@index')->name('payments.index')->middleware('portal_enabled');
- Route::get('payments/{payment}', 'ClientPortal\PaymentController@show')->name('payments.show');
- Route::post('payments/process', 'ClientPortal\PaymentController@process')->name('payments.process');
- Route::post('payments/process/response', 'ClientPortal\PaymentController@response')->name('payments.response');
- Route::get('payments/process/response', 'ClientPortal\PaymentController@response')->name('payments.response.get');
+ Route::get('invoices', 'ClientPortal\InvoiceController@index')->name('invoices.index')->middleware('portal_enabled');
+ Route::post('invoices/payment', 'ClientPortal\InvoiceController@bulk')->name('invoices.bulk');
+ Route::get('invoices/{invoice}', 'ClientPortal\InvoiceController@show')->name('invoice.show');
+ Route::get('invoices/{invoice_invitation}', 'ClientPortal\InvoiceController@show')->name('invoice.show_invitation');
- Route::get('profile/{client_contact}/edit', 'ClientPortal\ProfileController@edit')->name('profile.edit');
- Route::put('profile/{client_contact}/edit', 'ClientPortal\ProfileController@update')->name('profile.update');
- Route::put('profile/{client_contact}/edit_client', 'ClientPortal\ProfileController@updateClient')->name('profile.edit_client');
- Route::put('profile/{client_contact}/localization', 'ClientPortal\ProfileController@updateClientLocalization')->name('profile.edit_localization');
+ Route::get('recurring_invoices', 'ClientPortal\RecurringInvoiceController@index')->name('recurring_invoices.index')->middleware('portal_enabled');
+ Route::get('recurring_invoices/{recurring_invoice}', 'ClientPortal\RecurringInvoiceController@show')->name('recurring_invoices.show');
+ Route::get('recurring_invoices/{recurring_invoice}/request_cancellation', 'ClientPortal\RecurringInvoiceController@requestCancellation')->name('recurring_invoices.request_cancellation');
- Route::resource('payment_methods', 'ClientPortal\PaymentMethodController');// name = (payment_methods. index / create / show / update / destroy / edit
+ Route::get('payments', 'ClientPortal\PaymentController@index')->name('payments.index')->middleware('portal_enabled');
+ Route::get('payments/{payment}', 'ClientPortal\PaymentController@show')->name('payments.show');
+ Route::post('payments/process', 'ClientPortal\PaymentController@process')->name('payments.process');
+ Route::post('payments/process/response', 'ClientPortal\PaymentController@response')->name('payments.response');
+ Route::get('payments/process/response', 'ClientPortal\PaymentController@response')->name('payments.response.get');
- Route::post('document', 'ClientPortal\DocumentController@store')->name('document.store');
- Route::delete('document', 'ClientPortal\DocumentController@destroy')->name('document.destroy');
-
- Route::get('logout', 'Auth\ContactLoginController@logout')->name('logout');
+ Route::get('profile/{client_contact}/edit', 'ClientPortal\ProfileController@edit')->name('profile.edit');
+ Route::put('profile/{client_contact}/edit', 'ClientPortal\ProfileController@update')->name('profile.update');
+ Route::put('profile/{client_contact}/edit_client', 'ClientPortal\ProfileController@updateClient')->name('profile.edit_client');
+ Route::put('profile/{client_contact}/localization', 'ClientPortal\ProfileController@updateClientLocalization')->name('profile.edit_localization');
+
+ Route::resource('payment_methods', 'ClientPortal\PaymentMethodController');// name = (payment_methods. index / create / show / update / destroy / edit
+
+ Route::match(['GET', 'POST'], 'quotes/approve', 'ClientPortal\QuoteController@bulk')->name('quotes.bulk');
+ Route::resource('quotes', 'ClientPortal\QuoteController')->only('index', 'show');
+
+ Route::resource('credits', 'ClientPortal\CreditController')->only('index', 'show');
+
+ Route::post('document', 'ClientPortal\DocumentController@store')->name('document.store');
+ Route::delete('document', 'ClientPortal\DocumentController@destroy')->name('document.destroy');
+
+ Route::get('logout', 'Auth\ContactLoginController@logout')->name('logout');
});
Route::group(['middleware' => ['invite_db'], 'prefix' => 'client', 'as' => 'client.'], function () {
- Route::get('invoice/{invitation_key}/download_pdf', 'InvoiceController@downloadPdf');
- Route::get('quote/{invitation_key}/download_pdf', 'QuoteController@downloadPdf');
- Route::get('credit/{invitation_key}/download_pdf', 'CreditController@downloadPdf');
- Route::get('{entity}/{invitation_key}/download', 'ClientPortal\InvitationController@routerForDownload');
-
- /*Invitation catches*/
- Route::get('{entity}/{invitation_key}', 'ClientPortal\InvitationController@router');
- Route::get('{entity}/{client_hash}/{invitation_key}', 'ClientPortal\InvitationController@routerForIframe'); //should never need this
- Route::get('payment_hook/{company_gateway_id}/{gateway_type_id}', 'ClientPortal\PaymentHookController@process');
+
+ Route::get('invoice/{invitation_key}/download_pdf', 'InvoiceController@downloadPdf');
+ Route::get('quote/{invitation_key}/download_pdf', 'QuoteController@downloadPdf');
+ Route::get('credit/{invitation_key}/download_pdf', 'CreditController@downloadPdf');
+ Route::get('{entity}/{invitation_key}/download', 'ClientPortal\InvitationController@routerForDownload');
+
+ /*Invitation catches*/
+ Route::get('{entity}/{invitation_key}','ClientPortal\InvitationController@router');
+ Route::get('{entity}/{client_hash}/{invitation_key}','ClientPortal\InvitationController@routerForIframe'); //should never need this
+ Route::get('payment_hook/{company_gateway_id}/{gateway_type_id}','ClientPortal\PaymentHookController@process');
+
});
Route::fallback('BaseController@notFoundClient');
diff --git a/webpack.mix.js b/webpack.mix.js
index 42ef06d2ccbb..2bea83aaf28f 100644
--- a/webpack.mix.js
+++ b/webpack.mix.js
@@ -1,19 +1,14 @@
const mix = require("laravel-mix");
const tailwindcss = require("tailwindcss");
-/*
- |--------------------------------------------------------------------------
- | Mix Asset Management
- |--------------------------------------------------------------------------
- |
- | Mix provides a clean, fluent API for defining some Webpack build steps
- | for your Laravel application. By default, we are compiling the Sass
- | file for the application as well as bundling up all the JS files.
- |
- */
-
mix.js("resources/js/app.js", "public/js")
- .sass("resources/sass/app.scss", "public/css")
+ .js("resources/js/clients/payment_methods/authorize-stripe-card.js", "public/js/clients/payment_methods/authorize-stripe-card.js")
+ .js("resources/js/clients/invoices/action-selectors.js", "public/js/clients/invoices/action-selectors.js")
+ .js("resources/js/clients/invoices/payment.js", "public/js/clients/invoices/payment.js")
+ .js("resources/js/clients/quotes/action-selectors.js", "public/js/clients/quotes/action-selectors.js")
+ .js("resources/js/clients/quotes/approve.js", "public/js/clients/quotes/approve.js");
+
+mix.sass("resources/sass/app.scss", "public/css")
.options({
processCssUrls: false,
postCss: [tailwindcss("./tailwind.config.js")]