1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-10 05:02:36 +01:00

[V2] Client portal rework (#3516)

* Client login, reset and update password page

* Client dashboard, sidebar, PortalComposer.php

* wip

* Personal page & update for details

* Invoices, paying & pagination.blade.php

* Invoices, recurring invoice & buttons

* Payments, link component

* Payment methods

* Breadcrums, clean up & wrap up

* Remove format_date() method to formatDate on object

* Payments
- $this->render is now proxy for render()
- Removed logic from Controller.php to ClientPortal.php
- Added MakesDates to ClientGatewayToken.php
- StripePaymentDriver.php now returns correct views
- Refactor of adding new payment method
- Ignoring all local builds for public/js/clients/*

* Signature, wip

* Fix "Pay now" on single invoice

* Payments:
- Added ProcessInvoicesInBulk request class
- Refactor InvoiceController::bulk()
- Displaying terms & payments
- New signature.blade.php
- Removed comment from webpack.mix.js

* Quotes:
- Refactor ProcessInvoicesInBulk.php to ProcessInvoicesInBulkRequest.php
- Add new 'Quotes' field inside of PortalComposer.php
- Added MakesDates to Quote.php
- Added Quote::badgeForStatus()
- Cleanup payment.blade.php
- Quote showing and approving
- New resource 'quotes' in client.php
- New image for quotes, align-left.svg

* Credits:
- New 'credits' resource in client.php
- Fixes for client.php typo

* Breadcrumbs:
- Quotes
- Credits

* Placeholder for translations.

* Restore whereIn & client scope

Co-authored-by: David Bomba <turbo124@gmail.com>
This commit is contained in:
Benjamin Beganović 2020-03-23 18:10:42 +01:00 committed by GitHub
parent 36d08129a5
commit ac5525c9ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
83 changed files with 3615 additions and 399 deletions

1
.gitignore vendored
View File

@ -30,3 +30,4 @@ storage/migrations
# Ignore Tailwind & Javascript build file >2mb without PurgeCSS (development-only) # Ignore Tailwind & Javascript build file >2mb without PurgeCSS (development-only)
public/css/app.css public/css/app.css
public/js/app.js public/js/app.js
public/js/clients/*

View File

@ -0,0 +1,51 @@
<?php
/**
* 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
*/
/**
* Check if passed page is currently active.
*
* @param $page
* @param bool $boolean
* @return bool
*/
function isActive($page, bool $boolean = false)
{
$current_page = Route::currentRouteName();
if ($page == $current_page && $boolean)
return true;
if ($page == $current_page)
return 'active-page';
return false;
}
/**
* New render method that works with themes.
*
* @param string $path
* @param array $options
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
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);
}

View File

@ -44,11 +44,11 @@ class ContactForgotPasswordController extends Controller
/** /**
* Show the reset email form. * Show the reset email form.
* *
* @return \Illuminate\Http\Response * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/ */
public function showLinkRequestForm() public function showLinkRequestForm()
{ {
return view('portal.default.auth.passwords.email', [ return $this->render('auth.passwords.request', [
'title' => 'Client Password Reset', 'title' => 'Client Password Reset',
'passwordEmailRoute' => 'client.password.email' 'passwordEmailRoute' => 'client.password.email'
]); ]);

View File

@ -31,7 +31,7 @@ class ContactLoginController extends Controller
public function showLoginForm() public function showLoginForm()
{ {
return view('portal.default.auth.login'); return $this->render('auth.login');
} }

View File

@ -60,7 +60,7 @@ class ContactResetPasswordController extends Controller
*/ */
public function showResetForm(Request $request, $token = null) 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] ['token' => $token, 'email' => $request->email]
); );
} }

View File

@ -0,0 +1,32 @@
<?php
namespace App\Http\Controllers\ClientPortal;
use App\Http\Controllers\Controller;
use App\Http\Requests\ClientPortal\ShowCreditRequest;
use App\Models\Credit;
use Illuminate\Http\Request;
class CreditController extends Controller
{
/**
* Display listing of client credits.
*
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function index()
{
$credits = auth()->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,
]);
}
}

View File

@ -16,80 +16,11 @@ use App\Http\Controllers\Controller;
class DashboardController extends Controller class DashboardController extends Controller
{ {
/** /**
* Display a listing of the resource.
*
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/ */
public function index() public function index()
{ {
return view('dashboard.index'); return $this->render('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)
{
//
} }
} }

View File

@ -13,21 +13,18 @@ namespace App\Http\Controllers\ClientPortal;
use App\Filters\InvoiceFilters; use App\Filters\InvoiceFilters;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\ClientPortal\ProcessInvoicesInBulkRequest;
use App\Http\Requests\ClientPortal\ShowInvoiceRequest; use App\Http\Requests\ClientPortal\ShowInvoiceRequest;
use App\Http\Requests\Request;
use App\Jobs\Entity\ActionEntity; use App\Jobs\Entity\ActionEntity;
use App\Models\Invoice; use App\Models\Invoice;
use App\Repositories\BaseRepository;
use App\Utils\Number; use App\Utils\Number;
use App\Utils\Traits\MakesDates; use App\Utils\Traits\MakesDates;
use App\Utils\Traits\MakesHash; 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 Yajra\DataTables\Html\Builder;
use ZipStream\Option\Archive; use ZipStream\Option\Archive;
use ZipStream\ZipStream; use ZipStream\ZipStream;
use function GuzzleHttp\Promise\all;
/** /**
* Class InvoiceController * Class InvoiceController
@ -38,42 +35,20 @@ class InvoiceController extends Controller
{ {
use MakesHash; use MakesHash;
use MakesDates; use MakesDates;
/** /**
* Show the list of Invoices * 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) 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 $this->render('invoices.index', ['invoices' => $invoices]);
return DataTables::of($invoices)->addColumn('action', function ($invoice) {
return $this->buildClientButtons($invoice);
})
->addColumn('checkbox', function ($invoice) {
return '<input type="checkbox" name="hashed_ids[]" value="'. $invoice->hashed_id .'"/>';
})
->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);
} }
private function buildClientButtons($invoice) private function buildClientButtons($invoice)
@ -96,7 +71,7 @@ class InvoiceController extends Controller
* *
* @param \App\Models\Invoice $invoice The invoice * @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) public function show(ShowInvoiceRequest $request, Invoice $invoice)
{ {
@ -104,23 +79,26 @@ class InvoiceController extends Controller
'invoice' => $invoice, 'invoice' => $invoice,
]; ];
return view('portal.default.invoices.show', $data); return $this->render('invoices.show', $data);
} }
/** /**
* Pay one or more invoices * 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') { if (request()->input('action') == 'payment') {
return $this->makePayment($transformed_ids); return $this->makePayment((array)$transformed_ids);
} elseif (request()->input('action') == 'download') { } 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, 'total' => $total,
]; ];
return view('portal.default.invoices.payment', $data); return $this->render('invoices.payment', $data);
} }
private function downloadInvoicePDF(array $ids) private function downloadInvoicePDF(array $ids)

View File

@ -38,54 +38,35 @@ class PaymentController extends Controller
/** /**
* Show the list of Invoices * 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) public function index(PaymentFilters $filters, Builder $builder)
{ {
//$payments = Payment::filter($filters); //$payments = Payment::filter($filters);
$payments = Payment::with('type', 'client'); $payments = Payment::with('type', 'client')->paginate(10);
if (request()->ajax()) { return $this->render('payments.index', [
return DataTables::of($payments)->addColumn('action', function ($payment) { 'payments' => $payments,
return '<a href="/client/payments/'. $payment->hashed_id .'" class="btn btn-xs btn-primary"><i class="glyphicon glyphicon-edit"></i>'.ctrans('texts.view').'</a>'; ]);
})->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);
} }
/** /**
* Display the specified resource. * Display the specified resource.
* *
* @param \App\Models\Invoice $invoice The invoice * @param Request $request
* * @param Payment $payment
* @return \Illuminate\Http\Response * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/ */
public function show(Request $request, Payment $payment) public function show(Request $request, Payment $payment)
{ {
$payment->load('invoices'); $payment->load('invoices');
$data['payment'] = $payment; return $this->render('payments.show', [
'payment' => $payment,
return view('portal.default.payments.show', $data); ]);
} }
/** /**

View File

@ -16,9 +16,7 @@ use App\Http\Controllers\Controller;
use App\Models\ClientGatewayToken; use App\Models\ClientGatewayToken;
use App\Utils\Traits\MakesDates; use App\Utils\Traits\MakesDates;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Yajra\DataTables\Facades\DataTables;
use Yajra\DataTables\Html\Builder; use Yajra\DataTables\Html\Builder;
class PaymentMethodController extends Controller class PaymentMethodController extends Controller
@ -28,49 +26,18 @@ class PaymentMethodController extends Controller
/** /**
* Display a listing of the resource. * 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) public function index(Builder $builder)
{ {
$payment_methods = ClientGatewayToken::whereClientId(auth()->user()->client->id); $payment_methods = ClientGatewayToken::with('gateway_type')
$payment_methods->with('gateway_type'); ->whereClientId(auth()->user()->client->id)
->paginate(10);
if (request()->ajax()) { return $this->render('payment_methods.index', [
return DataTables::of($payment_methods)->addColumn('action', function ($payment_method) { 'payment_methods' => $payment_methods,
return '<a href="/client/payment_methods/' . $payment_method->hashed_id . '" class="btn btn-xs btn-primary"><i class="glyphicon glyphicon-edit"></i>' . ctrans('texts.view') . '</a>'; ]);
})
->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);
} }
/** /**
@ -112,7 +79,9 @@ class PaymentMethodController extends Controller
*/ */
public function show(ClientGatewayToken $payment_method) 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 back();
} }
return redirect()->route('client.payment_methods.index'); return redirect()
->route('client.payment_methods.index')
->withSuccess('Payment method has been successfully removed.');
} }
} }

View File

@ -22,59 +22,39 @@ use Illuminate\Support\Facades\Log;
class ProfileController extends Controller 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. * Show the form for editing the specified resource.
* *
* @param int $id * @param ClientContact $client_contact
* @return \Illuminate\Http\Response * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/ */
public function edit(ClientContact $client_contact) public function edit(ClientContact $client_contact)
{ {
/* Dropzone configuration */ return $this->render('profile.index');
$data = [
'params' => [
'is_avatar' => true,
],
'url' => '/client/document',
'multi_upload' => false,
];
return view('portal.default.profile.index', $data);
} }
/** /**
* Update the specified resource in storage. * Update the specified resource in storage.
* *
* @param \Illuminate\Http\Request $request * @param UpdateContactRequest $request
* @param int $id * @param ClientContact $client_contact
* @return \Illuminate\Http\Response * @return \Illuminate\Http\RedirectResponse
*/ */
public function update(UpdateContactRequest $request, ClientContact $client_contact) public function update(UpdateContactRequest $request, ClientContact $client_contact)
{ {
$client_contact->fill($request->all()); $client_contact->fill($request->all());
//update password if needed if ($request->has('password')) {
if ($request->input('password')) { $client_contact->password = encrypt($request->password);
$client_contact->password = Hash::make($request->input('password'));
} }
$client_contact->save(); $client_contact->save();
// auth()->user()->fresh(); // auth()->user()->fresh();
return back(); return back()->withSuccess(
ctrans('texts.profile_updated_successfully')
);
} }
public function updateClient(UpdateClientRequest $request, ClientContact $client_contact) public function updateClient(UpdateClientRequest $request, ClientContact $client_contact)
@ -93,6 +73,8 @@ class ProfileController extends Controller
$client->fill($request->all()); $client->fill($request->all());
$client->save(); $client->save();
return back(); return back()->withSuccess(
ctrans('texts.profile_updated_successfully')
);
} }
} }

View File

