From ac5525c9ac6074ca7a343b6f57e2dabccf111e51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Mon, 23 Mar 2020 18:10:42 +0100 Subject: [PATCH] [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 --- .gitignore | 1 + app/Helpers/ClientPortal.php | 51 +++ .../Auth/ContactForgotPasswordController.php | 4 +- .../Auth/ContactLoginController.php | 10 +- .../Auth/ContactResetPasswordController.php | 2 +- .../ClientPortal/CreditController.php | 32 ++ .../ClientPortal/DashboardController.php | 71 +--- .../ClientPortal/InvoiceController.php | 64 ++-- .../ClientPortal/PaymentController.php | 53 +-- .../ClientPortal/PaymentMethodController.php | 57 +-- .../ClientPortal/ProfileController.php | 46 +-- .../ClientPortal/QuoteController.php | 114 ++++++ .../RecurringInvoiceController.php | 67 +--- app/Http/Controllers/Controller.php | 13 +- .../ProcessInvoicesInBulkRequest.php | 30 ++ .../ProcessQuotesInBulkRequest.php | 30 ++ .../ClientPortal/ShowCreditRequest.php | 30 ++ .../ClientPortal/ShowQuoteRequest.php | 32 ++ .../ClientPortal/UpdateClientRequest.php | 4 +- app/Http/ViewComposers/PortalComposer.php | 12 +- app/Models/ClientContact.php | 19 +- app/Models/ClientGatewayToken.php | 5 +- app/Models/Invoice.php | 4 + app/Models/Quote.php | 44 ++- app/PaymentDrivers/StripePaymentDriver.php | 66 ++-- config/breadcrumbs.php | 2 +- public/images/svg/activity.svg | 1 + public/images/svg/align-left.svg | 1 + public/images/svg/credit-card.svg | 1 + public/images/svg/file-text.svg | 1 + public/images/svg/file.svg | 1 + public/images/svg/shield.svg | 1 + public/images/svg/user.svg | 1 + .../js/clients/invoices/action-selectors.js | 69 ++++ resources/js/clients/invoices/payment.js | 91 +++++ .../payment_methods/authorize-stripe-card.js | 90 +++++ .../js/clients/quotes/action-selectors.js | 62 +++ resources/js/clients/quotes/approve.js | 51 +++ resources/lang/en/texts.php | 5 + resources/sass/app.scss | 5 + resources/sass/components/badge.scss | 31 ++ resources/sass/components/buttons.scss | 30 +- resources/sass/components/inputs.scss | 6 +- .../portal/default/invoices/payment.blade.php | 10 +- .../portal/ninja2020/auth/login.blade.php | 59 +++ .../auth/passwords/request.blade.php | 44 +++ .../ninja2020/auth/passwords/reset.blade.php | 67 ++++ .../components/breadcrumbs.blade.php | 33 ++ .../general/messages/success.blade.php | 4 + .../general/sidebar/desktop.blade.php | 23 ++ .../general/sidebar/header.blade.php | 65 ++++ .../components/general/sidebar/main.blade.php | 31 ++ .../general/sidebar/mobile.blade.php | 34 ++ .../portal/ninja2020/credits/index.blade.php | 87 +++++ .../portal/ninja2020/credits/show.blade.php | 57 +++ .../ninja2020/dashboard/index.blade.php | 30 +- .../gateways/stripe/add_credit_card.blade.php | 79 ++++ .../invoices/includes/signature.blade.php | 44 +++ .../invoices/includes/terms.blade.php | 49 +++ .../portal/ninja2020/invoices/index.blade.php | 125 ++++++ .../ninja2020/invoices/payment.blade.php | 113 ++++++ .../portal/ninja2020/invoices/show.blade.php | 43 +++ .../portal/ninja2020/layout/app.blade.php | 76 ++++ .../portal/ninja2020/layout/clean.blade.php | 1 + .../payment_methods/create.blade.php | 0 .../includes/modals/removal.blade.php | 52 +++ .../ninja2020/payment_methods/index.blade.php | 109 ++++++ .../ninja2020/payment_methods/show.blade.php | 100 +++++ .../portal/ninja2020/payments/index.blade.php | 85 +++++ .../portal/ninja2020/payments/show.blade.php | 92 +++++ .../portal/ninja2020/profile/index.blade.php | 356 ++++++++++++++++++ .../portal/ninja2020/quotes/approve.blade.php | 90 +++++ .../quotes/includes/signature.blade.php | 44 +++ .../portal/ninja2020/quotes/index.blade.php | 117 ++++++ .../portal/ninja2020/quotes/show.blade.php | 44 +++ .../cancellation/index.blade.php | 74 ++++ .../includes/modals/cancellation.blade.php | 49 +++ .../recurring_invoices/index.blade.php | 85 +++++ .../recurring_invoices/show.blade.php | 87 +++++ .../ninja2020/vendor/pagination.blade.php | 67 ++++ routes/breadcrumbs.php | 86 ++++- routes/client.php | 74 ++-- webpack.mix.js | 19 +- 83 files changed, 3615 insertions(+), 399 deletions(-) create mode 100644 app/Helpers/ClientPortal.php create mode 100644 app/Http/Controllers/ClientPortal/CreditController.php create mode 100644 app/Http/Controllers/ClientPortal/QuoteController.php create mode 100644 app/Http/Requests/ClientPortal/ProcessInvoicesInBulkRequest.php create mode 100644 app/Http/Requests/ClientPortal/ProcessQuotesInBulkRequest.php create mode 100644 app/Http/Requests/ClientPortal/ShowCreditRequest.php create mode 100644 app/Http/Requests/ClientPortal/ShowQuoteRequest.php create mode 100644 public/images/svg/activity.svg create mode 100644 public/images/svg/align-left.svg create mode 100644 public/images/svg/credit-card.svg create mode 100644 public/images/svg/file-text.svg create mode 100644 public/images/svg/file.svg create mode 100644 public/images/svg/shield.svg create mode 100644 public/images/svg/user.svg create mode 100644 resources/js/clients/invoices/action-selectors.js create mode 100644 resources/js/clients/invoices/payment.js create mode 100644 resources/js/clients/payment_methods/authorize-stripe-card.js create mode 100644 resources/js/clients/quotes/action-selectors.js create mode 100644 resources/js/clients/quotes/approve.js create mode 100644 resources/sass/components/badge.scss create mode 100644 resources/views/portal/ninja2020/auth/login.blade.php create mode 100644 resources/views/portal/ninja2020/auth/passwords/request.blade.php create mode 100644 resources/views/portal/ninja2020/auth/passwords/reset.blade.php create mode 100644 resources/views/portal/ninja2020/components/breadcrumbs.blade.php create mode 100644 resources/views/portal/ninja2020/components/general/messages/success.blade.php create mode 100644 resources/views/portal/ninja2020/components/general/sidebar/desktop.blade.php create mode 100644 resources/views/portal/ninja2020/components/general/sidebar/header.blade.php create mode 100644 resources/views/portal/ninja2020/components/general/sidebar/main.blade.php create mode 100644 resources/views/portal/ninja2020/components/general/sidebar/mobile.blade.php create mode 100644 resources/views/portal/ninja2020/credits/index.blade.php create mode 100644 resources/views/portal/ninja2020/credits/show.blade.php create mode 100644 resources/views/portal/ninja2020/gateways/stripe/add_credit_card.blade.php create mode 100644 resources/views/portal/ninja2020/invoices/includes/signature.blade.php create mode 100644 resources/views/portal/ninja2020/invoices/includes/terms.blade.php create mode 100644 resources/views/portal/ninja2020/invoices/index.blade.php create mode 100644 resources/views/portal/ninja2020/invoices/payment.blade.php create mode 100644 resources/views/portal/ninja2020/invoices/show.blade.php create mode 100644 resources/views/portal/ninja2020/layout/app.blade.php create mode 100644 resources/views/portal/ninja2020/payment_methods/create.blade.php create mode 100644 resources/views/portal/ninja2020/payment_methods/includes/modals/removal.blade.php create mode 100644 resources/views/portal/ninja2020/payment_methods/index.blade.php create mode 100644 resources/views/portal/ninja2020/payment_methods/show.blade.php create mode 100644 resources/views/portal/ninja2020/payments/index.blade.php create mode 100644 resources/views/portal/ninja2020/payments/show.blade.php create mode 100644 resources/views/portal/ninja2020/profile/index.blade.php create mode 100644 resources/views/portal/ninja2020/quotes/approve.blade.php create mode 100644 resources/views/portal/ninja2020/quotes/includes/signature.blade.php create mode 100644 resources/views/portal/ninja2020/quotes/index.blade.php create mode 100644 resources/views/portal/ninja2020/quotes/show.blade.php create mode 100644 resources/views/portal/ninja2020/recurring_invoices/cancellation/index.blade.php create mode 100644 resources/views/portal/ninja2020/recurring_invoices/includes/modals/cancellation.blade.php create mode 100644 resources/views/portal/ninja2020/recurring_invoices/index.blade.php create mode 100644 resources/views/portal/ninja2020/recurring_invoices/show.blade.php create mode 100644 resources/views/portal/ninja2020/vendor/pagination.blade.php diff --git a/.gitignore b/.gitignore index 7f98460740..8dc8e56c2d 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,4 @@ storage/migrations # Ignore Tailwind & Javascript build file >2mb without PurgeCSS (development-only) public/css/app.css public/js/app.js +public/js/clients/* diff --git a/app/Helpers/ClientPortal.php b/app/Helpers/ClientPortal.php new file mode 100644 index 0000000000..c3a8600e6f --- /dev/null +++ b/app/Helpers/ClientPortal.php @@ -0,0 +1,51 @@ +render('auth.passwords.request', [ 'title' => 'Client Password Reset', 'passwordEmailRoute' => 'client.password.email' ]); diff --git a/app/Http/Controllers/Auth/ContactLoginController.php b/app/Http/Controllers/Auth/ContactLoginController.php index 0c4aeaf82e..43a4bde989 100644 --- a/app/Http/Controllers/Auth/ContactLoginController.php +++ b/app/Http/Controllers/Auth/ContactLoginController.php @@ -28,12 +28,12 @@ class ContactLoginController extends Controller { $this->middleware('guest:contact', ['except' => ['logout']]); } - + public function showLoginForm() { - return view('portal.default.auth.login'); + return $this->render('auth.login'); } - + public function login(Request $request) { @@ -65,10 +65,10 @@ class ContactLoginController extends Controller if (session()->get('url.intended')) { return redirect(session()->get('url.intended')); } - + return redirect(route('client.dashboard')); } - + public function logout() { Auth::guard('contact')->logout(); diff --git a/app/Http/Controllers/Auth/ContactResetPasswordController.php b/app/Http/Controllers/Auth/ContactResetPasswordController.php index 8ba1d67948..4ac6eeae67 100644 --- a/app/Http/Controllers/Auth/ContactResetPasswordController.php +++ b/app/Http/Controllers/Auth/ContactResetPasswordController.php @@ -60,7 +60,7 @@ class ContactResetPasswordController extends Controller */ public function showResetForm(Request $request, $token = null) { - return view('portal.default.auth.passwords.reset')->with( + return $this->render('auth.passwords.reset')->with( ['token' => $token, 'email' => $request->email] ); } diff --git a/app/Http/Controllers/ClientPortal/CreditController.php b/app/Http/Controllers/ClientPortal/CreditController.php new file mode 100644 index 0000000000..618039a477 --- /dev/null +++ b/app/Http/Controllers/ClientPortal/CreditController.php @@ -0,0 +1,32 @@ +user()->company->credits()->paginate(10); + + return $this->render('credits.index', [ + 'credits' => $credits, + ]); + } + + public function show(ShowCreditRequest $request, Credit $credit) + { + return $this->render('credits.show', [ + 'credit' => $credit, + ]); + } +} diff --git a/app/Http/Controllers/ClientPortal/DashboardController.php b/app/Http/Controllers/ClientPortal/DashboardController.php index 208dd76795..19ff8f57aa 100644 --- a/app/Http/Controllers/ClientPortal/DashboardController.php +++ b/app/Http/Controllers/ClientPortal/DashboardController.php @@ -16,80 +16,11 @@ use App\Http\Controllers\Controller; class DashboardController extends Controller { - /** - * Display a listing of the resource. - * * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View */ public function index() { - return view('dashboard.index'); - } - - /** - * Show the form for creating a new resource. - * - * @return \Illuminate\Http\Response - */ - public function create() - { - // - } - - /** - * Store a newly created resource in storage. - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\Response - */ - public function store(Request $request) - { - // - } - - /** - * Display the specified resource. - * - * @param int $id - * @return \Illuminate\Http\Response - */ - public function show($id) - { - // - } - - /** - * Show the form for editing the specified resource. - * - * @param int $id - * @return \Illuminate\Http\Response - */ - public function edit($id) - { - // - } - - /** - * Update the specified resource in storage. - * - * @param \Illuminate\Http\Request $request - * @param int $id - * @return \Illuminate\Http\Response - */ - public function update(Request $request, $id) - { - // - } - - /** - * Remove the specified resource from storage. - * - * @param int $id - * @return \Illuminate\Http\Response - */ - public function destroy($id) - { - // + return $this->render('dashboard.index'); } } diff --git a/app/Http/Controllers/ClientPortal/InvoiceController.php b/app/Http/Controllers/ClientPortal/InvoiceController.php index 48572b54a0..95b96ea83d 100644 --- a/app/Http/Controllers/ClientPortal/InvoiceController.php +++ b/app/Http/Controllers/ClientPortal/InvoiceController.php @@ -13,21 +13,18 @@ namespace App\Http\Controllers\ClientPortal; use App\Filters\InvoiceFilters; use App\Http\Controllers\Controller; +use App\Http\Requests\ClientPortal\ProcessInvoicesInBulkRequest; use App\Http\Requests\ClientPortal\ShowInvoiceRequest; +use App\Http\Requests\Request; use App\Jobs\Entity\ActionEntity; use App\Models\Invoice; -use App\Repositories\BaseRepository; use App\Utils\Number; use App\Utils\Traits\MakesDates; use App\Utils\Traits\MakesHash; -use Illuminate\Http\Request; -use Illuminate\Support\Facades\File; -use Illuminate\Support\Facades\Log; -use Illuminate\Support\Facades\Storage; -use Yajra\DataTables\Facades\DataTables; use Yajra\DataTables\Html\Builder; use ZipStream\Option\Archive; use ZipStream\ZipStream; +use function GuzzleHttp\Promise\all; /** * Class InvoiceController @@ -38,42 +35,20 @@ class InvoiceController extends Controller { use MakesHash; use MakesDates; + /** * Show the list of Invoices * - * @param \App\Filters\InvoiceFilters $filters The filters + * @param \App\Filters\InvoiceFilters $filters The filters * - * @return \Illuminate\Http\Response + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + * @throws \Exception */ public function index(InvoiceFilters $filters, Builder $builder) { - $invoices = Invoice::filter($filters)->with('client', 'client.country'); + $invoices = auth()->user()->client->company->invoices()->paginate(10); - if (request()->ajax()) { - return DataTables::of($invoices)->addColumn('action', function ($invoice) { - return $this->buildClientButtons($invoice); - }) - ->addColumn('checkbox', function ($invoice) { - return ''; - }) - ->editColumn('status_id', function ($invoice) { - return Invoice::badgeForStatus($invoice->status); - })->editColumn('date', function ($invoice) { - return $this->formatDate($invoice->date, $invoice->client->date_format()); - })->editColumn('due_date', function ($invoice) { - return $this->formatDate($invoice->due_date, $invoice->client->date_format()); - })->editColumn('balance', function ($invoice) { - return Number::formatMoney($invoice->balance, $invoice->client); - })->editColumn('amount', function ($invoice) { - return Number::formatMoney($invoice->amount, $invoice->client); - }) - ->rawColumns(['checkbox', 'action', 'status_id']) - ->make(true); - } - - $data['html'] = $builder; - - return view('portal.default.invoices.index', $data); + return $this->render('invoices.index', ['invoices' => $invoices]); } private function buildClientButtons($invoice) @@ -96,31 +71,34 @@ class InvoiceController extends Controller * * @param \App\Models\Invoice $invoice The invoice * - * @return \Illuminate\Http\Response + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View */ public function show(ShowInvoiceRequest $request, Invoice $invoice) { $data = [ 'invoice' => $invoice, ]; - - return view('portal.default.invoices.show', $data); + + return $this->render('invoices.show', $data); } /** * Pay one or more invoices * - * @return View + * @param ProcessInvoicesInBulkRequest $request + * @return mixed */ - public function bulk() + public function bulk(ProcessInvoicesInBulkRequest $request) { - $transformed_ids = $this->transformKeys(explode(",", request()->input('hashed_ids'))); + $transformed_ids = $this->transformKeys($request->invoices); if (request()->input('action') == 'payment') { - return $this->makePayment($transformed_ids); + return $this->makePayment((array)$transformed_ids); } elseif (request()->input('action') == 'download') { - return $this->downloadInvoicePDF($transformed_ids); + return $this->downloadInvoicePDF((array)$transformed_ids); } + + return redirect()->back(); } @@ -159,7 +137,7 @@ class InvoiceController extends Controller 'total' => $total, ]; - return view('portal.default.invoices.payment', $data); + return $this->render('invoices.payment', $data); } private function downloadInvoicePDF(array $ids) diff --git a/app/Http/Controllers/ClientPortal/PaymentController.php b/app/Http/Controllers/ClientPortal/PaymentController.php index cf94d8e79f..8b5bd8b56f 100644 --- a/app/Http/Controllers/ClientPortal/PaymentController.php +++ b/app/Http/Controllers/ClientPortal/PaymentController.php @@ -38,54 +38,35 @@ class PaymentController extends Controller /** * Show the list of Invoices * - * @param \App\Filters\InvoiceFilters $filters The filters + * @param PaymentFilters $filters The filters * - * @return \Illuminate\Http\Response + * @param Builder $builder + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View */ public function index(PaymentFilters $filters, Builder $builder) { //$payments = Payment::filter($filters); - $payments = Payment::with('type', 'client'); + $payments = Payment::with('type', 'client')->paginate(10); - if (request()->ajax()) { - return DataTables::of($payments)->addColumn('action', function ($payment) { - return ''.ctrans('texts.view').''; - })->editColumn('type_id', function ($payment) { - return $payment->type->name; - }) - ->editColumn('status_id', function ($payment) { - return Payment::badgeForStatus($payment->status_id); - }) - ->editColumn('date', function ($payment) { - //return $payment->date; - return $payment->formatDate($payment->date, $payment->client->date_format()); - }) - ->editColumn('amount', function ($payment) { - return Number::formatMoney($payment->amount, $payment->client); - }) - ->rawColumns(['action', 'status_id','type_id']) - ->make(true); - } - - $data['html'] = $builder; - - return view('portal.default.payments.index', $data); + return $this->render('payments.index', [ + 'payments' => $payments, + ]); } /** * Display the specified resource. * - * @param \App\Models\Invoice $invoice The invoice - * - * @return \Illuminate\Http\Response + * @param Request $request + * @param Payment $payment + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View */ public function show(Request $request, Payment $payment) { $payment->load('invoices'); - $data['payment'] = $payment; - - return view('portal.default.payments.show', $data); + return $this->render('payments.show', [ + 'payment' => $payment, + ]); } /** @@ -111,7 +92,7 @@ class PaymentController extends Controller if ($invoices->count() == 0) { return back()->with(['warning' => 'No payable invoices selected']); } - + $invoices->map(function ($invoice) { $invoice->balance = Number::formatMoney($invoice->balance, $invoice->client); $invoice->due_date = $this->formatDate($invoice->due_date, $invoice->client->date_format()); @@ -127,7 +108,7 @@ class PaymentController extends Controller //if there is a gateway fee, now is the time to calculate it //and add it to the invoice - + $data = [ 'invoices' => $invoices, 'amount' => $amount, @@ -137,8 +118,8 @@ class PaymentController extends Controller 'payment_method_id' => $payment_method_id, 'hashed_ids' => explode(",", request()->input('hashed_ids')), ]; - - + + return $gateway->driver(auth()->user()->client)->processPaymentView($data); } diff --git a/app/Http/Controllers/ClientPortal/PaymentMethodController.php b/app/Http/Controllers/ClientPortal/PaymentMethodController.php index fb837de64e..3432732b98 100644 --- a/app/Http/Controllers/ClientPortal/PaymentMethodController.php +++ b/app/Http/Controllers/ClientPortal/PaymentMethodController.php @@ -16,9 +16,7 @@ use App\Http\Controllers\Controller; use App\Models\ClientGatewayToken; use App\Utils\Traits\MakesDates; use Illuminate\Http\Request; -use Illuminate\Http\Response; use Illuminate\Support\Facades\Log; -use Yajra\DataTables\Facades\DataTables; use Yajra\DataTables\Html\Builder; class PaymentMethodController extends Controller @@ -28,49 +26,18 @@ class PaymentMethodController extends Controller /** * Display a listing of the resource. * - * @return \Illuminate\Http\Response + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + * @throws \Exception */ public function index(Builder $builder) { - $payment_methods = ClientGatewayToken::whereClientId(auth()->user()->client->id); - $payment_methods->with('gateway_type'); + $payment_methods = ClientGatewayToken::with('gateway_type') + ->whereClientId(auth()->user()->client->id) + ->paginate(10); - if (request()->ajax()) { - return DataTables::of($payment_methods)->addColumn('action', function ($payment_method) { - return '' . ctrans('texts.view') . ''; - }) - ->editColumn('gateway_type_id', function ($payment_method) { - return ctrans("texts.{$payment_method->gateway_type->alias}"); - })->editColumn('created_at', function ($payment_method) { - return $this->formatDateTimestamp($payment_method->created_at, auth()->user()->client->date_format()); - })->editColumn('is_default', function ($payment_method) { - return $payment_method->is_default ? ctrans('texts.default') : ''; - })->editColumn('meta', function ($payment_method) { - if (isset($payment_method->meta->exp_month) && isset($payment_method->meta->exp_year)) { - return "{$payment_method->meta->exp_month}/{$payment_method->meta->exp_year}"; - } else { - return ""; - } - })->addColumn('last4', function ($payment_method) { - if (isset($payment_method->meta->last4)) { - return $payment_method->meta->last4; - } else { - return ""; - } - })->addColumn('brand', function ($payment_method) { - if (isset($payment_method->meta->brand)) { - return $payment_method->meta->brand; - } else { - return ""; - } - }) - ->rawColumns(['action', 'status_id', 'last4', 'brand']) - ->make(true); - } - - $data['html'] = $builder; - - return view('portal.default.payment_methods.index', $data); + return $this->render('payment_methods.index', [ + 'payment_methods' => $payment_methods, + ]); } /** @@ -112,7 +79,9 @@ class PaymentMethodController extends Controller */ public function show(ClientGatewayToken $payment_method) { - return view('portal.default.payment_methods.show', compact('payment_method')); + return $this->render('payment_methods.show', [ + 'payment_method' => $payment_method, + ]); } /** @@ -154,6 +123,8 @@ class PaymentMethodController extends Controller return back(); } - return redirect()->route('client.payment_methods.index'); + return redirect() + ->route('client.payment_methods.index') + ->withSuccess('Payment method has been successfully removed.'); } } diff --git a/app/Http/Controllers/ClientPortal/ProfileController.php b/app/Http/Controllers/ClientPortal/ProfileController.php index 2e80fa42c6..f33f6a9671 100644 --- a/app/Http/Controllers/ClientPortal/ProfileController.php +++ b/app/Http/Controllers/ClientPortal/ProfileController.php @@ -22,59 +22,39 @@ use Illuminate\Support\Facades\Log; class ProfileController extends Controller { - - /** - * Display the specified resource. - * - * @param int $id - * @return \Illuminate\Http\Response - */ - public function show($id) - { - // - } - /** * Show the form for editing the specified resource. * - * @param int $id - * @return \Illuminate\Http\Response + * @param ClientContact $client_contact + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View */ public function edit(ClientContact $client_contact) { - /* Dropzone configuration */ - $data = [ - 'params' => [ - 'is_avatar' => true, - ], - 'url' => '/client/document', - 'multi_upload' => false, - ]; - - return view('portal.default.profile.index', $data); + return $this->render('profile.index'); } /** * Update the specified resource in storage. * - * @param \Illuminate\Http\Request $request - * @param int $id - * @return \Illuminate\Http\Response + * @param UpdateContactRequest $request + * @param ClientContact $client_contact + * @return \Illuminate\Http\RedirectResponse */ public function update(UpdateContactRequest $request, ClientContact $client_contact) { $client_contact->fill($request->all()); - //update password if needed - if ($request->input('password')) { - $client_contact->password = Hash::make($request->input('password')); + if ($request->has('password')) { + $client_contact->password = encrypt($request->password); } $client_contact->save(); // auth()->user()->fresh(); - return back(); + return back()->withSuccess( + ctrans('texts.profile_updated_successfully') + ); } public function updateClient(UpdateClientRequest $request, ClientContact $client_contact) @@ -93,6 +73,8 @@ class ProfileController extends Controller $client->fill($request->all()); $client->save(); - return back(); + return back()->withSuccess( + ctrans('texts.profile_updated_successfully') + ); } } diff --git a/app/Http/Controllers/ClientPortal/QuoteController.php b/app/Http/Controllers/ClientPortal/QuoteController.php new file mode 100644 index 0000000000..549d8f71fe --- /dev/null +++ b/app/Http/Controllers/ClientPortal/QuoteController.php @@ -0,0 +1,114 @@ +user()->company->quotes()->paginate(10); + + return $this->render('quotes.index', [ + 'quotes' => $quotes, + ]); + } + + + /** + * Display the specified resource. + * + * @param ShowQuoteRequest $request + * @param Quote $quote + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + */ + public function show(ShowQuoteRequest $request, Quote $quote) + { + return $this->render('quotes.show', [ + 'quote' => $quote, + ]); + } + + public function bulk(ProcessQuotesInBulkRequest $request) + { + $transformed_ids = $this->transformKeys($request->quotes); + + if ($request->action == 'download') { + return $this->downloadQuotePdf((array)$transformed_ids); + } + + if ($request->action = 'approve') { + return $this->approve((array)$transformed_ids, $request->has('process')); + } + + return back(); + } + + protected function downloadQuotePdf(array $ids) + { + $quotes = Quote::whereIn('id', $ids) + ->whereClientId(auth()->user()->client->id) + ->get(); + + if (!$quotes || $quotes->count() == 0) { + return; + } + + if ($quotes->count() == 1) { + return response()->download(public_path($quotes->first()->pdf_file_path())); + } + + # enable output of HTTP headers + $options = new Archive(); + $options->setSendHttpHeaders(true); + + # create a new zipstream object + $zip = new ZipStream(date('Y-m-d') . '_' . str_replace(' ', '_', trans('texts.invoices')) . ".zip", $options); + + foreach ($quotes as $quote) { + $zip->addFileFromPath(basename($quote->pdf_file_path()), public_path($quote->pdf_file_path())); + } + + # finish the zip stream + $zip->finish(); + } + + protected function approve(array $ids, $process = false) + { + $quotes = Quote::whereIn('id', $ids) + ->whereClientId(auth()->user()->client->id) + ->get(); + + if (!$quotes || $quotes->count() == 0) { + return redirect()->route('client.quotes.index'); + } + + if ($process) { + + foreach ($quotes as $quote) { + $quote->service()->approve()->save(); + } + + return route('client.quotes.index')->withSuccess('Quote(s) approved successfully.'); + } + + return $this->render('quotes.approve', [ + 'quotes' => $quotes, + ]); + } +} diff --git a/app/Http/Controllers/ClientPortal/RecurringInvoiceController.php b/app/Http/Controllers/ClientPortal/RecurringInvoiceController.php index 52407639e1..4ffbf270b2 100644 --- a/app/Http/Controllers/ClientPortal/RecurringInvoiceController.php +++ b/app/Http/Controllers/ClientPortal/RecurringInvoiceController.php @@ -35,75 +35,48 @@ class RecurringInvoiceController extends Controller { use MakesHash; use MakesDates; + /** - * Show the list of Invoices + * Show the list of recurring invoices. * - * @param \App\Filters\InvoiceFilters $filters The filters - * - * @return \Illuminate\Http\Response + * @param Builder $builder + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View */ public function index(Builder $builder) { $invoices = RecurringInvoice::whereClientId(auth()->user()->client->id) - ->whereIn('status_id', [RecurringInvoice::STATUS_PENDING, RecurringInvoice::STATUS_ACTIVE, RecurringInvoice::STATUS_COMPLETED]) - ->orderBy('status_id', 'asc') - ->with('client') - ->get(); + ->whereIn('status_id', [RecurringInvoice::STATUS_PENDING, RecurringInvoice::STATUS_ACTIVE, RecurringInvoice::STATUS_COMPLETED]) + ->orderBy('status_id', 'asc') + ->with('client') + ->paginate(10); - if (request()->ajax()) { - return DataTables::of($invoices)->addColumn('action', function ($invoice) { - return ''.ctrans('texts.view').''; - })->addColumn('frequency_id', function ($invoice) { - return RecurringInvoice::frequencyForKey($invoice->frequency_id); - }) - ->editColumn('status_id', function ($invoice) { - return RecurringInvoice::badgeForStatus($invoice->status); - }) - ->editColumn('start_date', function ($invoice) { - return $this->formatDate($invoice->date, $invoice->client->date_format()); - }) - ->editColumn('next_send_date', function ($invoice) { - return $this->formatDate($invoice->next_send_date, $invoice->client->date_format()); - }) - ->editColumn('amount', function ($invoice) { - return Number::formatMoney($invoice->amount, $invoice->client); - }) - ->rawColumns(['action', 'status_id']) - ->make(true); - } - - $data['html'] = $builder; - - return view('portal.default.recurring_invoices.index', $data); + return $this->render('recurring_invoices.index', [ + 'invoices' => $invoices, + ]); } /** - * Display the specified resource. + * Display the recurring invoice. * - * @param \App\Models\Invoice $invoice The invoice - * - * @return \Illuminate\Http\Response + * @param ShowRecurringInvoiceRequest $request + * @param RecurringInvoice $recurring_invoice + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View */ public function show(ShowRecurringInvoiceRequest $request, RecurringInvoice $recurring_invoice) { - $data = [ + return $this->render('recurring_invoices.show', [ 'invoice' => $recurring_invoice->load('invoices'), - ]; - - return view('portal.default.recurring_invoices.show', $data); + ]); } - public function requestCancellation(Request $request, RecurringInvoice $recurring_invoice) { - $data = [ - 'invoice' => $recurring_invoice - ]; - //todo double check the user is able to request a cancellation //can add locale specific by chaining ->locale(); $recurring_invoice->user->notify(new ClientContactRequestCancellation($recurring_invoice, auth()->user())); - return view('portal.default.recurring_invoices.request_cancellation', $data); + return $this->render('recurring_invoices.cancellation.index', [ + 'invoice' => $recurring_invoice, + ]); } } diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 5a68ef46b3..2d50312520 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -22,6 +22,8 @@ class Controller extends BaseController use AuthorizesRequests, DispatchesJobs, ValidatesRequests; /** + * Proxy method for rendering views. + * * @param string $path * @param array $options * @@ -29,15 +31,6 @@ class Controller extends BaseController */ public function render(string $path, array $options = []) { - $theme = array_key_exists('theme', $options) ? $options['theme'] : 'ninja2020'; - - if (array_key_exists('root', $options)) { - return view( - sprintf('%s.%s.%s', $options['root'], $theme, $path), - $options - ); - } - - return view("portal.$theme.$path", $options); + return render($path, $options); } } diff --git a/app/Http/Requests/ClientPortal/ProcessInvoicesInBulkRequest.php b/app/Http/Requests/ClientPortal/ProcessInvoicesInBulkRequest.php new file mode 100644 index 0000000000..9a41f91069 --- /dev/null +++ b/app/Http/Requests/ClientPortal/ProcessInvoicesInBulkRequest.php @@ -0,0 +1,30 @@ + ['array'], + ]; + } +} diff --git a/app/Http/Requests/ClientPortal/ProcessQuotesInBulkRequest.php b/app/Http/Requests/ClientPortal/ProcessQuotesInBulkRequest.php new file mode 100644 index 0000000000..f99e60880f --- /dev/null +++ b/app/Http/Requests/ClientPortal/ProcessQuotesInBulkRequest.php @@ -0,0 +1,30 @@ + ['array'], + ]; + } +} diff --git a/app/Http/Requests/ClientPortal/ShowCreditRequest.php b/app/Http/Requests/ClientPortal/ShowCreditRequest.php new file mode 100644 index 0000000000..0de22c7e9e --- /dev/null +++ b/app/Http/Requests/ClientPortal/ShowCreditRequest.php @@ -0,0 +1,30 @@ +user()->client->id === $this->quote->client_id; + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + return [ + // + ]; + } +} diff --git a/app/Http/Requests/ClientPortal/UpdateClientRequest.php b/app/Http/Requests/ClientPortal/UpdateClientRequest.php index de0b8c9c74..069d93c989 100644 --- a/app/Http/Requests/ClientPortal/UpdateClientRequest.php +++ b/app/Http/Requests/ClientPortal/UpdateClientRequest.php @@ -17,7 +17,7 @@ use App\Utils\Traits\MakesHash; class UpdateClientRequest extends Request { use MakesHash; - + /** * Determine if the user is authorized to make this request. * @@ -32,7 +32,7 @@ class UpdateClientRequest extends Request public function rules() { return [ - 'name' => 'required', + 'name' => 'sometimes|required', 'file' => 'sometimes|nullable|max:100000|mimes:png,svg,jpeg,gif,jpg,bmp' ]; } diff --git a/app/Http/ViewComposers/PortalComposer.php b/app/Http/ViewComposers/PortalComposer.php index 1ccce4925b..812fb958a1 100644 --- a/app/Http/ViewComposers/PortalComposer.php +++ b/app/Http/ViewComposers/PortalComposer.php @@ -58,11 +58,13 @@ class PortalComposer { $data = []; - $data[] = [ 'title' => ctrans('texts.dashboard'), 'url' => 'client.dashboard', 'icon' => 'fa fa-tachometer fa-fw fa-2x']; - $data[] = [ 'title' => ctrans('texts.invoices'), 'url' => 'client.invoices.index', 'icon' => 'fa fa-file-pdf-o fa-fw fa-2x']; - $data[] = [ 'title' => ctrans('texts.recurring_invoices'), 'url' => 'client.recurring_invoices.index', 'icon' => 'fa fa-files-o fa-fw fa-2x']; - $data[] = [ 'title' => ctrans('texts.payments'), 'url' => 'client.payments.index', 'icon' => 'fa fa-credit-card fa-fw fa-2x']; - $data[] = [ 'title' => ctrans('texts.payment_methods'), 'url' => 'client.payment_methods.index', 'icon' => 'fa fa-cc-stripe fa-fw fa-2x']; + $data[] = [ 'title' => ctrans('texts.dashboard'), 'url' => 'client.dashboard', 'icon' => 'activity']; + $data[] = [ 'title' => ctrans('texts.invoices'), 'url' => 'client.invoices.index', 'icon' => 'file-text']; + $data[] = [ 'title' => ctrans('texts.recurring_invoices'), 'url' => 'client.recurring_invoices.index', 'icon' => 'file']; + $data[] = [ 'title' => ctrans('texts.payments'), 'url' => 'client.payments.index', 'icon' => 'credit-card']; + $data[] = [ 'title' => ctrans('texts.payment_methods'), 'url' => 'client.payment_methods.index', 'icon' => 'shield']; + $data[] = [ 'title' => ctrans('texts.quotes'), 'url' => 'client.quotes.index', 'icon' => 'align-left']; + $data[] = [ 'title' => ctrans('texts.credits'), 'url' => 'client.credits.index', 'icon' => 'credit-card']; return $data; } diff --git a/app/Models/ClientContact.php b/app/Models/ClientContact.php index 0388b17750..bdc9bda15c 100644 --- a/app/Models/ClientContact.php +++ b/app/Models/ClientContact.php @@ -45,7 +45,7 @@ class ClientContact extends Authenticatable implements HasLocalePreference protected $dates = [ 'deleted_at' ]; - + protected $appends = [ 'hashed_id' ]; @@ -85,12 +85,12 @@ class ClientContact extends Authenticatable implements HasLocalePreference 'email', 'is_primary', ]; - + public function getHashedIdAttribute() { return $this->encodePrimaryKey($this->id); } - + /**/ public function getRouteKeyName() { @@ -139,7 +139,7 @@ class ClientContact extends Authenticatable implements HasLocalePreference public function preferredLocale() { $languages = Cache::get('languages'); - + return $languages->filter(function ($item) { return $item->id == $this->client->getSetting('language_id'); })->first()->locale; @@ -162,4 +162,15 @@ class ClientContact extends Authenticatable implements HasLocalePreference ->withTrashed() ->where('id', $this->decodePrimaryKey($value))->firstOrFail(); } + + /** + * @return mixed|string + */ + public function avatar() + { + if($this->avatar) + return $this->avatar; + + return asset('images/svg/user.svg'); + } } diff --git a/app/Models/ClientGatewayToken.php b/app/Models/ClientGatewayToken.php index ec823beb05..25eb936e17 100644 --- a/app/Models/ClientGatewayToken.php +++ b/app/Models/ClientGatewayToken.php @@ -16,9 +16,12 @@ use App\Models\Company; use App\Models\CompanyGateway; use App\Models\GatewayType; use App\Models\User; +use App\Utils\Traits\MakesDates; class ClientGatewayToken extends BaseModel { + use MakesDates; + protected $casts = [ 'meta' => 'object', 'updated_at' => 'timestamp', @@ -50,7 +53,7 @@ class ClientGatewayToken extends BaseModel { return $this->hasOne(User::class)->withTrashed(); } - + /** * Retrieve the model for a bound value. * diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index 8560e8cc3e..f2debf5f29 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -114,6 +114,10 @@ class Invoice extends BaseModel 'status' ]; + protected $dates = [ + 'date', + ]; + const STATUS_DRAFT = 1; const STATUS_SENT = 2; const STATUS_PARTIAL = 3; diff --git a/app/Models/Quote.php b/app/Models/Quote.php index 6fed6f116a..3875a5d4b8 100644 --- a/app/Models/Quote.php +++ b/app/Models/Quote.php @@ -17,6 +17,7 @@ use App\Jobs\Invoice\CreateInvoicePdf; use App\Jobs\Quote\CreateQuotePdf; use App\Models\Filterable; use App\Services\Quote\QuoteService; +use App\Utils\Traits\MakesDates; use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesInvoiceValues; use App\Utils\Traits\MakesReminders; @@ -29,6 +30,7 @@ use Laracasts\Presenter\PresentableTrait; class Quote extends BaseModel { use MakesHash; + use MakesDates; use Filterable; use SoftDeletes; use MakesReminders; @@ -150,7 +152,8 @@ class Quote extends BaseModel { $storage_path = 'storage/' . $this->client->quote_filepath() . $this->number . '.pdf'; - if (Storage::exists($storage_path)) { + if (Storage::exists($storage_path)) + { return $storage_path; } @@ -162,4 +165,43 @@ class Quote extends BaseModel return $storage_path; } + + /** + * @param int $status + * @return string + */ + public static function badgeForStatus(int $status) + { + switch ($status) { + case Quote::STATUS_DRAFT: + return '
' . ctrans('texts.draft') . '
'; + break; + case Quote::STATUS_SENT: + return '
' . ctrans('texts.sent') . '
'; + break; + case Quote::STATUS_APPROVED: + return '
' . ctrans('texts.approved') . '
'; + break; + case Quote::STATUS_EXPIRED: + return '
' . ctrans('texts.expired') . '
'; + break; + default: + # code... + break; + } + } + + /** + * Check if the quote has been approved. + * + * @return bool + */ + public function isApproved() + { + if($this->status_id === $this::STATUS_APPROVED) { + return true; + } + + return false; + } } diff --git a/app/PaymentDrivers/StripePaymentDriver.php b/app/PaymentDrivers/StripePaymentDriver.php index f6dea8a96e..aa760fce9f 100644 --- a/app/PaymentDrivers/StripePaymentDriver.php +++ b/app/PaymentDrivers/StripePaymentDriver.php @@ -71,7 +71,7 @@ class StripePaymentDriver extends BasePaymentDriver GatewayType::CREDIT_CARD, //GatewayType::TOKEN, ]; - + if ($this->company_gateway->getSofortEnabled() && $this->invitation && $this->client() && isset($this->client()->country) && in_array($this->client()->country, ['AUT', 'BEL', 'DEU', 'ITA', 'NLD', 'ESP'])) { $types[] = GatewayType::SOFORT; } @@ -83,19 +83,19 @@ class StripePaymentDriver extends BasePaymentDriver if ($this->company_gateway->getSepaEnabled()) { $types[] = GatewayType::SEPA; } - + if ($this->company_gateway->getBitcoinEnabled()) { $types[] = GatewayType::CRYPTO; } - + if ($this->company_gateway->getAlipayEnabled()) { $types[] = GatewayType::ALIPAY; } - + if ($this->company_gateway->getApplePayEnabled()) { $types[] = GatewayType::APPLE_PAY; } - + return $types; } @@ -104,55 +104,48 @@ class StripePaymentDriver extends BasePaymentDriver { switch ($gateway_type_id) { case GatewayType::CREDIT_CARD: - return 'portal.default.gateways.stripe.credit_card'; - break; case GatewayType::TOKEN: - return 'portal.default.gateways.stripe.credit_card'; + return 'gateways.stripe.credit_card'; break; case GatewayType::SOFORT: - return 'portal.default.gateways.stripe.sofort'; + return 'gateways.stripe.sofort'; break; case GatewayType::BANK_TRANSFER: - return 'portal.default.gateways.stripe.ach'; + return 'gateways.stripe.ach'; break; case GatewayType::SEPA: - return 'portal.default.gateways.stripe.sepa'; + return 'gateways.stripe.sepa'; break; case GatewayType::CRYPTO: - return 'portal.default.gateways.stripe.other'; - break; case GatewayType::ALIPAY: - return 'portal.default.gateways.stripe.other'; - break; case GatewayType::APPLE_PAY: - return 'portal.default.gateways.stripe.other'; + return 'gateways.stripe.other'; break; default: - # code... break; } } /** + * Authorises a credit card for future use. * - * Authorises a credit card for future use * @param array $data Array of variables needed for the view - * - * @return view The gateway specific partial to be rendered - * + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View */ public function authorizeCreditCardView(array $data) { $intent['intent'] = $this->getSetupIntent(); - return view('portal.default.gateways.stripe.add_credit_card', array_merge($data, $intent)); + return render('gateways.stripe.add_credit_card', array_merge($data, $intent)); } /** - * Processes the gateway response for credti card authorization - * @param Request $request The returning request object + * Processes the gateway response for credit card authorization. + * + * @param Request $request The returning request object * @return view Returns the user to payment methods screen. + * @throws \Stripe\Exception\ApiErrorException */ public function authorizeCreditCardResponse($request) { @@ -202,18 +195,11 @@ class StripePaymentDriver extends BasePaymentDriver } /** - * Processes the payment with this gateway + * Process the payment with gateway. * - * @var invoices - * @var amount - * @var fee - * @var amount_with_fee - * @var token - * @var payment_method_id - * @var hashed_ids - * - * @param array $data variables required to build payment page - * @return view Gateway and payment method specific view + * @param array $data + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View|void + * @throws \Exception */ public function processPaymentView(array $data) { @@ -237,7 +223,7 @@ class StripePaymentDriver extends BasePaymentDriver $data['gateway'] = $this; - return view($this->viewForType($data['payment_method_id']), $data); + return render($this->viewForType($data['payment_method_id']), $data); } /** @@ -292,7 +278,7 @@ class StripePaymentDriver extends BasePaymentDriver * requires_payment_method * */ - + if ($this->getContact()) { $client_contact = $this->getContact(); } else { @@ -322,7 +308,7 @@ class StripePaymentDriver extends BasePaymentDriver if ($save_card == 'true') { $stripe_payment_method->attach(['customer' => $customer]); - + $cgt = new ClientGatewayToken; $cgt->company_id = $this->client->company->id; $cgt->client_id = $this->client->id; @@ -340,7 +326,7 @@ class StripePaymentDriver extends BasePaymentDriver $cgt->save(); } } - + //todo need to fix this to support payment types other than credit card.... sepa etc etc if (!$payment_type) { $payment_type = PaymentType::CREDIT_CARD_OTHER; @@ -363,7 +349,7 @@ class StripePaymentDriver extends BasePaymentDriver $payment->service()->UpdateInvoicePayment(); //UpdateInvoicePayment::dispatchNow($payment, $payment->company); - + SystemLogger::dispatch( [ 'server_response' => $payment_intent, diff --git a/config/breadcrumbs.php b/config/breadcrumbs.php index 08902ab83d..742db9deb2 100644 --- a/config/breadcrumbs.php +++ b/config/breadcrumbs.php @@ -22,7 +22,7 @@ return [ | */ - 'view' => 'breadcrumbs::bootstrap4', + 'view' => 'portal.ninja2020.components.breadcrumbs', /* |-------------------------------------------------------------------------- diff --git a/public/images/svg/activity.svg b/public/images/svg/activity.svg new file mode 100644 index 0000000000..ad2574938f --- /dev/null +++ b/public/images/svg/activity.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/svg/align-left.svg b/public/images/svg/align-left.svg new file mode 100644 index 0000000000..97a69a8eaa --- /dev/null +++ b/public/images/svg/align-left.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/svg/credit-card.svg b/public/images/svg/credit-card.svg new file mode 100644 index 0000000000..e89a77fb1f --- /dev/null +++ b/public/images/svg/credit-card.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/svg/file-text.svg b/public/images/svg/file-text.svg new file mode 100644 index 0000000000..d988a9438a --- /dev/null +++ b/public/images/svg/file-text.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/svg/file.svg b/public/images/svg/file.svg new file mode 100644 index 0000000000..53e959a2e0 --- /dev/null +++ b/public/images/svg/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/svg/shield.svg b/public/images/svg/shield.svg new file mode 100644 index 0000000000..469f892d3c --- /dev/null +++ b/public/images/svg/shield.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/svg/user.svg b/public/images/svg/user.svg new file mode 100644 index 0000000000..4941bd2fa9 --- /dev/null +++ b/public/images/svg/user.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/js/clients/invoices/action-selectors.js b/resources/js/clients/invoices/action-selectors.js new file mode 100644 index 0000000000..d4d7c95fa0 --- /dev/null +++ b/resources/js/clients/invoices/action-selectors.js @@ -0,0 +1,69 @@ +/** + * Invoice Ninja (https://invoiceninja.com) + * + * @link https://github.com/invoiceninja/invoiceninja source repository + * + * @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com) + * + * @license https://opensource.org/licenses/AAL + */ + +class ActionSelectors { + constructor() { + this.parentElement = document.querySelector(".form-check-parent"); + this.parentForm = document.getElementById("bulkActions"); + } + + watchCheckboxes(parentElement) { + document.querySelectorAll(".form-check-child").forEach(child => { + if (parentElement.checked) { + child.checked = parentElement.checked; + this.processChildItem(child, document.getElementById("bulkActions")); + } else { + child.checked = false; + document + .querySelectorAll(".child-hidden-input") + .forEach(element => element.remove()); + } + }); + } + + processChildItem(element, parent, options = {}) { + if (options.hasOwnProperty("single")) { + document + .querySelectorAll(".child-hidden-input") + .forEach(element => element.remove()); + } + + let _temp = document.createElement("INPUT"); + + _temp.setAttribute("name", "invoices[]"); + _temp.setAttribute("value", element.dataset.value); + _temp.setAttribute("class", "child-hidden-input"); + _temp.hidden = true; + + parent.append(_temp); + } + + handle() { + this.parentElement.addEventListener("click", () => { + this.watchCheckboxes(this.parentElement); + }); + + for (let child of document.querySelectorAll(".pay-now-button")) { + child.addEventListener("click", () => { + this.processChildItem(child, this.parentForm, { single: true }); + document.querySelector('button[value="payment"]').click(); + }); + } + + for (let child of document.querySelectorAll(".form-check-child")) { + child.addEventListener("click", () => { + this.processChildItem(child, this.parentForm); + }); + } + } +} + +/** @handle **/ +new ActionSelectors().handle(); diff --git a/resources/js/clients/invoices/payment.js b/resources/js/clients/invoices/payment.js new file mode 100644 index 0000000000..432d2ebe65 --- /dev/null +++ b/resources/js/clients/invoices/payment.js @@ -0,0 +1,91 @@ +/** + * Invoice Ninja (https://invoiceninja.com) + * + * @link https://github.com/invoiceninja/invoiceninja source repository + * + * @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com) + * + * @license https://opensource.org/licenses/AAL + */ + +class Payment { + constructor(displayTerms, displaySignature) { + this.shouldDisplayTerms = displayTerms; + this.shouldDisplaySignature = displaySignature; + this.termsAccepted = false; + } + + handleMethodSelect(element) { + + if (this.shouldDisplaySignature && !this.shouldDisplayTerms) { + this.displayTerms(); + + document.getElementById('accept-terms-button').addEventListener('click', () => { + this.termsAccepted = true; + this.submitForm(); + }); + } + + if (!this.shouldDisplaySignature && this.shouldDisplayTerms) { + this.displaySignature(); + + document.getElementById('signature-next-step').addEventListener('click', () => { + this.submitForm(); + }); + } + + if (this.shouldDisplaySignature && this.shouldDisplayTerms) { + this.displaySignature(); + + document.getElementById('signature-next-step').addEventListener('click', () => { + this.displayTerms(); + + document.getElementById('accept-terms-button').addEventListener('click', () => { + this.termsAccepted = true; + this.submitForm(); + }); + }); + } + + if (!this.shouldDisplaySignature && !this.shouldDisplayTerms) { + this.submitForm(); + } + } + + submitForm() { + document.getElementById('payment-form').submit(); + } + + displayTerms() { + let displayTermsModal = document.getElementById('displayTermsModal'); + displayTermsModal.removeAttribute('style'); + } + + displaySignature() { + let displaySignatureModal = document.getElementById('displaySignatureModal'); + displaySignatureModal.removeAttribute('style'); + + const signaturePad = new SignaturePad(document.getElementById('signature-pad'), { + backgroundColor: 'rgb(240,240,240)', + penColor: 'rgb(0, 0, 0)' + }); + } + + handle() { + document.querySelectorAll('.dropdown-gateway-button').forEach((element) => { + element.addEventListener('click', () => this.handleMethodSelect(element)); + }); + } +} + +const signature = document.querySelector( + 'meta[name="require-invoice-signature"]' +).content; + +const terms = document.querySelector( + 'meta[name="show-invoice-terms"]' +).content; + +new Payment(Boolean(+signature), Boolean(+terms)).handle(); + + diff --git a/resources/js/clients/payment_methods/authorize-stripe-card.js b/resources/js/clients/payment_methods/authorize-stripe-card.js new file mode 100644 index 0000000000..fbb3c1da00 --- /dev/null +++ b/resources/js/clients/payment_methods/authorize-stripe-card.js @@ -0,0 +1,90 @@ +/** + * Invoice Ninja (https://invoiceninja.com) + * + * @link https://github.com/invoiceninja/invoiceninja source repository + * + * @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com) + * + * @license https://opensource.org/licenses/AAL + */ + +class AuthorizeStripeCard { + constructor(key) { + this.key = key; + this.cardHolderName = document.getElementById("cardholder-name"); + this.cardButton = document.getElementById("card-button"); + this.clientSecret = this.cardButton.dataset.secret; + } + + setupStripe() { + this.stripe = Stripe(this.key); + this.elements = this.stripe.elements(); + + return this; + } + + createElement() { + this.cardElement = this.elements.create("card"); + + return this; + } + + mountCardElement() { + this.cardElement.mount("#card-element"); + + return this; + } + + handleStripe(stripe, cardHolderName) { + stripe + .handleCardSetup(this.clientSecret, this.cardElement, { + payment_method_data: { + billing_details: { name: cardHolderName.value } + } + }) + .then(result => { + if (result.error) { + return this.handleFailure(result); + } + return this.handleSuccess(result); + }); + } + + handleFailure(result) { + let errors = document.getElementById("errors"); + + errors.textContent = ""; + errors.textContent = result.error.message; + errors.hidden = false; + } + + handleSuccess(result) { + document.getElementById("gateway_response").value = JSON.stringify( + result.setupIntent + ); + document.getElementById("is_default").value = document.getElementById( + "proxy_is_default" + ).checked; + + document.getElementById("server_response").submit(); + } + + handle() { + this.setupStripe() + .createElement() + .mountCardElement(); + + this.cardButton.addEventListener("click", () => { + this.handleStripe(this.stripe, this.cardHolderName); + }); + + return this; + } +} + +const publishableKey = document.querySelector( + 'meta[name="stripe-publishable-key"]' +).content; + +/** @handle */ +new AuthorizeStripeCard(publishableKey).handle(); diff --git a/resources/js/clients/quotes/action-selectors.js b/resources/js/clients/quotes/action-selectors.js new file mode 100644 index 0000000000..751bad0431 --- /dev/null +++ b/resources/js/clients/quotes/action-selectors.js @@ -0,0 +1,62 @@ +/** + * Invoice Ninja (https://invoiceninja.com) + * + * @link https://github.com/invoiceninja/invoiceninja source repository + * + * @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com) + * + * @license https://opensource.org/licenses/AAL + */ + +class ActionSelectors { + constructor() { + this.parentElement = document.querySelector(".form-check-parent"); + this.parentForm = document.getElementById("bulkActions"); + } + + watchCheckboxes(parentElement) { + document.querySelectorAll(".form-check-child").forEach(child => { + if (parentElement.checked) { + child.checked = parentElement.checked; + this.processChildItem(child, document.getElementById("bulkActions")); + } else { + child.checked = false; + document + .querySelectorAll(".child-hidden-input") + .forEach(element => element.remove()); + } + }); + } + + processChildItem(element, parent, options = {}) { + if (options.hasOwnProperty("single")) { + document + .querySelectorAll(".child-hidden-input") + .forEach(element => element.remove()); + } + + let _temp = document.createElement("INPUT"); + + _temp.setAttribute("name", "quotes[]"); + _temp.setAttribute("value", element.dataset.value); + _temp.setAttribute("class", "child-hidden-input"); + _temp.hidden = true; + + parent.append(_temp); + } + + handle() { + this.parentElement.addEventListener("click", () => { + this.watchCheckboxes(this.parentElement); + }); + + for (let child of document.querySelectorAll(".form-check-child")) { + child.addEventListener("click", () => { + this.processChildItem(child, this.parentForm); + }); + } + } +} + +/** @handle **/ +new ActionSelectors().handle(); diff --git a/resources/js/clients/quotes/approve.js b/resources/js/clients/quotes/approve.js new file mode 100644 index 0000000000..90cbe471ce --- /dev/null +++ b/resources/js/clients/quotes/approve.js @@ -0,0 +1,51 @@ +/** + * Invoice Ninja (https://invoiceninja.com) + * + * @link https://github.com/invoiceninja/invoiceninja source repository + * + * @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com) + * + * @license https://opensource.org/licenses/AAL + */ + +class Approve { + constructor(displaySignature) { + this.shouldDisplaySignature = displaySignature; + } + + submitForm() { + document.getElementById('approve-form').submit(); + } + + displaySignature() { + let displaySignatureModal = document.getElementById('displaySignatureModal'); + displaySignatureModal.removeAttribute('style'); + + const signaturePad = new SignaturePad(document.getElementById('signature-pad'), { + backgroundColor: 'rgb(240,240,240)', + penColor: 'rgb(0, 0, 0)' + }); + } + + handle() { + document.getElementById('approve-button').addEventListener('click', () => { + if (this.shouldDisplaySignature) { + this.displaySignature(); + + document.getElementById('signature-next-step').addEventListener('click', () => { + this.submitForm(); + }); + } + + if (!this.shouldDisplaySignature) this.submitForm(); + }) + } +} + +const signature = document.querySelector( + 'meta[name="require-quote-signature"]' +).content; + +new Approve(Boolean(+signature)).handle(); + + diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index e1d2c748f3..894ebb4ef2 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -2,6 +2,7 @@ return [ 'continue' => 'Continue', + 'back' => 'Back', 'complete' => 'Complete', 'next' => 'Next', 'next_step' => 'Next step', @@ -504,6 +505,7 @@ return [ 'api_tokens' => 'API Tokens', 'users_and_tokens' => 'Users & Tokens', 'account_login' => 'Account Login', + 'account_login_text' => 'Welcome back! Glad to see you.', 'recover_password' => 'Recover your password', 'forgot_password' => 'Forgot your password?', 'email_address' => 'Email address', @@ -711,6 +713,7 @@ return [ 'reminder_subject' => 'Reminder: Invoice :invoice from :account', 'reset' => 'Reset', 'invoice_not_found' => 'The requested invoice is not available', + 'request_cancellation' => 'Request cancellation', 'referral_program' => 'Referral Program', 'referral_code' => 'Referral URL', 'last_sent_on' => 'Sent Last: :date', @@ -2196,6 +2199,7 @@ return [ 'create_expense_category' => 'Create category', 'pro_plan_reports' => ':link to enable reports by joining the Pro Plan', 'mark_ready' => 'Mark Ready', + 'profile_updated_successfully' => 'The profile has been updated successfully.', 'limits' => 'Limits', 'fees' => 'Fees', @@ -2705,6 +2709,7 @@ return [ 'amount_greater_than_balance' => 'The amount is greater than the invoice balance, a credit will be created with the remaining amount.', 'custom_fields_tip' => 'Use Label|Option1,Option2 to show a select box.', 'client_information' => 'Client Information', + 'client_information_text' => 'Use a permanent address where you can receive mail.', 'updated_client_details' => 'Successfully updated client details', 'auto' => 'Auto', 'tax_amount' => 'Tax Amount', diff --git a/resources/sass/app.scss b/resources/sass/app.scss index 88ab33ef0e..d46045ccc4 100644 --- a/resources/sass/app.scss +++ b/resources/sass/app.scss @@ -7,6 +7,11 @@ @import 'components/validation'; @import 'components/inputs'; @import 'components/alerts'; +@import 'components/badge'; + +.active-page { + @apply bg-blue-900 #{!important}; +} // .. @tailwind utilities; diff --git a/resources/sass/components/badge.scss b/resources/sass/components/badge.scss new file mode 100644 index 0000000000..4c76a4d2ad --- /dev/null +++ b/resources/sass/components/badge.scss @@ -0,0 +1,31 @@ +.badge { + @apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium leading-4; +} + +.badge-light { + @apply bg-gray-100 text-gray-800; +} + +.badge-primary { + @apply bg-blue-200 text-blue-500; +} + +.badge-danger { + @apply bg-red-100 text-red-500; +} + +.badge-success { + @apply bg-green-100 text-green-500; +} + +.badge-secondary { + @apply bg-gray-800 text-gray-200; +} + +.badge-warning { + @apply bg-orange-100 text-orange-500; +} + +.badge-info { + @apply bg-blue-100 text-blue-500; +} diff --git a/resources/sass/components/buttons.scss b/resources/sass/components/buttons.scss index 78129e5914..88b8e378ad 100644 --- a/resources/sass/components/buttons.scss +++ b/resources/sass/components/buttons.scss @@ -1,5 +1,5 @@ .button { - @apply rounded py-3 px-4; + @apply rounded py-3 px-4 text-sm leading-4 transition duration-150 ease-in-out font-semibold; } .button-primary { @@ -13,3 +13,31 @@ .button-block { @apply block w-full; } + +.button-danger { + @apply bg-red-500 text-white; + + &:hover { + @apply bg-red-600; + } +} + +.button-secondary { + @apply bg-gray-100; + + &:hover { + @apply bg-gray-200; + } +} + +.button-link { + @apply text-blue-600; + + &:hover { + @apply text-blue-700 underline; + } + + &:focus { + @apply outline-none underline; + } +} diff --git a/resources/sass/components/inputs.scss b/resources/sass/components/inputs.scss index ea940a5bc3..016bbd2e60 100644 --- a/resources/sass/components/inputs.scss +++ b/resources/sass/components/inputs.scss @@ -1,5 +1,5 @@ .input { - @apply items-center border border-gray-300 rounded mt-2 w-full py-3 px-4; + @apply items-center border border-gray-300 rounded mt-2 w-full py-2 px-4 text-sm; &:focus { @apply outline-none border-blue-500; @@ -9,3 +9,7 @@ .input-label { @apply text-sm text-gray-600; } + +.input-slim { + @apply py-2; +} diff --git a/resources/views/portal/default/invoices/payment.blade.php b/resources/views/portal/default/invoices/payment.blade.php index e3f78e9ddd..4e21ebd358 100644 --- a/resources/views/portal/default/invoices/payment.blade.php +++ b/resources/views/portal/default/invoices/payment.blade.php @@ -164,12 +164,12 @@ $('#terms_accepted').on('click', function(e){ //push to payment - + }); $("#modal_pay_now_button").on('click', function(e){ - + //disable to prevent firing twice $("#modal_pay_now_button").attr("disabled", true); @@ -187,11 +187,11 @@ $("#modal_pay_now_button").on('click', function(e){ function getSignature() { - //check in signature is required + //check in signature is required $("#signature").jSignature({ 'UndoButton': true, }).bind('change', function(e) { if( $("#signature").jSignature('getData', 'native').length >= 1) { - + $("#modal_pay_now_button").removeAttr("disabled"); } else { @@ -214,7 +214,7 @@ $("#modal_pay_now_button").on('click', function(e){ //var data = false; } - + @endpush @section('footer') diff --git a/resources/views/portal/ninja2020/auth/login.blade.php b/resources/views/portal/ninja2020/auth/login.blade.php new file mode 100644 index 0000000000..092a65e0b5 --- /dev/null +++ b/resources/views/portal/ninja2020/auth/login.blade.php @@ -0,0 +1,59 @@ +@extends('portal.ninja2020.layout.clean') +@section('meta_title', ctrans('texts.login')) + +@section('body') +
+ +
+
+
+

{{ ctrans('texts.account_login') }}

+

{{ ctrans('texts.account_login_text') }}

+
+ @csrf +
+ + + @error('email') +
+ {{ $message }} +
+ @enderror +
+
+
+ + {{ trans('texts.forgot_password') }} +
+ + @error('password') +
+ {{ $message }} +
+ @enderror +
+
+ +
+
+ + {{ trans('texts.login_create_an_account') }} + +
+
+
+
+ +@endsection diff --git a/resources/views/portal/ninja2020/auth/passwords/request.blade.php b/resources/views/portal/ninja2020/auth/passwords/request.blade.php new file mode 100644 index 0000000000..e7d9a16366 --- /dev/null +++ b/resources/views/portal/ninja2020/auth/passwords/request.blade.php @@ -0,0 +1,44 @@ +@extends('portal.ninja2020.layout.clean') +@section('meta_title', $title) + +@section('body') +
+ +
+
+
+

{{ ctrans('texts.password_recovery') }}

+

{{ ctrans('texts.reset_password_text') }}

+ @if(session('status')) +
+ {{ session('status') }} +
+ @endif +
+ @csrf +
+ + + @error('email') +
+ {{ $message }} +
+ @enderror +
+
+ +
+
+
+
+
+
+ +@endsection diff --git a/resources/views/portal/ninja2020/auth/passwords/reset.blade.php b/resources/views/portal/ninja2020/auth/passwords/reset.blade.php new file mode 100644 index 0000000000..a8284ffd6c --- /dev/null +++ b/resources/views/portal/ninja2020/auth/passwords/reset.blade.php @@ -0,0 +1,67 @@ +@extends('portal.ninja2020.layout.clean') +@section('meta_title', ctrans('texts.password_recovery')) + +@section('body') +
+ +
+
+
+

{{ ctrans('texts.password_recovery') }}

+

{{ ctrans('texts.reset_password_text') }}

+ @if(session('status')) +
+ {{ session('status') }} +
+ @endif +
+ @csrf + +
+ + + @error('email') +
+ {{ $message }} +
+ @enderror +
+
+ + + @error('password') +
+ {{ $message }} +
+ @enderror +
+
+ + + @error('password_confirmation') +
+ {{ $message }} +
+ @enderror +
+
+ +
+
+
+
+
+
+ +@endsection diff --git a/resources/views/portal/ninja2020/components/breadcrumbs.blade.php b/resources/views/portal/ninja2020/components/breadcrumbs.blade.php new file mode 100644 index 0000000000..f13095694e --- /dev/null +++ b/resources/views/portal/ninja2020/components/breadcrumbs.blade.php @@ -0,0 +1,33 @@ + + +@if (count($breadcrumbs)) + + + +@endif diff --git a/resources/views/portal/ninja2020/components/general/messages/success.blade.php b/resources/views/portal/ninja2020/components/general/messages/success.blade.php new file mode 100644 index 0000000000..f905211148 --- /dev/null +++ b/resources/views/portal/ninja2020/components/general/messages/success.blade.php @@ -0,0 +1,4 @@ +
+ {{ session('success') }} +
+ diff --git a/resources/views/portal/ninja2020/components/general/sidebar/desktop.blade.php b/resources/views/portal/ninja2020/components/general/sidebar/desktop.blade.php new file mode 100644 index 0000000000..ef6183732b --- /dev/null +++ b/resources/views/portal/ninja2020/components/general/sidebar/desktop.blade.php @@ -0,0 +1,23 @@ + + diff --git a/resources/views/portal/ninja2020/components/general/sidebar/header.blade.php b/resources/views/portal/ninja2020/components/general/sidebar/header.blade.php new file mode 100644 index 0000000000..59cce6cd21 --- /dev/null +++ b/resources/views/portal/ninja2020/components/general/sidebar/header.blade.php @@ -0,0 +1,65 @@ +
+ +
+
+
+ +
+
+ + + +
+ +
+
+
+
+ +
+
+ +
+ +
+
+
+
diff --git a/resources/views/portal/ninja2020/components/general/sidebar/main.blade.php b/resources/views/portal/ninja2020/components/general/sidebar/main.blade.php new file mode 100644 index 0000000000..c92cb944e2 --- /dev/null +++ b/resources/views/portal/ninja2020/components/general/sidebar/main.blade.php @@ -0,0 +1,31 @@ +
+ + +@include('portal.ninja2020.components.general.sidebar.mobile') + + + @include('portal.ninja2020.components.general.sidebar.desktop') + +
+ @include('portal.ninja2020.components.general.sidebar.header') +
+ +
+ @yield('header') +
+ +
+
+ @includeWhen(session()->has('success'), 'portal.ninja2020.components.general.messages.success') + {{ $slot }} +
+
+
+
+
diff --git a/resources/views/portal/ninja2020/components/general/sidebar/mobile.blade.php b/resources/views/portal/ninja2020/components/general/sidebar/mobile.blade.php new file mode 100644 index 0000000000..fde52895ca --- /dev/null +++ b/resources/views/portal/ninja2020/components/general/sidebar/mobile.blade.php @@ -0,0 +1,34 @@ +
+
+
+
+ +
+
+ {{ config('app.name') }} +
+ +
+
diff --git a/resources/views/portal/ninja2020/credits/index.blade.php b/resources/views/portal/ninja2020/credits/index.blade.php new file mode 100644 index 0000000000..e8c1aa7e8e --- /dev/null +++ b/resources/views/portal/ninja2020/credits/index.blade.php @@ -0,0 +1,87 @@ +@extends('portal.ninja2020.layout.app') +@section('meta_title', ctrans('texts.credits')) + +@section('header') + {{ Breadcrumbs::render('credits') }} + + @if($errors->any()) +
+ @foreach($errors->all() as $error) +

{{ $error }}

+ @endforeach +
+ @endif + +
+
+
+
+

+ {{ ctrans('texts.credits') }} +

+
+

+ {{ ctrans('texts.list_of_credits') }} +

+
+
+
+
+
+@endsection + +@section('body') +
+
+
+ + + + + + + + + + + + @foreach($credits as $credit) + + + + + + + + @endforeach + +
+ {{ ctrans('texts.amount') }} + + {{ ctrans('texts.balance') }} + + {{ ctrans('texts.credit_date') }} + + {{ ctrans('texts.public_notes') }} +
+ {{ App\Utils\Number::formatMoney($credit->amount, $credit->client) }} + + {{ App\Utils\Number::formatMoney($credit->balance, $credit->client) }} + + {{ $credit->formatDate($credit->date, $credit->client->date_format()) }} + + {{ empty($credit->public_notes) ? '/' : $credit->public_notes }} + + + @lang('texts.view') + +
+
+
+
+ {{ $credits->links('portal.ninja2020.vendor.pagination') }} +
+
+@endsection diff --git a/resources/views/portal/ninja2020/credits/show.blade.php b/resources/views/portal/ninja2020/credits/show.blade.php new file mode 100644 index 0000000000..f4889ca70e --- /dev/null +++ b/resources/views/portal/ninja2020/credits/show.blade.php @@ -0,0 +1,57 @@ +@extends('portal.ninja2020.layout.app') +@section('meta_title', ctrans('texts.credit')) + +@section('header') + {{ Breadcrumbs::render('credits.show', $credit) }} +@endsection + +@section('body') +
+
+
+

+ {{ ctrans('texts.credit') }} +

+

+ +

+
+
+
+
+
+ {{ ctrans('texts.amount') }} +
+
+ {{ App\Utils\Number::formatMoney($credit->amount, $credit->client) }} +
+
+
+
+ {{ ctrans('texts.balance') }} +
+
+ {{ App\Utils\Number::formatMoney($credit->balance, $credit->client) }} +
+
+
+
+ {{ ctrans('texts.credit_date') }} +
+
+ {{ $credit->formatDate($credit->date, $credit->client->date_format()) }} +
+
+
+
+ {{ ctrans('texts.public_notes') }} +
+
+ {{ $credit->public_notes }} +
+
+
+
+
+
+@endsection diff --git a/resources/views/portal/ninja2020/dashboard/index.blade.php b/resources/views/portal/ninja2020/dashboard/index.blade.php index 04624fbd2a..737c4d5203 100644 --- a/resources/views/portal/ninja2020/dashboard/index.blade.php +++ b/resources/views/portal/ninja2020/dashboard/index.blade.php @@ -1,7 +1,29 @@ -@extends('portal.ninja2020.layout.clean') +@extends('portal.ninja2020.layout.app') +@section('meta_title', ctrans('texts.dashboard')) -@section('body') -
- +@section('header') + {{ Breadcrumbs::render('dashboard') }} + +
+
+
+
+

+ {{ ctrans('texts.dashboard') }} +

+
+

+ {{ ctrans('texts.quick_overview_statistics') }} +

+
+
+
+
@endsection + +@section('body') + Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet esse magnam nam numquam omnis optio, pariatur + perferendis quae quaerat quam, quas quos repellat sapiente sit soluta, tenetur totam ut vel veritatis voluptatibus? + Aut, dolor illo? Asperiores eum eveniet quae sed? +@endsection diff --git a/resources/views/portal/ninja2020/gateways/stripe/add_credit_card.blade.php b/resources/views/portal/ninja2020/gateways/stripe/add_credit_card.blade.php new file mode 100644 index 0000000000..e6dda2e0ec --- /dev/null +++ b/resources/views/portal/ninja2020/gateways/stripe/add_credit_card.blade.php @@ -0,0 +1,79 @@ +@extends('portal.ninja2020.layout.app') +@section('meta_title', ctrans('texts.add_credit_card')) + +@push('head') + +@endpush + +@section('header') + {{ Breadcrumbs::render('payment_methods.add_credit_card') }} +@endsection + +@section('body') +
+ @csrf + + + + +
+
+
+
+ +
+
+

+ {{ ctrans('texts.add_credit_card') }} +

+

+ {{ ctrans('texts.authorize_for_future_use') }} +

+
+
+
+
+
+ {{ ctrans('texts.name') }} +
+
+ +
+
+
+ {{ ctrans('texts.credit_card') }} +
+
+
+
+
+
+
+ {{ ctrans('texts.save_as_default') }} +
+
+ +
+
+
+ +
+
+
+
+
+
+
+@endsection + +@push('footer') + + +@endpush diff --git a/resources/views/portal/ninja2020/invoices/includes/signature.blade.php b/resources/views/portal/ninja2020/invoices/includes/signature.blade.php new file mode 100644 index 0000000000..3da52cec87 --- /dev/null +++ b/resources/views/portal/ninja2020/invoices/includes/signature.blade.php @@ -0,0 +1,44 @@ + diff --git a/resources/views/portal/ninja2020/invoices/includes/terms.blade.php b/resources/views/portal/ninja2020/invoices/includes/terms.blade.php new file mode 100644 index 0000000000..3a7f44539e --- /dev/null +++ b/resources/views/portal/ninja2020/invoices/includes/terms.blade.php @@ -0,0 +1,49 @@ + diff --git a/resources/views/portal/ninja2020/invoices/index.blade.php b/resources/views/portal/ninja2020/invoices/index.blade.php new file mode 100644 index 0000000000..21539be06b --- /dev/null +++ b/resources/views/portal/ninja2020/invoices/index.blade.php @@ -0,0 +1,125 @@ +@extends('portal.ninja2020.layout.app') +@section('meta_title', ctrans('texts.invoices')) + +@section('header') + {{ Breadcrumbs::render('invoices') }} + + @if($errors->any()) +
+ @foreach($errors->all() as $error) +

{{ $error }}

+ @endforeach +
+ @endif + +
+
+
+
+

+ {{ ctrans('texts.invoices') }} +

+
+

+ {{ ctrans('texts.list_of_invoices') }} +

+
+
+
+
+
+@endsection + +@section('body') +
+ {{ ctrans('texts.with_selected') }} +
+ @csrf + + +
+
+
+
+
+ + + + + + + + + + + + + + @foreach($invoices as $invoice) + + + + + + + + + + @endforeach + +
+ + + {{ ctrans('texts.invoice_number') }} + + {{ ctrans('texts.invoice_date') }} + + {{ ctrans('texts.balance') }} + + {{ ctrans('texts.due_date') }} + + {{ ctrans('texts.status') }} +
+ + + {{ $invoice->number }} + + {{ $invoice->formatDate($invoice->date, $invoice->client->date_format()) }} + + {{ App\Utils\Number::formatMoney($invoice->balance, $invoice->client) }} + + {{ $invoice->formatDate($invoice->due_date, $invoice->client->date_format()) }} + + {!! App\Models\Invoice::badgeForStatus($invoice->status) !!} + + @if($invoice->isPayable()) + + @endif + + @lang('texts.view') + +
+
+
+
+ {{ $invoices->links('portal.ninja2020.vendor.pagination') }} +
+
+@endsection + +@push('footer') + +@endpush diff --git a/resources/views/portal/ninja2020/invoices/payment.blade.php b/resources/views/portal/ninja2020/invoices/payment.blade.php new file mode 100644 index 0000000000..666874e8e5 --- /dev/null +++ b/resources/views/portal/ninja2020/invoices/payment.blade.php @@ -0,0 +1,113 @@ +@extends('portal.ninja2020.layout.app') +@section('meta_title', ctrans('texts.pay_now')) + +@push('head') + + + +@endpush + +@section('body') +
+ @csrf + + + +
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+ @foreach($payment_methods as $payment_method) + + {{ $payment_method['label'] }} + + @endforeach +
+
+
+
+
+
+ @foreach($invoices as $invoice) +
+
+

+ {{ ctrans('texts.invoice') }} + + (#{{ $invoice->number }}) + +

+

+

+
+
+
+
+
+ {{ ctrans('texts.invoice_number') }} +
+
+ {{ $invoice->number }} +
+
+
+
+ {{ ctrans('texts.due_date') }} +
+
+ {{ $invoice->due_date }} +
+
+
+
+ {{ ctrans('texts.additional_info') }} +
+
+ @if($invoice->po_number) + {{ $invoice->po_number }} + @elseif($invoice->public_notes) + {{ $invoice->public_notes }} + @else + {{ $invoice->invoice_date}} + @endif +
+
+
+
+
+ @endforeach +
+
+
+ + @include('portal.ninja2020.invoices.includes.terms') + @include('portal.ninja2020.invoices.includes.signature') +@endsection + +@push('footer') + +@endpush diff --git a/resources/views/portal/ninja2020/invoices/show.blade.php b/resources/views/portal/ninja2020/invoices/show.blade.php new file mode 100644 index 0000000000..19f44e5bda --- /dev/null +++ b/resources/views/portal/ninja2020/invoices/show.blade.php @@ -0,0 +1,43 @@ +@extends('portal.ninja2020.layout.app') +@section('meta_title', ctrans('texts.view_invoice')) + +@section('header') + {{ Breadcrumbs::render('invoices.show', $invoice) }} +@endsection + +@section('body') + + @if($invoice->isPayable()) +
+ @csrf +
+
+
+
+

+ {{ ctrans('texts.unpaid') }} +

+
+

+ {{ ctrans('texts.invoice_unpaid') }} + +

+
+
+
+
+ + + +
+
+
+
+
+
+ @endif + + + +@endsection diff --git a/resources/views/portal/ninja2020/layout/app.blade.php b/resources/views/portal/ninja2020/layout/app.blade.php new file mode 100644 index 0000000000..1747d8adf5 --- /dev/null +++ b/resources/views/portal/ninja2020/layout/app.blade.php @@ -0,0 +1,76 @@ + + + + + + + + + @if (config('services.analytics.tracking_id')) + + + + @else + + @endif + + + @yield('meta_title', 'Invoice Ninja') — {{ config('app.name') }} + + + + + + + + + + + + + + + + + + + + {{-- --}} + + + + {{-- Feel free to push anything to header using @push('header') --}} + @stack('head') + + + + + @component('portal.ninja2020.components.general.sidebar.main') + @yield('body') + @endcomponent + + +
+ @yield('footer') + @stack('footer') +
+ + diff --git a/resources/views/portal/ninja2020/layout/clean.blade.php b/resources/views/portal/ninja2020/layout/clean.blade.php index c9066d529e..97b3237b5a 100644 --- a/resources/views/portal/ninja2020/layout/clean.blade.php +++ b/resources/views/portal/ninja2020/layout/clean.blade.php @@ -44,6 +44,7 @@ + diff --git a/resources/views/portal/ninja2020/payment_methods/create.blade.php b/resources/views/portal/ninja2020/payment_methods/create.blade.php new file mode 100644 index 0000000000..e69de29bb2 diff --git a/resources/views/portal/ninja2020/payment_methods/includes/modals/removal.blade.php b/resources/views/portal/ninja2020/payment_methods/includes/modals/removal.blade.php new file mode 100644 index 0000000000..0248d1209d --- /dev/null +++ b/resources/views/portal/ninja2020/payment_methods/includes/modals/removal.blade.php @@ -0,0 +1,52 @@ +
+
+
+
+ +
+
+
+ + + +
+
+

+ Are you sure? +

+
+

+ Warning! This action can't be reversed. +

+
+
+
+
+
+
+ @csrf + @method('DELETE') + +
+
+
+ + +
+
+
+
diff --git a/resources/views/portal/ninja2020/payment_methods/index.blade.php b/resources/views/portal/ninja2020/payment_methods/index.blade.php new file mode 100644 index 0000000000..d9eea112fc --- /dev/null +++ b/resources/views/portal/ninja2020/payment_methods/index.blade.php @@ -0,0 +1,109 @@ +@extends('portal.ninja2020.layout.app') +@section('meta_title', ctrans('texts.payment_methods')) + +@section('header') + {{ Breadcrumbs::render('payment_methods') }} + +
+
+
+
+

+ {{ ctrans('texts.payment_methods') }} +

+
+

+ {{ ctrans('texts.list_of_payment_methods') }} + +

+
+
+ +
+
+
+@endsection + +@section('body') +
+
+
+ + + + + + + + + + + + + + @foreach($payment_methods as $payment_method) + + + + + + + + + + @endforeach + +
+ {{ ctrans('texts.created_at') }} + + {{ ctrans('texts.payment_type_id') }} + + {{ ctrans('texts.type') }} + + {{ ctrans('texts.expires') }} + + {{ ctrans('texts.card_number') }} + + {{ ctrans('texts.default') }} +
+ {{ $payment_method->formatDateTimestamp($payment_method->created_at, auth()->user()->client->date_format()) }} + + {{ ctrans("texts.{$payment_method->gateway_type->alias}") }} + + {{ ucfirst(optional($payment_method->meta)->brand) }} + + @if(isset($payment_method->meta->exp_month) && isset($payment_method->meta->exp_year)) + {{ $payment_method->meta->exp_month}} / {{ $payment_method->meta->exp_year }} + @endif + + @isset($payment_method->meta->last4) + **** {{ $payment_method->meta->last4 }} + @endisset + + @if($payment_method->is_default) + + + + @endif + + + @lang('texts.view') + +
+
+
+
+ {{ $payment_methods->links('portal.ninja2020.vendor.pagination') }} +
+
+@endsection diff --git a/resources/views/portal/ninja2020/payment_methods/show.blade.php b/resources/views/portal/ninja2020/payment_methods/show.blade.php new file mode 100644 index 0000000000..be3e5c3d18 --- /dev/null +++ b/resources/views/portal/ninja2020/payment_methods/show.blade.php @@ -0,0 +1,100 @@ +@extends('portal.ninja2020.layout.app') +@section('meta_title', ucfirst($payment_method->gateway_type->name)) + +@section('header') + {{ Breadcrumbs::render('payment_methods.show', $payment_method) }} +@endsection + +@section('body') +
+
+
+

+ {{ ctrans("texts.{$payment_method->gateway_type->alias}") }} +

+

+ + {{ ctrans('texts.details_of_method') }} +

+
+
+
+
+
+ {{ ctrans('texts.payment_type') }} +
+
+ {{ ucfirst($payment_method->gateway_type->name) }} +
+
+
+
+ {{ ctrans('texts.type') }} +
+
+ {{ ucfirst($payment_method->meta->brand) }} +
+
+
+
+ {{ ctrans('texts.card_number') }} +
+
+ **** {{ ucfirst($payment_method->meta->last4) }} +
+
+
+
+ {{ ctrans('texts.date_created') }} +
+
+ {{ $payment_method->formatDateTimestamp($payment_method->created_at, auth()->user()->client->date_format()) }} +
+
+
+
+ {{ ctrans('texts.default') }} +
+
+ {{ $payment_method->is_default ? ctrans('texts.yes') : ctrans('texts.no') }} +
+
+ @isset($payment_method->meta->exp_month) +
+
+ {{ ctrans('texts.expires') }} +
+
+ {{ $payment_method->meta->exp_month }} / {{ $payment_method->meta->exp_year }} +
+
+ @endisset +
+
+
+
+
+
+
+

+ Remove +

+
+

+ Permanently remove this payment method. +

+
+
+
+
+ + @include('portal.ninja2020.payment_methods.includes.modals.removal') +
+
+
+
+
+
+@endsection diff --git a/resources/views/portal/ninja2020/payments/index.blade.php b/resources/views/portal/ninja2020/payments/index.blade.php new file mode 100644 index 0000000000..c0790d8d2b --- /dev/null +++ b/resources/views/portal/ninja2020/payments/index.blade.php @@ -0,0 +1,85 @@ +@extends('portal.ninja2020.layout.app') +@section('meta_title', ctrans('texts.payments')) + +@section('header') + {{ Breadcrumbs::render('payments') }} + +
+
+
+
+

+ {{ ctrans('texts.payments') }} +

+
+

+ {{ ctrans('texts.List of your payments.') }} +

+
+
+
+
+
+@endsection + +@section('body') +
+
+
+ + + + + + + + + + + + + @foreach($payments as $payment) + + + + + + + + + @endforeach + +
+ {{ ctrans('texts.payment_date') }} + + {{ ctrans('texts.payment_type_id') }} + + {{ ctrans('texts.amount') }} + + {{ ctrans('texts.transaction_reference') }} + + {{ ctrans('texts.status') }} +
+ {{ $payment->formatDate($payment->date, $payment->client->date_format()) }} + + {{ $payment->type->name }} + + {{ \App\Utils\Number::formatMoney($payment->amount, $payment->client) }} + + {{ $payment->transaction_reference }} + + {!! \App\Models\Payment::badgeForStatus($payment->status_id) !!} + + + @lang('texts.view') + +
+
+
+
+ {{ $payments->links('portal.ninja2020.vendor.pagination') }} +
+
+@endsection diff --git a/resources/views/portal/ninja2020/payments/show.blade.php b/resources/views/portal/ninja2020/payments/show.blade.php new file mode 100644 index 0000000000..8bc4dc4ab1 --- /dev/null +++ b/resources/views/portal/ninja2020/payments/show.blade.php @@ -0,0 +1,92 @@ +@extends('portal.ninja2020.layout.app') +@section('meta_title', ctrans('texts.payment')) + +@section('header') + {{ Breadcrumbs::render('payments.show', $payment) }} +@endsection + +@section('body') +
+
+
+

+ {{ ctrans('texts.payment') }} +

+

+ {{ ctrans('texts.Details of the payment.') }} +

+
+
+
+
+
+ {{ ctrans('texts.payment_date') }} +
+
+ {{ $payment->clientPaymentDate() }} +
+
+
+
+ {{ ctrans('texts.transaction_reference') }} +
+
+ {{ $payment->transaction_reference }} +
+
+
+
+ {{ ctrans('texts.method') }} +
+
+ {{ $payment->type->name }} +
+
+
+
+ {{ ctrans('texts.amount') }} +
+
+ {{ $payment->formattedAmount() }} +
+
+
+
+ {{ ctrans('texts.status') }} +
+
+ {!! \App\Models\Payment::badgeForStatus($payment->status_id) !!} +
+
+
+
+
+
+
+

+ {{ ctrans('texts.invoices') }} +

+

+ {{ ctrans('texts.List of invoices affected by payment.') }} +

+
+
+
+ @foreach($payment->invoices as $invoice) +
+
+ {{ ctrans('texts.invoice_number') }} +
+ +
+ @endforeach +
+
+
+
+@endsection diff --git a/resources/views/portal/ninja2020/profile/index.blade.php b/resources/views/portal/ninja2020/profile/index.blade.php new file mode 100644 index 0000000000..8c5fc03a1b --- /dev/null +++ b/resources/views/portal/ninja2020/profile/index.blade.php @@ -0,0 +1,356 @@ +@extends('portal.ninja2020.layout.app') + +@section('meta_title', ctrans('texts.client_information')) + +@section('header') +

{{ ctrans('texts.Update your personal information.') }}

+@endsection + +@section('body') + + +
+
+
+
+

{{ ctrans('texts.profile') }}

+

+ @lang('texts.client_information_text') +

+
+
+
+
+ @csrf + @method('PUT') +
+
+
+
+ + + @error('first_name') +
+ {{ $message }} +
+ @enderror +
+ +
+ + + @error('last_name') +
+ {{ $message }} +
+ @enderror +
+ +
+ + + @error('email') +
+ {{ $message }} +
+ @enderror +
+ +
+ + + @error('phone') +
+ {{ $message }} +
+ @enderror +
+ +
+ + + @error('password') +
+ {{ $message }} +
+ @enderror +
+ +
+ + + @error('password_confirmation') +
+ {{ $message }} +
+ @enderror +
+
+
+
+ +
+
+
+
+
+
+ + +
+
+
+
+

{{ ctrans('texts.name_website_logo') }}

+

+ {{ ctrans('texts. Make sure you use full link to your site.') }} +

+
+
+
+
+ @csrf + @method('PUT') +
+
+
+
+ + + @error('name') +
+ {{ $message }} +
+ @enderror +
+
+ + + @error('website') +
+ {{ $message }} +
+ @enderror +
+ +
+
+
+ +
+
+
+
+
+
+ + +
+
+
+
+

{{ ctrans('texts.personal_address') }}

+

+ {{ ctrans('texts.your_personal_address') }} +

+
+
+
+
+ @csrf + @method('PUT') +
+
+
+
+ + + @error('address1') +
+ {{ $message }} +
+ @enderror +
+
+ + + @error('address2') +
+ {{ $message }} +
+ @enderror +
+
+ + + @error('city') +
+ {{ $message }} +
+ @enderror +
+
+ + + @error('state') +
+ {{ $message }} +
+ @enderror +
+
+ + + @error('postal_code') +
+ {{ $message }} +
+ @enderror +
+
+ + + @error('country') +
+ {{ $message }} +
+ @enderror +
+
+
+
+ +
+
+
+
+
+
+ + +
+
+
+
+

{{ ctrans('texts.shipping_address') }}

+

+ {{ ctrans('texts.your_shipping_address') }} +

+
+
+
+
+ @csrf + @method('PUT') +
+
+
+
+ + + @error('shipping_address1') +
+ {{ $message }} +
+ @enderror +
+
+ + + @error('shipping_address2') +
+ {{ $message }} +
+ @enderror +
+
+ + + @error('shipping_city') +
+ {{ $message }} +
+ @enderror +
+
+ + + @error('shipping_state') +
+ {{ $message }} +
+ @enderror +
+
+ + + @error('shipping_postal_code') +
+ {{ $message }} +
+ @enderror +
+
+ + + @error('country') +
+ {{ $message }} +
+ @enderror +
+
+
+
+ +
+
+
+
+
+
+ +@endsection diff --git a/resources/views/portal/ninja2020/quotes/approve.blade.php b/resources/views/portal/ninja2020/quotes/approve.blade.php new file mode 100644 index 0000000000..2e5c22644b --- /dev/null +++ b/resources/views/portal/ninja2020/quotes/approve.blade.php @@ -0,0 +1,90 @@ +@extends('portal.ninja2020.layout.app') +@section('meta_title', ctrans('texts.approve')) + +@push('head') + + +@endpush + +@section('header') + {{ Breadcrumbs::render('quotes.approve') }} +@endsection + +@section('body') +
+ @csrf + + + @foreach($quotes as $quote) + + @endforeach +
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+ @foreach($quotes as $quote) +
+
+

+ {{ ctrans('texts.invoice') }} + + ({{ $quote->number }}) + +

+

+

+
+
+
+
+
+ {{ ctrans('texts.quote_number') }} +
+
+ {{ $quote->number }} +
+
+
+
+ {{ ctrans('texts.quote_date') }} +
+
+ {{ $quote->formatDate($quote->date, $quote->client->date_format()) }} +
+
+
+
+ {{ ctrans('texts.balance') }} +
+
+ {{ App\Utils\Number::formatMoney($quote->balance, $quote->client) }} +
+
+
+
+
+ @endforeach +
+
+
+ + @include('portal.ninja2020.invoices.includes.signature') +@endsection + +@push('footer') + +@endpush diff --git a/resources/views/portal/ninja2020/quotes/includes/signature.blade.php b/resources/views/portal/ninja2020/quotes/includes/signature.blade.php new file mode 100644 index 0000000000..3da52cec87 --- /dev/null +++ b/resources/views/portal/ninja2020/quotes/includes/signature.blade.php @@ -0,0 +1,44 @@ + diff --git a/resources/views/portal/ninja2020/quotes/index.blade.php b/resources/views/portal/ninja2020/quotes/index.blade.php new file mode 100644 index 0000000000..e83547a733 --- /dev/null +++ b/resources/views/portal/ninja2020/quotes/index.blade.php @@ -0,0 +1,117 @@ +@extends('portal.ninja2020.layout.app') +@section('meta_title', ctrans('texts.quotes')) + +@section('header') + {{ Breadcrumbs::render('quotes') }} + + @if($errors->any()) +
+ @foreach($errors->all() as $error) +

{{ $error }}

+ @endforeach +
+ @endif + +
+
+
+
+

+ {{ ctrans('texts.quotes') }} +

+
+

+ {{ ctrans('texts.list_of_quotes') }} +

+
+
+
+
+
+@endsection + +@section('body') +
+ {{ ctrans('texts.with_selected') }} +
+ @csrf + + +
+
+
+
+
+ + + + + + + + + + + + + + @foreach($quotes as $quote) + + + + + + + + + + @endforeach + +
+ + + {{ ctrans('texts.quote_number') }} + + {{ ctrans('texts.quote_date') }} + + {{ ctrans('texts.balance') }} + + {{ ctrans('texts.valid_until') }} + + {{ ctrans('texts.status') }} +
+ + + {{ $quote->number }} + + {{ $quote->formatDate($quote->date, $quote->client->date_format()) }} + + {{ App\Utils\Number::formatMoney($quote->balance, $quote->client) }} + + {{ $quote->formatDate($quote->date, $quote->client->date_format()) }} + + {!! App\Models\Quote::badgeForStatus($quote->status_id) !!} + + + @lang('texts.view') + +
+
+
+
+ {{ $quotes->links('portal.ninja2020.vendor.pagination') }} +
+
+@endsection + +@push('footer') + +@endpush diff --git a/resources/views/portal/ninja2020/quotes/show.blade.php b/resources/views/portal/ninja2020/quotes/show.blade.php new file mode 100644 index 0000000000..94caa1a3b5 --- /dev/null +++ b/resources/views/portal/ninja2020/quotes/show.blade.php @@ -0,0 +1,44 @@ +@extends('portal.ninja2020.layout.app') +@section('meta_title', ctrans('texts.view_quote')) + +@section('header') + {{ Breadcrumbs::render('quotes.show', $quote) }} +@endsection + +@section('body') + + @if(!$quote->isApproved()) +
+ @csrf + + +
+
+
+
+

+ {{ ctrans('texts.waitin_for_approval') }} +

+
+

+ {{ ctrans('texts.quote_still_not_approved') }} +

+
+
+
+
+ + +
+
+
+
+
+
+ @endif + + + +@endsection diff --git a/resources/views/portal/ninja2020/recurring_invoices/cancellation/index.blade.php b/resources/views/portal/ninja2020/recurring_invoices/cancellation/index.blade.php new file mode 100644 index 0000000000..46209cfc0f --- /dev/null +++ b/resources/views/portal/ninja2020/recurring_invoices/cancellation/index.blade.php @@ -0,0 +1,74 @@ +@extends('portal.ninja2020.layout.app') + +@section('header') + {{ Breadcrumbs::render('recurring_invoices.request_cancellation', $invoice) }} +@stop + +@section('body') +
+
+
+

+ {{ ctrans('texts.recurring_invoices') }} +

+

+ Details of the recurring invoice. +

+
+
+
+
+
+ {{ ctrans('texts.start_date') }} +
+
+ {{ $invoice->formatDate($invoice->start_date, $invoice->client->date_format()) }} +
+
+
+
+ {{ ctrans('texts.next_send_date') }} +
+
+ {{ $invoice->formatDate($invoice->next_send_date, $invoice->client->date_format()) }} +
+
+
+
+ {{ ctrans('texts.frequency') }} +
+
+ {{ \App\Models\RecurringInvoice::frequencyForKey($invoice->frequency_id) }} +
+
+
+
+ {{ ctrans('texts.cycles_remaining') }} +
+
+ {{ $invoice->remaining_cycles }} +
+
+
+
+ {{ ctrans('texts.amount') }} +
+
+ {{ \App\Utils\Number::formatMoney($invoice->amount, $invoice->client) }} +
+
+
+
+
+
+
+
+

+ Cancellation pending, we'll be in touch! +

+
+
+
+
+
+@endsection diff --git a/resources/views/portal/ninja2020/recurring_invoices/includes/modals/cancellation.blade.php b/resources/views/portal/ninja2020/recurring_invoices/includes/modals/cancellation.blade.php new file mode 100644 index 0000000000..8e075c6c8a --- /dev/null +++ b/resources/views/portal/ninja2020/recurring_invoices/includes/modals/cancellation.blade.php @@ -0,0 +1,49 @@ +
+
+
+
+ +
+
+
+ + + +
+
+

+ Request Cancellation +

+
+

+ Warning! You are requesting a cancellation of this service. + Your service may be cancelled with no further notification to you. +

+
+
+
+
+ +
+ +
+
+
+
diff --git a/resources/views/portal/ninja2020/recurring_invoices/index.blade.php b/resources/views/portal/ninja2020/recurring_invoices/index.blade.php new file mode 100644 index 0000000000..1c8565be48 --- /dev/null +++ b/resources/views/portal/ninja2020/recurring_invoices/index.blade.php @@ -0,0 +1,85 @@ +@extends('portal.ninja2020.layout.app') +@section('meta_title', ctrans('texts.recurring_invoices')) + +@section('header') + {{ Breadcrumbs::render('recurring_invoices') }} + +
+
+
+
+

+ {{ ctrans('texts.recurring_invoices') }} +

+
+

+ {{ ctrans('texts.list_of_recurring_invoices') }} +

+
+
+
+
+
+@endsection + +@section('body') +
+
+
+ + + + + + + + + + + + + @foreach($invoices as $invoice) + + + + + + + + + @endforeach + +
+ {{ ctrans('texts.frequency') }} + + {{ ctrans('texts.start_date') }} + + {{ ctrans('texts.next_send_date') }} + + {{ ctrans('texts.cycles_remaining') }} + + {{ ctrans('texts.amount') }} +
+ {{ \App\Models\RecurringInvoice::frequencyForKey($invoice->frequency_id) }} + + {{ $invoice->formatDate($invoice->date, $invoice->client->date_format()) }} + + {{ $invoice->formatDate($invoice->next_send_date, $invoice->client->date_format()) }} + + {{ $invoice->remaining_cycles }} + + {{ \App\Utils\Number::formatMoney($invoice->amount, $invoice->client) }} + + + @lang('texts.view') + +
+
+
+
+ {{ $invoices->links('portal.ninja2020.vendor.pagination') }} +
+
+@endsection diff --git a/resources/views/portal/ninja2020/recurring_invoices/show.blade.php b/resources/views/portal/ninja2020/recurring_invoices/show.blade.php new file mode 100644 index 0000000000..dfc3ce54b8 --- /dev/null +++ b/resources/views/portal/ninja2020/recurring_invoices/show.blade.php @@ -0,0 +1,87 @@ +@extends('portal.ninja2020.layout.app') +@section('meta_title', ctrans('texts.recurring_invoice')) + +@section('header') + {{ Breadcrumbs::render('recurring_invoices.show', $invoice) }} +@endsection + +@section('body') +
+
+
+

+ {{ ctrans('texts.recurring_invoices') }} +

+

+ {{ ctrans('texts.details_of_recurring_invoice') }}. +

+
+
+
+
+
+ {{ ctrans('texts.start_date') }} +
+
+ {{ $invoice->formatDate($invoice->start_date, $invoice->client->date_format()) }} +
+
+
+
+ {{ ctrans('texts.next_send_date') }} +
+
+ {{ $invoice->formatDate($invoice->next_send_date, $invoice->client->date_format()) }} +
+
+
+
+ {{ ctrans('texts.frequency') }} +
+
+ {{ \App\Models\RecurringInvoice::frequencyForKey($invoice->frequency_id) }} +
+
+
+
+ {{ ctrans('texts.cycles_remaining') }} +
+
+ {{ $invoice->remaining_cycles }} +
+
+
+
+ {{ ctrans('texts.amount') }} +
+
+ {{ \App\Utils\Number::formatMoney($invoice->amount, $invoice->client) }} +
+
+
+
+
+
+
+
+

+ {{ ctrans('texts.cancellation') }} +

+
+

+ {{ ctrans('texts.In case you want to stop the recurring invoice, please click the request the + cancellation.') }} +

+
+
+
+
+ + @include('portal.ninja2020.recurring_invoices.includes.modals.cancellation') +
+
+
+
+
+
+@endsection diff --git a/resources/views/portal/ninja2020/vendor/pagination.blade.php b/resources/views/portal/ninja2020/vendor/pagination.blade.php new file mode 100644 index 0000000000..12b4a24f9b --- /dev/null +++ b/resources/views/portal/ninja2020/vendor/pagination.blade.php @@ -0,0 +1,67 @@ +
+ + + + @if ($paginator->hasMorePages()) + + @else + + @endif +
diff --git a/routes/breadcrumbs.php b/routes/breadcrumbs.php index 0a94a65e1f..8b22d20c29 100644 --- a/routes/breadcrumbs.php +++ b/routes/breadcrumbs.php @@ -2,7 +2,91 @@ // Dashboard Breadcrumbs::for('dashboard', function ($trail) { - $trail->push(trans('texts.dashboard'), route('dashboard.index')); + $trail->push(trans('texts.dashboard'), route('client.dashboard')); +}); + +// Invoices +Breadcrumbs::for('invoices', function ($trail) { + $trail->push(ctrans('texts.invoices'), route('client.invoices.index')); +}); + +// Invoices > Show invoice +Breadcrumbs::for('invoices.show', function ($trail, $invoice) { + $trail->parent('invoices'); + $trail->push(sprintf('%s: %s', ctrans('texts.invoice'), $invoice->number), route('client.invoices.index', $invoice->hashed_id)); +}); + +// Recurring invoices +Breadcrumbs::for('recurring_invoices', function ($trail) { + $trail->push(ctrans('texts.recurring_invoices'), route('client.recurring_invoices.index')); +}); + +// Recurring invoices > Show recurring invoice +Breadcrumbs::for('recurring_invoices.show', function ($trail, $invoice) { + $trail->parent('recurring_invoices'); + $trail->push(sprintf('%s: %s', ctrans('texts.recurring_invoice'), $invoice->hashed_id), route('client.recurring_invoices.index', $invoice->hashed_id)); +}); + +// Recurring invoices > Show recurring invoice +Breadcrumbs::for('recurring_invoices.request_cancellation', function ($trail, $invoice) { + $trail->parent('recurring_invoices.show', $invoice); + $trail->push(ctrans('texts.request_cancellation'), route('client.recurring_invoices.request_cancellation', $invoice->hashed_id)); +}); + +// Payments +Breadcrumbs::for('payments', function ($trail) { + $trail->push(ctrans('texts.payments'), route('client.payments.index')); +}); + +// Payments > Show payment +Breadcrumbs::for('payments.show', function ($trail, $invoice) { + $trail->parent('payments'); + $trail->push(sprintf('%s: %s', ctrans('texts.payment'), $invoice->hashed_id), route('client.payments.index', $invoice->hashed_id)); +}); + +// Payment methods +Breadcrumbs::for('payment_methods', function ($trail) { + $trail->push(ctrans('texts.payment_methods'), route('client.payment_methods.index')); +}); + +// Payment methods > Show payment method +Breadcrumbs::for('payment_methods.show', function ($trail, $invoice) { + $trail->parent('payment_methods'); + $trail->push(sprintf('%s: %s', ctrans('texts.payment_methods'), $invoice->hashed_id), route('client.payment_methods.index', $invoice->hashed_id)); +}); + +// Payment methods > Create method +Breadcrumbs::for('payment_methods.add_credit_card', function ($trail) { + $trail->parent('payment_methods'); + $trail->push(ctrans('texts.add_credit_card')); +}); + +// Quotes +Breadcrumbs::for('quotes', function ($trail) { + $trail->push(ctrans('texts.quotes'), route('client.quotes.index')); +}); + +// Quotes > Show quote +Breadcrumbs::for('quotes.show', function ($trail, $quote) { + $trail->parent('quotes'); + $trail->push(sprintf('%s: %s', ctrans('texts.quotes'), $quote->hashed_id), route('client.quotes.index', $quote->hashed_id)); +}); + +// Quotes > Approve +Breadcrumbs::for('quotes.approve', function ($trail) { + $trail->parent('quotes'); + $trail->push(ctrans('texts.approve')); +}); + +// Quotes +Breadcrumbs::for('credits', function ($trail) { + $trail->push(ctrans('texts.credits'), route('client.credits.index')); +}); + +// Quotes > Show quote +Breadcrumbs::for('credits.show', function ($trail, $credit) { + $trail->parent('credits'); + $trail->push(sprintf('%s: %s', ctrans('texts.credits'), $credit->hashed_id), route('client.credits.index', $credit->hashed_id)); }); // Dashboard > Client diff --git a/routes/client.php b/routes/client.php index 146807f4f3..cbb1dc9aaf 100644 --- a/routes/client.php +++ b/routes/client.php @@ -1,5 +1,7 @@ name('client.login'); //catch all Route::get('client/login', 'Auth\ContactLoginController@showLoginForm')->name('client.login')->middleware('locale'); @@ -12,46 +14,54 @@ Route::post('client/password/reset', 'Auth\ContactResetPasswordController@reset' //todo implement domain DB Route::group(['middleware' => ['auth:contact','locale'], 'prefix' => 'client', 'as' => 'client.'], function () { - Route::get('dashboard', 'ClientPortal\DashboardController@index')->name('dashboard'); // name = (dashboard. index / create / show / update / destroy / edit - Route::get('invoices', 'ClientPortal\InvoiceController@index')->name('invoices.index')->middleware('portal_enabled'); - Route::post('invoices/payment', 'ClientPortal\InvoiceController@bulk')->name('invoices.bulk'); - Route::get('invoices/{invoice}', 'ClientPortal\InvoiceController@show')->name('invoice.show'); - Route::get('invoices/{invoice_invitation}', 'ClientPortal\InvoiceController@show')->name('invoice.show_invitation'); + Route::get('dashboard', 'ClientPortal\DashboardController@index')->name('dashboard'); // name = (dashboard. index / create / show / update / destroy / edit - Route::get('recurring_invoices', 'ClientPortal\RecurringInvoiceController@index')->name('recurring_invoices.index')->middleware('portal_enabled'); - Route::get('recurring_invoices/{recurring_invoice}', 'ClientPortal\RecurringInvoiceController@show')->name('recurring_invoices.show'); - Route::get('recurring_invoices/{recurring_invoice}/request_cancellation', 'ClientPortal\RecurringInvoiceController@requestCancellation')->name('recurring_invoices.request_cancellation'); - - Route::get('payments', 'ClientPortal\PaymentController@index')->name('payments.index')->middleware('portal_enabled'); - Route::get('payments/{payment}', 'ClientPortal\PaymentController@show')->name('payments.show'); - Route::post('payments/process', 'ClientPortal\PaymentController@process')->name('payments.process'); - Route::post('payments/process/response', 'ClientPortal\PaymentController@response')->name('payments.response'); - Route::get('payments/process/response', 'ClientPortal\PaymentController@response')->name('payments.response.get'); + Route::get('invoices', 'ClientPortal\InvoiceController@index')->name('invoices.index')->middleware('portal_enabled'); + Route::post('invoices/payment', 'ClientPortal\InvoiceController@bulk')->name('invoices.bulk'); + Route::get('invoices/{invoice}', 'ClientPortal\InvoiceController@show')->name('invoice.show'); + Route::get('invoices/{invoice_invitation}', 'ClientPortal\InvoiceController@show')->name('invoice.show_invitation'); - Route::get('profile/{client_contact}/edit', 'ClientPortal\ProfileController@edit')->name('profile.edit'); - Route::put('profile/{client_contact}/edit', 'ClientPortal\ProfileController@update')->name('profile.update'); - Route::put('profile/{client_contact}/edit_client', 'ClientPortal\ProfileController@updateClient')->name('profile.edit_client'); - Route::put('profile/{client_contact}/localization', 'ClientPortal\ProfileController@updateClientLocalization')->name('profile.edit_localization'); + Route::get('recurring_invoices', 'ClientPortal\RecurringInvoiceController@index')->name('recurring_invoices.index')->middleware('portal_enabled'); + Route::get('recurring_invoices/{recurring_invoice}', 'ClientPortal\RecurringInvoiceController@show')->name('recurring_invoices.show'); + Route::get('recurring_invoices/{recurring_invoice}/request_cancellation', 'ClientPortal\RecurringInvoiceController@requestCancellation')->name('recurring_invoices.request_cancellation'); - Route::resource('payment_methods', 'ClientPortal\PaymentMethodController');// name = (payment_methods. index / create / show / update / destroy / edit + Route::get('payments', 'ClientPortal\PaymentController@index')->name('payments.index')->middleware('portal_enabled'); + Route::get('payments/{payment}', 'ClientPortal\PaymentController@show')->name('payments.show'); + Route::post('payments/process', 'ClientPortal\PaymentController@process')->name('payments.process'); + Route::post('payments/process/response', 'ClientPortal\PaymentController@response')->name('payments.response'); + Route::get('payments/process/response', 'ClientPortal\PaymentController@response')->name('payments.response.get'); - Route::post('document', 'ClientPortal\DocumentController@store')->name('document.store'); - Route::delete('document', 'ClientPortal\DocumentController@destroy')->name('document.destroy'); - - Route::get('logout', 'Auth\ContactLoginController@logout')->name('logout'); + Route::get('profile/{client_contact}/edit', 'ClientPortal\ProfileController@edit')->name('profile.edit'); + Route::put('profile/{client_contact}/edit', 'ClientPortal\ProfileController@update')->name('profile.update'); + Route::put('profile/{client_contact}/edit_client', 'ClientPortal\ProfileController@updateClient')->name('profile.edit_client'); + Route::put('profile/{client_contact}/localization', 'ClientPortal\ProfileController@updateClientLocalization')->name('profile.edit_localization'); + + Route::resource('payment_methods', 'ClientPortal\PaymentMethodController');// name = (payment_methods. index / create / show / update / destroy / edit + + Route::match(['GET', 'POST'], 'quotes/approve', 'ClientPortal\QuoteController@bulk')->name('quotes.bulk'); + Route::resource('quotes', 'ClientPortal\QuoteController')->only('index', 'show'); + + Route::resource('credits', 'ClientPortal\CreditController')->only('index', 'show'); + + Route::post('document', 'ClientPortal\DocumentController@store')->name('document.store'); + Route::delete('document', 'ClientPortal\DocumentController@destroy')->name('document.destroy'); + + Route::get('logout', 'Auth\ContactLoginController@logout')->name('logout'); }); Route::group(['middleware' => ['invite_db'], 'prefix' => 'client', 'as' => 'client.'], function () { - Route::get('invoice/{invitation_key}/download_pdf', 'InvoiceController@downloadPdf'); - Route::get('quote/{invitation_key}/download_pdf', 'QuoteController@downloadPdf'); - Route::get('credit/{invitation_key}/download_pdf', 'CreditController@downloadPdf'); - Route::get('{entity}/{invitation_key}/download', 'ClientPortal\InvitationController@routerForDownload'); - - /*Invitation catches*/ - Route::get('{entity}/{invitation_key}', 'ClientPortal\InvitationController@router'); - Route::get('{entity}/{client_hash}/{invitation_key}', 'ClientPortal\InvitationController@routerForIframe'); //should never need this - Route::get('payment_hook/{company_gateway_id}/{gateway_type_id}', 'ClientPortal\PaymentHookController@process'); + + Route::get('invoice/{invitation_key}/download_pdf', 'InvoiceController@downloadPdf'); + Route::get('quote/{invitation_key}/download_pdf', 'QuoteController@downloadPdf'); + Route::get('credit/{invitation_key}/download_pdf', 'CreditController@downloadPdf'); + Route::get('{entity}/{invitation_key}/download', 'ClientPortal\InvitationController@routerForDownload'); + + /*Invitation catches*/ + Route::get('{entity}/{invitation_key}','ClientPortal\InvitationController@router'); + Route::get('{entity}/{client_hash}/{invitation_key}','ClientPortal\InvitationController@routerForIframe'); //should never need this + Route::get('payment_hook/{company_gateway_id}/{gateway_type_id}','ClientPortal\PaymentHookController@process'); + }); Route::fallback('BaseController@notFoundClient'); diff --git a/webpack.mix.js b/webpack.mix.js index 42ef06d2cc..2bea83aaf2 100644 --- a/webpack.mix.js +++ b/webpack.mix.js @@ -1,19 +1,14 @@ const mix = require("laravel-mix"); const tailwindcss = require("tailwindcss"); -/* - |-------------------------------------------------------------------------- - | Mix Asset Management - |-------------------------------------------------------------------------- - | - | Mix provides a clean, fluent API for defining some Webpack build steps - | for your Laravel application. By default, we are compiling the Sass - | file for the application as well as bundling up all the JS files. - | - */ - mix.js("resources/js/app.js", "public/js") - .sass("resources/sass/app.scss", "public/css") + .js("resources/js/clients/payment_methods/authorize-stripe-card.js", "public/js/clients/payment_methods/authorize-stripe-card.js") + .js("resources/js/clients/invoices/action-selectors.js", "public/js/clients/invoices/action-selectors.js") + .js("resources/js/clients/invoices/payment.js", "public/js/clients/invoices/payment.js") + .js("resources/js/clients/quotes/action-selectors.js", "public/js/clients/quotes/action-selectors.js") + .js("resources/js/clients/quotes/approve.js", "public/js/clients/quotes/approve.js"); + +mix.sass("resources/sass/app.scss", "public/css") .options({ processCssUrls: false, postCss: [tailwindcss("./tailwind.config.js")]