mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2024-11-09 12:42:36 +01:00
Support signing for invoice
This commit is contained in:
parent
fbf618a226
commit
3ccb33ec21
@ -747,12 +747,7 @@ class AccountController extends BaseController
|
||||
private function saveClientPortal()
|
||||
{
|
||||
$account = Auth::user()->account;
|
||||
|
||||
$account->enable_client_portal = !!Input::get('enable_client_portal');
|
||||
$account->enable_client_portal_dashboard = !!Input::get('enable_client_portal_dashboard');
|
||||
$account->enable_portal_password = !!Input::get('enable_portal_password');
|
||||
$account->send_portal_password = !!Input::get('send_portal_password');
|
||||
$account->enable_buy_now_buttons = !!Input::get('enable_buy_now_buttons');
|
||||
$account->fill(Input::all());
|
||||
|
||||
// Only allowed for pro Invoice Ninja users or white labeled self-hosted users
|
||||
if (Auth::user()->account->hasFeature(FEATURE_CLIENT_PORTAL_CSS)) {
|
||||
|
@ -198,6 +198,19 @@ class ClientPortalController extends BaseController
|
||||
return $pdfString;
|
||||
}
|
||||
|
||||
public function sign($invitationKey)
|
||||
{
|
||||
if (!$invitation = $this->invoiceRepo->findInvoiceByInvitation($invitationKey)) {
|
||||
return RESULT_FAILURE;
|
||||
}
|
||||
|
||||
$invitation->signature_base64 = Input::get('signature');
|
||||
$invitation->signature_date = date_create();
|
||||
$invitation->save();
|
||||
|
||||
return RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
public function dashboard($contactKey = false)
|
||||
{
|
||||
if ($contactKey) {
|
||||
|
@ -218,6 +218,8 @@ class InvoiceController extends BaseController
|
||||
$contact->invitation_viewed = $invitation->viewed_date && $invitation->viewed_date != '0000-00-00 00:00:00' ? $invitation->viewed_date : false;
|
||||
$contact->invitation_openend = $invitation->opened_date && $invitation->opened_date != '0000-00-00 00:00:00' ? $invitation->opened_date : false;
|
||||
$contact->invitation_status = $contact->email_error ? false : $invitation->getStatus();
|
||||
$contact->invitation_signature_svg = $invitation->signature_base64;
|
||||
$contact->invitation_signature_date = $invitation->signature_date;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -38,6 +38,7 @@ Route::post('/get_started', 'AccountController@getStarted');
|
||||
Route::group(['middleware' => 'auth:client'], function() {
|
||||
Route::get('view/{invitation_key}', 'ClientPortalController@view');
|
||||
Route::get('download/{invitation_key}', 'ClientPortalController@download');
|
||||
Route::put('sign/{invitation_key}', 'ClientPortalController@sign');
|
||||
Route::get('view', 'HomeController@viewLogo');
|
||||
Route::get('approve/{invitation_key}', 'QuoteController@approve');
|
||||
Route::get('payment/{invitation_key}/{gateway_type?}/{source_id?}', 'OnlinePaymentController@showPayment');
|
||||
|
@ -101,7 +101,9 @@ class Utils
|
||||
return false;
|
||||
}
|
||||
|
||||
return \App\Models\Account::first()->hasFeature(FEATURE_WHITE_LABEL);
|
||||
$account = \App\Models\Account::first();
|
||||
|
||||
return $account && $account->hasFeature(FEATURE_WHITE_LABEL);
|
||||
}
|
||||
|
||||
public static function getResllerType()
|
||||
|
@ -70,6 +70,15 @@ class Account extends Eloquent
|
||||
'include_item_taxes_inline',
|
||||
'start_of_week',
|
||||
'financial_year_start',
|
||||
'enable_client_portal',
|
||||
'enable_client_portal_dashboard',
|
||||
'enable_portal_password',
|
||||
'send_portal_password',
|
||||
'enable_buy_now_buttons',
|
||||
'show_accept_invoice_terms',
|
||||
'show_accept_quote_terms',
|
||||
'require_invoice_signature',
|
||||
'require_quote_signature',
|
||||
];
|
||||
|
||||
/**
|
||||
@ -1861,6 +1870,29 @@ class Account extends Eloquent
|
||||
|
||||
return $this->enabled_modules & static::$modules[$entityType];
|
||||
}
|
||||
|
||||
public function showAuthenticatePanel($invoice)
|
||||
{
|
||||
return $this->showAcceptTerms($invoice) || $this->showSignature($invoice);
|
||||
}
|
||||
|
||||
public function showAcceptTerms($invoice)
|
||||
{
|
||||
if ( ! $this->isPro() || ! $invoice->terms) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $invoice->is_quote ? $this->show_accept_quote_terms : $this->show_accept_invoice_terms;
|
||||
}
|
||||
|
||||
public function showSignature($invoice)
|
||||
{
|
||||
if ( ! $this->isPro()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $invoice->is_quote ? $this->require_quote_signature : $this->require_invoice_signature;
|
||||
}
|
||||
}
|
||||
|
||||
Account::updated(function ($account)
|
||||
|
@ -31,7 +31,8 @@
|
||||
"dropzone": "~4.3.0",
|
||||
"nouislider": "~8.5.1",
|
||||
"bootstrap-daterangepicker": "~2.1.24",
|
||||
"sweetalert2": "^5.3.8"
|
||||
"sweetalert2": "^5.3.8",
|
||||
"jSignature": "brinley/jSignature#^2.1.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"jquery": "~1.11"
|
||||
|
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AddInvoiceSignature extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('invitations', function($table)
|
||||
{
|
||||
$table->text('signature_base64')->nullable();
|
||||
$table->timestamp('signature_date')->nullable();
|
||||
});
|
||||
|
||||
Schema::table('companies', function($table)
|
||||
{
|
||||
$table->string('utm_source')->nullable();
|
||||
$table->string('utm_medium')->nullable();
|
||||
$table->string('utm_campaign')->nullable();
|
||||
$table->string('utm_term')->nullable();
|
||||
$table->string('utm_content')->nullable();
|
||||
});
|
||||
|
||||
Schema::table('payment_methods', function($table)
|
||||
{
|
||||
$table->dropForeign('payment_methods_account_gateway_token_id_foreign');
|
||||
});
|
||||
|
||||
Schema::table('payment_methods', function($table)
|
||||
{
|
||||
$table->foreign('account_gateway_token_id')->references('id')->on('account_gateway_tokens')->onDelete('cascade');
|
||||
});
|
||||
|
||||
Schema::table('payments', function($table)
|
||||
{
|
||||
$table->dropForeign('payments_payment_method_id_foreign');
|
||||
});
|
||||
|
||||
Schema::table('payments', function($table)
|
||||
{
|
||||
$table->foreign('payment_method_id')->references('id')->on('payment_methods')->onDelete('cascade');;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('invitations', function($table)
|
||||
{
|
||||
$table->dropColumn('signature_base64');
|
||||
$table->dropColumn('signature_date');
|
||||
});
|
||||
|
||||
Schema::table('companies', function($table)
|
||||
{
|
||||
$table->dropColumn('utm_source');
|
||||
$table->dropColumn('utm_medium');
|
||||
$table->dropColumn('utm_campaign');
|
||||
$table->dropColumn('utm_term');
|
||||
$table->dropColumn('utm_content');
|
||||
});
|
||||
}
|
||||
}
|
@ -80,6 +80,10 @@ elixir(function(mix) {
|
||||
bowerDir + '/bootstrap-daterangepicker/daterangepicker.js'
|
||||
], 'public/js/daterangepicker.min.js');
|
||||
|
||||
mix.scripts([
|
||||
bowerDir + '/jSignature/libs/jSignature.min.js'
|
||||
], 'public/js/jSignature.min.js');
|
||||
|
||||
mix.scripts([
|
||||
bowerDir + '/jquery/dist/jquery.js',
|
||||
bowerDir + '/jquery-ui/jquery-ui.js',
|
||||
|
4
public/css/built.css
vendored
4
public/css/built.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4
public/css/built.public.css
vendored
4
public/css/built.public.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
public/js/jSignature.min.js
vendored
Normal file
2
public/js/jSignature.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
public/js/jSignature.min.js.map
Normal file
1
public/js/jSignature.min.js.map
Normal file
File diff suppressed because one or more lines are too long
@ -2175,6 +2175,19 @@ $LANG = array(
|
||||
'created_by' => 'Created by :name',
|
||||
'modules' => 'Modules',
|
||||
'financial_year_start' => 'First Month of the Year',
|
||||
'authentication' => 'Authentication',
|
||||
'checkbox' => 'Checkbox',
|
||||
'invoice_signature' => 'Signature',
|
||||
'show_accept_invoice_terms' => 'Invoice Terms Checkbox',
|
||||
'show_accept_invoice_terms_help' => 'Require client to confirm that they accept the invoice terms.',
|
||||
'show_accept_quote_terms' => 'Quote Terms Checkbox',
|
||||
'show_accept_quote_terms_help' => 'Require client to confirm that they accept the quote terms.',
|
||||
'require_invoice_signature' => 'Invoice Signature',
|
||||
'require_invoice_signature_help' => 'Require client to provide their signature.',
|
||||
'require_quote_signature' => 'Quote Signature',
|
||||
'require_quote_signature_help' => 'Require client to provide their signature.',
|
||||
'i_agree' => 'I Agree To The Terms & Conditions',
|
||||
'sign_here' => 'Please sign here:',
|
||||
|
||||
);
|
||||
|
||||
|
@ -27,6 +27,10 @@
|
||||
{!! Former::populateField('enable_portal_password', intval($enable_portal_password)) !!}
|
||||
{!! Former::populateField('send_portal_password', intval($send_portal_password)) !!}
|
||||
{!! Former::populateField('enable_buy_now_buttons', intval($account->enable_buy_now_buttons)) !!}
|
||||
{!! Former::populateField('show_accept_invoice_terms', intval($account->show_accept_invoice_terms)) !!}
|
||||
{!! Former::populateField('show_accept_quote_terms', intval($account->show_accept_quote_terms)) !!}
|
||||
{!! Former::populateField('require_invoice_signature', intval($account->require_invoice_signature)) !!}
|
||||
{!! Former::populateField('require_quote_signature', intval($account->require_quote_signature)) !!}
|
||||
|
||||
@if (!Utils::isNinja() && !Auth::user()->account->hasFeature(FEATURE_WHITE_LABEL))
|
||||
<div class="alert alert-warning" style="font-size:larger;">
|
||||
@ -61,20 +65,71 @@
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{!! trans('texts.security') !!}</h3>
|
||||
<h3 class="panel-title">{!! trans('texts.authentication') !!}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
{!! Former::checkbox('enable_portal_password')
|
||||
->text(trans('texts.enable'))
|
||||
->help(trans('texts.enable_portal_password_help'))
|
||||
->label(trans('texts.enable_portal_password')) !!}
|
||||
<div role="tabpanel">
|
||||
<ul class="nav nav-tabs" role="tablist" style="border: none">
|
||||
<li role="presentation" class="active"><a href="#password" aria-controls="password" role="tab" data-toggle="tab">{{ trans('texts.password') }}</a></li>
|
||||
<li role="presentation"><a href="#checkbox" aria-controls="checkbox" role="tab" data-toggle="tab">{{ trans('texts.checkbox') }}</a></li>
|
||||
<li role="presentation"><a href="#signature" aria-controls="signature" role="tab" data-toggle="tab">{{ trans('texts.invoice_signature') }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
{!! Former::checkbox('send_portal_password')
|
||||
->text(trans('texts.enable'))
|
||||
->help(trans('texts.send_portal_password_help'))
|
||||
->label(trans('texts.send_portal_password')) !!}
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane active" id="password">
|
||||
<div class="panel-body">
|
||||
<div class="row">
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
{!! Former::checkbox('enable_portal_password')
|
||||
->text(trans('texts.enable'))
|
||||
->help(trans('texts.enable_portal_password_help'))
|
||||
->label(trans('texts.enable_portal_password')) !!}
|
||||
</div>
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
{!! Former::checkbox('send_portal_password')
|
||||
->text(trans('texts.enable'))
|
||||
->help(trans('texts.send_portal_password_help'))
|
||||
->label(trans('texts.send_portal_password')) !!}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane" id="checkbox">
|
||||
<div class="panel-body">
|
||||
<div class="row">
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
{!! Former::checkbox('show_accept_invoice_terms')
|
||||
->text(trans('texts.enable'))
|
||||
->help(trans('texts.show_accept_invoice_terms_help'))
|
||||
->label(trans('texts.show_accept_invoice_terms')) !!}
|
||||
</div>
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
{!! Former::checkbox('show_accept_quote_terms')
|
||||
->text(trans('texts.enable'))
|
||||
->help(trans('texts.show_accept_quote_terms_help'))
|
||||
->label(trans('texts.show_accept_quote_terms')) !!}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane" id="signature">
|
||||
<div class="panel-body">
|
||||
<div class="row">
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
{!! Former::checkbox('require_invoice_signature')
|
||||
->text(trans('texts.enable'))
|
||||
->help(trans('texts.require_invoice_signature_help'))
|
||||
->label(trans('texts.require_invoice_signature')) !!}
|
||||
</div>
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
{!! Former::checkbox('require_quote_signature')
|
||||
->text(trans('texts.enable'))
|
||||
->help(trans('texts.require_quote_signature_help'))
|
||||
->label(trans('texts.require_quote_signature')) !!}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -120,11 +120,13 @@
|
||||
<span data-bind="visible: !$root.invoice().is_recurring()">
|
||||
<span data-bind="html: $data.view_as_recipient"></span>
|
||||
@if (Utils::isConfirmed())
|
||||
<span style="vertical-align:text-top;color:red" class="fa fa-exclamation-triangle"
|
||||
data-bind="visible: $data.email_error, tooltip: {title: $data.email_error}"></span>
|
||||
<span style="vertical-align:text-top" class="glyphicon glyphicon-info-sign"
|
||||
data-bind="visible: $data.invitation_status, tooltip: {title: $data.invitation_status, html: true},
|
||||
style: {color: $data.info_color}"></span>
|
||||
<span style="vertical-align:text-top;color:red" class="fa fa-exclamation-triangle"
|
||||
data-bind="visible: $data.email_error, tooltip: {title: $data.email_error}"></span>
|
||||
<span style="vertical-align:text-top" class="fa fa-info-circle"
|
||||
data-bind="visible: $data.invitation_status, tooltip: {title: $data.invitation_status, html: true},
|
||||
style: {color: $data.info_color}"></span>
|
||||
<span style="vertical-align:text-top" class="fa fa-user-circle"
|
||||
data-bind="visible: $data.invitation_signature_svg, tooltip: {title: '', html: true}"></span>
|
||||
@endif
|
||||
</span>
|
||||
@endif
|
||||
@ -563,7 +565,7 @@
|
||||
</div>
|
||||
<p> </p>
|
||||
|
||||
@if (Auth::user()->account->live_preview))
|
||||
@if (Auth::user()->account->live_preview)
|
||||
@include('invoices.pdf', ['account' => Auth::user()->account])
|
||||
@else
|
||||
<script type="text/javascript">
|
||||
|
@ -596,6 +596,8 @@ function ContactModel(data) {
|
||||
self.invitation_openend = ko.observable(false);
|
||||
self.invitation_viewed = ko.observable(false);
|
||||
self.email_error = ko.observable('');
|
||||
self.invitation_signature_svg = ko.observable('');
|
||||
self.invitation_signature_date = ko.observable('');
|
||||
|
||||
if (data) {
|
||||
ko.mapping.fromJS(data, {}, this);
|
||||
|
@ -6,20 +6,31 @@
|
||||
@include('money_script')
|
||||
|
||||
@foreach ($invoice->client->account->getFontFolders() as $font)
|
||||
<script src="{{ asset('js/vfs_fonts/'.$font.'.js') }}" type="text/javascript"></script>
|
||||
<script src="{{ asset('js/vfs_fonts/'.$font.'.js') }}" type="text/javascript"></script>
|
||||
@endforeach
|
||||
|
||||
<script src="{{ asset('pdf.built.js') }}?no_cache={{ NINJA_VERSION }}" type="text/javascript"></script>
|
||||
|
||||
@if ($account->showSignature($invoice))
|
||||
<script src="{{ asset('js/jSignature.min.js') }}"></script>
|
||||
@endif
|
||||
|
||||
<style type="text/css">
|
||||
body {
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
|
||||
|
||||
.dropdown-menu li a{
|
||||
overflow:hidden;
|
||||
margin-top:5px;
|
||||
margin-bottom:5px;
|
||||
}
|
||||
|
||||
#signature {
|
||||
border: 2px dotted black;
|
||||
background-color:lightgrey;
|
||||
}
|
||||
</style>
|
||||
|
||||
@if (!empty($transactionToken) && $accountGateway->gateway_id == GATEWAY_BRAINTREE)
|
||||
@ -99,7 +110,7 @@
|
||||
@if (!empty($partialView))
|
||||
@include($partialView)
|
||||
@else
|
||||
<div class="pull-right" style="text-align:right">
|
||||
<div id="paymentButtons" class="pull-right" style="text-align:right">
|
||||
@if ($invoice->isQuote())
|
||||
{!! Button::normal(trans('texts.download_pdf'))->withAttributes(['onclick' => 'onDownloadClick()'])->large() !!}
|
||||
@if ($showApprove)
|
||||
@ -191,6 +202,33 @@
|
||||
@else
|
||||
refreshPDF();
|
||||
@endif
|
||||
|
||||
@if ($account->showAuthenticatePanel($invoice))
|
||||
$('#paymentButtons a').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
window.pendingPaymentHref = $(this).attr('href');
|
||||
@if ($account->showSignature($invoice))
|
||||
if (window.pendingPaymentInit) {
|
||||
$("#signature").jSignature('reset');
|
||||
}
|
||||
@endif
|
||||
@if ($account->showAcceptTerms($invoice))
|
||||
$('#termsCheckbox').attr('checked', false);
|
||||
@endif
|
||||
$('#authenticationModal').modal('show');
|
||||
});
|
||||
|
||||
@if ($account->showSignature($invoice))
|
||||
$('#authenticationModal').on('shown.bs.modal', function () {
|
||||
if ( ! window.pendingPaymentInit) {
|
||||
window.pendingPaymentInit = true;
|
||||
$("#signature").jSignature().bind('change', function(e) {
|
||||
setModalPayNowEnabled();
|
||||
});;
|
||||
}
|
||||
});
|
||||
@endif
|
||||
@endif
|
||||
});
|
||||
|
||||
function onDownloadClick() {
|
||||
@ -203,6 +241,48 @@
|
||||
$('#customGatewayModal').modal('show');
|
||||
}
|
||||
|
||||
function onModalPayNowClick() {
|
||||
@if ($account->showSignature($invoice))
|
||||
var data = {
|
||||
signature: $('#signature').jSignature('getData', 'svgbase64')[1]
|
||||
};
|
||||
$.ajax({
|
||||
url: "{{ URL::to('sign/' . $invitation->invitation_key) }}",
|
||||
type: 'PUT',
|
||||
data: data,
|
||||
success: function(response) {
|
||||
redirectToPayment();
|
||||
}
|
||||
});
|
||||
@else
|
||||
redirectToPayment();
|
||||
@endif
|
||||
}
|
||||
|
||||
function redirectToPayment() {
|
||||
$('#authenticationModal').modal('hide');
|
||||
location.href = window.pendingPaymentHref;
|
||||
}
|
||||
|
||||
function setModalPayNowEnabled() {
|
||||
var disabled = false;
|
||||
|
||||
@if ($account->showAcceptTerms($invoice))
|
||||
if ( ! $('#termsCheckbox').is(':checked')) {
|
||||
disabled = true;
|
||||
}
|
||||
@endif
|
||||
|
||||
@if ($account->showSignature($invoice))
|
||||
if ( ! $('#signature').jSignature('isModified')) {
|
||||
disabled = true;
|
||||
}
|
||||
@endif
|
||||
|
||||
$('#modalPayNowButton').attr('disabled', disabled);
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
@include('invoices.pdf', ['account' => $invoice->client->account, 'viewPDF' => true])
|
||||
@ -232,4 +312,42 @@
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($account->showAuthenticatePanel($invoice))
|
||||
<div class="modal fade" id="authenticationModal" tabindex="-1" role="dialog" aria-labelledby="authenticationModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h4 class="modal-title"> </h4>
|
||||
</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<div class="well">
|
||||
{!! nl2br(e($invoice->terms)) !!}
|
||||
</div>
|
||||
@if ($account->showSignature($invoice))
|
||||
<div>
|
||||
{{ trans('texts.sign_here') }}
|
||||
</div>
|
||||
<div id="signature"></div><br/>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
@if ($account->showAcceptTerms($invoice))
|
||||
<div class="pull-left">
|
||||
<label for="termsCheckbox" style="font-weight:normal">
|
||||
<input id="termsCheckbox" type="checkbox" onclick="setModalPayNowEnabled()"/>
|
||||
{{ trans('texts.i_agree') }}
|
||||
</label>
|
||||
</div>
|
||||
@endif
|
||||
<button id="modalPayNowButton" type="button" class="btn btn-success" onclick="onModalPayNowClick()" disabled="">{{ trans('texts.pay_now') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@stop
|
||||
|
Loading…
Reference in New Issue
Block a user