@ -0,0 +1,114 @@
<?php
namespace App\Http\Controllers\ClientPortal;
use App\Http\Controllers\Controller;
use App\Http\Requests\ClientPortal\ProcessQuotesInBulkRequest;
use App\Http\Requests\ClientPortal\ShowQuoteRequest;
use App\Models\Company;
use App\Models\Quote;
use App\Utils\Traits\MakesHash;
use ZipStream\Option\Archive;
use ZipStream\ZipStream;
class QuoteController extends Controller
{
use MakesHash;
/**
* Display a listing of the quotes.
*
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function index()
{
$quotes = auth()->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,
]);
}
}

View File

@ -35,12 +35,12 @@ class RecurringInvoiceController extends Controller
{ {
use MakesHash; use MakesHash;
use MakesDates; use MakesDates;
/** /**
* Show the list of Invoices * Show the list of recurring invoices.
* *
* @param \App\Filters\InvoiceFilters $filters The filters * @param Builder $builder
* * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
* @return \Illuminate\Http\Response
*/ */
public function index(Builder $builder) public function index(Builder $builder)
{ {
@ -48,62 +48,35 @@ class RecurringInvoiceController extends Controller
->whereIn('status_id', [RecurringInvoice::STATUS_PENDING, RecurringInvoice::STATUS_ACTIVE, RecurringInvoice::STATUS_COMPLETED]) ->whereIn('status_id', [RecurringInvoice::STATUS_PENDING, RecurringInvoice::STATUS_ACTIVE, RecurringInvoice::STATUS_COMPLETED])
->orderBy('status_id', 'asc') ->orderBy('status_id', 'asc')
->with('client') ->with('client')
->get(); ->paginate(10);
if (request()->ajax()) { return $this->render('recurring_invoices.index', [
return DataTables::of($invoices)->addColumn('action', function ($invoice) { 'invoices' => $invoices,
return '<a href="/client/recurring_invoices/'. $invoice->hashed_id .'" class="btn btn-xs btn-primary"><i class="glyphicon glyphicon-edit"></i>'.ctrans('texts.view').'</a>'; ]);
})->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);
} }
/** /**
* Display the specified resource. * Display the recurring invoice.
* *
* @param \App\Models\Invoice $invoice The invoice * @param ShowRecurringInvoiceRequest $request
* * @param RecurringInvoice $recurring_invoice
* @return \Illuminate\Http\Response * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/ */
public function show(ShowRecurringInvoiceRequest $request, RecurringInvoice $recurring_invoice) public function show(ShowRecurringInvoiceRequest $request, RecurringInvoice $recurring_invoice)
{ {
$data = [ return $this->render('recurring_invoices.show', [
'invoice' => $recurring_invoice->load('invoices'), 'invoice' => $recurring_invoice->load('invoices'),
]; ]);
return view('portal.default.recurring_invoices.show', $data);
} }
public function requestCancellation(Request $request, RecurringInvoice $recurring_invoice) public function requestCancellation(Request $request, RecurringInvoice $recurring_invoice)
{ {
$data = [
'invoice' => $recurring_invoice
];
//todo double check the user is able to request a cancellation //todo double check the user is able to request a cancellation
//can add locale specific by chaining ->locale(); //can add locale specific by chaining ->locale();
$recurring_invoice->user->notify(new ClientContactRequestCancellation($recurring_invoice, auth()->user())); $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,
]);
} }
} }

View File

@ -22,6 +22,8 @@ class Controller extends BaseController
use AuthorizesRequests, DispatchesJobs, ValidatesRequests; use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
/** /**
* Proxy method for rendering views.
*
* @param string $path * @param string $path
* @param array $options * @param array $options
* *
@ -29,15 +31,6 @@ class Controller extends BaseController
*/ */
public function render(string $path, array $options = []) public function render(string $path, array $options = [])
{ {
$theme = array_key_exists('theme', $options) ? $options['theme'] : 'ninja2020'; return render($path, $options);
if (array_key_exists('root', $options)) {
return view(
sprintf('%s.%s.%s', $options['root'], $theme, $path),
$options
);
}
return view("portal.$theme.$path", $options);
} }
} }

View File

@ -0,0 +1,30 @@
<?php
namespace App\Http\Requests\ClientPortal;
use Illuminate\Foundation\Http\FormRequest;
class ProcessInvoicesInBulkRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true; // TODO.
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'invoices' => ['array'],
];
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Http\Requests\ClientPortal;
use Illuminate\Foundation\Http\FormRequest;
class ProcessQuotesInBulkRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'quotes' => ['array'],
];
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Http\Requests\ClientPortal;
use Illuminate\Foundation\Http\FormRequest;
class ShowCreditRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
//
];
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\Http\Requests\ClientPortal;
use Illuminate\Foundation\Http\FormRequest;
class ShowQuoteRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
// return auth()->user()->client->id === $this->quote->client_id;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
//
];
}
}

View File

@ -32,7 +32,7 @@ class UpdateClientRequest extends Request
public function rules() public function rules()
{ {
return [ return [
'name' => 'required', 'name' => 'sometimes|required',
'file' => 'sometimes|nullable|max:100000|mimes:png,svg,jpeg,gif,jpg,bmp' 'file' => 'sometimes|nullable|max:100000|mimes:png,svg,jpeg,gif,jpg,bmp'
]; ];
} }

View File

@ -58,11 +58,13 @@ class PortalComposer
{ {
$data = []; $data = [];
$data[] = [ 'title' => ctrans('texts.dashboard'), 'url' => 'client.dashboard', 'icon' => 'fa fa-tachometer fa-fw fa-2x']; $data[] = [ 'title' => ctrans('texts.dashboard'), 'url' => 'client.dashboard', 'icon' => 'activity'];
$data[] = [ 'title' => ctrans('texts.invoices'), 'url' => 'client.invoices.index', 'icon' => 'fa fa-file-pdf-o fa-fw fa-2x']; $data[] = [ 'title' => ctrans('texts.invoices'), 'url' => 'client.invoices.index', 'icon' => 'file-text'];
$data[] = [ 'title' => ctrans('texts.recurring_invoices'), 'url' => 'client.recurring_invoices.index', 'icon' => 'fa fa-files-o fa-fw fa-2x']; $data[] = [ 'title' => ctrans('texts.recurring_invoices'), 'url' => 'client.recurring_invoices.index', 'icon' => 'file'];
$data[] = [ 'title' => ctrans('texts.payments'), 'url' => 'client.payments.index', 'icon' => 'fa fa-credit-card fa-fw fa-2x']; $data[] = [ 'title' => ctrans('texts.payments'), 'url' => 'client.payments.index', 'icon' => 'credit-card'];
$data[] = [ 'title' => ctrans('texts.payment_methods'), 'url' => 'client.payment_methods.index', 'icon' => 'fa fa-cc-stripe fa-fw fa-2x']; $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; return $data;
} }

View File

@ -162,4 +162,15 @@ class ClientContact extends Authenticatable implements HasLocalePreference
->withTrashed() ->withTrashed()
->where('id', $this->decodePrimaryKey($value))->firstOrFail(); ->where('id', $this->decodePrimaryKey($value))->firstOrFail();
} }
/**
* @return mixed|string
*/
public function avatar()
{
if($this->avatar)
return $this->avatar;
return asset('images/svg/user.svg');
}
} }

View File

