From c5a1abf66f84c77b7e57dc38ea4152ff923596ac Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 2 Dec 2015 15:26:06 +0200 Subject: [PATCH] Added HTML email designs --- app/Console/Commands/CheckData.php | 81 ++++++++++++---- app/Http/Controllers/AccountController.php | 5 + app/Http/routes.php | 6 +- app/Libraries/Utils.php | 14 ++- app/Models/Account.php | 38 ++++++-- app/Models/Gateway.php | 10 +- app/Models/Invitation.php | 4 +- app/Ninja/Mailers/ContactMailer.php | 60 +++++++++--- app/Ninja/Mailers/Mailer.php | 2 +- app/Ninja/Presenters/InvoicePresenter.php | 11 +++ app/Ninja/Repositories/ActivityRepository.php | 2 +- app/Providers/AppServiceProvider.php | 23 +++++ app/Services/UserService.php | 2 +- ...11_05_180133_confide_setup_users_table.php | 6 +- .../2015_11_30_133206_add_email_designs.php | 34 +++++++ public/css/built.css | 15 +++ public/js/built.js | 2 +- readme.md | 8 +- resources/lang/en/texts.php | 11 +++ resources/views/accounts/template.blade.php | 49 ++++++---- .../templates_and_reminders.blade.php | 96 +++++++++++++++---- resources/views/emails/contact_html.blade.php | 1 - resources/views/emails/contact_text.blade.php | 1 - resources/views/emails/design1_html.blade.php | 46 +++++++++ resources/views/emails/design1_text.blade.php | 1 + resources/views/emails/design2_html.blade.php | 46 +++++++++ resources/views/emails/design2_text.blade.php | 1 + resources/views/emails/invoice_html.blade.php | 2 +- resources/views/emails/master.blade.php | 85 ++++++++++++++++ .../emails/partials/account_logo.blade.php | 11 +++ .../views/partials/email_button.blade.php | 22 +++++ 31 files changed, 593 insertions(+), 102 deletions(-) create mode 100644 database/migrations/2015_11_30_133206_add_email_designs.php delete mode 100644 resources/views/emails/contact_html.blade.php delete mode 100644 resources/views/emails/contact_text.blade.php create mode 100644 resources/views/emails/design1_html.blade.php create mode 100644 resources/views/emails/design1_text.blade.php create mode 100644 resources/views/emails/design2_html.blade.php create mode 100644 resources/views/emails/design2_text.blade.php create mode 100644 resources/views/emails/master.blade.php create mode 100644 resources/views/emails/partials/account_logo.blade.php create mode 100644 resources/views/partials/email_button.blade.php diff --git a/app/Console/Commands/CheckData.php b/app/Console/Commands/CheckData.php index 64c8dd00db..a5c5fe6a53 100644 --- a/app/Console/Commands/CheckData.php +++ b/app/Console/Commands/CheckData.php @@ -46,29 +46,78 @@ class CheckData extends Command { public function fire() { $this->info(date('Y-m-d') . ' Running CheckData...'); - $today = new DateTime(); if (!$this->option('client_id')) { - // update client paid_to_date value - $clients = DB::table('clients') - ->join('payments', 'payments.client_id', '=', 'clients.id') - ->join('invoices', 'invoices.id', '=', 'payments.invoice_id') - ->where('payments.is_deleted', '=', 0) - ->where('invoices.is_deleted', '=', 0) - ->groupBy('clients.id') - ->havingRaw('clients.paid_to_date != sum(payments.amount) and clients.paid_to_date != 999999999.9999') - ->get(['clients.id', 'clients.paid_to_date', DB::raw('sum(payments.amount) as amount')]); - $this->info(count($clients) . ' clients with incorrect paid to date'); + $this->checkPaidToDate(); + } + + $this->checkBalances(); + + $this->checkActivityAccount(); + + $this->info('Done'); + } + + private function checkActivityAccount() + { + $entityTypes = [ + ENTITY_INVOICE, + ENTITY_CLIENT, + ENTITY_CONTACT, + ENTITY_PAYMENT, + ENTITY_INVITATION, + ]; + + foreach ($entityTypes as $entityType) { + $activities = DB::table('activities') + ->join("{$entityType}s", "{$entityType}s.id", '=', "activities.{$entityType}_id"); + + if ($entityType != ENTITY_CLIENT) { + $activities = $activities->join('clients', 'clients.id', '=', 'activities.client_id'); + } + $activities = $activities->where('activities.account_id', '!=', DB::raw("{$entityType}s.account_id")) + ->get(['activities.id', "clients.account_id", "clients.user_id"]); + + $this->info(count($activities) . " {$entityType} activity with incorrect account id"); + if ($this->option('fix') == 'true') { - foreach ($clients as $client) { - DB::table('clients') - ->where('id', $client->id) - ->update(['paid_to_date' => $client->amount]); + foreach ($activities as $activity) { + DB::table('activities') + ->where('id', $activity->id) + ->update([ + 'account_id' => $activity->account_id, + 'user_id' => $activity->user_id, + ]); } } } + } + private function checkPaidToDate() + { + // update client paid_to_date value + $clients = DB::table('clients') + ->join('payments', 'payments.client_id', '=', 'clients.id') + ->join('invoices', 'invoices.id', '=', 'payments.invoice_id') + ->where('payments.is_deleted', '=', 0) + ->where('invoices.is_deleted', '=', 0) + ->groupBy('clients.id') + ->havingRaw('clients.paid_to_date != sum(payments.amount) and clients.paid_to_date != 999999999.9999') + ->get(['clients.id', 'clients.paid_to_date', DB::raw('sum(payments.amount) as amount')]); + $this->info(count($clients) . ' clients with incorrect paid to date'); + + if ($this->option('fix') == 'true') { + foreach ($clients as $client) { + DB::table('clients') + ->where('id', $client->id) + ->update(['paid_to_date' => $client->amount]); + } + } + } + + private function checkBalances() + { // find all clients where the balance doesn't equal the sum of the outstanding invoices $clients = DB::table('clients') ->join('invoices', 'invoices.client_id', '=', 'clients.id') @@ -249,8 +298,6 @@ class CheckData extends Command { ->update($data); } } - - $this->info('Done'); } protected function getArguments() diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index d7722136d2..1146fe4fc9 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -432,6 +432,11 @@ class AccountController extends BaseController { if (Auth::user()->account->isPro()) { $account = Auth::user()->account; + $account->email_design_id = Input::get('email_design_id'); + + if (Utils::isNinja()) { + $account->enable_email_markup = Input::get('enable_email_markup') ? true : false; + } foreach ([ENTITY_INVOICE, ENTITY_QUOTE, ENTITY_PAYMENT, REMINDER1, REMINDER2, REMINDER3] as $type) { $subjectField = "email_subject_{$type}"; diff --git a/app/Http/routes.php b/app/Http/routes.php index eed6098281..738ef1e4aa 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -253,6 +253,7 @@ if (!defined('CONTACT_EMAIL')) { define('ENTITY_CONTACT', 'contact'); define('ENTITY_INVOICE', 'invoice'); define('ENTITY_INVOICE_ITEMS', 'invoice_items'); + define('ENTITY_INVITATION', 'invitation'); define('ENTITY_RECURRING_INVOICE', 'recurring_invoice'); define('ENTITY_PAYMENT', 'payment'); define('ENTITY_CREDIT', 'credit'); @@ -446,6 +447,7 @@ if (!defined('CONTACT_EMAIL')) { define('PHANTOMJS_CLOUD', 'http://api.phantomjscloud.com/api/browser/v2/'); define('PHP_DATE_FORMATS', 'http://php.net/manual/en/function.date.php'); define('REFERRAL_PROGRAM_URL', 'https://www.invoiceninja.com/referral-program/'); + define('EMAIL_MARKUP_URL', 'https://developers.google.com/gmail/markup/overview'); define('COUNT_FREE_DESIGNS', 4); define('COUNT_FREE_DESIGNS_SELF_HOST', 5); // include the custom design @@ -498,6 +500,9 @@ if (!defined('CONTACT_EMAIL')) { define('API_SERIALIZER_ARRAY', 'array'); define('API_SERIALIZER_JSON', 'json'); + define('EMAIL_DESIGN_PLAIN', 1); + define('FLAT_BUTTON_CSS', 'border:0 none;border-radius:6px;padding:12px 40px;margin:0 6px;cursor:hand;display:inline-block;font-size:14px;color:#fff;text-transform:none'); + $creditCards = [ 1 => ['card' => 'images/credit_cards/Test-Visa-Icon.png', 'text' => 'Visa'], 2 => ['card' => 'images/credit_cards/Test-MasterCard-Icon.png', 'text' => 'Master Card'], @@ -522,7 +527,6 @@ if (!defined('CONTACT_EMAIL')) { 'invoiceStatus' => 'App\Models\InvoiceStatus', 'frequencies' => 'App\Models\Frequency', 'gateways' => 'App\Models\Gateway', - 'themes' => 'App\Models\Theme', ]; define('CACHED_TABLES', serialize($cachedTables)); diff --git a/app/Libraries/Utils.php b/app/Libraries/Utils.php index 4af034825d..4c061b8b81 100644 --- a/app/Libraries/Utils.php +++ b/app/Libraries/Utils.php @@ -279,9 +279,19 @@ class Utils return json_decode(json_encode((array) $data), true); } - public static function toSpaceCase($camelStr) + public static function toSpaceCase($string) { - return preg_replace('/([a-z])([A-Z])/s', '$1 $2', $camelStr); + return preg_replace('/([a-z])([A-Z])/s', '$1 $2', $string); + } + + public static function toSnakeCase($string) + { + return preg_replace('/([a-z])([A-Z])/s', '$1_$2', $string); + } + + public static function toCamelCase($string) + { + return lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $string)))); } public static function timestampToDateTimeString($timestamp) diff --git a/app/Models/Account.php b/app/Models/Account.php index 0842f36434..162b7c6501 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -5,6 +5,7 @@ use Utils; use Session; use DateTime; use Event; +use Cache; use App; use File; use App\Events\UserSettingsChanged; @@ -204,10 +205,26 @@ class Account extends Eloquent return $this->date_format ? $this->date_format->format : DEFAULT_DATE_FORMAT; } + public function formatMoney($amount, $client = null) + { + $currency = ($client && $client->currency) ? $client->currency : $this->currency; + + if ( ! $currency) { + $currencies = Cache::get('currencies')->filter(function($item) { + return $item->id == DEFAULT_CURRENCY; + }); + $currency = $currencies->first(); + } + + return $currency->symbol . number_format($amount, $currency->precision, $currency->decimal_separator, $currency->thousand_separator); + } + public function formatDate($date) { - if (!$date) { + if ( ! $date) { return null; + } elseif ( ! $date instanceof \DateTime) { + $date = new \DateTime($date); } return $date->format($this->getCustomDateFormat()); @@ -677,9 +694,9 @@ class Account extends Eloquent $entityType = ENTITY_INVOICE; } - $template = "
\$client,

" . - "
" . trans("texts.{$entityType}_message", ['amount' => '$amount']) . "

" . - "
\$link

"; + $template = "
\$client,

" . + "
" . trans("texts.{$entityType}_message", ['amount' => '$amount']) . "

" . + "
\$viewLink

"; if ($message) { $template .= "$message

\r\n\r\n"; @@ -693,13 +710,14 @@ class Account extends Eloquent if ($this->isPro()) { $field = "email_template_{$entityType}"; $template = $this->$field; - - if ($template) { - return $template; - } } - return $this->getDefaultEmailTemplate($entityType, $message); + if (!$template) { + $template = $this->getDefaultEmailTemplate($entityType, $message); + } + + //
is causing page breaks with the email designs + return str_replace('/>', ' />', $template); } public function getEmailFooter() @@ -708,7 +726,7 @@ class Account extends Eloquent // Add line breaks if HTML isn't already being used return strip_tags($this->email_footer) == $this->email_footer ? nl2br($this->email_footer) : $this->email_footer; } else { - return "

" . trans('texts.email_signature') . "\n
\$account

"; + return "

" . trans('texts.email_signature') . "\n
\$account"; } } diff --git a/app/Models/Gateway.php b/app/Models/Gateway.php index 55fc2056f4..da556dfb0e 100644 --- a/app/Models/Gateway.php +++ b/app/Models/Gateway.php @@ -2,6 +2,7 @@ use Eloquent; use Omnipay; +use Utils; class Gateway extends Eloquent { @@ -44,13 +45,20 @@ class Gateway extends Eloquent return $this->id == $gatewayId; } + public static function getPaymentTypeName($type) + { + return Utils::toCamelCase(strtolower(str_replace('PAYMENT_TYPE_', '', $type))); + } + + /* public static function getPaymentTypeLinks() { $data = []; foreach (self::$paymentTypes as $type) { - $data[] = strtolower(str_replace('PAYMENT_TYPE_', '', $type)); + $data[] = Utils::toCamelCase(strtolower(str_replace('PAYMENT_TYPE_', '', $type))); } return $data; } + */ public function getHelp() { diff --git a/app/Models/Invitation.php b/app/Models/Invitation.php index 16d5043499..2cc7102957 100644 --- a/app/Models/Invitation.php +++ b/app/Models/Invitation.php @@ -29,7 +29,7 @@ class Invitation extends EntityModel return $this->belongsTo('App\Models\Account'); } - public function getLink() + public function getLink($type = 'view') { if (!$this->account) { $this->load('account'); @@ -46,7 +46,7 @@ class Invitation extends EntityModel } } - return "{$url}/view/{$this->invitation_key}"; + return "{$url}/{$type}/{$this->invitation_key}"; } public function getStatus() diff --git a/app/Ninja/Mailers/ContactMailer.php b/app/Ninja/Mailers/ContactMailer.php index 67ba09a17d..3d9f473ce2 100644 --- a/app/Ninja/Mailers/ContactMailer.php +++ b/app/Ninja/Mailers/ContactMailer.php @@ -1,5 +1,6 @@ load('invitations', 'client.language', 'account'); @@ -97,6 +113,7 @@ class ContactMailer extends Mailer 'invoiceId' => $invoice->id, 'invitation' => $invitation, 'account' => $account, + 'client' => $client, 'invoice' => $invoice, ]; @@ -107,8 +124,14 @@ class ContactMailer extends Mailer $subject = $this->processVariables($subject, $variables); $fromEmail = $user->email; + + if ($account->email_design_id == EMAIL_DESIGN_PLAIN) { + $view = ENTITY_INVOICE; + } else { + $view = 'design' . ($account->email_design_id - 1); + } - $response = $this->sendTo($invitation->contact->email, $fromEmail, $account->getDisplayName(), $subject, ENTITY_INVOICE, $data); + $response = $this->sendTo($invitation->contact->email, $fromEmail, $account->getDisplayName(), $subject, $view, $data); if ($response === true) { return true; @@ -192,22 +215,33 @@ class ContactMailer extends Mailer private function processVariables($template, $data) { + $account = $data['account']; + $client = $data['client']; + $invitation = $data['invitation']; + $invoice = $invitation->invoice; + $variables = [ - '$footer' => $data['account']->getEmailFooter(), - '$link' => $data['invitation']->getLink(), - '$client' => $data['client']->getDisplayName(), - '$account' => $data['account']->getDisplayName(), - '$contact' => $data['invitation']->contact->getDisplayName(), - '$firstName' => $data['invitation']->contact->first_name, - '$amount' => Utils::formatMoney($data['amount'], $data['client']->getCurrencyId()), - '$invoice' => $data['invitation']->invoice->invoice_number, - '$quote' => $data['invitation']->invoice->invoice_number, - '$advancedRawInvoice->' => '$' + '$footer' => $account->getEmailFooter(), + '$client' => $client->getDisplayName(), + '$account' => $account->getDisplayName(), + '$contact' => $invitation->contact->getDisplayName(), + '$firstName' => $invitation->contact->first_name, + '$amount' => Utils::formatMoney($data['amount'], $client->getCurrencyId()), + '$invoice' => $invoice->invoice_number, + '$quote' => $invoice->invoice_number, + '$link' => $invitation->getLink(), + '$viewLink' => $invitation->getLink(), + '$viewButton' => HTML::emailViewButton($invitation->getLink(), $invoice->getEntityType()), + '$paymentLink' => $invitation->getLink('payment'), + '$paymentButton' => HTML::emailPaymentButton($invitation->getLink('payment')), ]; // Add variables for available payment types - foreach (Gateway::getPaymentTypeLinks() as $type) { - $variables["\${$type}_link"] = URL::to("/payment/{$data['invitation']->invitation_key}/{$type}"); + foreach (Gateway::$paymentTypes as $type) { + $camelType = Gateway::getPaymentTypeName($type); + $type = Utils::toSnakeCase($camelType); + $variables["\${$camelType}Link"] = $invitation->getLink() . "/{$type}"; + $variables["\${$camelType}Button"] = HTML::emailPaymentButton($invitation->getLink('payment') . "/{$type}"); } $str = str_replace(array_keys($variables), array_values($variables), $template); diff --git a/app/Ninja/Mailers/Mailer.php b/app/Ninja/Mailers/Mailer.php index 34db634f88..d1aa10ec7c 100644 --- a/app/Ninja/Mailers/Mailer.php +++ b/app/Ninja/Mailers/Mailer.php @@ -67,7 +67,7 @@ class Mailer private function handleFailure($exception) { - if (isset($_ENV['POSTMARK_API_TOKEN']) && $exception->getResponse()) { + if (isset($_ENV['POSTMARK_API_TOKEN']) && method_exists($exception, 'getResponse')) { $response = $exception->getResponse()->getBody()->getContents(); $response = json_decode($response); $emailError = nl2br($response->Message); diff --git a/app/Ninja/Presenters/InvoicePresenter.php b/app/Ninja/Presenters/InvoicePresenter.php index dff59c241b..8a03ff4ac1 100644 --- a/app/Ninja/Presenters/InvoicePresenter.php +++ b/app/Ninja/Presenters/InvoicePresenter.php @@ -15,6 +15,17 @@ class InvoicePresenter extends Presenter { return $this->entity->user->getDisplayName(); } + public function balanceDueLabel() + { + if ($this->entity->partial) { + return 'amount_due'; + } elseif ($this->entity->is_quote) { + return 'total'; + } else { + return 'balance_due'; + } + } + public function balance_due() { $amount = $this->entity->getRequestedAmount(); diff --git a/app/Ninja/Repositories/ActivityRepository.php b/app/Ninja/Repositories/ActivityRepository.php index 811b5fd5c5..8ac316c747 100644 --- a/app/Ninja/Repositories/ActivityRepository.php +++ b/app/Ninja/Repositories/ActivityRepository.php @@ -44,7 +44,7 @@ class ActivityRepository { $activity = new Activity(); - if (Auth::check()) { + if (Auth::check() && Auth::user()->account_id == $entity->account_id) { $activity->user_id = Auth::user()->id; $activity->account_id = Auth::user()->account_id; } else { diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 8a7e9bfad2..bf67c12498 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -65,6 +65,29 @@ class AppServiceProvider extends ServiceProvider { return 'data:image/jpeg;base64,' . base64_encode(file_get_contents(public_path().'/'.$imagePath)); }); + HTML::macro('flatButton', function($label, $color) { + return ''; + }); + + HTML::macro('emailViewButton', function($link = '#', $entityType = ENTITY_INVOICE) { + return view('partials.email_button') + ->with([ + 'link' => $link, + 'field' => "view_{$entityType}", + 'color' => '#0b4d78', + ]) + ->render(); + }); + + HTML::macro('emailPaymentButton', function($link = '#') { + return view('partials.email_button') + ->with([ + 'link' => $link, + 'field' => 'pay_now', + 'color' => '#36c157', + ]) + ->render(); + }); HTML::macro('breadcrumbs', function() { $str = '