diff --git a/Gruntfile.js b/Gruntfile.js index 7b687181b2..9fec5aee6c 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -31,8 +31,7 @@ module.exports = function(grunt) { 'public/vendor/spectrum/spectrum.js', 'public/js/bootstrap-combobox.js', 'public/vendor/jspdf/dist/jspdf.min.js', - //'public/js/jspdf.source.js', - //'public/js/jspdf.plugin.split_text_to_size.js', + 'public/vendor/lightbox2/js/lightbox.min.js', 'public/js/script.js', ], dest: 'public/built.js', @@ -58,6 +57,7 @@ module.exports = function(grunt) { 'public/vendor/spectrum/spectrum.css', 'public/css/bootstrap-combobox.css', 'public/css/typeahead.js-bootstrap.css', + 'public/vendor/lightbox2/css/lightbox.css', 'public/css/style.css', ], dest: 'public/built.css', diff --git a/app/controllers/AccountController.php b/app/controllers/AccountController.php index 550b60aa1a..37b3b45fee 100755 --- a/app/controllers/AccountController.php +++ b/app/controllers/AccountController.php @@ -272,7 +272,7 @@ class AccountController extends \BaseController { $invoice = new stdClass(); $client = new stdClass(); $invoiceItem = new stdClass(); - + $client->name = 'Sample Client'; $client->address1 = ''; $client->city = ''; @@ -280,10 +280,10 @@ class AccountController extends \BaseController { $client->postal_code = ''; $client->work_phone = ''; $client->work_email = ''; - + $invoice->invoice_number = Auth::user()->account->getNextInvoiceNumber(); $invoice->invoice_date = date_create()->format('Y-m-d'); - $invoice->account = Auth::user()->account; + $invoice->account = json_decode(Auth::user()->account->toJson()); $invoice->amount = $invoice->balance = 100; $invoiceItem->cost = 100; @@ -295,7 +295,7 @@ class AccountController extends \BaseController { $invoice->invoice_items = [$invoiceItem]; $data['invoice'] = $invoice; - $data['invoiceDesigns'] = InvoiceDesign::remember(DEFAULT_QUERY_CACHE)->orderBy('id')->get(); + $data['invoiceDesigns'] = InvoiceDesign::remember(DEFAULT_QUERY_CACHE, 'invoice_designs_cache')->where('id', '<=', Auth::user()->maxInvoiceDesignId())->orderBy('id')->get(); } return View::make("accounts.{$subSection}", $data); diff --git a/app/controllers/InvoiceController.php b/app/controllers/InvoiceController.php index 197db91f41..fda9f643b8 100755 --- a/app/controllers/InvoiceController.php +++ b/app/controllers/InvoiceController.php @@ -243,7 +243,7 @@ class InvoiceController extends \BaseController { 'sizes' => Size::remember(DEFAULT_QUERY_CACHE)->orderBy('id')->get(), 'paymentTerms' => PaymentTerm::remember(DEFAULT_QUERY_CACHE)->orderBy('num_days')->get(['name', 'num_days']), 'industries' => Industry::remember(DEFAULT_QUERY_CACHE)->orderBy('name')->get(), - 'invoiceDesigns' => InvoiceDesign::remember(DEFAULT_QUERY_CACHE)->orderBy('id')->get(), + 'invoiceDesigns' => InvoiceDesign::remember(DEFAULT_QUERY_CACHE, 'invoice_designs_cache')->where('id', '<=', Auth::user()->maxInvoiceDesignId())->orderBy('id')->get(), 'frequencies' => array( 1 => 'Weekly', 2 => 'Two weeks', diff --git a/app/controllers/PaymentController.php b/app/controllers/PaymentController.php index 1bf0f3c15a..1e0e9f734c 100755 --- a/app/controllers/PaymentController.php +++ b/app/controllers/PaymentController.php @@ -19,16 +19,16 @@ class PaymentController extends \BaseController $this->contactMailer = $contactMailer; } - public function index() - { + public function index() + { return View::make('list', array( 'entityType'=>ENTITY_PAYMENT, 'title' => trans('texts.payments'), 'columns'=>Utils::trans(['checkbox', 'invoice', 'client', 'transaction_reference', 'method', 'payment_amount', 'payment_date', 'action']) )); - } + } - public function getDatatable($clientPublicId = null) + public function getDatatable($clientPublicId = null) { $payments = $this->paymentRepo->find($clientPublicId, Input::get('sSearch')); $table = Datatable::query($payments); @@ -47,7 +47,7 @@ class PaymentController extends \BaseController ->addColumn('payment_type', function($model) { return $model->payment_type ? $model->payment_type : ($model->account_gateway_id ? 'Online payment' : ''); }); return $table->addColumn('amount', function($model) { return Utils::formatMoney($model->amount, $model->currency_id); }) - ->addColumn('payment_date', function($model) { return Utils::dateToString($model->payment_date); }) + ->addColumn('payment_date', function($model) { return Utils::dateToString($model->payment_date); }) ->addColumn('dropdown', function($model) { return '
'; }) - ->make(); + ->make(); } @@ -132,13 +132,13 @@ class PaymentController extends \BaseController return $gateway; } - private function getLicensePaymentDetails($input) + private function getLicensePaymentDetails($input, $affiliate) { $data = self::convertInputForOmnipay($input); $card = new CreditCard($data); return [ - 'amount' => LICENSE_PRICE, + 'amount' => $affiliate->price, 'card' => $card, 'currency' => 'USD', 'returnUrl' => URL::to('license_complete'), @@ -172,7 +172,7 @@ class PaymentController extends \BaseController private function getPaymentDetails($invoice, $input = null) { $key = $invoice->invoice_number . '_details'; - $gateway = $invoice->client->account->account_gateways[0]->gateway; + $gateway = $invoice->client->account->account_gateways[0]->gateway; $paymentLibrary = $gateway->paymentlibrary; if ($input && $paymentLibrary->id == PAYMENT_LIBRARY_OMNIPAY) @@ -181,9 +181,9 @@ class PaymentController extends \BaseController Session::put($key, $data); } - else if ($input && $paymentLibrary->id == PAYMENT_LIBRARY_PHP_PAYMENTS) + else if ($input && $paymentLibrary->id == PAYMENT_LIBRARY_PHP_PAYMENTS) { - $input = Input::all(); + $input = Input::all(); $data = [ 'first_name' => $input['first_name'], 'last_name' => $input['last_name'], @@ -200,29 +200,29 @@ class PaymentController extends \BaseController 'ship_to_city' => $input['city'], 'ship_to_state' => $input['state'], 'ship_to_postal_code' => $input['postal_code'], - 'currency_code' => $invoice->client->currency->code, + 'currency_code' => $invoice->client->currency->code, ]; - - switch($gateway->id) - { - case GATEWAY_BEANSTREAM: - $data['phone'] = $input['phone']; - $data['email'] = $input['email']; - $data['country'] = $input['country']; - $data['ship_to_country'] = $input['country']; - break; - case GATEWAY_BRAINTREE: - $data['ship_to_state'] = 'Ohio'; //$input['state']; - break; - } - - if(strlen($data['cc_exp']) == 5) - { - $data['cc_exp'] = '0'.$data['cc_exp']; - } + + switch($gateway->id) + { + case GATEWAY_BEANSTREAM: + $data['phone'] = $input['phone']; + $data['email'] = $input['email']; + $data['country'] = $input['country']; + $data['ship_to_country'] = $input['country']; + break; + case GATEWAY_BRAINTREE: + $data['ship_to_state'] = 'Ohio'; //$input['state']; + break; + } + + if(strlen($data['cc_exp']) == 5) + { + $data['cc_exp'] = '0'.$data['cc_exp']; + } Session::put($key, $data); - return $data; + return $data; } else if (Session::get($key)) { @@ -233,22 +233,22 @@ class PaymentController extends \BaseController $data = []; } - if($paymentLibrary->id == PAYMENT_LIBRARY_OMNIPAY) - { - $card = new CreditCard($data); - - return [ - 'amount' => $invoice->amount, - 'card' => $card, - 'currency' => $invoice->client->currency->code, - 'returnUrl' => URL::to('complete'), - 'cancelUrl' => URL::to('/') - ]; - } - else - { - return $data; - } + if($paymentLibrary->id == PAYMENT_LIBRARY_OMNIPAY) + { + $card = new CreditCard($data); + + return [ + 'amount' => $invoice->amount, + 'card' => $card, + 'currency' => $invoice->client->currency->code, + 'returnUrl' => URL::to('complete'), + 'cancelUrl' => URL::to('/') + ]; + } + else + { + return $data; + } } public function show_payment($invitationKey) @@ -306,16 +306,18 @@ class PaymentController extends \BaseController { if ($affiliate = Affiliate::where('affiliate_key', '=', Input::get('affiliate_key'))->first()) { - Session::set('affiliate_id', $affiliate->id); + Session::set('affiliate_id', $affiliate->id); } } + Session::set('product_id', Input::get('product_id', PRODUCT_ONE_CLICK_INSTALL)); + if (!Session::get('affiliate_id')) { return Utils::fatalError(); } - if (Input::has('test_mode')) + if (Utils::isNinjaDev() && Input::has('test_mode')) { Session::set('test_mode', Input::get('test_mode')); } @@ -334,7 +336,7 @@ class PaymentController extends \BaseController 'showBreadcrumbs' => false, 'hideHeader' => true, 'url' => 'license', - 'amount' => LICENSE_PRICE, + 'amount' => $affiliate->price, 'client' => false, 'contact' => false, 'paymentLibrary' => $paymentLibrary, @@ -380,6 +382,8 @@ class PaymentController extends \BaseController try { + $affiliate = Affiliate::find(Session::get('affiliate_id')); + if ($testMode) { $ref = 'TEST_MODE'; @@ -387,7 +391,7 @@ class PaymentController extends \BaseController else { $gateway = self::createGateway($accountGateway); - $details = self::getLicensePaymentDetails(Input::all()); + $details = self::getLicensePaymentDetails(Input::all(), $affiliate); $response = $gateway->purchase($details)->send(); $ref = $response->getTransactionReference(); @@ -415,10 +419,9 @@ class PaymentController extends \BaseController $license->transaction_reference = $ref; $license->license_key = $licenseKey; $license->affiliate_id = Session::get('affiliate_id'); + $license->product_id = Session::get('product_id'); $license->save(); - $affiliate = Affiliate::find(Session::get('affiliate_id')); - $data = [ 'message' => $affiliate->payment_subtitle, 'license' => $licenseKey, @@ -426,11 +429,16 @@ class PaymentController extends \BaseController ]; $name = "{$license->first_name} {$license->last_name}"; - $this->contactMailer->sendLicensePaymentConfirmation($name, $license->email, LICENSE_PRICE, $license->license_key); + $this->contactMailer->sendLicensePaymentConfirmation($name, $license->email, $affiliate->price, $license->license_key, $license->product_id); - return View::make('public.license', $data); - - //return Redirect::away(Session::get('return_url') . "?license_key={$license->license_key}"); + if (Session::has('return_url')) + { + return Redirect::away(Session::get('return_url') . "?license_key={$license->license_key}&product_id=" . Session::get('product_id')); + } + else + { + return View::make('public.license', $data); + } } catch (\Exception $e) { @@ -443,8 +451,13 @@ class PaymentController extends \BaseController public function claim_license() { - $license = License::where('license_key', '=', Input::get('license_key')) - ->where('is_claimed', '=', false)->first(); + $licenseKey = Input::get('license_key'); + $productId = Input::get('product_id', PRODUCT_ONE_CLICK_INSTALL); + + $license = License::where('license_key', '=', $licenseKey) + ->where('is_claimed', '=', false) + ->where('product_id', '=', $productId) + ->first(); if ($license) { @@ -454,7 +467,7 @@ class PaymentController extends \BaseController $license->save(); } - return 'valid'; + return $productId == PRODUCT_ONE_CLICK_INSTALL ? 'valid' : $_ENV['INVOICE_DESIGNS']; } else { @@ -503,84 +516,84 @@ class PaymentController extends \BaseController $client->postal_code = trim(Input::get('postal_code')); $client->save(); } - + try { - if($paymentLibrary->id == PAYMENT_LIBRARY_OMNIPAY) - { - $gateway = self::createGateway($accountGateway); - $details = self::getPaymentDetails($invoice, Input::all()); - $response = $gateway->purchase($details)->send(); - $ref = $response->getTransactionReference(); - - if (!$ref) - { - Session::flash('error', $response->getMessage()); - return Redirect::to('payment/' . $invitationKey) - ->withInput(); - } + if($paymentLibrary->id == PAYMENT_LIBRARY_OMNIPAY) + { + $gateway = self::createGateway($accountGateway); + $details = self::getPaymentDetails($invoice, Input::all()); + $response = $gateway->purchase($details)->send(); + $ref = $response->getTransactionReference(); + + if (!$ref) + { + Session::flash('error', $response->getMessage()); + return Redirect::to('payment/' . $invitationKey) + ->withInput(); + } - if ($response->isSuccessful()) - { - $payment = self::createPayment($invitation, $ref); - - Session::flash('message', trans('texts.applied_payment')); - return Redirect::to('view/' . $payment->invitation->invitation_key); - } - else if ($response->isRedirect()) - { - $invitation->transaction_reference = $ref; - $invitation->save(); - - $response->redirect(); - } - else - { - Session::flash('error', $response->getMessage()); - return Utils::fatalError('Sorry, there was an error processing your payment. Please try again later.', $response->getMessage()); - } + if ($response->isSuccessful()) + { + $payment = self::createPayment($invitation, $ref); + + Session::flash('message', trans('texts.applied_payment')); + return Redirect::to('view/' . $payment->invitation->invitation_key); + } + else if ($response->isRedirect()) + { + $invitation->transaction_reference = $ref; + $invitation->save(); + + $response->redirect(); + } + else + { + Session::flash('error', $response->getMessage()); + return Utils::fatalError('Sorry, there was an error processing your payment. Please try again later.
', $response->getMessage()); + } } - else if ($paymentLibrary->id == PAYMENT_LIBRARY_PHP_PAYMENTS) - { - $gateway = $accountGateway->gateway; - $provider = $gateway->provider; - $p = new PHP_Payments(array('mode' => 'test')); - - $config = Payment_Utility::load('config', 'drivers/'.$provider); - - switch($gateway->id) - { - case GATEWAY_BEANSTREAM: - $config['delay_charge'] = FALSE; - $config['bill_outstanding'] = TRUE; - break; - case GATEWAY_AMAZON: - $config['return_url'] = URL::to('complete'); - $config['abandon_url'] = URL::to('/'); - $config['immediate_return'] = 0; - $config['process_immediate'] = 1; - $config['ipn_url'] = URL::to('ipn'); - $config['collect_shipping_address'] = false; - break; - } - - $details = self::getPaymentDetails($invoice, Input::all()); - - $response = $p->oneoff_payment($provider, $details, $config); + else if ($paymentLibrary->id == PAYMENT_LIBRARY_PHP_PAYMENTS) + { + $gateway = $accountGateway->gateway; + $provider = $gateway->provider; + $p = new PHP_Payments(array('mode' => 'test')); + + $config = Payment_Utility::load('config', 'drivers/'.$provider); + + switch($gateway->id) + { + case GATEWAY_BEANSTREAM: + $config['delay_charge'] = FALSE; + $config['bill_outstanding'] = TRUE; + break; + case GATEWAY_AMAZON: + $config['return_url'] = URL::to('complete'); + $config['abandon_url'] = URL::to('/'); + $config['immediate_return'] = 0; + $config['process_immediate'] = 1; + $config['ipn_url'] = URL::to('ipn'); + $config['collect_shipping_address'] = false; + break; + } + + $details = self::getPaymentDetails($invoice, Input::all()); + + $response = $p->oneoff_payment($provider, $details, $config); - if (strtolower($response->status) == 'success') - { - $payment = self::createPayment($invitation, $response->response_message); - - Session::flash('message', trans('texts.applied_payment')); - return Redirect::to('view/' . $payment->invitation->invitation_key); - } - else - { - Session::flash('error', $response->response_message); - return Utils::fatalError('Sorry, there was an error processing your payment. Please try again later.
', $response->response_message); - } - } + if (strtolower($response->status) == 'success') + { + $payment = self::createPayment($invitation, $response->response_message); + + Session::flash('message', trans('texts.applied_payment')); + return Redirect::to('view/' . $payment->invitation->invitation_key); + } + else + { + Session::flash('error', $response->response_message); + return Utils::fatalError('Sorry, there was an error processing your payment. Please try again later.
', $response->response_message); + } + } } catch (\Exception $e) { @@ -715,4 +728,4 @@ class PaymentController extends \BaseController return Redirect::to('payments'); } -} +} \ No newline at end of file diff --git a/app/controllers/QuoteController.php b/app/controllers/QuoteController.php index 683cc0bf00..497e7cc74a 100644 --- a/app/controllers/QuoteController.php +++ b/app/controllers/QuoteController.php @@ -95,7 +95,7 @@ class QuoteController extends \BaseController { 'sizes' => Size::remember(DEFAULT_QUERY_CACHE)->orderBy('id')->get(), 'paymentTerms' => PaymentTerm::remember(DEFAULT_QUERY_CACHE)->orderBy('num_days')->get(['name', 'num_days']), 'industries' => Industry::remember(DEFAULT_QUERY_CACHE)->orderBy('name')->get(), - 'invoiceDesigns' => InvoiceDesign::remember(DEFAULT_QUERY_CACHE)->orderBy('id')->get(), + 'invoiceDesigns' => InvoiceDesign::remember(DEFAULT_QUERY_CACHE, 'invoice_designs_cache')->where('id', '<=', Auth::user()->maxInvoiceDesignId())->orderBy('id')->get(), 'invoiceLabels' => Auth::user()->account->getInvoiceLabels() ]; } diff --git a/app/database/migrations/2014_10_22_174452_add_affiliate_price.php b/app/database/migrations/2014_10_22_174452_add_affiliate_price.php new file mode 100644 index 0000000000..305ad1f421 --- /dev/null +++ b/app/database/migrations/2014_10_22_174452_add_affiliate_price.php @@ -0,0 +1,44 @@ +decimal('price', 7, 2)->nullable(); + }); + + Schema::table('licenses', function($table) + { + $table->unsignedInteger('product_id')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('affiliates', function($table) + { + $table->dropColumn('price'); + }); + + Schema::table('licenses', function($table) + { + $table->dropColumn('product_id'); + }); + } + +} diff --git a/app/filters.php b/app/filters.php index 9c252974fa..dd667f9600 100755 --- a/app/filters.php +++ b/app/filters.php @@ -77,6 +77,34 @@ App::before(function($request) $locale = Session::get(SESSION_LOCALE, DEFAULT_LOCALE); App::setLocale($locale); } + + $claimingLicense = Utils::startsWith($_SERVER['REQUEST_URI'], '/claim_license'); + if (!$claimingLicense && Input::has('license_key') && Input::has('product_id')) + { + $licenseKey = Input::get('license_key'); + $productId = Input::get('product_id'); + + if ($productId == PRODUCT_INVOICE_DESIGNS) + { + //$data = file_get_contents("http://ninja.dev/claim_license?license_key={$licenseKey}&product_id={$productId}"); + $data = file_get_contents(NINJA_URL . "/claim_license?license_key={$licenseKey}&product_id={$productId}"); + + if ($data = json_decode($data)) + { + foreach ($data as $item) + { + $design = new InvoiceDesign(); + $design->id = $item->id; + $design->name = $item->name; + $design->javascript = $item->javascript; + $design->save(); + } + + Cache::forget('invoice_designs_cache'); + Session::flash('message', trans('texts.bought_designs')); + } + } + } }); diff --git a/app/lang/en/texts.php b/app/lang/en/texts.php index 876457dc22..28228cf0fe 100644 --- a/app/lang/en/texts.php +++ b/app/lang/en/texts.php @@ -449,4 +449,14 @@ return array( 'gateway_help_23' => 'Note: use your secret API key, not your publishable API key.', 'gateway_help_27' => ':link to sign up for TwoCheckout.', + 'more_designs' => 'More designs', + 'more_designs_title' => 'Additional Invoice Designs', + 'more_designs_cloud_header' => 'Go Pro for more invoice designs', + 'more_designs_cloud_text' => '', + 'more_designs_self_host_header' => 'Get 6 more invoice designs for just $20', + 'more_designs_self_host_text' => '', + 'buy' => 'Buy', + 'bought_designs' => 'Successfully added additional invoice designs', + + ); \ No newline at end of file diff --git a/app/models/User.php b/app/models/User.php index 60e7500c9d..d46b7160c5 100755 --- a/app/models/User.php +++ b/app/models/User.php @@ -85,6 +85,11 @@ class User extends ConfideUser implements UserInterface, RemindableInterface return $this->account->id == Utils::getDemoAccountId(); } + public function maxInvoiceDesignId() + { + return $this->isPro() ? 10 : COUNT_FREE_DESIGNS; + } + public function getDisplayName() { if ($this->getFullName()) diff --git a/app/ninja/mailers/ContactMailer.php b/app/ninja/mailers/ContactMailer.php index 271edef7ad..fd9da56aa4 100755 --- a/app/ninja/mailers/ContactMailer.php +++ b/app/ninja/mailers/ContactMailer.php @@ -37,7 +37,7 @@ class ContactMailer extends Mailer { 'contactName' => $invitation->contact->getDisplayName(), 'invoiceAmount' => Utils::formatMoney($invoice->amount, $invoice->client->currency_id), 'emailFooter' => $invoice->account->email_footer, - 'showNinjaFooter' => !$invoice->account->isPro() || !Utils::isNinjaProd() + 'showNinjaFooter' => !$invoice->account->isPro() ]; $fromEmail = $invitation->user->email; @@ -67,25 +67,31 @@ class ContactMailer extends Mailer { 'clientName' => $payment->client->getDisplayName(), 'emailFooter' => $payment->account->email_footer, 'paymentAmount' => Utils::formatMoney($payment->amount, $payment->client->currency_id), - 'showNinjaFooter' => !$payment->account->isPro() || !Utils::isNinjaProd() + 'showNinjaFooter' => !$payment->account->isPro() ]; $user = $payment->invitation->user; $this->sendTo($payment->contact->email, $user->email, $user->getDisplayName(), $subject, $view, $data); } - public function sendLicensePaymentConfirmation($name, $email, $amount, $license) + public function sendLicensePaymentConfirmation($name, $email, $amount, $license, $productId) { $view = 'payment_confirmation'; $subject = trans('texts.payment_subject'); + if ($productId == PRODUCT_ONE_CLICK_INSTALL) { + $message = "Softaculous install license: $license"; + } else if ($productId == PRODUCT_INVOICE_DESIGNS) { + $message = "Invoice designs license: $license"; + } + $data = [ 'accountName' => trans('texts.email_from'), 'clientName' => $name, 'emailFooter' => false, 'paymentAmount' => Utils::formatMoney($amount, 1), 'showNinjaFooter' => false, - 'emailMessage' => "Your license: $license", + 'emailMessage' => $message, ]; $this->sendTo($email, CONTACT_EMAIL, CONTACT_NAME, $subject, $view, $data); diff --git a/app/routes.php b/app/routes.php index f41f95a687..5cab648897 100755 --- a/app/routes.php +++ b/app/routes.php @@ -13,7 +13,7 @@ */ //apc_clear_cache(); -//Cache::flush(); +Cache::flush(); //dd(DB::getQueryLog()); //dd(Client::getPrivateId(1)); @@ -248,8 +248,11 @@ define('NINJA_URL', 'https://www.invoiceninja.com'); define('NINJA_VERSION', '1.4.0'); define('RELEASES_URL', 'https://github.com/hillelcoren/invoice-ninja/releases/'); +define('COUNT_FREE_DESIGNS', 4); define('PRO_PLAN_PRICE', 50); -define('LICENSE_PRICE', 30.00); +define('PRODUCT_ONE_CLICK_INSTALL', 1); +define('PRODUCT_INVOICE_DESIGNS', 2); +define('DESIGNS_AFFILIATE_KEY', 'T3RS74'); define('USER_TYPE_SELF_HOST', 'SELF_HOST'); define('USER_TYPE_CLOUD_HOST', 'CLOUD_HOST'); diff --git a/app/views/accounts/invoice_design.blade.php b/app/views/accounts/invoice_design.blade.php index b004dd7865..de76b6db24 100644 --- a/app/views/accounts/invoice_design.blade.php +++ b/app/views/accounts/invoice_design.blade.php @@ -17,7 +17,13 @@ function getDesignJavascript() { var id = $('#invoice_design_id').val(); - return invoiceDesigns[id-1].javascript; + if (id == '-1') { + showMoreDesigns(); + $('#invoice_design_id').val(1); + return invoiceDesigns[0].javascript; + } else { + return invoiceDesigns[id-1].javascript; + } } function getPDFString() { @@ -64,8 +70,15 @@ {{ Former::populateField('hide_paid_to_date', intval($account->hide_paid_to_date)) }} {{ Former::legend('invoice_design') }} - {{ Former::select('invoice_design_id')->style('display:inline;width:120px') - ->fromQuery($invoiceDesigns, 'name', 'id') }} + + + @if (InvoiceDesign::count() == COUNT_FREE_DESIGNS) + {{ Former::select('invoice_design_id')->style('display:inline;width:120px')->fromQuery($invoiceDesigns, 'name', 'id')->addOption(trans('texts.more_designs') . '...', '-1') }} + @else + {{ Former::select('invoice_design_id')->style('display:inline;width:120px')->fromQuery($invoiceDesigns, 'name', 'id') }} + @endif + + {{ Former::text('primary_color') }} {{ Former::text('secondary_color') }} diff --git a/app/views/invoices/edit.blade.php b/app/views/invoices/edit.blade.php index fea5a414b4..fa51355c6c 100755 --- a/app/views/invoices/edit.blade.php +++ b/app/views/invoices/edit.blade.php @@ -265,11 +265,12 @@ + @if (InvoiceDesign::count() == COUNT_FREE_DESIGNS) + {{ Former::select('invoice_design_id')->style('display:inline;width:150px')->raw()->fromQuery($invoiceDesigns, 'name', 'id')->data_bind("value: invoice_design_id")->addOption(trans('texts.more_designs') . '...', '-1') }} + @else + {{ Former::select('invoice_design_id')->style('display:inline;width:150px')->raw()->fromQuery($invoiceDesigns, 'name', 'id')->data_bind("value: invoice_design_id") }} + @endif - {{ Former::select('invoice_design_id')->style('display:inline;width:120px')->raw() - ->fromQuery($invoiceDesigns, 'name', 'id')->data_bind("value: invoice_design_id") }} - - {{ Button::primary(trans('texts.download_pdf'), array('onclick' => 'onDownloadClick()'))->append_with_icon('download-alt'); }} @if (!$invoice || (!$invoice->trashed() && !$invoice->client->trashed())) @@ -646,6 +647,7 @@ function getPDFString() { var invoice = createInvoiceModel(); var design = getDesignJavascript(); + if (!design) return; var doc = generatePDF(invoice, design); if (!doc) return; return doc.output('datauristring'); @@ -653,13 +655,21 @@ function getDesignJavascript() { var id = $('#invoice_design_id').val(); - return invoiceDesigns[id-1].javascript; + if (id == '-1') { + showMoreDesigns(); + $('#invoice_design_id').val(1); + return invoiceDesigns[0].javascript; + } else { + return invoiceDesigns[id-1].javascript; + } } function onDownloadClick() { trackUrl('/download_pdf'); var invoice = createInvoiceModel(); - var doc = generatePDF(invoice, true); + var design = getDesignJavascript(); + if (!design) return; + var doc = generatePDF(invoice, design, true); doc.save('Invoice-' + $('#invoice_number').val() + '.pdf'); } diff --git a/app/views/invoices/pdf.blade.php b/app/views/invoices/pdf.blade.php index d5f8635828..4ab59ba3a3 100644 --- a/app/views/invoices/pdf.blade.php +++ b/app/views/invoices/pdf.blade.php @@ -1,6 +1,54 @@ +
+ + + \ No newline at end of file diff --git a/bower.json b/bower.json index 281fc64a96..b5b19c0677 100644 --- a/bower.json +++ b/bower.json @@ -17,7 +17,8 @@ "typeahead.js": "~0.9.3", "accounting": "~0.*", "spectrum": "~1.3.4", - "d3": "~3.4.11" + "d3": "~3.4.11", + "lightbox2": "~2.7.1" }, "resolutions": { "jquery": "~1.11" diff --git a/public/built.css b/public/built.css index e081951a12..7660eed314 100644 --- a/public/built.css +++ b/public/built.css @@ -1773,6 +1773,217 @@ See http://bgrins.github.io/spectrum/themes/ for instructions. border-radius: 6px; line-height: 1.33; } +/* Preload images */ +body:after { + content: url(images/lightbox/close.png) url(images/lightbox/loading.gif) url(images/lightbox/prev.png) url(images/lightbox/next.png); + display: none; +} + +.lightboxOverlay { + position: absolute; + top: 0; + left: 0; + z-index: 9999; + background-color: black; + filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=80); + opacity: 0.8; + display: none; +} + +.lightbox { + position: absolute; + left: 0; + width: 100%; + z-index: 10000; + text-align: center; + line-height: 0; + font-weight: normal; +} + +.lightbox .lb-image { + display: block; + height: auto; + max-width: inherit; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + -ms-border-radius: 3px; + -o-border-radius: 3px; + border-radius: 3px; +} + +.lightbox a img { + border: none; +} + +.lb-outerContainer { + position: relative; + background-color: white; + *zoom: 1; + width: 250px; + height: 250px; + margin: 0 auto; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + -ms-border-radius: 4px; + -o-border-radius: 4px; + border-radius: 4px; +} + +.lb-outerContainer:after { + content: ""; + display: table; + clear: both; +} + +.lb-container { + padding: 4px; +} + +.lb-loader { + position: absolute; + top: 43%; + left: 0; + height: 25%; + width: 100%; + text-align: center; + line-height: 0; +} + +.lb-cancel { + display: block; + width: 32px; + height: 32px; + margin: 0 auto; + background: url(images/lightbox/loading.gif) no-repeat; +} + +.lb-nav { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + z-index: 10; +} + +.lb-container > .nav { + left: 0; +} + +.lb-nav a { + outline: none; + background-image: url('data:image/gif;base64,R0lGODlhAQABAPAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=='); +} + +.lb-prev, .lb-next { + height: 100%; + cursor: pointer; + display: block; +} + +.lb-nav a.lb-prev { + width: 34%; + left: 0; + float: left; + background: url(images/lightbox/prev.png) left 48% no-repeat; + filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=0); + opacity: 0; + -webkit-transition: opacity 0.6s; + -moz-transition: opacity 0.6s; + -o-transition: opacity 0.6s; + transition: opacity 0.6s; +} + +.lb-nav a.lb-prev:hover { + filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=100); + opacity: 1; +} + +.lb-nav a.lb-next { + width: 64%; + right: 0; + float: right; + background: url(images/lightbox/next.png) right 48% no-repeat; + filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=0); + opacity: 0; + -webkit-transition: opacity 0.6s; + -moz-transition: opacity 0.6s; + -o-transition: opacity 0.6s; + transition: opacity 0.6s; +} + +.lb-nav a.lb-next:hover { + filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=100); + opacity: 1; +} + +.lb-dataContainer { + margin: 0 auto; + padding-top: 5px; + *zoom: 1; + width: 100%; + -moz-border-radius-bottomleft: 4px; + -webkit-border-bottom-left-radius: 4px; + border-bottom-left-radius: 4px; + -moz-border-radius-bottomright: 4px; + -webkit-border-bottom-right-radius: 4px; + border-bottom-right-radius: 4px; +} + +.lb-dataContainer:after { + content: ""; + display: table; + clear: both; +} + +.lb-data { + padding: 0 4px; + color: #ccc; +} + +.lb-data .lb-details { + width: 85%; + float: left; + text-align: left; + line-height: 1.1em; +} + +.lb-data .lb-caption { + font-size: 13px; + font-weight: bold; + line-height: 1em; +} + +.lb-data .lb-number { + display: block; + clear: left; + padding-bottom: 1em; + font-size: 12px; + color: #999999; +} + +.lb-data .lb-close { + display: block; + float: right; + width: 30px; + height: 30px; + background: url(images/lightbox/close.png) top right no-repeat; + text-align: right; + outline: none; + filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=70); + opacity: 0.7; + -webkit-transition: opacity 0.2s; + -moz-transition: opacity 0.2s; + -o-transition: opacity 0.2s; + transition: opacity 0.2s; +} + +.lb-data .lb-close:hover { + cursor: pointer; + filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=100); + opacity: 1; +} + body { background: #f8f8f8 !important; font-family: 'Roboto', sans-serif; } @@ -2482,6 +2693,10 @@ box-shadow: 0px 0px 15px 0px rgba(0, 5, 5, 0.2); .plans-table a .cta h2 span {background: #1e84a5;} +#designThumbs img { + border: 1px solid #CCCCCC; +} + @media screen and (min-width: 992px) { .hide-desktop {display: none;} } diff --git a/public/built.js b/public/built.js index 3267f24ab6..057cb33087 100644 --- a/public/built.js +++ b/public/built.js @@ -45923,6 +45923,16 @@ MIT license. for(l(0,(-1*e*c).toFixed(2),"Td"),s=0,o=a.length;s!==o;)a[s][0]&&this.RenderTextFragment(a[s][0],a[s][1]),s++;this.y+=c*r}return l("ET","Q"),this.y+=f}},a.prototype.setBlockBoundary=function(){return this.renderParagraph()},a.prototype.setBlockStyle=function(t){return this.paragraph.blockstyle=t},a.prototype.addText=function(t,e){return this.paragraph.text.push(t),this.paragraph.style.push(e)},r={helvetica:"helvetica","sans-serif":"helvetica",serif:"times",times:"times","times new roman":"times",monospace:"courier",courier:"courier"},s={100:"normal",200:"normal",300:"normal",400:"normal",500:"bold",600:"bold",700:"bold",800:"bold",900:"bold",normal:"normal",bold:"bold",bolder:"bold",lighter:"normal"},n={normal:"normal",italic:"italic",oblique:"italic"},l={normal:1},t.fromHTML=function(t,e,r,n,s,o){"use strict";return this.margins_doc=o||{top:0,bottom:0},n||(n={}),n.elementHandlers||(n.elementHandlers={}),p(this,t,e||4,r||4,n,s)}}(r.API),function(t){"use strict";var e,r,n;t.addJS=function(t){return n=t,this.internal.events.subscribe("postPutResources",function(){e=this.internal.newObject(),this.internal.write("<< /Names [(EmbeddedJS) "+(e+1)+" 0 R] >>","endobj"),r=this.internal.newObject(),this.internal.write("<< /S /JavaScript /JS (",n,") >>","endobj")}),this.internal.events.subscribe("putCatalog",function(){void 0!==e&&void 0!==r&&this.internal.write("/Names <>")}),this}}(r.API),function(t){"use strict";var e=function(){return"function"!=typeof PNG||"function"!=typeof a},r=function(e){return e!==t.image_compression.NONE&&n()},n=function(){var t="function"==typeof o;if(!t)throw new Error("requires deflate.js for compression");return t},s=function(e,r,n,s){var a=5,l=d;switch(s){case t.image_compression.FAST:a=3,l=f;break;case t.image_compression.MEDIUM:a=6,l=h;break;case t.image_compression.SLOW:a=9,l=p}e=c(e,r,n,l);var w=new Uint8Array(i(a)),m=u(e),y=new o(a),v=y.append(e),g=y.flush(),b=w.length+v.length+g.length,q=new Uint8Array(b+4);return q.set(w),q.set(v,w.length),q.set(g,w.length+v.length),q[b++]=m>>>24&255,q[b++]=m>>>16&255,q[b++]=m>>>8&255,q[b++]=255&m,t.arrayBufferToBinaryString(q)},i=function(t,e){var r=8,n=Math.LOG2E*Math.log(32768)-8,s=n<<4|r,o=s<<8,i=Math.min(3,(e-1&255)>>1);return o|=i<<6,o|=0,o+=31-o%31,[s,255&o&255]},u=function(t,e){for(var r,n=1,s=65535&n,o=n>>>16&65535,i=t.length,a=0;i>0;){r=i>e?e:i,i-=r;do s+=t[a++],o+=s;while(--r);s%=65521,o%=65521}return(o<<16|s)>>>0},c=function(t,e,r,n){for(var s,o,i,a=t.length/e,u=new Uint8Array(t.length+a),c=m(),l=0;a>l;l++){if(i=l*e,s=t.subarray(i,i+e),n)u.set(n(s,r,o),i+l);else{for(var f=0,d=c.length,h=[];d>f;f++)h[f]=c[f](s,r,o);var p=y(h.concat());u.set(h[p],i+l)}o=s}return u},l=function(t){var e=Array.apply([],t);return e.unshift(0),e},f=function(t,e){var r,n=[],s=0,o=t.length;for(n[0]=1;o>s;s++)r=t[s-e]||0,n[s+1]=t[s]-r+256&255;return n},d=function(t,e,r){var n,s=[],o=0,i=t.length;for(s[0]=2;i>o;o++)n=r&&r[o]||0,s[o+1]=t[o]-n+256&255;return s},h=function(t,e,r){var n,s,o=[],i=0,a=t.length;for(o[0]=3;a>i;i++)n=t[i-e]||0,s=r&&r[i]||0,o[i+1]=t[i]+256-(n+s>>>1)&255;return o},p=function(t,e,r){var n,s,o,i,a=[],u=0,c=t.length;for(a[0]=4;c>u;u++)n=t[u-e]||0,s=r&&r[u]||0,o=r&&r[u-e]||0,i=w(n,s,o),a[u+1]=t[u]-i+256&255;return a},w=function(t,e,r){var n=t+e-r,s=Math.abs(n-t),o=Math.abs(n-e),i=Math.abs(n-r);return o>=s&&i>=s?t:i>=o?e:r},m=function(){return[l,f,d,h,p]},y=function(t){for(var e,r,n,s=0,o=t.length;o>s;)e=v(t[s].slice(1)),(r>e||!r)&&(r=e,n=s),s++;return n},v=function(t){for(var e=0,r=t.length,n=0;r>e;)n+=Math.abs(t[e++]);return n};t.processPNG=function(t,n,o,i){var a,u,c,l,f,d,h=this.color_spaces.DEVICE_RGB,p=this.decode.FLATE_DECODE,w=8;if(this.isArrayBuffer(t)&&(t=new Uint8Array(t)),this.isArrayBufferView(t)){if(e())throw new Error("PNG support requires png.js and zlib.js");if(a=new PNG(t),t=a.imgData,w=a.bits,h=a.colorSpace,l=a.colors,-1!==[4,6].indexOf(a.colorType)){if(8===a.bits)for(var m,y,v=window["Uint"+a.pixelBitlength+"Array"],g=new v(a.decodePixels().buffer),b=g.length,q=new Uint8Array(b*a.colors),x=new Uint8Array(b),k=a.pixelBitlength-a.bits,_=0,A=0;b>_;_++){for(m=g[_],y=0;k>y;)q[A++]=m>>>y&255,y+=a.bits;x[_]=m>>>y&255}if(16===a.bits){for(var m,g=new Uint32Array(a.decodePixels().buffer),b=g.length,q=new Uint8Array(b*(32/a.pixelBitlength)*a.colors),x=new Uint8Array(b*(32/a.pixelBitlength)),C=a.colors>1,_=0,A=0,S=0;b>_;)m=g[_++],q[A++]=m>>>0&255,C&&(q[A++]=m>>>16&255,m=g[_++],q[A++]=m>>>0&255),x[S++]=m>>>16&255;w=8}r(i)?(t=s(q,a.width*a.colors,a.colors,i),d=s(x,a.width,1,i)):(t=q,d=x,p=null)}if(3===a.colorType&&(h=this.color_spaces.INDEXED,f=a.palette,a.transparency.indexed)){for(var E=a.transparency.indexed,z=0,_=0,b=E.length;b>_;++_)z+=E[_];if(z/=255,z===b-1&&-1!==E.indexOf(0))c=[E.indexOf(0)];else if(z!==b){for(var g=a.decodePixels(),x=new Uint8Array(g.length),_=0,b=g.length;b>_;_++)x[_]=E[g[_]];d=s(x,a.width,1)}}return u=p===this.decode.FLATE_DECODE?"/Predictor 15 /Colors "+l+" /BitsPerComponent "+w+" /Columns "+a.width:"/Colors "+l+" /BitsPerComponent "+w+" /Columns "+a.width,(this.isArrayBuffer(t)||this.isArrayBufferView(t))&&(t=this.arrayBufferToBinaryString(t)),(d&&this.isArrayBuffer(d)||this.isArrayBufferView(d))&&(d=this.arrayBufferToBinaryString(d)),this.createImageInfo(t,a.width,a.height,h,w,p,n,o,u,c,f,d)}throw new Error("Unsupported PNG image data, try using JPEG instead.")}}(r.API),function(t){"use strict";t.addSVG=function(t,e,r,n,s){function o(t,e){var r=e.createElement("style");r.type="text/css",r.styleSheet?r.styleSheet.cssText=t:r.appendChild(e.createTextNode(t)),e.getElementsByTagName("head")[0].appendChild(r)}function i(t){var e="childframe",r=t.createElement("iframe");return o(".jsPDF_sillysvg_iframe {display:none;position:absolute;}",t),r.name=e,r.setAttribute("width",0),r.setAttribute("height",0),r.setAttribute("frameborder","0"),r.setAttribute("scrolling","no"),r.setAttribute("seamless","seamless"),r.setAttribute("class","jsPDF_sillysvg_iframe"),t.body.appendChild(r),r}function a(t,e){var r=(e.contentWindow||e.contentDocument).document;return r.write(t),r.close(),r.getElementsByTagName("svg")[0]}function u(t){for(var e=parseFloat(t[1]),r=parseFloat(t[2]),n=[],s=3,o=t.length;o>s;)"c"===t[s]?(n.push([parseFloat(t[s+1]),parseFloat(t[s+2]),parseFloat(t[s+3]),parseFloat(t[s+4]),parseFloat(t[s+5]),parseFloat(t[s+6])]),s+=7):"l"===t[s]?(n.push([parseFloat(t[s+1]),parseFloat(t[s+2])]),s+=3):s+=1;return[e,r,n]}var c;if(e===c||e===c)throw new Error("addSVG needs values for 'x' and 'y'");var l=i(document),f=a(t,l),d=[1,1],h=parseFloat(f.getAttribute("width")),p=parseFloat(f.getAttribute("height"));h&&p&&(n&&s?d=[n/h,s/p]:n?d=[n/h,n/h]:s&&(d=[s/p,s/p]));var w,m,y,v,g=f.childNodes;for(w=0,m=g.length;m>w;w++)y=g[w],y.tagName&&"PATH"===y.tagName.toUpperCase()&&(v=u(y.getAttribute("d").split(" ")),v[0]=v[0]*d[0]+e,v[1]=v[1]*d[1]+r,this.lines.call(this,v[2],v[0],v[1],d));return this}}(r.API),function(t){"use strict";var e=t.getCharWidthsArray=function(t,e){e||(e={});var r,n,s,o=e.widths?e.widths:this.internal.getFont().metadata.Unicode.widths,i=o.fof?o.fof:1,a=e.kerning?e.kerning:this.internal.getFont().metadata.Unicode.kerning,u=a.fof?a.fof:1,c=0,l=o[0]||i,f=[];for(r=0,n=t.length;n>r;r++)s=t.charCodeAt(r),f.push((o[s]||l)/i+(a[s]&&a[s][c]||0)/u),c=s;return f},r=function(t){for(var e=t.length,r=0;e;)e--,r+=t[e];return r},n=t.getStringUnitWidth=function(t,n){return r(e.call(this,t,n))},s=function(t,e,r,n){for(var s=[],o=0,i=t.length,a=0;o!==i&&a+e[o]