mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-05-24 02:14:21 -04:00
Client Portal conditionals (#3039)
* Fixes for tests * add additional fields for company settings * fixes for travis * update company settings schema * Disable client portal * Client Portal middleware * Working on client portal * hide portal * Implement notification channgels for User and ClientContact models * Push notifications onto queue * Force authentication if client portal is password protected
This commit is contained in:
parent
ea2f15d1a9
commit
b7d3f4e7aa
@ -26,6 +26,13 @@ class CompanySettings extends BaseSettings
|
||||
public $auto_archive_invoice = false;
|
||||
public $lock_sent_invoices = false;
|
||||
|
||||
public $enable_client_portal_tasks = false;
|
||||
public $enable_client_portal_password = false;
|
||||
public $enable_client_portal = true; //implemented
|
||||
public $enable_client_portal_dashboard = true; //implemented
|
||||
public $signature_on_pdf = false;
|
||||
public $document_email_attachment = false;
|
||||
public $send_portal_password = false;
|
||||
|
||||
public $timezone_id = '';
|
||||
public $date_format_id = '';
|
||||
@ -52,7 +59,6 @@ class CompanySettings extends BaseSettings
|
||||
|
||||
public $payment_terms = 1;
|
||||
public $send_reminders = false;
|
||||
public $show_tasks_in_portal = false;
|
||||
|
||||
public $custom_message_dashboard = '';
|
||||
public $custom_message_unpaid_invoice = '';
|
||||
@ -62,6 +68,7 @@ class CompanySettings extends BaseSettings
|
||||
public $auto_convert_quote = false;
|
||||
|
||||
public $inclusive_taxes = false;
|
||||
public $quote_footer = '';
|
||||
|
||||
public $translations;
|
||||
|
||||
@ -104,7 +111,7 @@ class CompanySettings extends BaseSettings
|
||||
|
||||
|
||||
public $shared_invoice_quote_counter = false;
|
||||
public $recurring_invoice_number_prefix = 'R';
|
||||
public $recurring_number_prefix = 'R';
|
||||
public $reset_counter_frequency_id = '0';
|
||||
public $reset_counter_date = '';
|
||||
public $counter_padding = 4;
|
||||
@ -130,7 +137,6 @@ class CompanySettings extends BaseSettings
|
||||
public $custom_fields = '';
|
||||
public $invoice_fields = '';
|
||||
|
||||
public $enable_portal_password = false;
|
||||
public $show_accept_invoice_terms = false;
|
||||
public $show_accept_quote_terms = false;
|
||||
public $require_invoice_signature = false;
|
||||
@ -147,9 +153,11 @@ class CompanySettings extends BaseSettings
|
||||
public $email_subject_invoice = '';
|
||||
public $email_subject_quote = '';
|
||||
public $email_subject_payment = '';
|
||||
public $email_subject_statement = '';
|
||||
public $email_template_invoice = '';
|
||||
public $email_template_quote = '';
|
||||
public $email_template_payment = '';
|
||||
public $email_template_statement = '';
|
||||
public $email_subject_reminder1 = '';
|
||||
public $email_subject_reminder2 = '';
|
||||
public $email_subject_reminder3 = '';
|
||||
@ -158,7 +166,8 @@ class CompanySettings extends BaseSettings
|
||||
public $email_template_reminder2 = '';
|
||||
public $email_template_reminder3 = '';
|
||||
public $email_template_reminder_endless = '';
|
||||
public $email_footer = '';
|
||||
public $email_signature = '';
|
||||
public $enable_email_markup = true;
|
||||
|
||||
/* Company Meta data that we can use to build sub companies*/
|
||||
|
||||
@ -187,6 +196,16 @@ class CompanySettings extends BaseSettings
|
||||
|
||||
|
||||
public static $casts = [
|
||||
'document_email_attachment' => 'bool',
|
||||
'enable_client_portal_password' => 'bool',
|
||||
'enable_email_markup' => 'bool',
|
||||
'enable_client_portal_dashboard' => 'bool',
|
||||
'enable_client_portal' => 'bool',
|
||||
'email_template_statement' => 'string',
|
||||
'email_subject_statement' => 'string',
|
||||
'signature_on_pdf' => 'bool',
|
||||
'send_portal_password' => 'bool',
|
||||
'quote_footer' => 'string',
|
||||
'page_size' => 'string',
|
||||
'font_size' => 'int',
|
||||
'primary_font' => 'string',
|
||||
@ -243,7 +262,7 @@ class CompanySettings extends BaseSettings
|
||||
'custom_message_unapproved_quote' => 'string',
|
||||
'custom_fields' => 'string',
|
||||
'default_task_rate' => 'float',
|
||||
'email_footer' => 'string',
|
||||
'email_signature' => 'string',
|
||||
'email_subject_invoice' => 'string',
|
||||
'email_subject_quote' => 'string',
|
||||
'email_subject_payment' => 'string',
|
||||
@ -258,7 +277,7 @@ class CompanySettings extends BaseSettings
|
||||
'email_template_reminder2' => 'string',
|
||||
'email_template_reminder3' => 'string',
|
||||
'email_template_reminder_endless' => 'string',
|
||||
'enable_portal_password' => 'bool',
|
||||
'enable_client_portal_password' => 'bool',
|
||||
'inclusive_taxes' => 'bool',
|
||||
'invoice_number_prefix' => 'string',
|
||||
'invoice_number_pattern' => 'string',
|
||||
@ -280,7 +299,7 @@ class CompanySettings extends BaseSettings
|
||||
'quote_number_pattern' => 'string',
|
||||
'quote_number_counter' => 'integer',
|
||||
'quote_terms' => 'string',
|
||||
'recurring_invoice_number_prefix' => 'string',
|
||||
'recurring_number_prefix' => 'string',
|
||||
'reset_counter_frequency_id' => 'integer',
|
||||
'reset_counter_date' => 'string',
|
||||
'require_invoice_signature' => 'bool',
|
||||
@ -304,7 +323,7 @@ class CompanySettings extends BaseSettings
|
||||
'language_id' => 'string',
|
||||
'show_currency_code' => 'bool',
|
||||
'send_reminders' => 'bool',
|
||||
'show_tasks_in_portal' => 'bool',
|
||||
'enable_client_portal_tasks' => 'bool',
|
||||
'lock_sent_invoices' => 'bool',
|
||||
'auto_archive_invoice' => 'bool',
|
||||
'auto_archive_quote' => 'bool',
|
||||
|
@ -12,6 +12,7 @@
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\ClientContact;
|
||||
use Auth;
|
||||
use Illuminate\Foundation\Auth\AuthenticatesUsers;
|
||||
use Illuminate\Http\Request;
|
||||
@ -26,7 +27,7 @@ class ContactLoginController extends Controller
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('guest:contact', ['except' => ['logout']]);
|
||||
$this->middleware('guest:contact', ['except' => ['logout']]);
|
||||
}
|
||||
|
||||
public function showLoginForm()
|
||||
@ -60,7 +61,16 @@ class ContactLoginController extends Controller
|
||||
return $this->sendFailedLoginResponse($request);
|
||||
}
|
||||
|
||||
public function authenticated(Request $request, ClientContact $client)
|
||||
{
|
||||
|
||||
Auth::guard('contact')->login($client, true);
|
||||
|
||||
if(session()->get('url.intended'))
|
||||
return redirect(session()->get('url.intended'));
|
||||
|
||||
return redirect()->intended();
|
||||
}
|
||||
|
||||
public function logout()
|
||||
{
|
||||
|
@ -32,13 +32,19 @@ class InvitationController extends Controller
|
||||
public function invoiceRouter(string $invitation_key)
|
||||
{
|
||||
|
||||
// $invitation = InvoiceInvitation::whereInvitationKey($invitation_key)->first();
|
||||
$invitation = InvoiceInvitation::whereRaw("BINARY `invitation_key`= ?", [$invitation_key])->first();
|
||||
|
||||
if($invitation){
|
||||
$invitation->markViewed();
|
||||
Auth::guard('contact')->loginUsingId($invitation->client_contact_id, true);
|
||||
return redirect()->route('client.invoice.show', ['invoice' => $this->encodePrimaryKey($invitation->invoice_id)]);
|
||||
|
||||
\Log::error("bool val = ".boolval($invitation->contact->client->getSetting('enable_client_portal_password')));
|
||||
|
||||
if((bool)$invitation->contact->client->getSetting('enable_client_portal_password') !== false)
|
||||
$this->middleware('auth:contact');
|
||||
|
||||
$invitation->markViewed();
|
||||
|
||||
return redirect()->route('client.invoice.show', ['invoice' => $this->encodePrimaryKey($invitation->invoice_id)]);
|
||||
|
||||
}
|
||||
else
|
||||
abort(404);
|
||||
|
@ -17,6 +17,7 @@ use App\Models\RecurringInvoice;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Yajra\DataTables\Facades\DataTables;
|
||||
use Yajra\DataTables\Html\Builder;
|
||||
|
||||
@ -80,8 +81,24 @@ class RecurringInvoiceController extends Controller
|
||||
];
|
||||
|
||||
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
|
||||
|
||||
Mail::to(config('ninja.contact.ninja_official_contact'))
|
||||
->send(new RecurringCancellationRequest($invoice));
|
||||
|
||||
return view('portal.default.recurring_invoices.request_cancellation', $data);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -21,7 +21,7 @@
|
||||
* @OA\Property(property="custom_invoice_taxes4", type="boolean", example=true, description="____________"),
|
||||
* @OA\Property(property="default_task_rate", type="number", format="float", example="10.00", description="____________"),
|
||||
* @OA\Property(property="send_reminders", type="boolean", example=true, description="____________"),
|
||||
* @OA\Property(property="show_tasks_in_portal", type="boolean", example=true, description="____________"),
|
||||
* @OA\Property(property="enable_client_portal_tasks", type="boolean", example=true, description="____________"),
|
||||
* @OA\Property(property="email_style", type="string", example="light", description="options include plain,light,dark,custom"),
|
||||
* @OA\Property(property="reply_to_email", type="string", example="email@gmail.com", description="The reply to email address"),
|
||||
* @OA\Property(property="bcc_email", type="string", example="email@gmail.com, contact@gmail.com", description="A comma separate list of BCC emails"),
|
||||
@ -134,6 +134,16 @@
|
||||
* @OA\Property(property="embed_documents", type="boolean", example=false, description="____________"),
|
||||
* @OA\Property(property="all_pages_header", type="boolean", example=false, description="____________"),
|
||||
* @OA\Property(property="all_pages_footer", type="boolean", example=false, description="____________"),
|
||||
* @OA\Property(property="document_email_attachment", type="boolean", example=false, description="____________"),
|
||||
* @OA\Property(property="enable_client_portal_password", type="boolean", example=false, description="____________"),
|
||||
* @OA\Property(property="enable_email_markup", type="boolean", example=false, description="____________"),
|
||||
* @OA\Property(property="enable_client_portal_dashboard", type="boolean", example=false, description="____________"),
|
||||
* @OA\Property(property="enable_client_portal", type="boolean", example=false, description="____________"),
|
||||
* @OA\Property(property="email_template_statement", type="string", example='template matter', description="____________"),
|
||||
* @OA\Property(property="email_subject_statement", type="string", example='subject matter', description="____________"),
|
||||
* @OA\Property(property="signature_on_pdf", type="boolean", example=false, description="____________"),
|
||||
* @OA\Property(property="send_portal_password", type="boolean", example=false, description="____________"),
|
||||
* @OA\Property(property="quote_footer", type="string", example='the quote footer', description="____________"),
|
||||
* )
|
||||
*/
|
||||
|
||||
|
@ -107,5 +107,6 @@ class Kernel extends HttpKernel
|
||||
'domain_db' => \App\Http\Middleware\SetDomainNameDb::class,
|
||||
'password_protected' => \App\Http\Middleware\PasswordProtection::class,
|
||||
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
|
||||
'portal_enabled' => \App\Http\Middleware\ClientPortalEnabled::class,
|
||||
];
|
||||
}
|
||||
|
36
app/Http/Middleware/ClientPortalEnabled.php
Normal file
36
app/Http/Middleware/ClientPortalEnabled.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com)
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2019. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://opensource.org/licenses/AAL
|
||||
*/
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Models\User;
|
||||
use Closure;
|
||||
|
||||
class ClientPortalEnabled
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
|
||||
if(auth()->user()->client->getSetting('enable_client_portal') === false)
|
||||
return redirect()->to('client/dashboard');
|
||||
|
||||
|
||||
return $next($request);
|
||||
|
||||
}
|
||||
}
|
@ -29,7 +29,7 @@ class RedirectIfAuthenticated
|
||||
switch ($guard) {
|
||||
case 'contact':
|
||||
if (Auth::guard($guard)->check()) {
|
||||
return redirect()->route('client.dashboard');
|
||||
return redirect()->route('client.dashboard');
|
||||
}
|
||||
break;
|
||||
case 'user':
|
||||
|
@ -49,6 +49,9 @@ class PortalComposer
|
||||
$data['countries'] = TranslationHelper::getCountries();
|
||||
$data['company'] = auth()->user()->company;
|
||||
$data['client'] = auth()->user()->client;
|
||||
$data['settings'] = auth()->user()->client->getMergedSettings();
|
||||
|
||||
//\Log::error(print_r($data['settings'],1));
|
||||
|
||||
return $data;
|
||||
|
||||
|
@ -17,11 +17,13 @@ use App\Jobs\Company\CreateCompanyToken;
|
||||
use App\Jobs\User\CreateUser;
|
||||
use App\Models\Account;
|
||||
use App\Models\User;
|
||||
use App\Notifications\NewAccountCreated;
|
||||
use App\Utils\Traits\UserSessionAttributes;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
|
||||
class CreateAccount
|
||||
{
|
||||
@ -89,6 +91,9 @@ class CreateAccount
|
||||
if($user)
|
||||
event(new AccountCreated($user));
|
||||
|
||||
Notification::route('slack', config('ninja.notification.slack'))
|
||||
->notify(new NewAccountCreated($user, $company));
|
||||
|
||||
return $account;
|
||||
}
|
||||
}
|
||||
|
@ -62,6 +62,7 @@ class CreateUser
|
||||
$user->fill($this->request);
|
||||
$user->email = $this->request['email'];//todo need to remove this in production
|
||||
$user->last_login = now();
|
||||
$user->ip = request()->ip();
|
||||
$user->save();
|
||||
|
||||
$user->companies()->attach($this->company->id, [
|
||||
|
35
app/Mail/RecurringCancellationRequest.php
Normal file
35
app/Mail/RecurringCancellationRequest.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class RecurringCancellationRequest extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public $recurring_invoice
|
||||
|
||||
public function __construct(RecurringInvoice $recurring_invoice)
|
||||
{
|
||||
$this->recurring_invoice = $recurring_invoice
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the message.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
return $this->view('view.name');
|
||||
}
|
||||
}
|
@ -47,7 +47,7 @@ class SupportMessageSent extends Mailable
|
||||
$log_lines = iterator_to_array($lines);
|
||||
}
|
||||
|
||||
return $this->from(config('mail.from.address'))
|
||||
return $this->from(config('mail.from.address')) //todo this needs to be fixed to handle the hosted version
|
||||
->subject(ctrans('texts.new_support_message'))
|
||||
->markdown('email.support.message', [
|
||||
'message' => $this->message,
|
||||
|
@ -12,20 +12,22 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Company;
|
||||
use App\Models\Language;
|
||||
use App\Models\User;
|
||||
use App\Notifications\ClientContactResetPassword as ResetPasswordNotification;
|
||||
use App\Notifications\ClientContactResetPassword;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use Hashids\Hashids;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Contracts\Translation\HasLocalePreference;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Laracasts\Presenter\PresentableTrait;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Laracasts\Presenter\PresentableTrait;
|
||||
|
||||
|
||||
class ClientContact extends Authenticatable
|
||||
class ClientContact extends Authenticatable implements HasLocalePreference
|
||||
{
|
||||
use Notifiable;
|
||||
use MakesHash;
|
||||
@ -133,4 +135,11 @@ class ClientContact extends Authenticatable
|
||||
{
|
||||
$this->notify(new ClientContactResetPassword($token));
|
||||
}
|
||||
|
||||
public function preferredLocale()
|
||||
{
|
||||
$lang = Language::find($this->client->getSetting('language_id'));
|
||||
|
||||
return $lang->locale;
|
||||
}
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ class InvoiceInvitation extends BaseModel
|
||||
*/
|
||||
public function contact()
|
||||
{
|
||||
return $this->belongsTo(ClientContact::class)->withTrashed();
|
||||
return $this->belongsTo(ClientContact::class,'client_contact_id','id')->withTrashed();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -15,11 +15,13 @@ use App\Models\Company;
|
||||
use App\Models\CompanyToken;
|
||||
use App\Models\CompanyUser;
|
||||
use App\Models\Filterable;
|
||||
use App\Models\Language;
|
||||
use App\Models\Traits\UserTrait;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use App\Utils\Traits\UserSessionAttributes;
|
||||
use App\Utils\Traits\UserSettings;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Contracts\Translation\HasLocalePreference;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
@ -28,7 +30,7 @@ use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Laracasts\Presenter\PresentableTrait;
|
||||
|
||||
class User extends Authenticatable implements MustVerifyEmail
|
||||
class User extends Authenticatable implements MustVerifyEmail,HasLocalePreference
|
||||
{
|
||||
use Notifiable;
|
||||
use SoftDeletes;
|
||||
@ -62,8 +64,8 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||
protected $fillable = [
|
||||
'first_name',
|
||||
'last_name',
|
||||
// 'email',
|
||||
// 'phone',
|
||||
'email',
|
||||
'phone',
|
||||
'signature',
|
||||
'avatar',
|
||||
'accepted_terms_version',
|
||||
@ -323,4 +325,19 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
public function routeNotificationForSlack($notification)
|
||||
{
|
||||
//todo need to return the company channel here for hosted users
|
||||
//else the env variable for selfhosted
|
||||
return config('ninja.notification.slack');
|
||||
}
|
||||
|
||||
public function preferredLocale()
|
||||
{
|
||||
$lang = Language::find($this->company()->settings->language_id);
|
||||
|
||||
return $lang->locale;
|
||||
}
|
||||
}
|
||||
|
||||
|
84
app/Notifications/NewAccountCreated.php
Normal file
84
app/Notifications/NewAccountCreated.php
Normal file
@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Messages\SlackMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class NewAccountCreated extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
/**
|
||||
* Create a new notification instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
|
||||
protected $user;
|
||||
|
||||
protected $company;
|
||||
|
||||
public function __construct($user, $company)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->company = $company;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the notification's delivery channels.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
* @return array
|
||||
*/
|
||||
public function via($notifiable)
|
||||
{
|
||||
return ['slack'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mail representation of the notification.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
* @return \Illuminate\Notifications\Messages\MailMessage
|
||||
*/
|
||||
public function toMail($notifiable)
|
||||
{
|
||||
return (new MailMessage)
|
||||
->line('The introduction to the notification.')
|
||||
->action('Notification Action', url('/'))
|
||||
->line('Thank you for using our application!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array representation of the notification.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
* @return array
|
||||
*/
|
||||
public function toArray($notifiable)
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public function toSlack($notifiable)
|
||||
{
|
||||
|
||||
$user_name = $this->user->first_name . " " . $this->user->last_name;
|
||||
$email = $this->user->email;
|
||||
$ip = $this->user->ip;
|
||||
|
||||
return (new SlackMessage)
|
||||
->success()
|
||||
->to("#devv2")
|
||||
->from("System")
|
||||
->image('https://app.invoiceninja.com/favicon.png')
|
||||
->content("A new account has been created by {$user_name} - {$email} - from IP: {$ip}");
|
||||
}
|
||||
|
||||
}
|
@ -27,6 +27,7 @@
|
||||
"intervention/image": "^2.4",
|
||||
"laracasts/presenter": "^0.2.1",
|
||||
"laravel/framework": "6.0.*",
|
||||
"laravel/slack-notification-channel": "^2.0",
|
||||
"laravel/socialite": "^4.0",
|
||||
"laravel/tinker": "^1.0",
|
||||
"laravelcollective/html": "6.0.*",
|
||||
|
@ -84,6 +84,9 @@ return [
|
||||
//'frequencies' => 'App\Models\Frequency',
|
||||
//'fonts' => 'App\Models\Font',
|
||||
],
|
||||
'notification' => [
|
||||
'slack' => env('SLACK_WEBHOOK_URL',''),
|
||||
],
|
||||
'payment_terms' => [
|
||||
[
|
||||
'num_days' => 0,
|
||||
|
@ -230,6 +230,8 @@ class CreateUsersTable extends Migration
|
||||
$table->string('first_name')->nullable();
|
||||
$table->string('last_name')->nullable();
|
||||
$table->string('phone')->nullable();
|
||||
$table->string('ip')->nullable();
|
||||
$table->string('device_token')->nullable();
|
||||
$table->string('email',100)->unique();
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->string('confirmation_code')->nullable();
|
||||
|
@ -3,6 +3,7 @@
|
||||
@section('body')
|
||||
<main class="main">
|
||||
<div class="container-fluid">
|
||||
@if($settings->enable_client_portal_dashboard == 'true')
|
||||
<div class="row" style="margin-top: 30px;">
|
||||
<div class="col-6 col-lg-4">
|
||||
<div class="card">
|
||||
@ -40,6 +41,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
<!-- client and supplier information -->
|
||||
<div class="row">
|
||||
<div class="col-sm-6 col-md-6">
|
||||
|
@ -56,4 +56,12 @@
|
||||
@yield('body')
|
||||
@include('portal.default.footer')
|
||||
@yield('footer')
|
||||
</html>
|
||||
</html>
|
||||
|
||||
<script type="text/javascript">
|
||||
@if($settings->enable_client_portal === false)
|
||||
$('.navbar-toggler-icon').hide();
|
||||
$('.app').removeClass("sidebar-lg-show");
|
||||
$('.app').addClass("sidebar-hidden");
|
||||
@endif
|
||||
</script>
|
@ -0,0 +1,40 @@
|
||||
@extends('portal.default.layouts.master')
|
||||
@section('header')
|
||||
@stop
|
||||
@section('body')
|
||||
<main class="main">
|
||||
<div class="container-fluid">
|
||||
<div class="row" style="padding-top: 30px;">
|
||||
<div class="col d-flex justify-content-center">
|
||||
<div class="card w-50 p-10">
|
||||
<div class="card-header">
|
||||
Request Cancellation
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-responsive-sm table-bordered">
|
||||
<tr><td style="text-align: right;">{{ctrans('texts.start_date')}}</td><td>{!! $invoice->start_date !!}</td></tr>
|
||||
<tr><td style="text-align: right;">{{ctrans('texts.next_send_date')}}</td><td>{!! $invoice->next_send_date !!}</td></tr>
|
||||
<tr><td style="text-align: right;">{{ctrans('texts.frequency')}}</td><td>{!! App\Models\RecurringInvoice::frequencyForKey($invoice->frequency_id) !!}</td></tr>
|
||||
<tr><td style="text-align: right;">{{ctrans('texts.cycles_remaining')}}</td><td>{!! $invoice->remaining_cycles !!}</td></tr>
|
||||
<tr><td style="text-align: right;">{{ctrans('texts.amount')}}</td><td>{!! $invoice->amount !!}</td></tr>
|
||||
|
||||
</table>
|
||||
|
||||
<div class="alert alert-primary" role="alert">Cancellation pending, we'll be in touch!</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
</body>
|
||||
@endsection
|
||||
@push('css')
|
||||
@endpush
|
||||
@push('scripts')
|
||||
@endpush
|
||||
@section('footer')
|
||||
@endsection
|
||||
|
@ -28,7 +28,7 @@
|
||||
|
||||
@if($invoice->remaining_cycles >=1)
|
||||
<div class="pull-right">
|
||||
<button class="btn btn-primary">Cancel</button>
|
||||
<button class="btn btn-danger mb-1" type="button" data-toggle="modal" data-target="#cancel_recurring">Request Cancellation</button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@ -38,6 +38,27 @@
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div class="modal fade show" id="cancel_recurring" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" style="display: none;" aria-hidden="true">
|
||||
<div class="modal-dialog modal-danger" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Request Cancellation</h4>
|
||||
<button class="close" type="button" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Warning! You are requesting a cancellation of this service.</p>
|
||||
<p>Your service may be cancelled with no further notification to you.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" type="button" data-dismiss="modal">Close</button>
|
||||
<a href="{{ route('client.recurring_invoices.request_cancellation',['recurring_invoice' => $invoice->hashed_id]) }}" class="btn btn-danger">Confirm Cancellation</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
@endsection
|
||||
@push('css')
|
||||
|
@ -16,15 +16,16 @@ Route::group(['middleware' => ['auth:contact'], 'prefix' => 'client', 'as' => 'c
|
||||
|
||||
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');
|
||||
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('recurring_invoices', 'ClientPortal\RecurringInvoiceController@index')->name('recurring_invoices.index');
|
||||
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');
|
||||
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');
|
||||
|
@ -72,7 +72,7 @@ class AccountTest extends TestCase
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'X-API-SECRET' => config('ninja.api_secret'),
|
||||
])->post('/api/v1/signup', $data);
|
||||
])->post('/api/v1/signup?include=account', $data);
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
|
@ -57,7 +57,7 @@ class ClientTest extends TestCase
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'X-API-SECRET' => config('ninja.api_secret'),
|
||||
])->post('/api/v1/signup', $data);
|
||||
])->post('/api/v1/signup?include=account', $data);
|
||||
|
||||
|
||||
$response->assertStatus(200);
|
||||
@ -98,7 +98,7 @@ class ClientTest extends TestCase
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'X-API-SECRET' => config('ninja.api_secret'),
|
||||
])->post('/api/v1/signup', $data);
|
||||
])->post('/api/v1/signup?include=account', $data);
|
||||
|
||||
$acc = $response->json();
|
||||
|
||||
|
@ -128,7 +128,7 @@ class CompanySettingsTest extends TestCase
|
||||
$settings->require_quote_signature = true;
|
||||
$settings->show_accept_quote_terms = false;
|
||||
$settings->show_accept_invoice_terms = "TRUE";
|
||||
$settings->show_tasks_in_portal = "FALSE";
|
||||
$settings->enable_client_portal_tasks = "FALSE";
|
||||
|
||||
$this->company->settings = $settings;
|
||||
|
||||
|
@ -58,7 +58,7 @@ class CompanyTest extends TestCase
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'X-API-SECRET' => config('ninja.api_secret'),
|
||||
])->post('/api/v1/signup', $data);
|
||||
])->post('/api/v1/signup?include=account', $data);
|
||||
|
||||
|
||||
$response->assertStatus(200);
|
||||
@ -80,7 +80,7 @@ class CompanyTest extends TestCase
|
||||
$response = $this->withHeaders([
|
||||
'X-API-SECRET' => config('ninja.api_secret'),
|
||||
'X-API-TOKEN' => $token,
|
||||
])->post('/api/v1/companies/',
|
||||
])->post('/api/v1/companies?include=company',
|
||||
[
|
||||
'name' => 'A New Company',
|
||||
'logo' => UploadedFile::fake()->image('avatar.jpg')
|
||||
|
@ -61,7 +61,7 @@ class InvoiceTest extends TestCase
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'X-API-SECRET' => config('ninja.api_secret'),
|
||||
])->post('/api/v1/signup', $data);
|
||||
])->post('/api/v1/signup?include=account', $data);
|
||||
|
||||
$acc = $response->json();
|
||||
|
||||
|
@ -58,7 +58,7 @@ class PaymentTest extends TestCase
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'X-API-SECRET' => config('ninja.api_secret'),
|
||||
])->post('/api/v1/signup', $data);
|
||||
])->post('/api/v1/signup?include=account', $data);
|
||||
|
||||
$acc = $response->json();
|
||||
|
||||
@ -122,7 +122,7 @@ class PaymentTest extends TestCase
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'X-API-SECRET' => config('ninja.api_secret'),
|
||||
])->post('/api/v1/signup', $data);
|
||||
])->post('/api/v1/signup?include=account', $data);
|
||||
|
||||
$acc = $response->json();
|
||||
|
||||
|
@ -55,7 +55,7 @@ class ProductTest extends TestCase
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'X-API-SECRET' => config('ninja.api_secret'),
|
||||
])->post('/api/v1/signup', $data);
|
||||
])->post('/api/v1/signup?include=account', $data);
|
||||
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
@ -57,7 +57,7 @@ class QuoteTest extends TestCase
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'X-API-SECRET' => config('ninja.api_secret'),
|
||||
])->post('/api/v1/signup', $data);
|
||||
])->post('/api/v1/signup?include=account', $data);
|
||||
|
||||
$acc = $response->json();
|
||||
|
||||
@ -121,7 +121,7 @@ class QuoteTest extends TestCase
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'X-API-SECRET' => config('ninja.api_secret'),
|
||||
])->post('/api/v1/signup', $data);
|
||||
])->post('/api/v1/signup?include=account', $data);
|
||||
|
||||
$acc = $response->json();
|
||||
|
||||
|
@ -57,7 +57,7 @@ class RecurringInvoiceTest extends TestCase
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'X-API-SECRET' => config('ninja.api_secret'),
|
||||
])->post('/api/v1/signup', $data);
|
||||
])->post('/api/v1/signup?include=account', $data);
|
||||
|
||||
$acc = $response->json();
|
||||
|
||||
@ -121,7 +121,7 @@ class RecurringInvoiceTest extends TestCase
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'X-API-SECRET' => config('ninja.api_secret'),
|
||||
])->post('/api/v1/signup', $data);
|
||||
])->post('/api/v1/signup?include=account', $data);
|
||||
|
||||
$acc = $response->json();
|
||||
$account = Account::find($this->decodePrimaryKey($acc['data'][0]['account']['id']));
|
||||
|
@ -57,7 +57,7 @@ class RecurringQuoteTest extends TestCase
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'X-API-SECRET' => config('ninja.api_secret'),
|
||||
])->post('/api/v1/signup', $data);
|
||||
])->post('/api/v1/signup?include=account', $data);
|
||||
|
||||
$acc = $response->json();
|
||||
|
||||
@ -121,7 +121,7 @@ class RecurringQuoteTest extends TestCase
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'X-API-SECRET' => config('ninja.api_secret'),
|
||||
])->post('/api/v1/signup', $data);
|
||||
])->post('/api/v1/signup?include=account', $data);
|
||||
|
||||
$acc = $response->json();
|
||||
|
||||
|
@ -56,7 +56,7 @@ class UserTest extends TestCase
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'X-API-SECRET' => config('ninja.api_secret'),
|
||||
])->post('/api/v1/signup', $data);
|
||||
])->post('/api/v1/signup?include=account', $data);
|
||||
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
Loading…
x
Reference in New Issue
Block a user