1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-09-20 16:31:33 +02:00

Merge branch 'v5-develop' into v5-develop

This commit is contained in:
David Bomba 2021-04-13 19:35:44 +10:00 committed by GitHub
commit a386a33a7e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 378920 additions and 373142 deletions

View File

@ -3,14 +3,24 @@
## [Unreleased (daily channel)](https://github.com/invoiceninja/invoiceninja/tree/v5-develop)
### Added:
- Subscriptions are now going to show the frequency in the table (#5412)
- Subscriptions: During upgrade webhook request message will be shown for easier debugging (#5411)
- PDF: Custom fields now will be shared across invoices, quotes & credits (#5410)
- Client portal: Invoices are now sorted in the descending order (#5408)
- Payments: ACH notification after the initial bank account connecting process (#5405)
### Fixed:
- Fixes for counters where patterns without {$counter} could causes endless recursion.
- Fixes for surcharge tax displayed amount on PDF.
- Fixes for custom designs not rendering the custom template
- Fixes for missing bulk actions on Subscriptions
- Fixes CSS padding on the show page for recurring invoices (#5412)
- Fixes for rendering invalid HTML & parsing invalid XML (#5395)
### Removed:
- Removed one-time payments table (#5412)
## v5.1.43
### Fixed:
- Whitelabel regression.
- Whitelabel regression.

View File

@ -89,29 +89,6 @@ class PaymentMethodController extends Controller
]);
}
/**
* Show the form for editing the specified resource.
*
* @param int $id
* @return void
*/
public function edit($id)
{
//
}
/**
* Update the specified resource in storage.
*
* @param Request $request
* @param int $id
* @return void
*/
public function update(Request $request, $id)
{
//
}
public function verify(ClientGatewayToken $payment_method)
{
$gateway = $this->getClientGateway();
@ -151,7 +128,7 @@ class PaymentMethodController extends Controller
event(new MethodDeleted($payment_method, auth('contact')->user()->company, Ninja::eventVars(auth('contact')->user()->id)));
$payment_method->delete();
} catch (Exception $e) {
nlog($e->getMessage());
return back();

View File

@ -111,6 +111,7 @@ class BillingPortalPurchase extends Component
'discount_applied' => false,
'show_loading_bar' => false,
'not_eligible' => null,
'not_eligible_message' => null,
];
/**
@ -327,6 +328,7 @@ class BillingPortalPurchase extends Component
if (is_array($is_eligible)) {
$this->steps['not_eligible'] = true;
$this->steps['not_eligible_message'] = $is_eligible['exception'];
$this->steps['show_loading_bar'] = false;
return;

View File

@ -26,6 +26,11 @@ class InvoicesTable extends Component
public $status = [];
public function mount()
{
$this->sort_asc = false;
}
public function render()
{
$local_status = [];
@ -62,6 +67,7 @@ class InvoicesTable extends Component
$query = $query
->where('client_id', auth('contact')->user()->client->id)
->where('status_id', '<>', Invoice::STATUS_DRAFT)
->withTrashed()
->paginate($this->per_page);
return render('components.livewire.invoices-table', [

View File

@ -1,40 +0,0 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Livewire;
use App\Models\Invoice;
use App\Utils\Traits\WithSorting;
use Livewire\Component;
use Livewire\WithPagination;
class SubscriptionInvoicesTable extends Component
{
use WithPagination;
use WithSorting;
public $per_page = 10;
public function render()
{
$query = Invoice::query()
->where('client_id', auth('contact')->user()->client->id)
->whereNotNull('subscription_id')
->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc')
->where('balance', '=', 0)
->paginate($this->per_page);
return render('components.livewire.subscriptions-invoices-table', [
'invoices' => $query,
]);
}
}

View File

@ -0,0 +1,43 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Mail\Gateways;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class ACHVerificationNotification extends Mailable
{
use Queueable, SerializesModels;
/**
* Create a new message instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
return $this->view('email.gateways.ach-verification-notification');
}
}

View File

@ -14,8 +14,11 @@ namespace App\PaymentDrivers\Stripe;
use App\Exceptions\PaymentFailed;
use App\Http\Requests\Request;
use App\Jobs\Mail\NinjaMailerJob;
use App\Jobs\Mail\NinjaMailerObject;
use App\Jobs\Mail\PaymentFailureMailer;
use App\Jobs\Util\SystemLogger;
use App\Mail\Gateways\ACHVerificationNotification;
use App\Models\ClientGatewayToken;
use App\Models\GatewayType;
use App\Models\Payment;
@ -47,7 +50,7 @@ class ACH
return render('gateways.stripe.ach.authorize', array_merge($data));
}
public function authorizeResponse($request)
public function authorizeResponse(Request $request)
{
$this->stripe->init();
@ -63,6 +66,14 @@ class ACH
$client_gateway_token = $this->storePaymentMethod($source, $request->input('method'), $customer);
$mailer = new NinjaMailerObject();
$mailer->mailable = new ACHVerificationNotification();
$mailer->company = auth('contact')->user()->client->company;
$mailer->settings = auth('contact')->user()->client->company->settings;
$mailer->to_user = auth('contact')->user();
NinjaMailerJob::dispatchNow($mailer);
return redirect()->route('client.payment_methods.verification', ['payment_method' => $client_gateway_token->hashed_id, 'method' => GatewayType::BANK_TRANSFER]);
}

View File

@ -209,8 +209,14 @@ trait DesignHelpers
$javascript = 'document.querySelectorAll("tbody > tr > td").forEach(t=>{if(""!==t.innerText){let e=t.getAttribute("data-ref").slice(0,-3);document.querySelector(`th[data-ref="${e}-th"]`).removeAttribute("hidden")}}),document.querySelectorAll("tbody > tr > td").forEach(t=>{let e=t.getAttribute("data-ref").slice(0,-3);(e=document.querySelector(`th[data-ref="${e}-th"]`)).hasAttribute("hidden")&&""==t.innerText&&t.setAttribute("hidden","true")});';
// Previously we've been decoding the HTML on the backend and XML parsing isn't good options because it requires,
// strict & valid HTML to even output/decode. Decoding is now done on the frontend with this piece of Javascript.
$html_decode = 'document.querySelectorAll(`[data-state="encoded-html"]`).forEach((element) => element.innerHTML = element.innerText)';
return ['element' => 'div', 'elements' => [
['element' => 'script', 'content' => $javascript],
['element' => 'script', 'content' => $html_decode],
]];
}

View File

@ -130,19 +130,14 @@ trait PdfMakerUtilities
}
if ($contains_html) {
// Support for injecting direct HTML into elements.
// Example: Without documentFragment(): <b>Hello!</b> will result: &lt;b&gt;Hello!&lt;/b&gt;
// With document fragment we can evaluate HTML directly.
// If the element contains the HTML, we gonna display it as is. DOMDocument, is going to
// encode it for us, preventing any errors on the backend due processing stage.
// Later, we decode this using Javascript so it looks like it's normal HTML being injected.
// To get all elements that need frontend decoding, we use 'data-ref' property.
$_child = $this->document->createElement($child['element'], '');
$fragment = $this->document->createDocumentFragment();
$fragment->appendXML(
strtr($child['content'], ['&' => '&amp;'])
);
$_child->appendChild($fragment);
$_child->setAttribute('data-state', 'encoded-html');
$_child->nodeValue = htmlspecialchars($child['content']);
} else {
// .. in case string doesn't contain any HTML, we'll just return
// raw $content.

View File

@ -214,6 +214,10 @@ class HtmlEngine
$data['$quote.valid_until'] = ['value' => $this->translateDate($this->entity->due_date, $this->client->date_format(), $this->entity->client->locale()), 'label' => ctrans('texts.valid_until')];
$data['$credit_amount'] = ['value' => Number::formatMoney($this->entity_calc->getTotal(), $this->client) ?: '&nbsp;', 'label' => ctrans('texts.credit_amount')];
$data['$credit_balance'] = ['value' => Number::formatMoney($this->entity->balance, $this->client) ?: '&nbsp;', 'label' => ctrans('texts.credit_balance')];
$data['$quote.custom1'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice1', $this->entity->custom_value1, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice1')];
$data['$quote.custom2'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice2', $this->entity->custom_value2, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice2')];
$data['$quote.custom3'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice3', $this->entity->custom_value3, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice3')];
$data['$quote.custom4'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice4', $this->entity->custom_value4, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice4')];
$data['$credit_number'] = &$data['$number'];
$data['$credit_no'] = &$data['$number'];

View File

@ -2138,6 +2138,35 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
boardview
BSD 2-Clause License
Copyright (c) 2020, Jacob Bonk
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
boolean_selector
build
@ -6282,6 +6311,31 @@ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
draggable_scrollbar
The MIT License (MIT)
Copyright (c) 2018 Draggable Scrollbar Authors
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
extended_image

View File

@ -10,7 +10,7 @@ const RESOURCES = {
"/": "23224b5e03519aaa87594403d54412cf",
"favicon.ico": "51636d3a390451561744c42188ccd628",
"assets/packages/material_design_icons_flutter/lib/fonts/materialdesignicons-webfont.ttf": "174c02fc4609e8fc4389f5d21f16a296",
"assets/NOTICES": "e80e999afd09f0f14597c78d582d9c7c",
"assets/NOTICES": "3bf0be7e0e4deca198e5f5c4800f9232",
"assets/fonts/MaterialIcons-Regular.otf": "1288c9e28052e028aba623321f7826ac",
"assets/AssetManifest.json": "659dcf9d1baf3aed3ab1b9c42112bf8f",
"assets/assets/images/google-icon.png": "0f118259ce403274f407f5e982e681c3",
@ -31,7 +31,7 @@ const RESOURCES = {
"assets/assets/images/payment_types/paypal.png": "8e06c094c1871376dfea1da8088c29d1",
"assets/assets/images/payment_types/maestro.png": "e533b92bfb50339fdbfa79e3dfe81f08",
"assets/FontManifest.json": "cf3c681641169319e61b61bd0277378f",
"main.dart.js": "7c03ff039c186c66a6f79f7e6d5e817c",
"main.dart.js": "589fe812bacbb3ceb0a5e0714f69a8cd",
"version.json": "e021a7a1750aa3e7d1d89b51ac9837e9"
};

252175
public/main.dart.js vendored

File diff suppressed because one or more lines are too long

251448
public/main.foss.dart.js vendored

File diff suppressed because one or more lines are too long

248079
public/main.wasm.dart.js vendored

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,14 @@
@component('email.template.master', ['design' => 'light'])
@slot('header')
@include('email.components.header', ['logo' => 'https://www.invoiceninja.com/wp-content/uploads/2015/10/logo-white-horizontal-1.png'])
@endslot
<p>Hello,</p>
<p>Connecting bank accounts require verification. Stripe will automatically sends two
small deposits for this purpose. These deposits take 1-2 business days to appear on the customer's online
statement.
</p>
<p>Thank you!</p>
@endcomponent

View File

@ -59,10 +59,12 @@
<div class="relative flex justify-center text-sm leading-5">
<h1 class="text-2xl font-bold tracking-wide bg-gray-50 px-6 py-0">
{{ ctrans('texts.total') }}: {{ \App\Utils\Number::formatMoney($price, $subscription->company) }}
{{ ctrans('texts.total') }}
: {{ \App\Utils\Number::formatMoney($price, $subscription->company) }}
@if($steps['discount_applied'])
<small class="ml-1 line-through text-gray-500">{{ \App\Utils\Number::formatMoney($subscription->price, $subscription->company) }}</small>
<small
class="ml-1 line-through text-gray-500">{{ \App\Utils\Number::formatMoney($subscription->price, $subscription->company) }}</small>
@endif
</h1>
</div>
@ -208,6 +210,10 @@
@if($steps['not_eligible'] && !is_null($steps['not_eligible']))
<h1>{{ ctrans('texts.payment_error') }}</h1>
@if($steps['not_eligible_message'])
<small class="mt-4 block">{{ $steps['not_eligible_message'] }}</small>
@endif
@endif
</div>
</div>

View File

@ -1,83 +0,0 @@
<div>
<p class="mb-4 uppercase leading-4 tracking-wide inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-primary text-white">
One-time payments
</p>
<div class="flex items-center justify-between mt-4">
<div class="flex items-center">
<span class="mr-2 text-sm hidden md:block">{{ ctrans('texts.per_page') }}</span>
<select wire:model="per_page" class="form-select py-1 text-sm">
<option>5</option>
<option selected>10</option>
<option>15</option>
<option>20</option>
</select>
</div>
</div>
<div class="-my-2 py-2 overflow-x-auto sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8">
<div class="align-middle inline-block min-w-full overflow-hidden rounded">
<table class="min-w-full shadow rounded border border-gray-200 mt-4 credits-table">
<thead>
<tr>
<th class="px-6 py-3 text-xs font-medium leading-4 tracking-wider text-left text-white uppercase border-b border-gray-200 bg-primary">
<p role="button" wire:click="sortBy('number')" class="cursor-pointer">
{{ ctrans('texts.subscription') }}
</p>
</th>
<th class="px-6 py-3 text-xs font-medium leading-4 tracking-wider text-left text-white uppercase border-b border-gray-200 bg-primary">
<p role="button" wire:click="sortBy('number')" class="cursor-pointer">
{{ ctrans('texts.invoice') }}
</p>
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-primary text-left text-xs leading-4 font-medium text-white uppercase tracking-wider">
<p role="button" wire:click="sortBy('amount')" class="cursor-pointer">
{{ ctrans('texts.amount') }}
</p>
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-primary text-left text-xs leading-4 font-medium text-white uppercase tracking-wider">
<p role="button" wire:click="sortBy('date')" class="cursor-pointer">
{{ ctrans('texts.date') }}
</p>
</th>
</tr>
</thead>
<tbody>
@forelse($invoices as $invoice)
<tr class="bg-white group hover:bg-gray-100">
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
{{ $invoice->subscription->name }}
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
<a href="{{ route('client.invoice.show', $invoice->hashed_id) }}"
class="button-link text-primary">
{{ $invoice->number }}
</a>
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
{{ App\Utils\Number::formatMoney($invoice->amount, $invoice->client) }}
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
{{ $invoice->formatDate($invoice->date, $invoice->client->date_format()) }}
</td>
</tr>
@empty
<tr class="bg-white group hover:bg-gray-100">
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500" colspan="100%">
{{ ctrans('texts.no_results') }}
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
<div class="flex justify-center md:justify-between mt-6 mb-6">
@if($invoices->total() > 0)
<span class="text-gray-700 text-sm hidden md:block">
{{ ctrans('texts.showing_x_of', ['first' => $invoices->firstItem(), 'last' => $invoices->lastItem(), 'total' => $invoices->total()]) }}
</span>
@endif
{{ $invoices->links('portal/ninja2020/vendor/pagination') }}
</div>
</div>

View File

@ -1,9 +1,5 @@
<div>
<p class="mb-4 uppercase leading-4 tracking-wide inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-primary text-white">
Subscriptions
</p>
<div class="flex items-center justify-between mt-4">
<div class="flex items-center justify-between">
<div class="flex items-center">
<span class="mr-2 text-sm hidden md:block">{{ ctrans('texts.per_page') }}</span>
<select wire:model="per_page" class="form-select py-1 text-sm">
@ -24,6 +20,11 @@
{{ ctrans('texts.subscription') }}
</p>
</th>
<th class="px-6 py-3 text-xs font-medium leading-4 tracking-wider text-left text-white uppercase border-b border-gray-200 bg-primary">
<p role="button" wire:click="sortBy('number')" class="cursor-pointer">
{{ ctrans('texts.frequency') }}
</p>
</th>
<th class="px-6 py-3 text-xs font-medium leading-4 tracking-wider text-left text-white uppercase border-b border-gray-200 bg-primary">
<p role="button" wire:click="sortBy('number')" class="cursor-pointer">
{{ ctrans('texts.invoice') }}
@ -47,6 +48,9 @@
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
{{ $recurring_invoice->subscription->name }}
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
{{ \App\Models\RecurringInvoice::frequencyForKey($recurring_invoice->frequency_id) }}
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
<a href="{{ route('client.recurring_invoice.show', $recurring_invoice->hashed_id) }}"
class="button-link text-primary">

View File

@ -3,7 +3,7 @@
@push('head')
<meta name="show-invoice-terms" content="{{ $settings->show_accept_invoice_terms ? true : false }}">
<meta name="require-invoice-signature" content="{{ $client->company->account->hasFeature(\App\Models\Account::FEATURE_INVOICE_SETTINGS) && $settings->require_invoice_signature }}">
<meta name="require-invoice-signature" content="{{ $client->user->account->hasFeature(\App\Models\Account::FEATURE_INVOICE_SETTINGS) && $settings->require_invoice_signature }}">
<script src="https://cdn.jsdelivr.net/npm/signature_pad@2.3.2/dist/signature_pad.min.js"></script>
@endpush

View File

@ -4,7 +4,7 @@
@push('head')
<meta name="pdf-url" content="{{ $invoice->pdf_file_path() }}">
<meta name="show-invoice-terms" content="{{ $settings->show_accept_invoice_terms ? true : false }}">
<meta name="require-invoice-signature" content="{{ $client->company->account->hasFeature(\App\Models\Account::FEATURE_INVOICE_SETTINGS) && $settings->require_invoice_signature }}">
<meta name="require-invoice-signature" content="{{ $client->user->account->hasFeature(\App\Models\Account::FEATURE_INVOICE_SETTINGS) && $settings->require_invoice_signature }}">
<script src="{{ asset('js/vendor/pdf.js/pdf.min.js') }}"></script>
<script src="https://cdn.jsdelivr.net/npm/signature_pad@2.3.2/dist/signature_pad.min.js"></script>
@endpush

View File

@ -60,7 +60,7 @@
</div>
@if(is_null($invoice->subscription_id) || optional($invoice->subscription)->allow_cancellation)
<div class="bg-white shadow sm:rounded-lg mb-4 mt-4">
<div class="bg-white shadow sm:rounded-lg mt-4">
<div class="px-4 py-5 sm:p-6">
<div class="sm:flex sm:items-start sm:justify-between">
<div>
@ -86,11 +86,11 @@
@endif
@if($invoice->subscription && $invoice->subscription->allow_plan_changes)
<div class="bg-white shadow overflow-hidden px-4 py-5 lg:rounded-lg">
<div class="bg-white shadow overflow-hidden px-4 py-5 lg:rounded-lg mt-4">
<h3 class="text-lg leading-6 font-medium text-gray-900">Switch Plans:</h3>
<p class="mt-1 max-w-2xl text-sm leading-5 text-gray-500">Upgrade or downgrade your current plan.</p>
<div class="flex mt-4">
<div class="flex mt-4 space-x-2">
@foreach($invoice->subscription->service()->getPlans() as $subscription)
<a href="{{ route('client.subscription.plan_switch', ['recurring_invoice' => $invoice->hashed_id, 'target' => $subscription->hashed_id]) }}" class="border rounded px-5 py-2 hover:border-gray-800 text-sm cursor-pointer">{{ $subscription->name }}</a>
@endforeach

View File

@ -3,7 +3,6 @@
@section('body')
<div class="flex flex-col">
@livewire('subscription-invoices-table')
@livewire('subscription-recurring-invoices-table')
</div>
@endsection

View File

@ -54,7 +54,7 @@ Route::group(['middleware' => ['auth:contact', 'locale', 'check_client_existence
Route::get('payment_methods/{payment_method}/verification', 'ClientPortal\PaymentMethodController@verify')->name('payment_methods.verification');
Route::post('payment_methods/{payment_method}/verification', 'ClientPortal\PaymentMethodController@processVerification');
Route::resource('payment_methods', 'ClientPortal\PaymentMethodController'); // name = (payment_methods. index / create / show / update / destroy / edit
Route::resource('payment_methods', 'ClientPortal\PaymentMethodController')->except(['edit', 'update']); // name = (payment_methods. index / create / show / update / destroy / edit
Route::match(['GET', 'POST'], 'quotes/approve', 'ClientPortal\QuoteController@bulk')->name('quotes.bulk');
Route::get('quotes', 'ClientPortal\QuoteController@index')->name('quotes.index')->middleware('portal_enabled');