Enabled contact selection when sending email

This commit is contained in:
Hillel Coren 2013-12-13 11:13:57 +02:00
parent cf7106e8ac
commit 19abec6f09
11 changed files with 438 additions and 394 deletions

1
.gitignore vendored
View File

@ -2,6 +2,7 @@
/app/config/development
/app/storage/
/public/logo
/public/build
/bootstrap/compiled.php
/vendor
composer.phar

View File

@ -24,7 +24,7 @@ class SendRecurringInvoices extends Command {
$today = new DateTime();
$invoices = Invoice::with('account', 'invoice_items')->whereRaw('frequency_id > 0 AND start_date <= ? AND (end_date IS NULL OR end_date >= ?)', array($today, $today))->get();
$invoices = Invoice::with('account', 'invoice_items')->whereRaw('is_recurring is true AND start_date <= ? AND (end_date IS NULL OR end_date >= ?)', array($today, $today))->get();
$this->info(count($invoices) . ' recurring invoice(s) found');
foreach ($invoices as $recurInvoice)
@ -59,7 +59,7 @@ class SendRecurringInvoices extends Command {
$recurInvoice->last_sent_date = new DateTime();
$recurInvoice->save();
$this->mailer->sendInvoice($invoice, $invoice->client->contacts()->first());
$this->mailer->sendInvoice($invoice);
}
$this->info('Done');

View File

@ -21,7 +21,7 @@ class InvoiceController extends \BaseController {
'columns'=>['checkbox', 'Invoice Number', 'Client', 'Invoice Date', 'Invoice Total', 'Balance Due', 'Due Date', 'Status', 'Action']
];
if (Invoice::scope()->where('frequency_id', '>', '0')->count() > 0)
if (Invoice::scope()->where('is_recurring', '=', true)->count() > 0)
{
$data['secEntityType'] = ENTITY_RECURRING_INVOICE;
$data['secColumns'] = ['checkbox', 'Frequency', 'Client', 'Start Date', 'End Date', 'Invoice Total', 'Action'];
@ -37,7 +37,7 @@ class InvoiceController extends \BaseController {
->join('invoice_statuses', 'invoice_statuses.id', '=', 'invoices.invoice_status_id')
->where('invoices.account_id', '=', Auth::user()->account_id)
->where('invoices.deleted_at', '=', null)
->where('invoices.frequency_id', '=', 0)
->where('invoices.is_recurring', '=', false)
->select('clients.public_id as client_public_id', 'invoice_number', 'clients.name as client_name', 'invoices.public_id', 'total', 'invoices.balance', 'invoice_date', 'due_date', 'invoice_statuses.name as invoice_status_name');
if ($clientPublicId) {
@ -97,7 +97,7 @@ class InvoiceController extends \BaseController {
->join('frequencies', 'frequencies.id', '=', 'invoices.frequency_id')
->where('invoices.account_id', '=', Auth::user()->account_id)
->where('invoices.deleted_at', '=', null)
->where('invoices.frequency_id', '>', 0)
->where('invoices.is_recurring', '=', true)
->select('clients.public_id as client_public_id', 'clients.name as client_name', 'invoices.public_id', 'total', 'frequencies.name as frequency', 'start_date', 'end_date');
if ($clientPublicId) {
@ -322,10 +322,20 @@ class InvoiceController extends \BaseController {
$invoice = Invoice::scope($publicId)->with('account.country', 'client', 'invoice_items')->firstOrFail();
Utils::trackViewed($invoice->invoice_number . ' - ' . $invoice->client->name, ENTITY_INVOICE);
$contactIds = DB::table('invitations')
->join('contacts', 'contacts.id', '=','invitations.contact_id')
->where('invitations.invoice_id', '=', $invoice->id)
->where('invitations.account_id', '=', Auth::user()->account_id)
->where('invitations.deleted_at', '=', null)
->select('contacts.public_id')->lists('public_id');
$data = array(
'account' => $invoice->account,
'invoice' => $invoice,
'method' => 'PUT',
'invitationContactIds' => $contactIds,
'clientSizes' => ClientSize::orderBy('id')->get(),
'clientIndustries' => ClientIndustry::orderBy('name')->get(),
'url' => 'invoices/' . $publicId,
'title' => '- ' . $invoice->invoice_number,
'client' => $invoice->client);
@ -349,6 +359,8 @@ class InvoiceController extends \BaseController {
'invoiceNumber' => $invoiceNumber,
'method' => 'POST',
'url' => 'invoices',
'clientSizes' => ClientSize::orderBy('id')->get(),
'clientIndustries' => ClientIndustry::orderBy('name')->get(),
'title' => '- New Invoice',
'client' => $client,
'items' => json_decode(Input::old('items')));
@ -393,20 +405,18 @@ class InvoiceController extends \BaseController {
{
return InvoiceController::bulk();
}
$rules = array(
'client' => 'required',
);
$validator = Validator::make(Input::all(), $rules);
if ($validator->fails()) {
$input = json_decode(Input::get('data'));
$inputClient = $input->client;
$inputClient->name = trim($inputClient->name);
if (!$inputClient->name) {
return Redirect::to('invoices/create')
->withInput()
->withErrors($validator);
->withInput();
} else {
$clientPublicId = Input::get('client');
$clientPublicId = $input->client->public_id;
if ($clientPublicId == "-1")
{
$client = Client::createNew();
@ -418,24 +428,58 @@ class InvoiceController extends \BaseController {
$client = Client::scope($clientPublicId)->with('contacts')->firstOrFail();
$contact = $client->contacts()->where('is_primary', '=', true)->firstOrFail();
}
$client->name = trim(Input::get('name'));
$client->work_phone = trim(Input::get('work_phone'));
$client->address1 = trim(Input::get('address1'));
$client->address2 = trim(Input::get('address2'));
$client->city = trim(Input::get('city'));
$client->state = trim(Input::get('state'));
$client->postal_code = trim(Input::get('postal_code'));
if (Input::get('country_id')) {
$client->country_id = Input::get('country_id');
}
$inputClient = $input->client;
$client->name = trim($inputClient->name);
$client->work_phone = trim($inputClient->work_phone);
$client->address1 = trim($inputClient->address1);
$client->address2 = trim($inputClient->address2);
$client->city = trim($inputClient->city);
$client->state = trim($inputClient->state);
$client->postal_code = trim($inputClient->postal_code);
$client->country_id = $inputClient->country_id ? $inputClient->country_id : null;
$client->save();
$contact->first_name = trim(Input::get('first_name'));
$contact->last_name = trim(Input::get('last_name'));
$contact->phone = trim(Input::get('phone'));
$contact->email = trim(Input::get('email'));
$client->contacts()->save($contact);
$isPrimary = true;
$contacts = [];
$contactIds = [];
$sendInvoiceIds = [];
foreach ($inputClient->contacts as $contact)
{
if (isset($contact->public_id) && $contact->public_id)
{
$record = Contact::scope($contact->public_id)->firstOrFail();
}
else
{
$record = Contact::createNew();
}
$record->email = trim($contact->email);
$record->first_name = trim($contact->first_name);
$record->last_name = trim($contact->last_name);
$record->phone = trim($contact->phone);
$record->is_primary = $isPrimary;
$isPrimary = false;
$client->contacts()->save($record);
$contacts[] = $record;
$contactIds[] = $record->public_id;
if ($contact->send_invoice)
{
$sendInvoiceIds[] = $record->id;
}
}
foreach ($client->contacts as $contact)
{
if (!in_array($contact->public_id, $contactIds))
{
$contact->forceDelete();
}
}
if ($publicId) {
$invoice = Invoice::scope($publicId)->firstOrFail();
@ -445,20 +489,22 @@ class InvoiceController extends \BaseController {
}
$invoice->client_id = $client->id;
$invoice->discount = Input::get('discount');
$invoice->invoice_number = trim(Input::get('invoice_number'));
$invoice->invoice_date = Utils::toSqlDate(Input::get('invoice_date'));
$invoice->due_date = Utils::toSqlDate(Input::get('due_date'));
$invoice->discount = $input->discount;
$invoice->invoice_number = trim($input->invoice_number);
$invoice->invoice_date = Utils::toSqlDate($input->invoice_date);
$invoice->due_date = Utils::toSqlDate($input->due_date);
$invoice->frequency_id = Input::get('recurring') ? Input::get('frequency') : 0;
$invoice->start_date = Utils::toSqlDate(Input::get('start_date'));
$invoice->end_date = Utils::toSqlDate(Input::get('end_date'));
$invoice->notes = Input::get('notes');
$invoice->po_number = Input::get('po_number');
$invoice->is_recurring = $input->is_recurring;
$invoice->frequency_id = $input->frequency_id ? $input->frequency_id : 0;
$invoice->start_date = Utils::toSqlDate($input->start_date);
$invoice->end_date = Utils::toSqlDate($input->end_date);
$invoice->notes = $input->notes;
$invoice->po_number = $input->po_number;
$client->invoices()->save($invoice);
$items = json_decode(Input::get('items'));
$items = $input->invoice_items;
$total = 0;
foreach ($items as $item)
@ -484,6 +530,24 @@ class InvoiceController extends \BaseController {
$invoice->total = $total;
$invoice->save();
foreach ($contacts as $contact)
{
$invitation = Invitation::scope()->whereContactId($contact->id)->whereInvoiceId($invoice->id)->first();
if (in_array($contact->id, $sendInvoiceIds) && !$invitation)
{
$invitation = Invitation::createNew();
$invitation->invoice_id = $invoice->id;
$invitation->contact_id = $contact->id;
$invitation->invitation_key = str_random(20);
$invitation->save();
}
else if (!in_array($contact->id, $sendInvoiceIds) && $invitation)
{
$invitation->forceDelete();
}
}
foreach ($items as $item)
{
if (!$item->cost && !$item->qty && !$item->product_key && !$item->notes)
@ -529,7 +593,7 @@ class InvoiceController extends \BaseController {
}
else if ($action == 'email')
{
$this->mailer->sendInvoice($invoice, $contact);
$this->mailer->sendInvoice($invoice);
Session::flash('message', 'Successfully emailed invoice');
} else {
@ -594,12 +658,12 @@ class InvoiceController extends \BaseController {
$invoice = Invoice::with('invoice_items')->scope($publicId)->firstOrFail();
$clone = Invoice::createNew();
foreach (['client_id', 'discount', 'invoice_date', 'due_date', 'frequency_id', 'start_date', 'end_date', 'notes'] as $field)
foreach (['client_id', 'discount', 'invoice_date', 'due_date', 'is_recurring', 'frequency_id', 'start_date', 'end_date', 'notes'] as $field)
{
$clone->$field = $invoice->$field;
}
if (!$clone->isRecurring())
if (!$clone->is_recurring)
{
$clone->invoice_number = Auth::user()->account->getNextInvoiceNumber();
}

View File

@ -204,7 +204,7 @@ class ConfideSetupUsersTable extends Migration {
$t->string('last_name');
$t->string('email');
$t->string('phone');
$t->timestamp('last_login');
$t->timestamp('last_login');
$t->foreign('client_id')->references('id')->on('clients')->onDelete('cascade');
$t->foreign('user_id')->references('id')->on('users');
@ -238,14 +238,15 @@ class ConfideSetupUsersTable extends Migration {
$t->string('invoice_number');
$t->float('discount');
$t->string('po_number');
$t->date('invoice_date');
$t->date('invoice_date')->nullable();
$t->date('due_date')->nullable();
$t->text('notes');
$t->boolean('is_recurring');
$t->unsignedInteger('frequency_id');
$t->date('start_date')->nullable();
$t->date('end_date')->nullable();
$t->date('last_sent_date')->nullable();
$t->timestamp('last_sent_date')->nullable();
$t->unsignedInteger('recurring_invoice_id')->index()->nullable();
@ -274,6 +275,7 @@ class ConfideSetupUsersTable extends Migration {
$t->timestamps();
$t->softDeletes();
$t->timestamp('sent_date');
$t->timestamp('viewed_date');
$t->foreign('user_id')->references('id')->on('users');

View File

@ -8,30 +8,24 @@ use Auth;
class ContactMailer extends Mailer {
public function sendInvoice(Invoice $invoice, Contact $contact)
public function sendInvoice(Invoice $invoice)
{
$view = 'invoice';
$data = array('link' => URL::to('view') . '/' . $invoice->invoice_key);
$subject = '';
if (Auth::check()) {
$invitation = Invitation::createNew();
} else {
$invitation = Invitation::createNew($invoice);
}
foreach ($invoice->invitations as $invitation)
{
//$invitation->date_sent =
$invitation->save();
$invitation->invoice_id = $invoice->id;
$invitation->user_id = Auth::check() ? Auth::user()->id : $invoice->user_id;
$invitation->contact_id = $contact->id;
$invitation->invitation_key = str_random(20);
$invitation->save();
$this->sendTo($invitation->contact->email, $subject, $view, $data);
}
if (!$invoice->isSent())
{
$invoice->invoice_status_id = INVOICE_STATUS_SENT;
$invoice->save();
}
return $this->sendTo($contact->email, $subject, $view, $data);
}
}

View File

@ -66,7 +66,7 @@ class Activity extends Eloquent
{
$userName = Auth::check() ? Auth::user()->getFullName() : '<i>System</i>';
if ($invoice->isRecurring()) {
if ($invoice->is_recurring) {
$message = $userName . ' created ' . link_to('invoices/'.$invoice->public_id, 'recuring invoice');
} else {
$message = $userName . ' created invoice ' . link_to('invoices/'.$invoice->public_id, $invoice->invoice_number);

View File

@ -2,7 +2,7 @@
class Invitation extends EntityModel
{
protected $hidden = array('id', 'created_at', 'updated_at', 'deleted_at', 'viewed_date');
protected $hidden = array('id', 'account_id', 'user_id', 'contact_id', 'created_at', 'updated_at', 'deleted_at', 'viewed_date');
public function invoice()
{

View File

@ -24,6 +24,11 @@ class Invoice extends EntityModel
return $this->belongsTo('InvoiceStatus');
}
public function invitations()
{
return $this->hasMany('Invitation');
}
public function getName()
{
return $this->invoice_number;
@ -34,11 +39,6 @@ class Invoice extends EntityModel
return ENTITY_INVOICE;
}
public function isRecurring()
{
return $this->frequency_id > 0;
}
public function isSent()
{
return $this->invoice_status_id >= INVOICE_STATUS_SENT;

View File

@ -207,6 +207,10 @@
cursor: move !important;
}
.closer-row {
margin-bottom: 2px;
}
/* Animate col width changes */
.row div {

View File

@ -15,61 +15,62 @@
'client' => 'required',
'product_key' => 'max:14',
)); }}
<!-- <h3>{{ $title }} Invoice</h3> -->
@if ($invoice)
{{ Former::populate($invoice); }}
{{ Former::populateField('id', $invoice->public_id); }}
{{ Former::populateField('invoice_date', Utils::fromSqlDate($invoice->invoice_date)); }}
{{ Former::populateField('due_date', Utils::fromSqlDate($invoice->due_date)); }}
{{ Former::populateField('start_date', Utils::fromSqlDate($invoice->start_date)); }}
{{ Former::populateField('end_date', Utils::fromSqlDate($invoice->end_date)); }}
@else
{{ Former::populateField('invoice_number', $invoiceNumber) }}
{{ Former::populateField('invoice_date', date('m/d/Y')) }}
{{ Former::populateField('start_date', date('m/d/Y')) }}
{{ Former::populateField('frequency', FREQUENCY_MONTHLY) }}
@endif
<div class="row" style="min-height:195px">
<div class="row" style="min-height:195px" onkeypress="formEnterClick(event)">
<div class="col-md-7" id="col_1">
{{ Former::select('client')->addOption('', '')->fromQuery($clients, 'name', 'public_id')->select($client ? $client->public_id : '')->addGroupClass('client_select')
->help('<a style="cursor:pointer" data-toggle="modal" id="modalLink" onclick="showCreateNew()">Create new client</a>') }}
{{ Former::select('client')->addOption('', '')->fromQuery($clients, 'name', 'public_id')->data_bind("dropdown: client")
->addGroupClass('client_select closer-row') }}
<div class="form-group" style="margin-bottom: 8px">
<div class="col-lg-8 col-lg-offset-4">
<a href="#" data-bind="click: showClientForm, text: showClientText"></a>
</div>
</div>
<div data-bind="with: client">
<div class="form-group" data-bind="visible: contacts().length > 1, foreach: contacts">
<div class="col-lg-8 col-lg-offset-4">
<label for="test" class="checkbox" data-bind="attr: {for: $index() + '_check'}">
<input type="checkbox" value="1" data-bind="checked: send_invoice, attr: {id: $index() + '_check'}">
<span data-bind="text: fullName"/>
</label>
</div>
</div>
</div>
{{ Former::text('discount')->data_bind("value: discount, valueUpdate: 'afterkeydown'") }}
{{ Former::textarea('notes') }}
{{ Former::textarea('notes')->data_bind("value: notes, valueUpdate: 'afterkeydown'") }}
</div>
<div class="col-md-4" id="col_2">
<div id="recurring_checkbox">
{{ Former::checkbox('recurring')->text('Enable | <a href="#">Learn more</a>')->onchange('toggleRecurring()')
<div data-bind="visible: invoice_status_id() < CONSTS.INVOICE_STATUS_SENT">
{{ Former::checkbox('recurring')->text('Enable | <a href="#">Learn more</a>')->data_bind("checked: is_recurring")
->inlineHelp($invoice && $invoice->last_sent_date ? 'Last invoice sent ' . Utils::timestampToDateString($invoice->last_sent_date) : '') }}
</div>
<div id="recurring_off">
{{ Former::text('invoice_number')->label('Invoice #') }}
{{ Former::text('po_number')->label('PO number') }}
{{ Former::text('invoice_date') }}
{{ Former::text('due_date') }}
@if ($invoice && $invoice->recurring_invoice_id)
<div style="padding-top: 6px">
Created by a {{ link_to('/invoices/'.$invoice->recurring_invoice_id, 'recurring invoice') }}
</div>
@endif
<div data-bind="visible: !is_recurring()">
{{ Former::text('invoice_number')->label('Invoice #')->data_bind("value: invoice_number, valueUpdate: 'afterkeydown'") }}
{{ Former::text('po_number')->label('PO number')->data_bind("value: po_number, valueUpdate: 'afterkeydown'") }}
{{ Former::text('invoice_date')->data_bind("value: invoice_date, valueUpdate: 'afterkeydown'") }}
{{ Former::text('due_date')->data_bind("value: due_date, valueUpdate: 'afterkeydown'") }}
{{-- Former::text('invoice_date')->label('Invoice Date')->data_date_format('yyyy-mm-dd') --}}
</div>
<div id="recurring_on" style="display:none">
{{ Former::select('frequency')->label('How often')->options($frequencies)->onchange('updateRecurringStats()') }}
{{ Former::text('start_date')->onchange('updateRecurringStats()') }}
{{ Former::text('end_date')->onchange('updateRecurringStats()') }}
<div data-bind="visible: is_recurring">
{{ Former::select('frequency_id')->label('How often')->options($frequencies)->data_bind("value: frequency_id") }}
{{ Former::text('start_date')->data_bind("value: start_date, valueUpdate: 'afterkeydown'") }}
{{ Former::text('end_date')->data_bind("value: end_date, valueUpdate: 'afterkeydown'") }}
</div>
</div>
<div class="col-md-3" id="col_3" style="display:none">
</div>
</div>
<p>&nbsp;</p>
{{ Former::hidden('items')->data_bind("value: ko.toJSON(items)") }}
{{ Former::hidden('data')->data_bind("value: ko.toJSON(model)") }}
<table class="table invoice-table" style="margin-bottom: 0px !important">
<thead>
@ -77,16 +78,16 @@
<th class="hide-border"></th>
<th>Item</th>
<th>Description</th>
<th>Rate</th>
<th>Units</th>
<th>Unit Cost</th>
<th>Quantity</th>
<th>Line&nbsp;Total</th>
<th class="hide-border"></th>
</tr>
</thead>
<tbody data-bind="sortable: { data: items, afterMove: onDragged }">
<tbody data-bind="sortable: { data: invoice_items, afterMove: onDragged }">
<tr data-bind="event: { mouseover: showActions, mouseout: hideActions }" class="sortable-row">
<td style="width:20px;" class="hide-border td-icon">
<i data-bind="visible: actionsVisible() &amp;&amp; $parent.items().length > 1" class="fa fa-sort"></i>
<i data-bind="visible: actionsVisible() &amp;&amp; $parent.invoice_items().length > 1" class="fa fa-sort"></i>
</td>
<td style="width:120px">
{{ Former::text('product_key')->useDatalist(Product::getProductKeys($products), 'key')->onkeyup('onChange()')
@ -110,7 +111,7 @@
<span data-bind="text: total"></span>
</td>
<td style="width:20px; cursor:pointer" class="hide-border td-icon">
&nbsp;<i data-bind="click: $parent.removeItem, visible: actionsVisible() &amp;&amp; $parent.items().length > 1" class="fa fa-minus-circle" title="Remove item"/>
&nbsp;<i data-bind="click: $parent.removeItem, visible: actionsVisible() &amp;&amp; $parent.invoice_items().length > 1" class="fa fa-minus-circle" title="Remove item"/>
</td>
</tr>
</tbody>
@ -189,55 +190,63 @@
<h4 class="modal-title" id="myModalLabel">New Client</h4>
</div>
<div class="row" style="padding-left:16px;padding-right:16px" onkeypress="preventFormSubmit(event)">
<div class="col-md-6">
{{ Former::legend('Organization') }}
{{ Former::text('name') }}
{{ Former::text('work_phone')->label('Phone') }}
{{ Former::legend('Contact') }}
{{ Former::text('first_name') }}
{{ Former::text('last_name') }}
{{ Former::text('email') }}
{{ Former::text('phone') }}
<div class="row" data-bind="with: client" style="padding-left:16px;padding-right:16px" onkeypress="modalEnterClick(event)">
<div class="col-md-6">
{{ Former::legend('Organization') }}
{{ Former::text('name')->data_bind("value: name, valueUpdate: 'afterkeydown'") }}
{{ Former::text('work_phone')->data_bind("value: work_phone, valueUpdate: 'afterkeydown'")->label('Phone') }}
{{ Former::legend('Address') }}
{{ Former::text('address1')->label('Street')->data_bind("value: address1, valueUpdate: 'afterkeydown'") }}
{{ Former::text('address2')->label('Apt/Floor')->data_bind("value: address2, valueUpdate: 'afterkeydown'") }}
{{ Former::text('city')->data_bind("value: city, valueUpdate: 'afterkeydown'") }}
{{ Former::text('state')->data_bind("value: state, valueUpdate: 'afterkeydown'") }}
{{ Former::text('postal_code')->data_bind("value: postal_code, valueUpdate: 'afterkeydown'") }}
{{ Former::select('country_id')->addOption('','')->label('Country')
->fromQuery($countries, 'name', 'id')->data_bind("value: country_id") }}
</div>
<div class="col-md-6">
{{ Former::legend('Address') }}
{{ Former::text('address1')->label('Street') }}
{{ Former::text('address2')->label('Apt/Floor') }}
{{ Former::text('city') }}
{{ Former::text('state') }}
{{ Former::text('postal_code') }}
{{ Former::select('country_id')->addOption('','')->label('Country')->addGroupClass('country_select')
->fromQuery($countries, 'name', 'id')->select($client ? $client->country_id : '') }}
<div class="col-md-6">
{{ Former::legend('Contacts') }}
<div data-bind='template: { foreach: contacts,
beforeRemove: hideContact,
afterAdd: showContact }'>
{{ Former::hidden('public_id')->data_bind("value: public_id, valueUpdate: 'afterkeydown'") }}
{{ Former::text('first_name')->data_bind("value: first_name, valueUpdate: 'afterkeydown'") }}
{{ Former::text('last_name')->data_bind("value: last_name, valueUpdate: 'afterkeydown'") }}
{{ Former::text('email')->data_bind("value: email, valueUpdate: 'afterkeydown'") }}
{{ Former::text('phone')->data_bind("value: phone, valueUpdate: 'afterkeydown'") }}
<div class="form-group">
<div class="col-lg-8 col-lg-offset-4">
<span data-bind="visible: $parent.contacts().length > 1">
{{ link_to('#', 'Remove contact', array('data-bind'=>'click: $parent.removeContact')) }}
</span>
<span data-bind="visible: $index() === ($parent.contacts().length - 1)" class="pull-right">
{{ link_to('#', 'Add contact', array('data-bind'=>'click: $parent.addContact')) }}
</span>
</div>
</div>
</div>
{{ Former::legend('Additional Info') }}
{{ Former::select('client_size_id')->addOption('','')->label('Size')
->fromQuery($clientSizes, 'name', 'id')->select($client ? $client->client_size_id : '') }}
{{ Former::select('client_industry_id')->addOption('','')->label('Industry')
->fromQuery($clientIndustries, 'name', 'id')->select($client ? $client->client_industry_id : '') }}
{{ Former::textarea('notes') }}
</div>
</div>
<!--
<div class="modal-body" style="min-height:80px">
<div class="form-group">
<label for="name" class="control-label col-lg-2 col-sm-4">Name<sup>*</sup></label>
<div class="col-lg-10 col-sm-8">
<input class="form-control" id="client_name" type="text" name="client_name" onkeypress="nameKeyPress(event)">
<span class="help-block" id="nameHelp" style="display: none">Please provide a value</span><span>&nbsp;</span>
</div>
</div>
<div class="form-group">
<label for="email" class="control-label col-lg-2 col-sm-4">Email<sup>*</sup></label>
<div class="col-lg-10 col-sm-8">
<input class="form-control" id="client_email" type="text" name="client_email" onkeypress="nameKeyPress(event)">
<span class="help-block" id="emailHelp" style="display: none">Please provide a value</span><span>&nbsp;</span>
</div>
</div>
</div>
-->
<div class="modal-footer">
<span class="error-block" id="nameError" style="display:none;float:left">Please provide a value for the name field.</span><span>&nbsp;</span>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" onclick="newClient()">Done</button>
<button type="button" class="btn btn-primary" data-bind="click: clientFormComplete">Done</button>
</div>
</div>
@ -247,17 +256,6 @@
{{ Former::close() }}
<script type="text/javascript">
/*
function textarea_height(TextArea, MaxHeight) {
textarea = document.getElementById(TextArea);
textareaRows = textarea.value.split("\n");
if(textareaRows[0] != "undefined" && textareaRows.length < MaxHeight) counter = textareaRows.length;
else if(textareaRows.length >= MaxHeight) counter = MaxHeight;
else counter = 1;
textarea.rows = counter;
}
*/
$(function() {
@ -280,13 +278,13 @@
$input.combobox();
$('.client_select input.form-control').on('change', function(e) {
var clientId = parseInt($('input[name=client]').val(), 10);
$('#modalLink').text(clientId ? 'Edit client details' : 'Create new client');
//$('#modalLink').text(clientId ? 'Edit client details' : 'Create new client');
if (clientId > 0) {
loadClientDetails(clientId);
ko.mapping.fromJS(clientMap[clientId], model.client.mapping, model.client);
} else {
model.client.public_id(0);
}
}).trigger('change');
//enableHoverClick($('.combobox-container input.form-control'), $('.combobox-container input[name=client]'), '{{ URL::to('clients') }}');
}).trigger('change');
@if ($client)
$('#invoice_number').focus();
@ -304,62 +302,16 @@
$('#name').focus();
})
$('#invoice_number').change(refreshPDF);
$('#actionDropDown > button:first').click(function() {
onSaveClick();
});
$('label.radio').addClass('radio-inline');
@if ($invoice && $invoice->isRecurring())
$('#recurring').prop('checked', true);
@elseif (isset($invoice->recurring_invoice_id) && $invoice->recurring_invoice_id)
$('#recurring_checkbox > div > div').html('Created by a {{ link_to('/invoices/'.$invoice->recurring_invoice_id, 'recurring invoice') }}').css('padding-top','6px');
@elseif ($invoice && $invoice->isSent())
$('#recurring_checkbox').hide();
@endif
toggleRecurring();
applyComboboxListeners();
refreshPDF();
});
function loadClientDetails(clientId) {
var client = clientMap[clientId];
$('#name').val(client.name);
$('#work_phone').val(client.work_phone);
$('#address1').val(client.address1);
$('#address2').val(client.address2);
$('#city').val(client.city);
$('#state').val(client.state);
$('#postal_code').val(client.postal_code);
$('#country_id').val(client.country_id).combobox('refresh');
for (var i=0; i<client.contacts.length; i++) {
var contact = client.contacts[i];
if (contact.is_primary) {
$('#first_name').val(contact.first_name);
$('#last_name').val(contact.last_name);
$('#email').val(contact.email);
$('#phone').val(contact.phone);
}
}
}
function showCreateNew() {
if (!$('input[name=client]').val()) {
$('#myModal input').val('');
$('#myModal #country_id').val('');
$('#nameError').css( "display", "none" );
}
$('#myModal').modal('show');
}
function applyComboboxListeners() {
var value;
$('.datalist').on('focus', function() {
@ -381,78 +333,13 @@
});
}
function runCode() {
var text = $('#pdfText').val();
eval(text);
}
function createInvoiceModel() {
var invoice = {
invoice_number: $('#invoice_number').val(),
invoice_date: $('#invoice_date').val(),
discount: parseFloat($('#discount').val()),
po_number: $('#po_number').val(),
account: {
name: "{{ $account->name }}",
address1: "{{ $account->address1 }}",
address2: "{{ $account->address2 }}",
city: "{{ $account->city }}",
state: "{{ $account->state }}",
postal_code: "{{ $account->postal_code }}",
country: {
name: "{{ $account->country ? $account->country->name : '' }}"
}
},
@if (file_exists($account->getLogoPath()))
image: "{{ HTML::image_data($account->getLogoPath()) }}",
imageWidth: {{ $account->getLogoWidth() }},
imageHeight: {{ $account->getLogoHeight() }},
@endif
invoice_items: []
};
var client = {
name: $('#name').val(),
address1: $('#address1').val(),
address2: $('#address2').val(),
city: $('#city').val(),
state: $('#state').val(),
postal_code: $('#postal_code').val(),
country: {
name: $('.country_select input[type=text]').val()
}
};
/*
var clientId = $('input[name=client]').val();
if (clientId == '-1') {
var client = {
name: $('#name').val(),
address1: $('#address1').val(),
address2: $('#address2').val(),
city: $('#city').val(),
state: $('#state').val(),
postal_code: $('#postal_code').val(),
country: {
name: $('.country_select input[type=text]').val()
}
};
} else if (clientMap.hasOwnProperty(clientId)) {
var client = clientMap[clientId];
}
*/
invoice.client = client;
for(var i=0; i<model.items().length; i++) {
var item = model.items()[i];
invoice.invoice_items.push({
product_key: item.product_key(),
notes: item.notes(),
cost: item.cost(),
qty: item.qty()
});
}
var invoice = ko.toJS(model);
@if (file_exists($account->getLogoPath()))
invoice.image = "{{ HTML::image_data($account->getLogoPath()) }}";
invoice.imageWidth = {{ $account->getLogoWidth() }};
invoice.imageHeight = {{ $account->getLogoHeight() }};
@endif
return invoice;
}
@ -464,6 +351,7 @@
function _refreshPDF() {
console.log("refreshPDF");
var invoice = createInvoiceModel();
var doc = generatePDF(invoice);
@ -524,81 +412,101 @@
}
}
function newClient() {
var name = $('#name').val();
if (!name) {
if (!name) $('#nameError').css( "display", "inline" );
} else {
$('select#client').combobox('setSelected');
if (!$('input[name=client]').val()) {
$('input[name=client]').val('-1');
function formEnterClick(event) {
if (event.keyCode === 13){
if (event.target.type == 'textarea') {
return;
}
$('.client_select input.form-control').val(name);
$('.client_select .combobox-container').addClass('combobox-selected');
$('#nameError').css( "display", "none" );
$('#modalLink').text('Edit client details');
$('#myModal').modal('hide');
$('.client_select input.form-control').focus();
refreshPDF();
event.preventDefault();
$('.main_form').submit();
return false;
}
}
}
function preventFormSubmit(event) {
function modalEnterClick(event) {
if (event.keyCode === 13){
event.preventDefault();
newClient();
model.clientFormComplete();
return false;
}
}
function InvoiceModel() {
var self = this;
self.discount = ko.observable();
self.items = ko.observableArray();
var self = this;
this.client = new ClientModel();
self.discount = ko.observable('');
self.frequency_id = ko.observable('');
self.notes = ko.observable('');
self.po_number = ko.observable('');
self.invoice_date = ko.observable('');
self.due_date = ko.observable('');
self.start_date = ko.observable('');
self.end_date = ko.observable('');
self.is_recurring = ko.observable(false);
self.invoice_status_id = ko.observable(0);
self.invoice_items = ko.observableArray();
@if (isset($items) && $items)
@foreach ($items as $item)
var item = new ItemModel();
item.product_key("{{ $item->product_key }}");
item.notes('{{ str_replace("\n", "\\n", $item->notes) }}');
item.cost("{{ isset($item->cost) ? $item->cost : '' }}");
item.qty("{{ isset($item->qty) ? $item->qty : '' }}");
self.items.push(item);
@endforeach
@elseif ($invoice)
self.discount({{ $invoice->discount }});
@if (count($invoice->invoice_items) > 0)
@foreach ($invoice->invoice_items as $item)
var item = new ItemModel();
item.product_key("{{ $item->product_key }}");
item.notes('{{ str_replace("\n", "\\n", $item->notes) }}');
item.cost("{{ $item->cost }}");
item.qty("{{ $item->qty }}");
self.items.push(item);
@endforeach
@endif
@endif
self.mapping = {
'invoice_items': {
create: function(options) {
return new ItemModel(options.data);
}
}
}
self.showClientText = ko.computed(function() {
return self.client.public_id() ? 'Edit client details' : 'Create new client';
});
self.showClientForm = function() {
if (self.client.public_id() == 0) {
$('#myModal input').val('');
$('#myModal #country_id').val('');
}
$('#nameError').css( "display", "none" );
$('#myModal').modal('show');
}
self.clientFormComplete = function() {
var name = $('#name').val();
if (!name) {
if (!name) $('#nameError').css( "display", "inline" );
return;
}
$('select#client').combobox('setSelected');
if (self.client.public_id() == 0) {
self.client.public_id(-1);
}
$('.client_select input.form-control').val(name);
$('.client_select .combobox-container').addClass('combobox-selected');
$('#nameError').css( "display", "none" );
$('.client_select input.form-control').focus();
refreshPDF();
$('#myModal').modal('hide');
}
self.items.push(new ItemModel());
self.removeItem = function(item) {
self.items.remove(item);
self.invoice_items.remove(item);
refreshPDF();
}
self.addItem = function() {
self.items.push(new ItemModel());
self.invoice_items.push(new ItemModel());
applyComboboxListeners();
}
this.rawSubtotal = ko.computed(function() {
var total = 0;
for(var p = 0; p < self.items().length; ++p)
for(var p = 0; p < self.invoice_items().length; ++p)
{
total += self.items()[p].rawTotal();
total += self.invoice_items()[p].rawTotal();
}
return total;
});
@ -630,8 +538,74 @@
}
}
function ItemModel() {
function ClientModel(data) {
var self = this;
self.public_id = ko.observable(0);
self.name = ko.observable('');
self.work_phone = ko.observable('');
self.notes = ko.observable('');
self.address1 = ko.observable('');
self.address2 = ko.observable('');
self.city = ko.observable('');
self.state = ko.observable('');
self.postal_code = ko.observable('');
self.country_id = ko.observable('');
self.client_size_id = ko.observable('');
self.client_industry_id = ko.observable('');
self.contacts = ko.observableArray();
self.mapping = {
'contacts': {
create: function(options) {
return new ContactModel(options.data);
}
}
}
self.showContact = function(elem) { if (elem.nodeType === 1) $(elem).hide().slideDown() }
self.hideContact = function(elem) { if (elem.nodeType === 1) $(elem).slideUp(function() { $(elem).remove(); }) }
self.addContact = function() {
var contact = new ContactModel();
console.log('num: ' + self.contacts().length);
if (self.contacts().length == 0) {
contact.send_invoice(true);
}
self.contacts.push(contact);
return false;
}
self.removeContact = function() {
self.contacts.remove(this);
}
if (data) {
ko.mapping.fromJS(data, {}, this);
} else {
self.addContact();
}
}
function ContactModel(data) {
var self = this;
self.public_id = ko.observable('');
self.first_name = ko.observable('');
self.last_name = ko.observable('');
self.email = ko.observable('');
self.phone = ko.observable('');
self.send_invoice = ko.observable(false);
if (data) {
ko.mapping.fromJS(data, {}, this);
}
self.fullName = ko.computed(function() {
return self.first_name() + ' ' + self.last_name();
});
}
function ItemModel(data) {
var self = this;
this.product_key = ko.observable('');
this.notes = ko.observable('');
this.cost = ko.observable();
@ -639,6 +613,10 @@
this.tax = ko.observable();
this.actionsVisible = ko.observable(false);
if (data) {
ko.mapping.fromJS(data, {}, this);
}
this.rawTotal = ko.computed(function() {
var cost = parseFloat(self.cost());
var qty = parseFloat(self.qty());
@ -701,8 +679,8 @@
function onChange()
{
var hasEmpty = false;
for(var i=0; i<model.items().length; i++) {
var item = model.items()[i];
for(var i=0; i<model.invoice_items().length; i++) {
var item = model.invoice_items()[i];
if (item.isEmpty()) {
hasEmpty = true;
}
@ -712,73 +690,38 @@
}
}
function toggleRecurring()
{
var enabled = $('#recurring').is(':checked');
if (enabled) {
$('#recurring_on').show();
$('#recurring_off').hide();
$('#email_button').prop('disabled', true);
} else {
$('#recurring_on').hide();
$('#recurring_off').show();
$('#email_button').prop('disabled', false);
}
/*
$('#col_1').toggleClass('col-md-6 col-md-5');
$('#col_2').toggleClass('col-md-5 col-md-3');
if (show) {
setTimeout(function() {
$('#col_3').show();
}, 500);
} else {
$('#col_3').hide();
}
$('#showRecurring,#hideRecurring').toggle();
if (!show) {
$('#how_often, #start_date, #end_date').val('')
}
*/
};
function updateRecurringStats()
{
/*
var howOften = $('#how_often').val();
var startDate = $('#start_date').val();
var endDate = $('#end_date').val();
console.log("%s %s %s", howOften, startDate, endDate);
var str = "Send ";
if (!endDate) {
str += " an unlimited number of ";
} else {
str += "";
}
str += " emails";
$('#stats').html(str);
*/
}
var products = {{ $products }};
var clients = {{ $clients }};
var clientMap = {};
for (var i=0; i<clients.length; i++) {
var client = clients[i];
for (var i=0; i<client.contacts.length; i++) {
var contact = client.contacts[i];
contact.send_invoice = contact.is_primary;
}
clientMap[client.public_id] = client;
}
window.model = new InvoiceModel();
window.model = new InvoiceModel();
@if ($invoice)
var invoice = {{ $invoice }};
ko.mapping.fromJS(invoice, model.mapping, model);
if (!model.discount()) model.discount('');
var invitationContactIds = {{ json_encode($invitationContactIds) }};
var client = clientMap[invoice.client.public_id];
for (var i=0; i<client.contacts.length; i++) {
var contact = client.contacts[i];
contact.send_invoice = invitationContactIds.indexOf(contact.public_id) >= 0;
}
@else
model.invoice_number = '{{ $invoiceNumber }}';
@endif
model.invoice_items.push(new ItemModel());
ko.applyBindings(model);
</script>
@stop

View File

@ -1,6 +1,6 @@
function generatePDF(invoice) {
var invoiceNumber = invoice.invoice_number;
var issuedOn = invoice.invoice_date;
var issuedOn = invoice.invoice_date ? invoice.invoice_date : '';
var amount = '$0.00';
var marginLeft = 90;
@ -560,4 +560,40 @@ function convertDataURIToBinary(dataURI) {
array[i] = raw.charCodeAt(i);
}
return array;
}
}
ko.bindingHandlers.dropdown = {
init: function (element, valueAccessor, allBindingsAccessor) {
var options = allBindingsAccessor().dropdownOptions|| {};
var value = ko.utils.unwrapObservable(valueAccessor());
var id = (value && value.public_id) ? value.public_id() : (value && value.id) ? value.id() : false;
if (id) $(element).val(id);
console.log("combo-init: %s", id);
$(element).combobox(options);
/*
ko.utils.registerEventHandler(element, "change", function () {
console.log("change: %s", $(element).val());
var
valueAccessor($(element).val());
//$(element).combobox('refresh');
});
*/
},
update: function (element, valueAccessor) {
var value = ko.utils.unwrapObservable(valueAccessor());
var id = (value && value.public_id) ? value.public_id() : (value && value.id) ? value.id() : false;
console.log("combo-update: %s", id);
if (id) $(element).val(id);
$(element).combobox('refresh');
}
};
var CONSTS = {};
CONSTS.INVOICE_STATUS_DRAFT = 1;
CONSTS.INVOICE_STATUS_SENT = 2;
CONSTS.INVOICE_STATUS_VIEWED = 3;
CONSTS.INVOICE_STATUS_PARTIAL = 4;
CONSTS.INVOICE_STATUS_PAID = 5;