@ -16,9 +16,12 @@ use App\Models\Company;
use App\Models\CompanyGateway; use App\Models\CompanyGateway;
use App\Models\GatewayType; use App\Models\GatewayType;
use App\Models\User; use App\Models\User;
use App\Utils\Traits\MakesDates;
class ClientGatewayToken extends BaseModel class ClientGatewayToken extends BaseModel
{ {
use MakesDates;
protected $casts = [ protected $casts = [
'meta' => 'object', 'meta' => 'object',
'updated_at' => 'timestamp', 'updated_at' => 'timestamp',

View File

@ -114,6 +114,10 @@ class Invoice extends BaseModel
'status' 'status'
]; ];
protected $dates = [
'date',
];
const STATUS_DRAFT = 1; const STATUS_DRAFT = 1;
const STATUS_SENT = 2; const STATUS_SENT = 2;
const STATUS_PARTIAL = 3; const STATUS_PARTIAL = 3;

View File

@ -17,6 +17,7 @@ use App\Jobs\Invoice\CreateInvoicePdf;
use App\Jobs\Quote\CreateQuotePdf; use App\Jobs\Quote\CreateQuotePdf;
use App\Models\Filterable; use App\Models\Filterable;
use App\Services\Quote\QuoteService; use App\Services\Quote\QuoteService;
use App\Utils\Traits\MakesDates;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use App\Utils\Traits\MakesInvoiceValues; use App\Utils\Traits\MakesInvoiceValues;
use App\Utils\Traits\MakesReminders; use App\Utils\Traits\MakesReminders;
@ -29,6 +30,7 @@ use Laracasts\Presenter\PresentableTrait;
class Quote extends BaseModel class Quote extends BaseModel
{ {
use MakesHash; use MakesHash;
use MakesDates;
use Filterable; use Filterable;
use SoftDeletes; use SoftDeletes;
use MakesReminders; use MakesReminders;
@ -150,7 +152,8 @@ class Quote extends BaseModel
{ {
$storage_path = 'storage/' . $this->client->quote_filepath() . $this->number . '.pdf'; $storage_path = 'storage/' . $this->client->quote_filepath() . $this->number . '.pdf';
if (Storage::exists($storage_path)) { if (Storage::exists($storage_path))
{
return $storage_path; return $storage_path;
} }
@ -162,4 +165,43 @@ class Quote extends BaseModel
return $storage_path; return $storage_path;
} }
/**
* @param int $status
* @return string
*/
public static function badgeForStatus(int $status)
{
switch ($status) {
case Quote::STATUS_DRAFT:
return '<h5><span class="badge badge-light">' . ctrans('texts.draft') . '</span></h5>';
break;
case Quote::STATUS_SENT:
return '<h5><span class="badge badge-primary">' . ctrans('texts.sent') . '</span></h5>';
break;
case Quote::STATUS_APPROVED:
return '<h5><span class="badge badge-success">' . ctrans('texts.approved') . '</span></h5>';
break;
case Quote::STATUS_EXPIRED:
return '<h5><span class="badge badge-danger">' . ctrans('texts.expired') . '</span></h5>';
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;
}
} }

View File

@ -104,55 +104,48 @@ class StripePaymentDriver extends BasePaymentDriver
{ {
switch ($gateway_type_id) { switch ($gateway_type_id) {
case GatewayType::CREDIT_CARD: case GatewayType::CREDIT_CARD:
return 'portal.default.gateways.stripe.credit_card';
break;
case GatewayType::TOKEN: case GatewayType::TOKEN:
return 'portal.default.gateways.stripe.credit_card'; return 'gateways.stripe.credit_card';
break; break;
case GatewayType::SOFORT: case GatewayType::SOFORT:
return 'portal.default.gateways.stripe.sofort'; return 'gateways.stripe.sofort';
break; break;
case GatewayType::BANK_TRANSFER: case GatewayType::BANK_TRANSFER:
return 'portal.default.gateways.stripe.ach'; return 'gateways.stripe.ach';
break; break;
case GatewayType::SEPA: case GatewayType::SEPA:
return 'portal.default.gateways.stripe.sepa'; return 'gateways.stripe.sepa';
break; break;
case GatewayType::CRYPTO: case GatewayType::CRYPTO:
return 'portal.default.gateways.stripe.other';
break;
case GatewayType::ALIPAY: case GatewayType::ALIPAY:
return 'portal.default.gateways.stripe.other';
break;
case GatewayType::APPLE_PAY: case GatewayType::APPLE_PAY:
return 'portal.default.gateways.stripe.other'; return 'gateways.stripe.other';
break; break;
default: default:
# code...
break; 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 * @param array $data Array of variables needed for the view
* * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
* @return view The gateway specific partial to be rendered
*
*/ */
public function authorizeCreditCardView(array $data) public function authorizeCreditCardView(array $data)
{ {
$intent['intent'] = $this->getSetupIntent(); $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 * Processes the gateway response for credit card authorization.
*
* @param Request $request The returning request object * @param Request $request The returning request object
* @return view Returns the user to payment methods screen. * @return view Returns the user to payment methods screen.
* @throws \Stripe\Exception\ApiErrorException
*/ */
public function authorizeCreditCardResponse($request) 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 * @param array $data
* @var amount * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View|void
* @var fee * @throws \Exception
* @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
*/ */
public function processPaymentView(array $data) public function processPaymentView(array $data)
{ {
@ -237,7 +223,7 @@ class StripePaymentDriver extends BasePaymentDriver
$data['gateway'] = $this; $data['gateway'] = $this;
return view($this->viewForType($data['payment_method_id']), $data); return render($this->viewForType($data['payment_method_id']), $data);
} }
/** /**

View File

@ -22,7 +22,7 @@ return [
| |
*/ */
'view' => 'breadcrumbs::bootstrap4', 'view' => 'portal.ninja2020.components.breadcrumbs',
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-activity"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline></svg>

After

Width:  |  Height:  |  Size: 277 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-align-left"><line x1="17" y1="10" x2="3" y2="10"></line><line x1="21" y1="6" x2="3" y2="6"></line><line x1="21" y1="14" x2="3" y2="14"></line><line x1="17" y1="18" x2="3" y2="18"></line></svg>

After

Width:  |  Height:  |  Size: 388 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-credit-card"><rect x="1" y="4" width="22" height="16" rx="2" ry="2"></rect><line x1="1" y1="10" x2="23" y2="10"></line></svg>

After

Width:  |  Height:  |  Size: 324 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>

After

Width:  |  Height:  |  Size: 468 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg>

After

Width:  |  Height:  |  Size: 332 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-shield"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg>

After

Width:  |  Height:  |  Size: 274 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-user"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>

After

Width:  |  Height:  |  Size: 308 B

View File

@ -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();

View File

@ -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();

View File

@ -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();

View File

@ -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();

51
resources/js/clients/quotes/approve.js vendored Normal file
View File

@ -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();

View File

@ -2,6 +2,7 @@
return [ return [
'continue' => 'Continue', 'continue' => 'Continue',
'back' => 'Back',
'complete' => 'Complete', 'complete' => 'Complete',
'next' => 'Next', 'next' => 'Next',
'next_step' => 'Next step', 'next_step' => 'Next step',
@ -504,6 +505,7 @@ return [
'api_tokens' => 'API Tokens', 'api_tokens' => 'API Tokens',
'users_and_tokens' => 'Users & Tokens', 'users_and_tokens' => 'Users & Tokens',
'account_login' => 'Account Login', 'account_login' => 'Account Login',
'account_login_text' => 'Welcome back! Glad to see you.',
'recover_password' => 'Recover your password', 'recover_password' => 'Recover your password',
'forgot_password' => 'Forgot your password?', 'forgot_password' => 'Forgot your password?',
'email_address' => 'Email address', 'email_address' => 'Email address',
@ -711,6 +713,7 @@ return [
'reminder_subject' => 'Reminder: Invoice :invoice from :account', 'reminder_subject' => 'Reminder: Invoice :invoice from :account',
'reset' => 'Reset', 'reset' => 'Reset',
'invoice_not_found' => 'The requested invoice is not available', 'invoice_not_found' => 'The requested invoice is not available',
'request_cancellation' => 'Request cancellation',
'referral_program' => 'Referral Program', 'referral_program' => 'Referral Program',
'referral_code' => 'Referral URL', 'referral_code' => 'Referral URL',
'last_sent_on' => 'Sent Last: :date', 'last_sent_on' => 'Sent Last: :date',
@ -2196,6 +2199,7 @@ return [
'create_expense_category' => 'Create category', 'create_expense_category' => 'Create category',
'pro_plan_reports' => ':link to enable reports by joining the Pro Plan', 'pro_plan_reports' => ':link to enable reports by joining the Pro Plan',
'mark_ready' => 'Mark Ready', 'mark_ready' => 'Mark Ready',
'profile_updated_successfully' => 'The profile has been updated successfully.',
'limits' => 'Limits', 'limits' => 'Limits',
'fees' => 'Fees', '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.', '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 <code>Label|Option1,Option2</code> to show a select box.', 'custom_fields_tip' => 'Use <code>Label|Option1,Option2</code> to show a select box.',
'client_information' => 'Client Information', 'client_information' => 'Client Information',
'client_information_text' => 'Use a permanent address where you can receive mail.',
'updated_client_details' => 'Successfully updated client details', 'updated_client_details' => 'Successfully updated client details',
'auto' => 'Auto', 'auto' => 'Auto',
'tax_amount' => 'Tax Amount', 'tax_amount' => 'Tax Amount',

View File

@ -7,6 +7,11 @@
@import 'components/validation'; @import 'components/validation';
@import 'components/inputs'; @import 'components/inputs';
@import 'components/alerts'; @import 'components/alerts';
@import 'components/badge';
.active-page {
@apply bg-blue-900 #{!important};
}
// .. // ..
@tailwind utilities; @tailwind utilities;

31
resources/sass/components/badge.scss vendored Normal file
View File

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

View File

@ -1,5 +1,5 @@
.button { .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 { .button-primary {
@ -13,3 +13,31 @@
.button-block { .button-block {
@apply block w-full; @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;
}
}

View File

@ -1,5 +1,5 @@
.input { .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 { &:focus {
@apply outline-none border-blue-500; @apply outline-none border-blue-500;
@ -9,3 +9,7 @@
.input-label { .input-label {
@apply text-sm text-gray-600; @apply text-sm text-gray-600;
} }
.input-slim {
@apply py-2;
}

View File

@ -0,0 +1,59 @@
@extends('portal.ninja2020.layout.clean')
@section('meta_title', ctrans('texts.login'))
@section('body')
<div class="grid lg:grid-cols-3">
<div class="hidden lg:block col-span-1 bg-red-100 h-screen">
<img src="https://www.invoiceninja.com/wp-content/uploads/2018/04/bg-home2018b.jpg"
class="w-full h-screen object-cover"
alt="Background image">
</div>
<div class="col-span-2 h-screen flex">
<div class="m-auto md:w-1/2 lg:w-1/4">
<div class="flex flex-col">
<h1 class="text-center text-3xl">{{ ctrans('texts.account_login') }}</h1>
<p class="text-center mt-1 text-gray-600">{{ ctrans('texts.account_login_text') }}</p>
<form action="{{ route('client.login') }}" method="post" class="mt-6">
@csrf
<div class="flex flex-col">
<label for="email" class="input-label">{{ ctrans('texts.email_address') }}</label>
<input type="email" name="email" id="email"
class="input"
value="{{ old('email') }}"
autofocus>
@error('email')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="flex flex-col mt-4">
<div class="flex justify-between items-center">
<label for="password" class="input-label">{{ ctrans('texts.password') }}</label>
<a class="text-xs text-gray-600 hover:text-gray-800 ease-in duration-100"
href="{{ route('client.password.request') }}">{{ trans('texts.forgot_password') }}</a>
</div>
<input type="password" name="password" id="password"
class="input"
autofocus>
@error('password')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="mt-5">
<button class="button button-primary button-block">
{{ trans('texts.login') }}
</button>
</div>
</form>
<a href="#" class="uppercase text-sm mt-4 text-center text-grey-600 hover:text-blue-600 ease-in duration-100">
{{ trans('texts.login_create_an_account') }}
</a>
</div>
</div>
</div>
</div>
</div>
@endsection

View File

@ -0,0 +1,44 @@
@extends('portal.ninja2020.layout.clean')
@section('meta_title', $title)
@section('body')
<div class="grid lg:grid-cols-3">
<div class="hidden lg:block col-span-1 bg-red-100 h-screen">
<img src="https://www.invoiceninja.com/wp-content/uploads/2018/04/bg-home2018b.jpg"
class="w-full h-screen object-cover"
alt="Background image">
</div>
<div class="col-span-2 h-screen flex">
<div class="m-auto w-1/2 md:w-1/3 lg:w-1/4">
<div class="flex flex-col">
<h1 class="text-center text-3xl">{{ ctrans('texts.password_recovery') }}</h1>
<p class="text-center mt-1 text-gray-600">{{ ctrans('texts.reset_password_text') }}</p>
@if(session('status'))
<div class="alert alert-success mt-4">
{{ session('status') }}
</div>
@endif
<form action="{{ route($passwordEmailRoute) }}" method="post" class="mt-6">
@csrf
<div class="flex flex-col">
<label for="email" class="input-label">{{ ctrans('texts.email_address') }}</label>
<input type="email" name="email" id="email"
class="input"
value="{{ old('email') }}"
autofocus>
@error('email')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="mt-5">
<button class="button button-primary button-block">{{ ctrans('texts.next_step') }}</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection

View File

@ -0,0 +1,67 @@
@extends('portal.ninja2020.layout.clean')
@section('meta_title', ctrans('texts.password_recovery'))
@section('body')
<div class="grid lg:grid-cols-3">
<div class="hidden lg:block col-span-1 bg-red-100 h-screen">
<img src="https://www.invoiceninja.com/wp-content/uploads/2018/04/bg-home2018b.jpg"
class="w-full h-screen object-cover"
alt="Background image">
</div>
<div class="col-span-2 h-screen flex">
<div class="m-auto w-1/2 md:w-1/3 lg:w-1/4">
<div class="flex flex-col">
<h1 class="text-center text-3xl">{{ ctrans('texts.password_recovery') }}</h1>
<p class="text-center mt-1 text-gray-600">{{ ctrans('texts.reset_password_text') }}</p>
@if(session('status'))
<div class="alert alert-success mt-4">
{{ session('status') }}
</div>
@endif
<form action="{{ route('client.password.update') }}" method="post" class="mt-6">
@csrf
<input type="hidden" name="token" value="{{ $token }}">
<div class="flex flex-col">
<label for="email" class="input-label">{{ ctrans('texts.email_address') }}</label>
<input type="email" name="email" id="email"
class="input"
value="{{ $email ?? old('email') }}"
autofocus>
@error('email')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="flex flex-col mt-4">
<label for="password" class="input-label">{{ ctrans('texts.password') }}</label>
<input type="password" name="password" id="password"
class="input"
autofocus>
@error('password')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="flex flex-col mt-4">
<label for="password" class="input-label">{{ ctrans('texts.password') }}</label>
<input type="password" name="password_confirmation" id="password_confirmation"
class="input"
autofocus>
@error('password_confirmation')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="mt-5">
<button class="button button-primary button-block">{{ ctrans('texts.complete') }}</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection

View File

@ -0,0 +1,33 @@
<nav class="sm:hidden mb-4">
<a href="{{ url()->previous() }}"
class="flex items-center text-sm leading-5 font-medium text-gray-500 hover:text-gray-700 focus:outline-none focus:underline transition duration-150 ease-in-out">
<svg class="flex-shrink-0 -ml-1 mr-1 h-5 w-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd"
d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"
clip-rule="evenodd"/>
</svg>
{{ ctrans('texts.back') }}
</a>
</nav>
@if (count($breadcrumbs))
<nav class="hidden sm:flex items-center text-sm leading-5 font-medium mb-4">
@foreach ($breadcrumbs as $breadcrumb)
@if ($breadcrumb->url && !$loop->last)
<a href="{{ $breadcrumb->url }}"
class="text-gray-500 hover:text-gray-700 focus:outline-none focus:underline transition duration-150 ease-in-out">{{ $breadcrumb->title }}</a>
<svg class="flex-shrink-0 mx-2 h-5 w-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clip-rule="evenodd"/>
</svg>
@else
<a class="text-gray-500 hover:text-gray-700 focus:outline-none focus:underline transition duration-150 ease-in-out">{{ $breadcrumb->title }}</a>
@endif
@endforeach
</nav>
@endif

View File

@ -0,0 +1,4 @@
<div class="validation validation-pass mb-4">
{{ session('success') }}
</div>

View File

@ -0,0 +1,23 @@
<div class="hidden md:flex md:flex-shrink-0">
<div class="flex flex-col w-64">
<div class="flex items-center h-16 flex-shrink-0 px-4 bg-blue-900">
<a href="{{ route('client.dashboard') }}">
<img class="h-6 w-auto"
src="{!! $settings->company_logo ?: 'https://www.invoiceninja.com/wp-content/themes/invoice-ninja/images/logo.png' !!}"
alt="{{ config('app.name') }}"/>
</a>
</div>
<div class="h-0 flex-1 flex flex-col overflow-y-auto">
<nav class="flex-1 py-4 bg-blue-800">
@foreach($sidebar as $row)
<a class="group flex items-center p-4 text-sm leading-5 font-medium text-white bg-blue-800 hover:bg-blue-900 focus:outline-none focus:bg-blue-900 transition ease-in-out duration-150 {{ isActive($row['url']) }}"
href="{{ route($row['url']) }}">
<img src="{{ asset('images/svg/' . $row['icon'] . '.svg') }}" class="w-5 h-5 fill-current text-white mr-3" alt=""/>
{{ $row['title'] }}
</a>
@endforeach
</nav>
</div>
</div>
</div>

View File

@ -0,0 +1,65 @@
<div class="relative z-10 flex-shrink-0 flex h-16 bg-white shadow" xmlns:x-transition="http://www.w3.org/1999/xhtml">
<button @click.stop="sidebarOpen = true"
class="px-4 border-r border-gray-200 text-gray-500 focus:outline-none focus:bg-gray-100 focus:text-gray-600 md:hidden">
<svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h7"/>
</svg>
</button>
<div class="flex-1 px-4 flex justify-between">
<div class="flex-1 flex">
<div class="w-full flex md:ml-0">
<label for="search_field" class="sr-only">{{ ctrans('texts.search') }}</label>
<div class="relative w-full text-gray-400 focus-within:text-gray-600">
<div class="absolute inset-y-0 left-0 flex items-center pointer-events-none">
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"/>
</svg>
</div>
<input id="search_field"
class="block w-full h-full pl-8 pr-3 py-2 rounded-md text-gray-900 placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 sm:text-sm"
placeholder="Search"/>
</div>
</div>
</div>
<div class="ml-4 flex items-center md:ml-6">
<button
class="p-1 text-gray-400 rounded-full hover:bg-gray-100 hover:text-gray-500 focus:outline-none focus:shadow-outline focus:text-gray-500">
<svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/>
</svg>
</button>
<div @click.away="open = false" class="ml-3 relative" x-data="{ open: false }">
<div>
<button @click="open = !open"
class="max-w-xs flex items-center text-sm rounded-full focus:outline-none focus:shadow-outline">
<img class="h-8 w-8 rounded-full"
src="{{ auth()->user()->avatar() }}"
alt=""/>
<span class="ml-2">{{ auth()->user()->present()->name() }}</span>
</button>
</div>
<div x-show="open"
x-transition:enter="transition ease-out duration-100"
x-transition:enter-start="transform opacity-0 scale-95"
x-transition:enter-end="transform opacity-100 scale-100"
x-transition:leave="transition ease-in duration-75"
x-transition:leave-start="transform opacity-100 scale-100"
x-transition:leave-end="transform opacity-0 scale-95"
class="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg">
<div class="py-1 rounded-md bg-white shadow-xs">
<a href="{{ route('client.profile.edit', ['client_contact' => auth()->user()->hashed_id]) }}"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition ease-in-out duration-150">
{{ ctrans('texts.profile') }}
</a>
<a href="{{ route('client.logout') }}"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition ease-in-out duration-150">
{{ ctrans('texts.logout') }}
</a>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,31 @@
<div
class="h-screen flex overflow-hidden bg-gray-100"
x-data="{ sidebarOpen: false }"
@keydown.window.escape="sidebarOpen = false">
<!-- Off-canvas menu for mobile -->
@include('portal.ninja2020.components.general.sidebar.mobile')
<!-- Static sidebar for desktop -->
@include('portal.ninja2020.components.general.sidebar.desktop')
<div class="flex flex-col w-0 flex-1 overflow-hidden">
@include('portal.ninja2020.components.general.sidebar.header')
<main
class="flex-1 relative z-0 overflow-y-auto py-6 focus:outline-none"
tabindex="0" x-data
x-init="$el.focus()">
<div class="mx-auto px-4 sm:px-6 md:px-8">
@yield('header')
</div>
<div class="mx-auto px-4 sm:px-6 md:px-8">
<div class="py-4">
@includeWhen(session()->has('success'), 'portal.ninja2020.components.general.messages.success')
{{ $slot }}
</div>
</div>
</main>
</div>
</div>

View File

@ -0,0 +1,34 @@
<div class="md:hidden">
<div @click="sidebarOpen = false"
class="fixed inset-0 z-30 bg-gray-600 opacity-0 pointer-events-none transition-opacity ease-linear duration-300"
:class="{'opacity-75 pointer-events-auto': sidebarOpen, 'opacity-0 pointer-events-none': !sidebarOpen}"></div>
<div
class="fixed inset-y-0 left-0 flex flex-col z-40 max-w-xs w-full pt-5 pb-4 bg-blue-800 transform ease-in-out duration-300 -translate-x-full"
:class="{'translate-x-0': sidebarOpen, '-translate-x-full': !sidebarOpen}">
<div class="absolute top-0 right-0 -mr-14 p-1">
<button x-show="sidebarOpen" @click="sidebarOpen = false"
class="flex items-center justify-center h-12 w-12 rounded-full focus:outline-none focus:bg-gray-600">
<svg class="h-6 w-6 text-white" stroke="currentColor" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<div class="flex-shrink-0 flex items-center px-4">
<img class="h-6 w-auto"
src="{!! $settings->company_logo ?: 'https://www.invoiceninja.com/wp-content/themes/invoice-ninja/images/logo.png' !!}"
alt="{{ config('app.name') }}"/>
</div>
<div class="mt-5 flex-1 h-0 overflow-y-auto">
<nav class="flex-1 py-4 bg-blue-800">
@foreach($sidebar as $row)
<a class="group flex items-center p-4 text-sm leading-5 font-medium text-white bg-blue-800 hover:bg-blue-900 focus:outline-none focus:bg-blue-900 transition ease-in-out duration-150 {{ isActive($row['url']) }}"
href="{{ route($row['url']) }}">
<img src="{{ asset('images/svg/' . $row['icon'] . '.svg') }}"
class="w-5 h-5 fill-current text-white mr-3" alt=""/>
{{ $row['title'] }}
</a>
@endforeach
</nav>
</div>
</div>
</div>

View File

@ -0,0 +1,87 @@
@extends('portal.ninja2020.layout.app')
@section('meta_title', ctrans('texts.credits'))
@section('header')
{{ Breadcrumbs::render('credits') }}
@if($errors->any())
<div class="alert alert-failure mb-4">
@foreach($errors->all() as $error)
<p>{{ $error }}</p>
@endforeach
</div>
@endif
<div class="bg-white shadow rounded mb-4" translate>
<div class="px-4 py-5 sm:p-6">
<div class="sm:flex sm:items-start sm:justify-between">
<div>
<h3 class="text-lg leading-6 font-medium text-gray-900">
{{ ctrans('texts.credits') }}
</h3>
<div class="mt-2 max-w-xl text-sm leading-5 text-gray-500">
<p translate>
{{ ctrans('texts.list_of_credits') }}
</p>
</div>
</div>
</div>
</div>
</div>
@endsection
@section('body')
<div class="flex flex-col mt-4">
<div class="-my-2 py-2 overflow-x-auto sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8">
<div
class="align-middle inline-block min-w-full shadow overflow-hidden rounded border-b border-gray-200">
<table class="min-w-full">
<thead>
<tr>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
{{ ctrans('texts.amount') }}
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
{{ ctrans('texts.balance') }}
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
{{ ctrans('texts.credit_date') }}
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
{{ ctrans('texts.public_notes') }}
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50"></th>
</tr>
</thead>
<tbody>
@foreach($credits as $credit)
<tr class="bg-white group hover:bg-gray-100">
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
{{ App\Utils\Number::formatMoney($credit->amount, $credit->client) }}
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
{{ App\Utils\Number::formatMoney($credit->balance, $credit->client) }}
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
{{ $credit->formatDate($credit->date, $credit->client->date_format()) }}
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
{{ empty($credit->public_notes) ? '/' : $credit->public_notes }}
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
<a href="{{ route('client.credits.show', $credit->hashed_id) }}"
class="button-link">
@lang('texts.view')
</a>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
<div class="my-6">
{{ $credits->links('portal.ninja2020.vendor.pagination') }}
</div>
</div>
@endsection

View File

@ -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')
<div class="container mx-auto">
<div class="bg-white shadow overflow-hidden sm:rounded-lg">
<div class="px-4 py-5 border-b border-gray-200 sm:px-6">
<h3 class="text-lg leading-6 font-medium text-gray-900">
{{ ctrans('texts.credit') }}
</h3>
<p class="mt-1 max-w-2xl text-sm leading-5 text-gray-500" translate>
</p>
</div>
<div>
<dl>
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.amount') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ App\Utils\Number::formatMoney($credit->amount, $credit->client) }}
</dd>
</div>
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.balance') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ App\Utils\Number::formatMoney($credit->balance, $credit->client) }}
</dd>
</div>
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.credit_date') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ $credit->formatDate($credit->date, $credit->client->date_format()) }}
</dd>
</div>
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.public_notes') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ $credit->public_notes }}
</dd>
</div>
</dl>
</div>
</div>
</div>
@endsection

View File

@ -1,7 +1,29 @@
@extends('portal.ninja2020.layout.clean') @extends('portal.ninja2020.layout.app')
@section('meta_title', ctrans('texts.dashboard'))
@section('body') @section('header')
<div class="m-4"> {{ Breadcrumbs::render('dashboard') }}
<button class="button">Hello world</button>
<div class="bg-white shadow rounded mb-4" translate>
<div class="px-4 py-5 sm:p-6">
<div class="sm:flex sm:items-start sm:justify-between">
<div>
<h3 class="text-lg leading-6 font-medium text-gray-900">
{{ ctrans('texts.dashboard') }}
</h3>
<div class="mt-2 max-w-xl text-sm leading-5 text-gray-500">
<p translate>
{{ ctrans('texts.quick_overview_statistics') }}
</p>
</div>
</div>
</div>
</div>
</div> </div>
@endsection @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

View File

@ -0,0 +1,79 @@
@extends('portal.ninja2020.layout.app')
@section('meta_title', ctrans('texts.add_credit_card'))
@push('head')
<meta name="stripe-publishable-key" content="{{ $gateway->getPublishableKey() }}">
@endpush
@section('header')
{{ Breadcrumbs::render('payment_methods.add_credit_card') }}
@endsection
@section('body')
<form action="{{ route('client.payment_methods.store') }}" method="post" id="server_response">
@csrf
<input type="hidden" name="company_gateway_id" value="{{ $gateway->gateway_id }}">
<input type="hidden" name="gateway_type_id" value="1">
<input type="hidden" name="gateway_response" id="gateway_response">
<input type="hidden" name="is_default" id="is_default">
</form>
<div class="container mx-auto">
<div class="grid grid-cols-6 gap-4">
<div class="col-span-6 md:col-start-2 md:col-span-4">
<div class="alert alert-failure mb-4" hidden id="errors"></div>
<div class="bg-white shadow overflow-hidden sm:rounded-lg">
<div class="px-4 py-5 border-b border-gray-200 sm:px-6">
<h3 class="text-lg leading-6 font-medium text-gray-900">
{{ ctrans('texts.add_credit_card') }}
</h3>
<p class="mt-1 max-w-2xl text-sm leading-5 text-gray-500" translate>
{{ ctrans('texts.authorize_for_future_use') }}
</p>
</div>
<div>
<dl>
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 flex items-center">
<dt class="text-sm leading-5 font-medium text-gray-500 mr-4">
{{ ctrans('texts.name') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
<input class="input" id="cardholder-name" type="text" placeholder="{{ ctrans('texts.name') }}">
</dd>
</div><div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.credit_card') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
<div id="card-element" class="form-control"></div>
</dd>
</div>
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.save_as_default') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
<input type="checkbox" class="form-check" name="proxy_is_default"
id="proxy_is_default"/>
</dd>
</div>
<div class="bg-white px-4 py-5 flex justify-end">
<button
type="button"
id="card-button"
data-secret="{{ $intent->client_secret }}"
class="button button-primary">
{{ ctrans('texts.save') }}
</button>
</div>
</dl>
</div>
</div>
</div>
</div>
</div>
@endsection
@push('footer')
<script src="https://js.stripe.com/v3/"></script>
<script src="{{ asset('js/clients/payment_methods/authorize-stripe-card.js') }}"></script>
@endpush

View File

@ -0,0 +1,44 @@
<div style="display: none;" id="displaySignatureModal"
class="fixed bottom-0 inset-x-0 px-4 pb-4 sm:inset-0 sm:flex sm:items-center sm:justify-center">
<div x-show="open" x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100" x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
class="fixed inset-0 transition-opacity">
<div class="absolute inset-0 bg-gray-500 opacity-75"></div>
</div>
<div x-show="open" x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100" x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
class="bg-white rounded-lg px-4 pt-5 pb-4 overflow-hidden shadow-xl transform transition-all sm:max-w-lg sm:w-full sm:p-6">
<div class="sm:flex sm:items-start">
<div
class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
<svg class="h-6 w-6 text-red-600" stroke="currentColor" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg>
</div>
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 class="text-lg leading-6 font-medium text-gray-900">
{{ ctrans('texts.sign_here') }}
</h3>
<div class="mt-2">
<p class="text-sm leading-5 text-gray-500">
<canvas id="signature-pad" class="signature-pad" width=400 height=200></canvas>
</p>
</div>
</div>
</div>
<div class="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<div class="flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto">
<button type="button" class="button button-primary" id="signature-next-step"
@click="document.getElementById('displaySignatureModal').style.display = 'none';">
{{ ctrans('texts.next_step') }}
</button>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,49 @@
<div style="display: none;" id="displayTermsModal"
class="fixed bottom-0 inset-x-0 px-4 pb-4 sm:inset-0 sm:flex sm:items-center sm:justify-center">
<div x-show="open" x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100" x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
class="fixed inset-0 transition-opacity">
<div class="absolute inset-0 bg-gray-500 opacity-75"></div>
</div>
<div x-show="open" x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100" x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
class="bg-white rounded-lg px-4 pt-5 pb-4 overflow-hidden shadow-xl transform transition-all sm:max-w-lg sm:w-full sm:p-6">
<div class="sm:flex sm:items-start">
<div
class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
<svg class="h-6 w-6 text-red-600" stroke="currentColor" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg>
</div>
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 class="text-lg leading-6 font-medium text-gray-900">
{{ ctrans('texts.terms') }}
</h3>
<div class="mt-2">
<p class="text-sm leading-5 text-gray-500">
{!! $invoice->terms !!}
</p>
</div>
</div>
</div>
<div class="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<div class="flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto">
<button type="button" id="accept-terms-button" class="button button-primary">
{{ ctrans('texts.agree_to_terms', ['terms' => trans('texts.invoice_terms')]) }}
</button>
</div>
<div class="mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto">
<button @click="document.getElementById('displayTermsModal').style.display = 'none';" type="button"
class="button button-secondary">
{{ ctrans('texts.close') }}
</button>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,125 @@
@extends('portal.ninja2020.layout.app')
@section('meta_title', ctrans('texts.invoices'))
@section('header')
{{ Breadcrumbs::render('invoices') }}
@if($errors->any())
<div class="alert alert-failure mb-4">
@foreach($errors->all() as $error)
<p>{{ $error }}</p>
@endforeach
</div>
@endif
<div class="bg-white shadow rounded mb-4" translate>
<div class="px-4 py-5 sm:p-6">
<div class="sm:flex sm:items-start sm:justify-between">
<div>
<h3 class="text-lg leading-6 font-medium text-gray-900">
{{ ctrans('texts.invoices') }}
</h3>
<div class="mt-2 max-w-xl text-sm leading-5 text-gray-500">
<p translate>
{{ ctrans('texts.list_of_invoices') }}
</p>
</div>
</div>
</div>
</div>
</div>
@endsection
@section('body')
<div class="flex justify-between items-center">
<span>{{ ctrans('texts.with_selected') }}</span>
<form action="{{ route('client.invoices.bulk') }}" method="post" id="bulkActions">
@csrf
<button type="submit" class="button button-primary" name="action"
value="download">{{ ctrans('texts.download') }}</button>
<button type="submit" class="button button-primary" name="action"
value="payment">{{ ctrans('texts.pay_now') }}</button>
</form>
</div>
<div class="flex flex-col mt-4">
<div class="-my-2 py-2 overflow-x-auto sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8">
<div
class="align-middle inline-block min-w-full shadow overflow-hidden rounded border-b border-gray-200">
<table class="min-w-full">
<thead>
<tr>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
<label>
<input type="checkbox" class="form-check form-check-parent">
</label>
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
{{ ctrans('texts.invoice_number') }}
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
{{ ctrans('texts.invoice_date') }}
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
{{ ctrans('texts.balance') }}
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
{{ ctrans('texts.due_date') }}
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
{{ ctrans('texts.status') }}
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50"></th>
</tr>
</thead>
<tbody>
@foreach($invoices as $invoice)
<tr class="bg-white group hover:bg-gray-100">
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 font-medium text-gray-900">
<label>
<input type="checkbox" class="form-check form-check-child"
data-value="{{ $invoice->hashed_id }}">
</label>
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
{{ $invoice->number }}
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
{{ $invoice->formatDate($invoice->date, $invoice->client->date_format()) }}
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
{{ App\Utils\Number::formatMoney($invoice->balance, $invoice->client) }}
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
{{ $invoice->formatDate($invoice->due_date, $invoice->client->date_format()) }}
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
{!! App\Models\Invoice::badgeForStatus($invoice->status) !!}
</td>
<td class="px-6 py-4 whitespace-no-wrap flex items-center justify-end text-sm leading-5 font-medium">
@if($invoice->isPayable())
<button
class="button button-primary py-1 px-2 text-xs uppercase mr-3 pay-now-button"
data-value="{{ $invoice->hashed_id }}">
@lang('texts.pay_now')
</button>
@endif
<a href="{{ route('client.invoice.show', $invoice->hashed_id) }}"
class="button-link">
@lang('texts.view')
</a>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
<div class="my-6">
{{ $invoices->links('portal.ninja2020.vendor.pagination') }}
</div>
</div>
@endsection
@push('footer')
<script src="{{ asset('js/clients/invoices/action-selectors.js') }}"></script>
@endpush

View File

@ -0,0 +1,113 @@
@extends('portal.ninja2020.layout.app')
@section('meta_title', ctrans('texts.pay_now'))
@push('head')
<meta name="show-invoice-terms" content="{{ $settings->show_accept_invoice_terms ? true : false }}">
<meta name="require-invoice-signature" content="{{ $settings->require_invoice_signature ? true : false }}">
<script src="https://cdn.jsdelivr.net/npm/signature_pad@2.3.2/dist/signature_pad.min.js"></script>
@endpush
@section('body')
<form action="{{ route('client.payments.process') }}" method="post" id="payment-form">
@csrf
<input type="hidden" name="hashed_ids" value="{{ $hashed_ids }}" id="hashed_ids">
<input type="hidden" name="company_gateway_id" id="company_gateway_id">
<input type="hidden" name="payment_method_id" id="payment_method_id">
</form>
<div class="container mx-auto">
<div class="grid grid-cols-6 gap-4">
<div class="col-span-6 md:col-start-2 md:col-span-4">
<div class="flex justify-end">
<div class="flex justify-end mb-2">
<div x-data="{ open: false }" @keydown.window.escape="open = false" @click.away="open = false"
class="relative inline-block text-left">
<div>
<div class="rounded-md shadow-sm">
<button @click="open = !open" type="button"
class="inline-flex justify-center w-full rounded-md border border-gray-300 px-4 py-2 bg-white text-sm leading-5 font-medium text-gray-700 hover:text-gray-500 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-50 active:text-gray-800 transition ease-in-out duration-150">
{{ ctrans('texts.pay_now') }}
<svg class="-mr-1 ml-2 h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clip-rule="evenodd"/>
</svg>
</button>
</div>
</div>
<div x-show="open" class="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg">
<div class="rounded-md bg-white shadow-xs">
<div class="py-1">
@foreach($payment_methods as $payment_method)
<a href="#" @click="{ open = false }"
data-company-gateway-id="{{ $payment_method['company_gateway_id'] }}"
data-gateway-type-id="{{ $payment_method['gateway_type_id'] }}"
class="dropdown-gateway-button block px-4 py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900">
{{ $payment_method['label'] }}
</a>
@endforeach
</div>
</div>
</div>
</div>
</div>
</div>
@foreach($invoices as $invoice)
<div class="bg-white shadow overflow-hidden sm:rounded-lg mb-4">
<div class="px-4 py-5 border-b border-gray-200 sm:px-6">
<h3 class="text-lg leading-6 font-medium text-gray-900">
{{ ctrans('texts.invoice') }}
<a class="button-link"
href="{{ route('client.invoice.show', $invoice->hashed_id) }}">
(#{{ $invoice->number }})
</a>
</h3>
<p class="mt-1 max-w-2xl text-sm leading-5 text-gray-500" translate>
</p>
</div>
<div>
<dl>
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.invoice_number') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ $invoice->number }}
</dd>
</div>
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.due_date') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ $invoice->due_date }}
</dd>
</div>
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.additional_info') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
@if($invoice->po_number)
{{ $invoice->po_number }}
@elseif($invoice->public_notes)
{{ $invoice->public_notes }}
@else
{{ $invoice->invoice_date}}
@endif
</dd>
</div>
</dl>
</div>
</div>
@endforeach
</div>
</div>
</div>
@include('portal.ninja2020.invoices.includes.terms')
@include('portal.ninja2020.invoices.includes.signature')
@endsection
@push('footer')
<script src="{{ asset('js/clients/invoices/payment.js') }}"></script>
@endpush

View File

@ -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())
<form action="{{ route('client.invoices.bulk') }}" method="post">
@csrf
<div class="bg-white shadow sm:rounded-lg mb-4" translate>
<div class="px-4 py-5 sm:p-6">
<div class="sm:flex sm:items-start sm:justify-between">
<div>
<h3 class="text-lg leading-6 font-medium text-gray-900">
{{ ctrans('texts.unpaid') }}
</h3>
<div class="mt-2 max-w-xl text-sm leading-5 text-gray-500">
<p translate>
{{ ctrans('texts.invoice_unpaid') }}
<!-- This invoice is still not paid. Click the button to complete the payment. -->
</p>
</div>
</div>
<div class="mt-5 sm:mt-0 sm:ml-6 sm:flex-shrink-0 sm:flex sm:items-center">
<div class="inline-flex rounded-md shadow-sm">
<input type="hidden" name="invoices[]" value="{{ $invoice->hashed_id }}">
<input type="hidden" name="action" value="payment">
<button class="button button-primary">@lang('texts.pay_now')</button>
</div>
</div>
</div>
</div>
</div>
</form>
@endif
<embed src="{{ asset($invoice->pdf_url()) }}#toolbar=1&navpanes=1&scrollbar=1" type="application/pdf" width="100%"
height="1180px"/>
@endsection

View File

@ -0,0 +1,76 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<!-- Source: https://github.com/invoiceninja/invoiceninja -->
<!-- Error: {{ session('error') }} -->
@if (config('services.analytics.tracking_id'))
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-122229484-1"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', '{{ config('services.analytics.tracking_id') }}', {'anonymize_ip': true});
function trackEvent(category, action) {
ga('send', 'event', category, action, this.src);
}
</script>
<script>
Vue.config.devtools = true;
</script>
@else
<script>
function gtag() {
}
</script>
@endif
<!-- Title -->
<title>@yield('meta_title', 'Invoice Ninja') {{ config('app.name') }}</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="@yield('meta_description')"/>
<!-- CSRF Token -->
<meta name="csrf-token" content="{{ csrf_token() }}">
<!-- Scripts -->
<script src="{{ mix('js/app.js') }}" defer></script>
<script src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.x.x/dist/alpine.js" defer></script>
<script src="https://kit.fontawesome.com/8a87eb8352.js" crossorigin="anonymous"></script>
<!-- Fonts -->
<link rel="dns-prefetch" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css?family=Open+Sans&display=swap" rel="stylesheet" type="text/css">
<!-- Styles -->
<link href="{{ mix('css/app.css') }}" rel="stylesheet">
{{-- <link href="{{ mix('favicon.png') }}" rel="shortcut icon" type="image/png"> --}}
<link rel="canonical" href="{{ config('ninja.site_url') }}/{{ request()->path() }}"/>
{{-- Feel free to push anything to header using @push('header') --}}
@stack('head')
</head>
<body class="antialiased">
@component('portal.ninja2020.components.general.sidebar.main')
@yield('body')
@endcomponent
</body>
<footer>
@yield('footer')
@stack('footer')
</footer>
</html>

View File

@ -44,6 +44,7 @@
<!-- Scripts --> <!-- Scripts -->
<script src="{{ mix('js/app.js') }}" defer></script> <script src="{{ mix('js/app.js') }}" defer></script>
<script src="https://kit.fontawesome.com/8a87eb8352.js" crossorigin="anonymous"></script>
<!-- Fonts --> <!-- Fonts -->
<link rel="dns-prefetch" href="https://fonts.gstatic.com"> <link rel="dns-prefetch" href="https://fonts.gstatic.com">

View File

@ -0,0 +1,52 @@
<div x-show="open" class="fixed bottom-0 inset-x-0 px-4 pb-4 sm:inset-0 sm:flex sm:items-center sm:justify-center">
<div x-show="open" x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100" x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
class="fixed inset-0 transition-opacity">
<div class="absolute inset-0 bg-gray-500 opacity-75"></div>
</div>
<div x-show="open" x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100" x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
class="bg-white rounded-lg px-4 pt-5 pb-4 overflow-hidden shadow-xl transform transition-all sm:max-w-lg sm:w-full sm:p-6">
<div class="sm:flex sm:items-start">
<div
class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
<svg class="h-6 w-6 text-red-600" stroke="currentColor" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg>
</div>
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 class="text-lg leading-6 font-medium text-gray-900" translate>
Are you sure?
</h3>
<div class="mt-2">
<p class="text-sm leading-5 text-gray-500">
Warning! This action can't be reversed.
</p>
</div>
</div>
</div>
<div class="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<div class="flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto">
<form action="{{ route('client.payment_methods.destroy', $payment_method->hashed_id) }}" method="post">
@csrf
@method('DELETE')
<button type="submit" class="button button-danger button-block">
{{ ctrans('texts.remove') }}
</button>
</form>
</div>
<div class="mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto">
<button @click="open = false" type="button" class="button button-secondary button-block">
{{ ctrans('texts.cancel') }}
</button>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,109 @@
@extends('portal.ninja2020.layout.app')
@section('meta_title', ctrans('texts.payment_methods'))
@section('header')
{{ Breadcrumbs::render('payment_methods') }}
<div class="bg-white shadow rounded mb-4" translate>
<div class="px-4 py-5 sm:p-6">
<div class="sm:flex sm:items-start sm:justify-between">
<div>
<h3 class="text-lg leading-6 font-medium text-gray-900">
{{ ctrans('texts.payment_methods') }}
</h3>
<div class="mt-2 max-w-xl text-sm leading-5 text-gray-500">
<p translate>
{{ ctrans('texts.list_of_payment_methods') }}
<!-- List of methods available for payments. -->
</p>
</div>
</div>
<div class="mt-5 sm:mt-0 sm:ml-6 sm:flex-shrink-0 sm:flex sm:items-center">
<div class="inline-flex rounded-md shadow-sm">
<input type="hidden" name="hashed_ids">
<input type="hidden" name="action" value="payment">
<a href="{{ route('client.payment_methods.create') }}" class="button button-primary">@lang('texts.add_payment_method')</a>
</div>
</div>
</div>
</div>
</div>
@endsection
@section('body')
<div class="flex flex-col">
<div class="-my-2 py-2 overflow-x-auto sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8">
<div
class="align-middle inline-block min-w-full shadow overflow-hidden rounded border-b border-gray-200">
<table class="min-w-full">
<thead>
<tr>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
{{ ctrans('texts.created_at') }}
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
{{ ctrans('texts.payment_type_id') }}
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
{{ ctrans('texts.type') }}
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
{{ ctrans('texts.expires') }}
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
{{ ctrans('texts.card_number') }}
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
{{ ctrans('texts.default') }}
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50"></th>
</tr>
</thead>
<tbody>
@foreach($payment_methods as $payment_method)
<tr class="bg-white group hover:bg-gray-100">
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
{{ $payment_method->formatDateTimestamp($payment_method->created_at, auth()->user()->client->date_format()) }}
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
{{ ctrans("texts.{$payment_method->gateway_type->alias}") }}
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
{{ ucfirst(optional($payment_method->meta)->brand) }}
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
@if(isset($payment_method->meta->exp_month) && isset($payment_method->meta->exp_year))
{{ $payment_method->meta->exp_month}} / {{ $payment_method->meta->exp_year }}
@endif
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
@isset($payment_method->meta->last4)
**** {{ $payment_method->meta->last4 }}
@endisset
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
@if($payment_method->is_default)
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" class="feather feather-check">
<path d="M20 6L9 17l-5-5"/>
</svg>
@endif
</td>
<td class="px-6 py-4 whitespace-no-wrap flex items-center justify-end text-sm leading-5 font-medium">
<a href="{{ route('client.payment_methods.show', $payment_method->hashed_id) }}"
class="text-blue-600 hover:text-indigo-900 focus:outline-none focus:underline">
@lang('texts.view')
</a>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
<div class="my-6">
{{ $payment_methods->links('portal.ninja2020.vendor.pagination') }}
</div>
</div>
@endsection

View File

@ -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')
<div class="container mx-auto">
<div class="bg-white shadow overflow-hidden sm:rounded-lg">
<div class="px-4 py-5 border-b border-gray-200 sm:px-6">
<h3 class="text-lg leading-6 font-medium text-gray-900">
{{ ctrans("texts.{$payment_method->gateway_type->alias}") }}
</h3>
<p class="mt-1 max-w-2xl text-sm leading-5 text-gray-500" translate>
<!-- Details of the payment method. -->
{{ ctrans('texts.details_of_method') }}
</p>
</div>
<div>
<dl>
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.payment_type') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ ucfirst($payment_method->gateway_type->name) }}
</dd>
</div>
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.type') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ ucfirst($payment_method->meta->brand) }}
</dd>
</div>
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.card_number') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
**** {{ ucfirst($payment_method->meta->last4) }}
</dd>
</div>
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.date_created') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ $payment_method->formatDateTimestamp($payment_method->created_at, auth()->user()->client->date_format()) }}
</dd>
</div>
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.default') }}
</dt>
<div class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ $payment_method->is_default ? ctrans('texts.yes') : ctrans('texts.no') }}
</div>
</div>
@isset($payment_method->meta->exp_month)
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.expires') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ $payment_method->meta->exp_month }} / {{ $payment_method->meta->exp_year }}
</dd>
</div>
@endisset
</dl>
</div>
</div>
<div class="bg-white shadow sm:rounded-lg mb-4 mt-4" translate>
<div class="px-4 py-5 sm:p-6">
<div class="sm:flex sm:items-start sm:justify-between">
<div>
<h3 class="text-lg leading-6 font-medium text-gray-900">
Remove
</h3>
<div class="mt-2 max-w-xl text-sm leading-5 text-gray-500">
<p>
Permanently remove this payment method.
</p>
</div>
</div>
<div class="mt-5 sm:mt-0 sm:ml-6 sm:flex-shrink-0 sm:flex sm:items-center">
<div class="inline-flex rounded-md shadow-sm" x-data="{ open: false }">
<button class="button button-danger" translate @click="open = true">
Remove payment method
</button>
@include('portal.ninja2020.payment_methods.includes.modals.removal')
</div>
</div>
</div>
</div>
</div>
</div>
@endsection

View File

@ -0,0 +1,85 @@
@extends('portal.ninja2020.layout.app')
@section('meta_title', ctrans('texts.payments'))
@section('header')
{{ Breadcrumbs::render('payments') }}
<div class="bg-white shadow rounded mb-4" translate>
<div class="px-4 py-5 sm:p-6">
<div class="sm:flex sm:items-start sm:justify-between">
<div>
<h3 class="text-lg leading-6 font-medium text-gray-900">
{{ ctrans('texts.payments') }}
</h3>
<div class="mt-2 max-w-xl text-sm leading-5 text-gray-500">
<p translate>
{{ ctrans('texts.List of your payments.') }}
</p>
</div>
</div>
</div>
</div>
</div>
@endsection
@section('body')
<div class="flex flex-col">
<div class="-my-2 py-2 overflow-x-auto sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8">
<div
class="align-middle inline-block min-w-full shadow overflow-hidden rounded border-b border-gray-200">
<table class="min-w-full">
<thead>
<tr>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
{{ ctrans('texts.payment_date') }}
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
{{ ctrans('texts.payment_type_id') }}
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
{{ ctrans('texts.amount') }}
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
{{ ctrans('texts.transaction_reference') }}
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
{{ ctrans('texts.status') }}
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50"></th>
</tr>
</thead>
<tbody>
@foreach($payments as $payment)
<tr class="cursor-pointer bg-white group hover:bg-gray-100" @click="window.location = '{{ route('client.payments.show', $payment->hashed_id) }}'">
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
{{ $payment->formatDate($payment->date, $payment->client->date_format()) }}
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
{{ $payment->type->name }}
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
{{ \App\Utils\Number::formatMoney($payment->amount, $payment->client) }}
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
{{ $payment->transaction_reference }}
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
{!! \App\Models\Payment::badgeForStatus($payment->status_id) !!}
</td>
<td class="px-6 py-4 whitespace-no-wrap flex items-center justify-end text-sm leading-5 font-medium">
<a href="{{ route('client.payments.show', $payment->hashed_id) }}"
class="text-blue-600 hover:text-indigo-900 focus:outline-none focus:underline">
@lang('texts.view')
</a>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
<div class="my-6">
{{ $payments->links('portal.ninja2020.vendor.pagination') }}
</div>
</div>
@endsection

View File

@ -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')
<div class="container mx-auto">
<div class="bg-white shadow overflow-hidden sm:rounded-lg">
<div class="px-4 py-5 border-b border-gray-200 sm:px-6">
<h3 class="text-lg leading-6 font-medium text-gray-900">
{{ ctrans('texts.payment') }}
</h3>
<p class="mt-1 max-w-2xl text-sm leading-5 text-gray-500" translate>
{{ ctrans('texts.Details of the payment.') }}
</p>
</div>
<div>
<dl>
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.payment_date') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ $payment->clientPaymentDate() }}
</dd>
</div>
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.transaction_reference') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ $payment->transaction_reference }}
</dd>
</div>
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.method') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ $payment->type->name }}
</dd>
</div>
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.amount') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ $payment->formattedAmount() }}
</dd>
</div>
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.status') }}
</dt>
<div class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{!! \App\Models\Payment::badgeForStatus($payment->status_id) !!}
</div>
</div>
</dl>
</div>
</div>
<div class="bg-white shadow overflow-hidden sm:rounded-lg mt-4">
<div class="px-4 py-5 border-b border-gray-200 sm:px-6">
<h3 class="text-lg leading-6 font-medium text-gray-900">
{{ ctrans('texts.invoices') }}
</h3>
<p class="mt-1 max-w-2xl text-sm leading-5 text-gray-500" translate>
{{ ctrans('texts.List of invoices affected by payment.') }}
</p>
</div>
<div>
<dl>
@foreach($payment->invoices as $invoice)
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.invoice_number') }}
</dt>
<div class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
<a class="button-link"
href="{{ route('client.invoice.show', ['invoice' => $invoice->hashed_id])}}">
{{ $invoice->number }}
</a>
</div>
</div>
@endforeach
</dl>
</div>
</div>
</div>
@endsection

View File

@ -0,0 +1,356 @@
@extends('portal.ninja2020.layout.app')
@section('meta_title', ctrans('texts.client_information'))
@section('header')
<p class="leading-5 text-gray-500" translate>{{ ctrans('texts.Update your personal information.') }}</p>
@endsection
@section('body')
<!-- Basic information: first & last name, e-mail address etc. -->
<div class="mt-2 sm:mt-6">
<div class="md:grid md:grid-cols-3 md:gap-6">
<div class="md:col-span-1">
<div class="sm:px-0">
<h3 class="text-lg font-medium leading-6 text-gray-900" translate>{{ ctrans('texts.profile') }}</h3>
<p class="mt-1 text-sm leading-5 text-gray-500">
@lang('texts.client_information_text')
</p>
</div>
</div>
<div class="mt-5 md:mt-0 md:col-span-2">
<form action="{{ route('client.profile.update', auth()->user()->hashed_id) }}" method="POST"
id="update_contact">
@csrf
@method('PUT')
<div class="shadow overflow-hidden rounded">
<div class="px-4 py-5 bg-white sm:p-6">
<div class="grid grid-cols-6 gap-6">
<div class="col-span-6 sm:col-span-3">
<label for="first_name" class="input-label">@lang('texts.first_name')</label>
<input id="first_name" class="input" name="first_name"
value="{{ auth()->user()->first_name }}"/>
@error('first_name')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-3">
<label for="last_name" class="input-label">@lang('texts.last_name')</label>
<input id="last_name" class="input" name="last_name"
value="{{ auth()->user()->last_name }}"/>
@error('last_name')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-4">
<label for="email_address" class="input-label">@lang('texts.email_address')</label>
<input id="email_address" class="input" type="email" name="email"
value="{{ auth()->user()->email }}"/>
@error('email')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-4">
<label for="phone" class="input-label">@lang('texts.phone')</label>
<input id="phone" class="input" name="phone" value="{{ auth()->user()->phone }}"/>
@error('phone')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-6 lg:col-span-3">
<label for="password" class="input-label">@lang('texts.password')</label>
<input id="password" class="input" name="password" type="password"/>
@error('password')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-3 lg:col-span-3">
<label for="state" class="input-label">@lang('texts.confirm_password')</label>
<input id="state" class="input" name="password_confirmation" type="password"/>
@error('password_confirmation')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
</div>
</div>
<div class="px-4 py-3 bg-gray-50 text-right sm:px-6">
<button class="button button-primary">
@lang('texts.save')
</button>
</div>
</div>
</form>
</div>
</div>
</div>
<!-- Name, website & logo -->
<div class="mt-10 sm:mt-6">
<div class="md:grid md:grid-cols-3 md:gap-6">
<div class="md:col-span-1">
<div class="sm:px-0">
<h3 class="text-lg font-medium leading-6 text-gray-900">{{ ctrans('texts.name_website_logo') }}</h3>
<p class="mt-1 text-sm leading-5 text-gray-500" translate>
{{ ctrans('texts. Make sure you use full link to your site.') }}
</p>
</div>
</div>
<div class="mt-5 md:mt-0 md:col-span-2">
<form action="{{ route('client.profile.edit_client', auth()->user()->hashed_id) }}" method="POST"
id="update_contact">
@csrf
@method('PUT')
<div class="shadow overflow-hidden rounded">
<div class="px-4 py-5 bg-white sm:p-6">
<div class="grid grid-cols-6 gap-6">
<div class="col-span-6 sm:col-span-3">
<label for="street" class="input-label">@lang('texts.name')</label>
<input id="name" class="input" name="name"
value="{{ auth()->user()->client->name }}"/>
@error('name')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-3">
<label for="website" class="input-label">@lang('texts.website')</label>
<input id="website" class="input" name="last_name"
value="{{ auth()->user()->client->website }}"/>
@error('website')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
</div>
</div>
<div class="px-4 py-3 bg-gray-50 text-right sm:px-6">
<button class="button button-primary">
@lang('texts.save')
</button>
</div>
</div>
</form>
</div>
</div>
</div>
<!-- Client personal address -->
<div class="mt-10 sm:mt-6">
<div class="md:grid md:grid-cols-3 md:gap-6">
<div class="md:col-span-1">
<div class="sm:px-0">
<h3 class="text-lg font-medium leading-6 text-gray-900">{{ ctrans('texts.personal_address') }}</h3>
<p class="mt-1 text-sm leading-5 text-gray-500" translate>
{{ ctrans('texts.your_personal_address') }}
</p>
</div>
</div>
<div class="mt-5 md:mt-0 md:col-span-2">
<form action="{{ route('client.profile.edit_client', auth()->user()->hashed_id) }}" method="POST"
id="update_contact">
@csrf
@method('PUT')
<div class="shadow overflow-hidden rounded">
<div class="px-4 py-5 bg-white sm:p-6">
<div class="grid grid-cols-6 gap-6">
<div class="col-span-6 sm:col-span-4">
<label for="address1" class="input-label">@lang('texts.address1')</label>
<input id="address1" class="input" name="address1"
value="{{ auth()->user()->client->address1 }}"/>
@error('address1')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-3">
<label for="address2" class="input-label">@lang('texts.address2')</label>
<input id="address2" class="input" name="address2"
value="{{ auth()->user()->client->address2 }}"/>
@error('address2')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-3">
<label for="city" class="input-label">@lang('texts.city')</label>
<input id="city" class="input" name="city"
value="{{ auth()->user()->client->city }}"/>
@error('city')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-2">
<label for="state" class="input-label">@lang('texts.state')</label>
<input id="state" class="input" name="state"
value="{{ auth()->user()->client->state }}"/>
@error('state')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-2">
<label for="postal_code" class="input-label">@lang('texts.postal_code')</label>
<input id="postal_code" class="input" name="postal_code"
value="{{ auth()->user()->client->postal_code }}"/>
@error('postal_code')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-2">
<label for="country" class="input-label">@lang('texts.country')</label>
<select id="country" class="input form-select" name="country">
@foreach($countries as $country)
<option
{{ $country == auth()->user()->client->country->id ? 'selected' : null }} value="{{ $country->id }}">{{ $country->full_name }}</option>
@endforeach
</select>
@error('country')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
</div>
</div>
<div class="px-4 py-3 bg-gray-50 text-right sm:px-6">
<button class="button button-primary">
@lang('texts.save')
</button>
</div>
</div>
</form>
</div>
</div>
</div>
<!-- Client shipping address -->
<div class="mt-10 sm:mt-6">
<div class="md:grid md:grid-cols-3 md:gap-6">
<div class="md:col-span-1">
<div class="sm:px-0">
<h3 class="text-lg font-medium leading-6 text-gray-900">{{ ctrans('texts.shipping_address') }}</h3>
<p class="mt-1 text-sm leading-5 text-gray-500" translate>
{{ ctrans('texts.your_shipping_address') }}
</p>
</div>
</div>
<div class="mt-5 md:mt-0 md:col-span-2">
<form action="{{ route('client.profile.edit_client', auth()->user()->hashed_id) }}" method="POST"
id="update_contact">
@csrf
@method('PUT')
<div class="shadow overflow-hidden rounded">
<div class="px-4 py-5 bg-white sm:p-6">
<div class="grid grid-cols-6 gap-6">
<div class="col-span-6 sm:col-span-4">
<label for="shipping_address1"
class="input-label">@lang('texts.shipping_address1')</label>
<input id="shipping_address1" class="input" name="shipping_address1"
value="{{ auth()->user()->client->shipping_address1 }}"/>
@error('shipping_address1')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-3">
<label for="shipping_address2"
class="input-label">@lang('texts.shipping_address2')</label>
<input id="shipping_address2" class="input" name="shipping_address2"
value="{{ auth()->user()->client->shipping_address2 }}"/>
@error('shipping_address2')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-3">
<label for="shipping_city" class="input-label">@lang('texts.shipping_city')</label>
<input id="shipping_city" class="input" name="shipping_city"
value="{{ auth()->user()->client->shipping_city }}"/>
@error('shipping_city')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-2">
<label for="shipping_state"
class="input-label">@lang('texts.shipping_state')</label>
<input id="shipping_state" class="input" name="shipping_state"
value="{{ auth()->user()->client->shipping_state }}"/>
@error('shipping_state')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-2">
<label for="shipping_postal_code"
class="input-label">@lang('texts.shipping_postal_code')</label>
<input id="shipping_postal_code" class="input" name="shipping_postal_code"
value="{{ auth()->user()->client->shipping_postal_code }}"/>
@error('shipping_postal_code')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
<div class="col-span-6 sm:col-span-2">
<label for="shipping_country"
class="input-label">@lang('texts.shipping_country')</label>
<select id="shipping_country" class="input form-select" name="shipping_country">
@foreach($countries as $country)
<option
{{ $country == auth()->user()->client->shipping_country->id ? 'selected' : null }} value="{{ $country->id }}">{{ $country->full_name }}
</option>
@endforeach
</select>
@error('country')
<div class="validation validation-fail">
{{ $message }}
</div>
@enderror
</div>
</div>
</div>
<div class="px-4 py-3 bg-gray-50 text-right sm:px-6">
<button class="button button-primary">
@lang('texts.save')
</button>
</div>
</div>
</form>
</div>
</div>
</div>
@endsection

View File

@ -0,0 +1,90 @@
@extends('portal.ninja2020.layout.app')
@section('meta_title', ctrans('texts.approve'))
@push('head')
<meta name="require-quote-signature" content="{{ $settings->require_invoice_signature ? true : false }}">
<script src="https://cdn.jsdelivr.net/npm/signature_pad@2.3.2/dist/signature_pad.min.js"></script>
@endpush
@section('header')
{{ Breadcrumbs::render('quotes.approve') }}
@endsection
@section('body')
<form action="{{ route('client.quotes.bulk') }}" method="post" id="approve-form">
@csrf
<input type="hidden" name="action" value="approve">
<input type="hidden" name="process" value="true">
@foreach($quotes as $quote)
<input type="hidden" name="quotes[]" value="{{ $quote->hashed_id }}">
@endforeach
</form>
<div class="container mx-auto">
<div class="grid grid-cols-6 gap-4">
<div class="col-span-6 md:col-start-2 md:col-span-4">
<div class="flex justify-end">
<div class="flex justify-end mb-2">
<div class="relative inline-block text-left">
<div>
<div class="rounded-md shadow-sm">
<button type="button" id="approve-button"
class="inline-flex justify-center w-full rounded-md border border-gray-300 px-4 py-2 bg-white text-sm leading-5 font-medium text-gray-700 hover:text-gray-500 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-50 active:text-gray-800 transition ease-in-out duration-150">
{{ ctrans('texts.approve') }}
</button>
</div>
</div>
</div>
</div>
</div>
@foreach($quotes as $quote)
<div class="bg-white shadow overflow-hidden sm:rounded-lg mb-4">
<div class="px-4 py-5 border-b border-gray-200 sm:px-6">
<h3 class="text-lg leading-6 font-medium text-gray-900">
{{ ctrans('texts.invoice') }}
<a class="button-link" href="{{ route('client.quotes.show', $quote->hashed_id) }}">
({{ $quote->number }})
</a>
</h3>
<p class="mt-1 max-w-2xl text-sm leading-5 text-gray-500" translate>
</p>
</div>
<div>
<dl>
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.quote_number') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ $quote->number }}
</dd>
</div>
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.quote_date') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ $quote->formatDate($quote->date, $quote->client->date_format()) }}
</dd>
</div>
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.balance') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ App\Utils\Number::formatMoney($quote->balance, $quote->client) }}
</dd>
</div>
</dl>
</div>
</div>
@endforeach
</div>
</div>
</div>
@include('portal.ninja2020.invoices.includes.signature')
@endsection
@push('footer')
<script src="{{ asset('js/clients/quotes/approve.js') }}"></script>
@endpush

View File

@ -0,0 +1,44 @@
<div style="display: none;" id="displaySignatureModal"
class="fixed bottom-0 inset-x-0 px-4 pb-4 sm:inset-0 sm:flex sm:items-center sm:justify-center">
<div x-show="open" x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100" x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
class="fixed inset-0 transition-opacity">
<div class="absolute inset-0 bg-gray-500 opacity-75"></div>
</div>
<div x-show="open" x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100" x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
class="bg-white rounded-lg px-4 pt-5 pb-4 overflow-hidden shadow-xl transform transition-all sm:max-w-lg sm:w-full sm:p-6">
<div class="sm:flex sm:items-start">
<div
class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
<svg class="h-6 w-6 text-red-600" stroke="currentColor" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg>
</div>
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 class="text-lg leading-6 font-medium text-gray-900">
{{ ctrans('texts.sign_here') }}
</h3>
<div class="mt-2">
<p class="text-sm leading-5 text-gray-500">
<canvas id="signature-pad" class="signature-pad" width=400 height=200></canvas>
</p>
</div>
</div>
</div>
<div class="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<div class="flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto">
<button type="button" class="button button-primary" id="signature-next-step"
@click="document.getElementById('displaySignatureModal').style.display = 'none';">
{{ ctrans('texts.next_step') }}
</button>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,117 @@
@extends('portal.ninja2020.layout.app')
@section('meta_title', ctrans('texts.quotes'))
@section('header')
{{ Breadcrumbs::render('quotes') }}
@if($errors->any())
<div class="alert alert-failure mb-4">
@foreach($errors->all() as $error)
<p>{{ $error }}</p>
@endforeach
</div>
@endif
<div class="bg-white shadow rounded mb-4" translate>
<div class="px-4 py-5 sm:p-6">
<div class="sm:flex sm:items-start sm:justify-between">
<div>
<h3 class="text-lg leading-6 font-medium text-gray-900">
{{ ctrans('texts.quotes') }}
</h3>
<div class="mt-2 max-w-xl text-sm leading-5 text-gray-500">
<p translate>
{{ ctrans('texts.list_of_quotes') }}
</p>
</div>
</div>
</div>
</div>
</div>
@endsection
@section('body')
<div class="flex justify-between items-center">
<span>{{ ctrans('texts.with_selected') }}</span>
<form action="{{ route('client.quotes.bulk') }}" method="post" id="bulkActions">
@csrf
<button type="submit" class="button button-primary" name="action"
value="download">{{ ctrans('texts.download') }}</button>
<button type="submit" class="button button-primary" name="action"
value="approve">{{ ctrans('texts.approve') }}</button>
</form>
</div>
<div class="flex flex-col mt-4">
<div class="-my-2 py-2 overflow-x-auto sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8">
<div class="align-middle inline-block min-w-full shadow overflow-hidden rounded border-b border-gray-200">
<table class="min-w-full">
<thead>
<tr>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
<label>
<input type="checkbox" class="form-check form-check-parent">
</label>
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
{{ ctrans('texts.quote_number') }}
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
{{ ctrans('texts.quote_date') }}
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
{{ ctrans('texts.balance') }}
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
{{ ctrans('texts.valid_until') }}
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
{{ ctrans('texts.status') }}
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50"></th>
</tr>
</thead>
<tbody>
@foreach($quotes as $quote)
<tr class="bg-white group hover:bg-gray-100">
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 font-medium text-gray-900">
<label>
<input type="checkbox" class="form-check form-check-child"
data-value="{{ $quote->hashed_id }}">
</label>
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
{{ $quote->number }}
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
{{ $quote->formatDate($quote->date, $quote->client->date_format()) }}
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
{{ App\Utils\Number::formatMoney($quote->balance, $quote->client) }}
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
{{ $quote->formatDate($quote->date, $quote->client->date_format()) }}
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
{!! App\Models\Quote::badgeForStatus($quote->status_id) !!}
</td>
<td class="px-6 py-4 whitespace-no-wrap flex items-center justify-end text-sm leading-5 font-medium">
<a href="{{ route('client.quotes.show', $quote->hashed_id) }}"
class="button-link">
@lang('texts.view')
</a>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
<div class="my-6">
{{ $quotes->links('portal.ninja2020.vendor.pagination') }}
</div>
</div>
@endsection
@push('footer')
<script src="{{ asset('js/clients/quotes/action-selectors.js') }}"></script>
@endpush

View File

@ -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())
<form action="{{ route('client.quotes.bulk') }}" method="post">
@csrf
<input type="hidden" name="action" value="approve">
<input type="hidden" name="quotes[]" value="{{ $quote->hashed_id }}">
<div class="bg-white shadow sm:rounded-lg mb-4" translate>
<div class="px-4 py-5 sm:p-6">
<div class="sm:flex sm:items-start sm:justify-between">
<div>
<h3 class="text-lg leading-6 font-medium text-gray-900" translate>
{{ ctrans('texts.waitin_for_approval') }}
</h3>
<div class="mt-2 max-w-xl text-sm leading-5 text-gray-500">
<p translate>
{{ ctrans('texts.quote_still_not_approved') }}
</p>
</div>
</div>
<div class="mt-5 sm:mt-0 sm:ml-6 sm:flex-shrink-0 sm:flex sm:items-center">
<div class="inline-flex rounded-md shadow-sm">
<input type="hidden" name="action" value="payment">
<button class="button button-primary">@lang('texts.approve')</button>
</div>
</div>
</div>
</div>
</div>
</form>
@endif
<embed src="{{ asset($quote->pdf_file_path()) }}#toolbar=1&navpanes=1&scrollbar=1" type="application/pdf"
width="100%"
height="1180px"/>
@endsection

View File

@ -0,0 +1,74 @@
@extends('portal.ninja2020.layout.app')
@section('header')
{{ Breadcrumbs::render('recurring_invoices.request_cancellation', $invoice) }}
@stop
@section('body')
<div class="container mx-auto">
<div class="bg-white shadow overflow-hidden rounded">
<div class="px-4 py-5 border-b border-gray-200 sm:px-6">
<h3 class="text-lg leading-6 font-medium text-gray-900">
{{ ctrans('texts.recurring_invoices') }}
</h3>
<p class="mt-1 max-w-2xl text-sm leading-5 text-gray-500" translate>
Details of the recurring invoice.
</p>
</div>
<div>
<dl>
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.start_date') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ $invoice->formatDate($invoice->start_date, $invoice->client->date_format()) }}
</dd>
</div>
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.next_send_date') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ $invoice->formatDate($invoice->next_send_date, $invoice->client->date_format()) }}
</dd>
</div>
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.frequency') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ \App\Models\RecurringInvoice::frequencyForKey($invoice->frequency_id) }}
</dd>
</div>
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.cycles_remaining') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ $invoice->remaining_cycles }}
</dd>
</div>
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.amount') }}
</dt>
<div class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ \App\Utils\Number::formatMoney($invoice->amount, $invoice->client) }}
</div>
</div>
</div>
</div>
<div class="bg-white shadow rounded-sm mb-4 mt-4 border-l-2 border-green-500" translate>
<div class="px-4 py-5 sm:p-6">
<div class="sm:flex sm:items-start sm:justify-between">
<div>
<h3 class="text-lg leading-6 font-medium text-gray-900">
Cancellation pending, we'll be in touch!
</h3>
</div>
</div>
</div>
</div>
</div>
@endsection

View File

@ -0,0 +1,49 @@
<div x-show="open" class="fixed bottom-0 inset-x-0 px-4 pb-4 sm:inset-0 sm:flex sm:items-center sm:justify-center">
<div x-show="open" x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100" x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
class="fixed inset-0 transition-opacity">
<div class="absolute inset-0 bg-gray-500 opacity-75"></div>
</div>
<div x-show="open" x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100" x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
class="bg-white rounded-lg px-4 pt-5 pb-4 overflow-hidden shadow-xl transform transition-all sm:max-w-lg sm:w-full sm:p-6">
<div class="sm:flex sm:items-start">
<div
class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
<svg class="h-6 w-6 text-red-600" stroke="currentColor" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg>
</div>
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 class="text-lg leading-6 font-medium text-gray-900" translate>
Request Cancellation
</h3>
<div class="mt-2">
<p class="text-sm leading-5 text-gray-500">
Warning! You are requesting a cancellation of this service.
Your service may be cancelled with no further notification to you.
</p>
</div>
</div>
</div>
<div class="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<div class="flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto">
<a href="{{ route('client.recurring_invoices.request_cancellation',['recurring_invoice' => $invoice->hashed_id]) }}"
class="button button-danger button-block">
Confirm cancellation
</a>
</div>
<div class="mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto">
<button @click="open = false" type="button" class="button button-secondary button-block">
Cancel
</button>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,85 @@
@extends('portal.ninja2020.layout.app')
@section('meta_title', ctrans('texts.recurring_invoices'))
@section('header')
{{ Breadcrumbs::render('recurring_invoices') }}
<div class="bg-white shadow rounded mb-4" translate>
<div class="px-4 py-5 sm:p-6">
<div class="sm:flex sm:items-start sm:justify-between">
<div>
<h3 class="text-lg leading-6 font-medium text-gray-900">
{{ ctrans('texts.recurring_invoices') }}
</h3>
<div class="mt-2 max-w-xl text-sm leading-5 text-gray-500">
<p>
{{ ctrans('texts.list_of_recurring_invoices') }}
</p>
</div>
</div>
</div>
</div>
</div>
@endsection
@section('body')
<div class="flex flex-col">
<div class="-my-2 py-2 overflow-x-auto sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8">
<div
class="align-middle inline-block min-w-full shadow overflow-hidden rounded border-b border-gray-200">
<table class="min-w-full">
<thead>
<tr>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
{{ ctrans('texts.frequency') }}
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
{{ ctrans('texts.start_date') }}
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
{{ ctrans('texts.next_send_date') }}
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
{{ ctrans('texts.cycles_remaining') }}
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
{{ ctrans('texts.amount') }}
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50"></th>
</tr>
</thead>
<tbody>
@foreach($invoices as $invoice)
<tr class="bg-white group hover:bg-gray-100">
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
{{ \App\Models\RecurringInvoice::frequencyForKey($invoice->frequency_id) }}
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
{{ $invoice->formatDate($invoice->date, $invoice->client->date_format()) }}
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
{{ $invoice->formatDate($invoice->next_send_date, $invoice->client->date_format()) }}
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
{{ $invoice->remaining_cycles }}
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
{{ \App\Utils\Number::formatMoney($invoice->amount, $invoice->client) }}
</td>
<td class="px-6 py-4 whitespace-no-wrap flex items-center justify-end text-sm leading-5 font-medium">
<a href="{{ route('client.recurring_invoices.show', $invoice->hashed_id) }}"
class="text-blue-600 hover:text-indigo-900 focus:outline-none focus:underline">
@lang('texts.view')
</a>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
<div class="my-6">
{{ $invoices->links('portal.ninja2020.vendor.pagination') }}
</div>
</div>
@endsection

View File

@ -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')
<div class="container mx-auto">
<div class="bg-white shadow overflow-hidden sm:rounded-lg">
<div class="px-4 py-5 border-b border-gray-200 sm:px-6">
<h3 class="text-lg leading-6 font-medium text-gray-900">
{{ ctrans('texts.recurring_invoices') }}
</h3>
<p class="mt-1 max-w-2xl text-sm leading-5 text-gray-500" translate>
{{ ctrans('texts.details_of_recurring_invoice') }}.
</p>
</div>
<div>
<dl>
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.start_date') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ $invoice->formatDate($invoice->start_date, $invoice->client->date_format()) }}
</dd>
</div>
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.next_send_date') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ $invoice->formatDate($invoice->next_send_date, $invoice->client->date_format()) }}
</dd>
</div>
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.frequency') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ \App\Models\RecurringInvoice::frequencyForKey($invoice->frequency_id) }}
</dd>
</div>
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.cycles_remaining') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ $invoice->remaining_cycles }}
</dd>
</div>
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.amount') }}
</dt>
<div class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ \App\Utils\Number::formatMoney($invoice->amount, $invoice->client) }}
</div>
</div>
</div>
</div>
<div class="bg-white shadow sm:rounded-lg mb-4 mt-4" translate>
<div class="px-4 py-5 sm:p-6">
<div class="sm:flex sm:items-start sm:justify-between">
<div>
<h3 class="text-lg leading-6 font-medium text-gray-900">
{{ ctrans('texts.cancellation') }}
</h3>
<div class="mt-2 max-w-xl text-sm leading-5 text-gray-500">
<p translate>
{{ ctrans('texts.In case you want to stop the recurring invoice, please click the request the
cancellation.') }}
</p>
</div>
</div>
<div class="mt-5 sm:mt-0 sm:ml-6 sm:flex-shrink-0 sm:flex sm:items-center">
<div class="inline-flex rounded-md shadow-sm" x-data="{ open: false }">
<button class="button button-danger" translate @click="open = true">Request Cancellation</button>
@include('portal.ninja2020.recurring_invoices.includes.modals.cancellation')
</div>
</div>
</div>
</div>
</div>
</div>
@endsection

View File

@ -0,0 +1,67 @@
<div class="border-t border-gray-200 px-4 flex items-center justify-between sm:px-0">
<div class="w-0 flex-1 flex">
<a href="{{ $paginator->previousPageUrl() }}"
class="-mt-px border-t-2 border-transparent pt-4 pr-1 inline-flex items-center text-sm leading-5 font-medium text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:outline-none focus:text-gray-700 focus:border-gray-400 transition ease-in-out duration-150">
<svg class="mr-3 h-5 w-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd"
d="M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z"
clip-rule="evenodd"/>
</svg>
@lang('texts.previous')
</a>
</div>
<div class="hidden md:flex">
@foreach ($elements as $element)
@if (is_string($element))
<span
class="-mt-px border-t-2 border-transparent pt-4 px-4 inline-flex items-center text-sm leading-5 font-medium text-gray-500">
...
</span>
@endif
@if (is_array($element))
@foreach ($element as $page => $url)
@if ($page == $paginator->currentPage())
<a href="#" disabled
class="-mt-px border-t-2 border-blue-600 pt-4 px-4 inline-flex items-center text-sm leading-5 font-medium text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:outline-none focus:text-gray-700 focus:border-gray-400 transition ease-in-out duration-150"
aria-current="page">
{{ $page }}
</a>
@else
<a href="{{ $url }}"
class="-mt-px border-t-2 border-transparent pt-4 px-4 inline-flex items-center text-sm leading-5 font-medium text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:outline-none focus:text-gray-700 focus:border-gray-400 transition ease-in-out duration-150">
{{ $page }}
</a>
@endif
@endforeach
@endif
@endforeach
</div>
@if ($paginator->hasMorePages())
<div class="w-0 flex-1 flex justify-end">
<a href="{{ $paginator->nextPageUrl() }}"
class="-mt-px border-t-2 border-transparent pt-4 pl-1 inline-flex items-center text-sm leading-5 font-medium text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:outline-none focus:text-gray-700 focus:border-gray-400 transition ease-in-out duration-150">
@lang('texts.next')
<svg class="ml-3 h-5 w-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd"
d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z"
clip-rule="evenodd"/>
</svg>
</a>
</div>
@else
<div class="w-0 flex-1 flex justify-end">
<a href="#"
class="-mt-px border-t-2 border-transparent pt-4 pl-1 inline-flex items-center text-sm leading-5 font-medium text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:outline-none focus:text-gray-700 focus:border-gray-400 transition ease-in-out duration-150">
@lang('texts.next')
<svg class="ml-3 h-5 w-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd"
d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z"
clip-rule="evenodd"/>
</svg>
</a>
</div>
@endif
</div>

View File

@ -2,7 +2,91 @@
// Dashboard // Dashboard
Breadcrumbs::for('dashboard', function ($trail) { 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 // Dashboard > Client

View File

@ -1,5 +1,7 @@
<?php <?php
use Illuminate\Support\Facades\Route;
Route::get('client', 'Auth\ContactLoginController@showLoginForm')->name('client.login'); //catch all Route::get('client', 'Auth\ContactLoginController@showLoginForm')->name('client.login'); //catch all
Route::get('client/login', 'Auth\ContactLoginController@showLoginForm')->name('client.login')->middleware('locale'); Route::get('client/login', 'Auth\ContactLoginController@showLoginForm')->name('client.login')->middleware('locale');
@ -12,6 +14,7 @@ Route::post('client/password/reset', 'Auth\ContactResetPasswordController@reset'
//todo implement domain DB //todo implement domain DB
Route::group(['middleware' => ['auth:contact','locale'], 'prefix' => 'client', 'as' => 'client.'], function () { 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('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::get('invoices', 'ClientPortal\InvoiceController@index')->name('invoices.index')->middleware('portal_enabled');
@ -36,6 +39,11 @@ Route::group(['middleware' => ['auth:contact','locale'], 'prefix' => 'client', '
Route::resource('payment_methods', 'ClientPortal\PaymentMethodController');// name = (payment_methods. index / create / show / update / destroy / edit 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::post('document', 'ClientPortal\DocumentController@store')->name('document.store');
Route::delete('document', 'ClientPortal\DocumentController@destroy')->name('document.destroy'); Route::delete('document', 'ClientPortal\DocumentController@destroy')->name('document.destroy');
@ -43,6 +51,7 @@ Route::group(['middleware' => ['auth:contact','locale'], 'prefix' => 'client', '
}); });
Route::group(['middleware' => ['invite_db'], 'prefix' => 'client', 'as' => 'client.'], function () { Route::group(['middleware' => ['invite_db'], 'prefix' => 'client', 'as' => 'client.'], function () {
Route::get('invoice/{invitation_key}/download_pdf', 'InvoiceController@downloadPdf'); Route::get('invoice/{invitation_key}/download_pdf', 'InvoiceController@downloadPdf');
Route::get('quote/{invitation_key}/download_pdf', 'QuoteController@downloadPdf'); Route::get('quote/{invitation_key}/download_pdf', 'QuoteController@downloadPdf');
Route::get('credit/{invitation_key}/download_pdf', 'CreditController@downloadPdf'); Route::get('credit/{invitation_key}/download_pdf', 'CreditController@downloadPdf');
@ -52,6 +61,7 @@ Route::group(['middleware' => ['invite_db'], 'prefix' => 'client', 'as' => 'clie
Route::get('{entity}/{invitation_key}','ClientPortal\InvitationController@router'); Route::get('{entity}/{invitation_key}','ClientPortal\InvitationController@router');
Route::get('{entity}/{client_hash}/{invitation_key}','ClientPortal\InvitationController@routerForIframe'); //should never need this 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('payment_hook/{company_gateway_id}/{gateway_type_id}','ClientPortal\PaymentHookController@process');
}); });
Route::fallback('BaseController@notFoundClient'); Route::fallback('BaseController@notFoundClient');

19
webpack.mix.js vendored
View File

@ -1,19 +1,14 @@
const mix = require("laravel-mix"); const mix = require("laravel-mix");
const tailwindcss = require("tailwindcss"); 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") 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({ .options({
processCssUrls: false, processCssUrls: false,
postCss: [tailwindcss("./tailwind.config.js")] postCss: [tailwindcss("./tailwind.config.js")]