diff --git a/LICENSE b/LICENSE old mode 100644 new mode 100755 diff --git a/README.md b/README.md old mode 100644 new mode 100755 diff --git a/app/commands/SendRecurringInvoices.php b/app/commands/SendRecurringInvoices.php index 4829555bf22d..93e4a3837195 100755 --- a/app/commands/SendRecurringInvoices.php +++ b/app/commands/SendRecurringInvoices.php @@ -3,7 +3,7 @@ use Illuminate\Console\Command; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputArgument; -use Ninja\Mailers\ContactMailer as Mailer; +use ninja\mailers\ContactMailer as Mailer; class SendRecurringInvoices extends Command { diff --git a/app/config/mail.php b/app/config/mail.php index 82ce8f4c0b45..61c8c5454cf6 100755 --- a/app/config/mail.php +++ b/app/config/mail.php @@ -54,7 +54,7 @@ return array( | */ - 'from' => array('address' => '', 'name' => ''), + 'from' => array('address' => 'contact@invoiceninja.com', 'name' => 'Invoice Ninja'), /* |-------------------------------------------------------------------------- diff --git a/app/controllers/AccountController.php b/app/controllers/AccountController.php index f7cf4ca319fe..5e1c19f934f2 100755 --- a/app/controllers/AccountController.php +++ b/app/controllers/AccountController.php @@ -389,11 +389,17 @@ class AccountController extends \BaseController { $account->date_format_id = Input::get('date_format_id') ? Input::get('date_format_id') : null; $account->datetime_format_id = Input::get('datetime_format_id') ? Input::get('datetime_format_id') : null; - Event::fire('user.refresh'); - $account->invoice_terms = Input::get('invoice_terms'); $account->save(); + $user = Auth::user(); + $user->notify_sent = Input::get('notify_sent'); + $user->notify_viewed = Input::get('notify_viewed'); + $user->notify_paid = Input::get('notify_paid'); + $user->save(); + + Event::fire('user.refresh'); + if ($gatewayId) { $accountGateway = new AccountGateway; diff --git a/app/controllers/InvoiceController.php b/app/controllers/InvoiceController.php index 07470dc42052..be8bd6d94029 100755 --- a/app/controllers/InvoiceController.php +++ b/app/controllers/InvoiceController.php @@ -1,16 +1,22 @@ mailer = $mailer; + $this->invoiceRepo = $invoiceRepo; + $this->clientRepo = $clientRepo; } public function index() @@ -32,30 +38,7 @@ class InvoiceController extends \BaseController { public function getDatatable($clientPublicId = null) { - $query = DB::table('invoices') - ->join('clients', 'clients.id', '=','invoices.client_id') - ->join('invoice_statuses', 'invoice_statuses.id', '=', 'invoices.invoice_status_id') - ->where('invoices.account_id', '=', Auth::user()->account_id) - ->where('invoices.deleted_at', '=', null) - ->where('clients.deleted_at', '=', null) - ->where('invoices.is_recurring', '=', false) - ->select('clients.public_id as client_public_id', 'invoice_number', 'clients.name as client_name', 'invoices.public_id', 'amount', 'invoices.balance', 'invoice_date', 'due_date', 'invoice_statuses.name as invoice_status_name'); - - if ($clientPublicId) { - $query->where('clients.public_id', '=', $clientPublicId); - } - - $filter = Input::get('sSearch'); - if ($filter) - { - $query->where(function($query) use ($filter) - { - $query->where('clients.name', 'like', '%'.$filter.'%') - ->orWhere('invoices.invoice_number', 'like', '%'.$filter.'%') - ->orWhere('invoice_statuses.name', 'like', '%'.$filter.'%'); - }); - } - + $query = $this->invoiceRepo->getInvoices(Auth::user()->account_id, $clientPublicId, Input::get('sSearch')); $table = Datatable::query($query); if (!$clientPublicId) { @@ -93,28 +76,7 @@ class InvoiceController extends \BaseController { public function getRecurringDatatable($clientPublicId = null) { - $query = DB::table('invoices') - ->join('clients', 'clients.id', '=','invoices.client_id') - ->join('frequencies', 'frequencies.id', '=', 'invoices.frequency_id') - ->where('invoices.account_id', '=', Auth::user()->account_id) - ->where('invoices.deleted_at', '=', null) - ->where('invoices.is_recurring', '=', true) - ->select('clients.public_id as client_public_id', 'clients.name as client_name', 'invoices.public_id', 'amount', 'frequencies.name as frequency', 'start_date', 'end_date'); - - if ($clientPublicId) { - $query->where('clients.public_id', '=', $clientPublicId); - } - - $filter = Input::get('sSearch'); - if ($filter) - { - $query->where(function($query) use ($filter) - { - $query->where('clients.name', 'like', '%'.$filter.'%') - ->orWhere('invoices.invoice_number', 'like', '%'.$filter.'%'); - }); - } - + $query = $this->invoiceRepo->getRecurringInvoices(Auth::user()->account_id, $clientPublicId, Input::get('sSearch')); $table = Datatable::query($query); if (!$clientPublicId) { @@ -421,131 +383,40 @@ class InvoiceController extends \BaseController { $inputClient = $input->client; $inputClient->name = trim($inputClient->name); - if (!$inputClient->name) { + if (!$inputClient->name) + { return Redirect::to('invoices/create') ->withInput(); - } else { - - $clientPublicId = $input->client->public_id; + } + else + { + $clientData = (array) $input->client; + $client = $this->clientRepo->save($input->client->public_id, $clientData); - if ($clientPublicId == "-1") - { - $client = Client::createNew(); - $contact = Contact::createNew(); - $contact->is_primary = true; - } - else - { - $client = Client::scope($clientPublicId)->with('contacts')->firstOrFail(); - $contact = $client->contacts()->where('is_primary', '=', true)->firstOrFail(); - } - - $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->notes = trim($inputClient->notes); - $client->client_size_id = $inputClient->client_size_id ? $inputClient->client_size_id : null; - $client->client_industry_id = $inputClient->client_industry_id ? $inputClient->client_industry_id : null; - $client->website = trim($inputClient->website); - $client->save(); - - $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(); - $invoice->invoice_items()->forceDelete(); - } else { - $invoice = Invoice::createNew(); - } - - $invoice->client_id = $client->id; - $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->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->terms = $input->terms; - $invoice->po_number = $input->po_number; - - - $client->invoices()->save($invoice); - - $items = $input->invoice_items; - $total = 0; - - foreach ($items as $item) - { - if (!isset($item->cost)) { - $item->cost = 0; - } - if (!isset($item->qty)) { - $item->qty = 0; - } - - $total += floatval($item->qty) * floatval($item->cost); - } + $invoiceData = (array) $input; + $invoiceData['client_id'] = $client->id; + $invoice = $this->invoiceRepo->save($publicId, $invoiceData); if ($action == 'email' && $invoice->invoice_status_id == INVOICE_STATUS_DRAFT) { $invoice->invoice_status_id = INVOICE_STATUS_SENT; - $client->balance = $invoice->client->balance + $invoice->amount; + $client->balance = $client->balance + $invoice->amount; $client->save(); } - $invoice->amount = $total; - $invoice->save(); + $client->load('contacts'); + $sendInvoiceIds = []; - foreach ($contacts as $contact) + foreach ($client->contacts as $contact) + { + if ($contact->send_invoice) + { + $sendInvoiceIds[] = $contact->id; + } + } + + foreach ($client->contacts as $contact) { $invitation = Invitation::scope()->whereContactId($contact->id)->whereInvoiceId($invoice->id)->first(); @@ -563,56 +434,25 @@ class InvoiceController extends \BaseController { } } - foreach ($items as $item) + $message = ''; + if ($input->client->public_id == '-1') { - if (!$item->cost && !$item->qty && !$item->product_key && !$item->notes) - { - continue; - } - - if ($item->product_key) - { - $product = Product::findProductByKey(trim($item->product_key)); - - if (!$product) - { - $product = Product::createNew(); - $product->product_key = trim($item->product_key); - } - - /* - $product->notes = $item->notes; - $product->cost = $item->cost; - $product->qty = $item->qty; - */ - - $product->save(); - } - - $invoiceItem = InvoiceItem::createNew(); - $invoiceItem->product_id = isset($product) ? $product->id : null; - $invoiceItem->product_key = trim($item->product_key); - $invoiceItem->notes = trim($item->notes); - $invoiceItem->cost = floatval($item->cost); - $invoiceItem->qty = floatval($item->qty); - - $invoice->invoice_items()->save($invoiceItem); + $message = ' and created client'; + $url = URL::to('clients/' . $client->public_id); + Utils::trackViewed($client->name, ENTITY_CLIENT, $url); } - - /* - */ - - $message = $clientPublicId == "-1" ? ' and created client' : ''; + if ($action == 'clone') { return InvoiceController::cloneInvoice($publicId); } else if ($action == 'email') { - $this->mailer->sendInvoice($invoice); - + $this->mailer->sendInvoice($invoice); Session::flash('message', 'Successfully emailed invoice'.$message); - } else { + } + else + { Session::flash('message', 'Successfully saved invoice'.$message); } diff --git a/app/database/migrations/2013_11_05_180133_confide_setup_users_table.php b/app/database/migrations/2013_11_05_180133_confide_setup_users_table.php index fbb4d3a8062c..1ca9ff22e518 100755 --- a/app/database/migrations/2013_11_05_180133_confide_setup_users_table.php +++ b/app/database/migrations/2013_11_05_180133_confide_setup_users_table.php @@ -68,7 +68,8 @@ class ConfideSetupUsersTable extends Migration { Schema::create('date_formats', function($t) { $t->increments('id'); - $t->string('format'); + $t->string('format'); + $t->string('picker_format'); $t->string('label'); }); @@ -149,6 +150,10 @@ class ConfideSetupUsersTable extends Migration { $t->boolean('confirmed')->default(false); $t->integer('theme_id'); + $t->boolean('notify_sent')->default(false); + $t->boolean('notify_viewed')->default(false); + $t->boolean('notify_paid')->default(true); + $t->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); $t->unsignedInteger('public_id'); @@ -220,6 +225,7 @@ class ConfideSetupUsersTable extends Migration { $t->softDeletes(); $t->boolean('is_primary'); + $t->boolean('send_invoice'); $t->string('first_name'); $t->string('last_name'); $t->string('email'); diff --git a/app/database/seeds/ConstantsSeeder.php b/app/database/seeds/ConstantsSeeder.php index 5b3a678f3764..50ace732c282 100755 --- a/app/database/seeds/ConstantsSeeder.php +++ b/app/database/seeds/ConstantsSeeder.php @@ -72,9 +72,16 @@ class ConstantsSeeder extends Seeder DatetimeFormat::create(array('format' => 'F j, Y, g:i a', 'label' => 'March 10, 2013, 6:15 pm')); DatetimeFormat::create(array('format' => 'D M jS, Y g:ia', 'label' => 'Mon March 10th, 2013, 6:15 pm')); - DateFormat::create(array('format' => 'F j, Y', 'label' => 'March 10, 2013')); - DateFormat::create(array('format' => 'D M jS', 'label' => 'Mon March 10th, 2013')); - + DateFormat::create(array('format' => 'F j, Y', 'picker_format' => 'MM d, yyyy', 'label' => 'March 10, 2013')); + DateFormat::create(array('format' => 'D M jS, Y', 'picker_format' => 'D MM d, yyyy', 'label' => 'Mon March 10th, 2013')); + + /* + d, dd: Numeric date, no leading zero and leading zero, respectively. Eg, 5, 05. + D, DD: Abbreviated and full weekday names, respectively. Eg, Mon, Monday. + m, mm: Numeric month, no leading zero and leading zero, respectively. Eg, 7, 07. + M, MM: Abbreviated and full month names, respectively. Eg, Jan, January + yy, yyyy: 2- and 4-digit years, respectively. Eg, 12, 2012.) + */ $gateways = [ array('name'=>'Authorize.Net AIM', 'provider'=>'AuthorizeNet_AIM'), diff --git a/app/handlers/InvoiceEventHandler.php b/app/handlers/InvoiceEventHandler.php new file mode 100755 index 000000000000..bbce7793b174 --- /dev/null +++ b/app/handlers/InvoiceEventHandler.php @@ -0,0 +1,46 @@ +mailer = $mailer; + } + + public function subscribe($events) + { + $events->listen('invoice.sent', 'InvoiceEventHandler@onSent'); + $events->listen('invoice.viewed', 'InvoiceEventHandler@onViewed'); + $events->listen('invoice.paid', 'InvoiceEventHandler@onPaid'); + } + + public function onSent($invoice) + { + $this->sendNotifications($invoice, 'sent'); + } + + public function onViewed($invoice) + { + + } + + public function onPaid($invoice) + { + + } + + private function sendNotifications($invoice, $type) + { + foreach ($invoice->account->users as $user) + { + if ($user->{'notify_' . $type}) + { + $this->mailer->sendNotification($user, $invoice, $type); + } + } + } +} \ No newline at end of file diff --git a/app/handlers/UserEventHandler.php b/app/handlers/UserEventHandler.php index af8095341121..8964b3484a33 100755 --- a/app/handlers/UserEventHandler.php +++ b/app/handlers/UserEventHandler.php @@ -31,6 +31,7 @@ class UserEventHandler Session::put(SESSION_TIMEZONE, $account->timezone ? $account->timezone->name : DEFAULT_TIMEZONE); Session::put(SESSION_DATE_FORMAT, $account->date_format ? $account->date_format->format : DEFAULT_DATE_FORMAT); + Session::put(SESSION_DATE_PICKER_FORMAT, $account->date_format ? $account->date_format->picker_format : DEFAULT_DATE_PICKER_FORMAT); Session::put(SESSION_DATETIME_FORMAT, $account->datetime_format ? $account->datetime_format->format : DEFAULT_DATETIME_FORMAT); } } \ No newline at end of file diff --git a/app/libraries/entity.php b/app/libraries/entity.php old mode 100644 new mode 100755 diff --git a/app/libraries/utils.php b/app/libraries/utils.php index 55b0ae907162..6b727307c32d 100755 --- a/app/libraries/utils.php +++ b/app/libraries/utils.php @@ -92,7 +92,13 @@ class Utils return null; } - return DateTime::createFromFormat('m/d/Y', $date); + /* + $timezone = Session::get(SESSION_TIMEZONE, DEFAULT_TIMEZONE); + $format = Session::get(SESSION_DATE_FORMAT, DEFAULT_DATE_FORMAT); + return DateTime::createFromFormat($format, $date, new DateTimeZone($timezone)); + */ + + return DateTime::createFromFormat('Y-m-d', $date); } public static function fromSqlDate($date) @@ -102,12 +108,23 @@ class Utils return ''; } - return DateTime::createFromFormat('Y-m-d', $date)->format('m/d/Y'); + /* + $timezone = Session::get(SESSION_TIMEZONE, DEFAULT_TIMEZONE); + return DateTime::createFromFormat('Y-m-d', $date, new DateTimeZone($timezone))->format($format); + */ + + $format = Session::get(SESSION_DATE_FORMAT, DEFAULT_DATE_FORMAT); + + return DateTime::createFromFormat('Y-m-d', $date)->format($format); } - public static function trackViewed($name, $type) + public static function trackViewed($name, $type, $url = false) { - $url = Request::url(); + if (!$url) + { + $url = Request::url(); + } + $viewed = Session::get(RECENTLY_VIEWED); if (!$viewed) diff --git a/app/mailers/UserMailer.php b/app/mailers/UserMailer.php deleted file mode 100755 index e69de29bb2d1..000000000000 diff --git a/app/models/Invitation.php b/app/models/Invitation.php old mode 100644 new mode 100755 diff --git a/app/mailers/ContactMailer.php b/app/ninja/mailers/ContactMailer.php similarity index 88% rename from app/mailers/ContactMailer.php rename to app/ninja/mailers/ContactMailer.php index 19714d596b57..e7bfaf0866af 100755 --- a/app/mailers/ContactMailer.php +++ b/app/ninja/mailers/ContactMailer.php @@ -1,4 +1,4 @@ -invoice_status_id = INVOICE_STATUS_SENT; $invoice->save(); } + + \Event::fire('invoice.sent', $invoice); } } \ No newline at end of file diff --git a/app/mailers/Mailer.php b/app/ninja/mailers/Mailer.php similarity index 71% rename from app/mailers/Mailer.php rename to app/ninja/mailers/Mailer.php index 0ec95884710e..4452e97760e4 100755 --- a/app/mailers/Mailer.php +++ b/app/ninja/mailers/Mailer.php @@ -1,4 +1,4 @@ - 'emails.'.$view.'_text' ]; - Mail::queue($views, $data, function($message) use($email, $subject) + Mail::queue($views, $data, function($message) use ($email, $subject) { $message->to($email)->subject($subject); }); diff --git a/app/ninja/mailers/UserMailer.php b/app/ninja/mailers/UserMailer.php new file mode 100755 index 000000000000..5331d77a818c --- /dev/null +++ b/app/ninja/mailers/UserMailer.php @@ -0,0 +1,23 @@ +email) + { + return; + } + + $view = 'invoice'; + //$data = array('link' => URL::to('view') . '/' . $invoice->invoice_key); + $data = []; + $subject = 'Notification - Invoice ' . $type; + + $this->sendTo($user->email, $subject, $view, $data); + } +} \ No newline at end of file diff --git a/app/ninja/repositories/ClientRepository.php b/app/ninja/repositories/ClientRepository.php new file mode 100755 index 000000000000..8cd152ee173d --- /dev/null +++ b/app/ninja/repositories/ClientRepository.php @@ -0,0 +1,74 @@ +is_primary = true; + } + else + { + $client = Client::scope($publicId)->with('contacts')->firstOrFail(); + $contact = $client->contacts()->where('is_primary', '=', true)->firstOrFail(); + } + + $client->name = trim($data['name']); + $client->work_phone = trim($data['work_phone']); + $client->address1 = trim($data['address1']); + $client->address2 = trim($data['address2']); + $client->city = trim($data['city']); + $client->state = trim($data['state']); + $client->postal_code = trim($data['postal_code']); + $client->country_id = $data['country_id'] ? $data['country_id'] : null; + $client->notes = trim($data['notes']); + $client->client_size_id = $data['client_size_id'] ? $data['client_size_id'] : null; + $client->client_industry_id = $data['client_industry_id'] ? $data['client_industry_id'] : null; + $client->website = trim($data['website']); + $client->save(); + + $isPrimary = true; + $contactIds = []; + + foreach ($data['contacts'] as $record) + { + $record = (array) $record; + + if (isset($record['public_id']) && $record['public_id']) + { + $contact = Contact::scope($record['public_id'])->firstOrFail(); + } + else + { + $contact = Contact::createNew(); + } + + $contact->email = trim($record['email']); + $contact->first_name = trim($record['first_name']); + $contact->last_name = trim($record['last_name']); + $contact->phone = trim($record['phone']); + $contact->is_primary = $isPrimary; + $contact->send_invoice = $record['send_invoice']; + $isPrimary = false; + + $client->contacts()->save($contact); + $contactIds[] = $contact->public_id; + } + + foreach ($client->contacts as $contact) + { + if (!in_array($contact->public_id, $contactIds)) + { + $contact->forceDelete(); + } + } + + return $client; + } +} \ No newline at end of file diff --git a/app/ninja/repositories/InvoiceRepository.php b/app/ninja/repositories/InvoiceRepository.php new file mode 100755 index 000000000000..cbc43c6b5e3f --- /dev/null +++ b/app/ninja/repositories/InvoiceRepository.php @@ -0,0 +1,150 @@ +join('clients', 'clients.id', '=','invoices.client_id') + ->join('invoice_statuses', 'invoice_statuses.id', '=', 'invoices.invoice_status_id') + ->where('invoices.account_id', '=', $accountId) + ->where('invoices.deleted_at', '=', null) + ->where('clients.deleted_at', '=', null) + ->where('invoices.is_recurring', '=', false) + ->select('clients.public_id as client_public_id', 'invoice_number', 'clients.name as client_name', 'invoices.public_id', 'amount', 'invoices.balance', 'invoice_date', 'due_date', 'invoice_statuses.name as invoice_status_name'); + + if ($clientPublicId) + { + $query->where('clients.public_id', '=', $clientPublicId); + } + + if ($filter) + { + $query->where(function($query) use ($filter) + { + $query->where('clients.name', 'like', '%'.$filter.'%') + ->orWhere('invoices.invoice_number', 'like', '%'.$filter.'%') + ->orWhere('invoice_statuses.name', 'like', '%'.$filter.'%'); + }); + } + + return $query; + } + + public function getRecurringInvoices($accountId, $clientPublicId = false, $filter = false) + { + $query = \DB::table('invoices') + ->join('clients', 'clients.id', '=','invoices.client_id') + ->join('frequencies', 'frequencies.id', '=', 'invoices.frequency_id') + ->where('invoices.account_id', '=', $accountId) + ->where('invoices.deleted_at', '=', null) + ->where('invoices.is_recurring', '=', true) + ->select('clients.public_id as client_public_id', 'clients.name as client_name', 'invoices.public_id', 'amount', 'frequencies.name as frequency', 'start_date', 'end_date'); + + if ($clientPublicId) + { + $query->where('clients.public_id', '=', $clientPublicId); + } + + if ($filter) + { + $query->where(function($query) use ($filter) + { + $query->where('clients.name', 'like', '%'.$filter.'%') + ->orWhere('invoices.invoice_number', 'like', '%'.$filter.'%'); + }); + } + + return $query; + } + + public function save($publicId, $data) + { + if ($publicId) + { + $invoice = Invoice::scope($publicId)->firstOrFail(); + $invoice->invoice_items()->forceDelete(); + } + else + { + $invoice = Invoice::createNew(); + } + + $invoice->client_id = $data['client_id']; + $invoice->discount = $data['discount']; + $invoice->invoice_number = trim($data['invoice_number']); + $invoice->invoice_date = Utils::toSqlDate($data['invoice_date']); + $invoice->due_date = Utils::toSqlDate($data['due_date']); + + $invoice->is_recurring = $data['is_recurring']; + $invoice->frequency_id = $data['frequency_id'] ? $data['frequency_id'] : 0; + $invoice->start_date = Utils::toSqlDate($data['start_date']); + $invoice->end_date = Utils::toSqlDate($data['end_date']); + $invoice->terms = trim($data['terms']); + $invoice->po_number = trim($data['po_number']); + + $total = 0; + + foreach ($data['invoice_items'] as $item) + { + if (!isset($item->cost)) + { + $item->cost = 0; + } + + if (!isset($item->qty)) + { + $item->qty = 0; + } + + $total += floatval($item->qty) * floatval($item->cost); + } + + $invoice->amount = $total; + $invoice->save(); + + foreach ($data['invoice_items'] as $item) + { + if (!$item->cost && !$item->qty && !$item->product_key && !$item->notes) + { + continue; + } + + if ($item->product_key) + { + $product = Product::findProductByKey(trim($item->product_key)); + + if (!$product) + { + $product = Product::createNew(); + $product->product_key = trim($item->product_key); + } + + /* + $product->notes = $item->notes; + $product->cost = $item->cost; + $product->qty = $item->qty; + */ + + $product->save(); + } + + $invoiceItem = InvoiceItem::createNew(); + $invoiceItem->product_id = isset($product) ? $product->id : null; + $invoiceItem->product_key = trim($item->product_key); + $invoiceItem->notes = trim($item->notes); + $invoiceItem->cost = floatval($item->cost); + $invoiceItem->qty = floatval($item->qty); + + $invoice->invoice_items()->save($invoiceItem); + $total += floatval($item->qty) * floatval($item->cost); + } + + return $invoice; + } +} diff --git a/app/routes.php b/app/routes.php index 767d28cc15b9..e85da0b2b53c 100755 --- a/app/routes.php +++ b/app/routes.php @@ -170,8 +170,10 @@ define('FREQUENCY_ANNUALLY', 7); define('SESSION_TIMEZONE', 'timezone'); define('SESSION_DATE_FORMAT', 'dateFormat'); +define('SESSION_DATE_PICKER_FORMAT', 'datePickerFormat'); define('SESSION_DATETIME_FORMAT', 'datetimeFormat'); define('DEFAULT_TIMEZONE', 'US/Eastern'); -define('DEFAULT_DATE_FORMAT', 'F j, Y'); +define('DEFAULT_DATE_FORMAT', 'M j, Y'); +define('DEFAULT_DATE_PICKER_FORMAT', 'yyyy-mm-dd'); define('DEFAULT_DATETIME_FORMAT', 'F j, Y, g:i a'); diff --git a/app/start/global.php b/app/start/global.php index 1f377f021b6e..1f458bbd736c 100755 --- a/app/start/global.php +++ b/app/start/global.php @@ -74,6 +74,7 @@ App::down(function() Event::subscribe('UserEventHandler'); +Event::subscribe('InvoiceEventHandler'); /* diff --git a/app/views/accounts/settings.blade.php b/app/views/accounts/settings.blade.php index fafe8f75558c..fb91eff3b6f3 100755 --- a/app/views/accounts/settings.blade.php +++ b/app/views/accounts/settings.blade.php @@ -5,6 +5,9 @@ {{ Former::open()->addClass('col-md-8 col-md-offset-2') }} {{ Former::populate($account) }} + {{ Former::populateField('notify_sent', Auth::user()->notify_sent) }} + {{ Former::populateField('notify_viewed', Auth::user()->notify_viewed) }} + {{ Former::populateField('notify_paid', Auth::user()->notify_paid) }} {{ Former::legend('Payment Gateway') }} @@ -44,6 +47,10 @@ {{ Former::select('datetime_format_id')->addOption('','')->label('Date/Time Format') ->fromQuery($datetimeFormats, 'label', 'id')->select($account->datetime_format_id) }} + {{ Former::legend('Notifications') }} + {{ Former::checkbox('notify_sent')->label(' ')->text('Email me when an invoice is sent') }} + {{ Former::checkbox('notify_viewed')->label(' ')->text('Email me when an invoice is viewed') }} + {{ Former::checkbox('notify_paid')->label(' ')->text('Email me when an invoice is paid') }} {{ Former::legend('Invoices') }} {{ Former::textarea('invoice_terms') }} diff --git a/app/views/credits/edit.blade.php b/app/views/credits/edit.blade.php index 54364fe02791..632ece24ebdd 100755 --- a/app/views/credits/edit.blade.php +++ b/app/views/credits/edit.blade.php @@ -29,7 +29,7 @@ {{ Former::select('client')->fromQuery($clients, 'name', 'public_id')->select($client ? $client->public_id : '')->addOption('', '')->addGroupClass('client-select') }} {{ Former::text('amount') }} - {{ Former::text('credit_date') }} + {{ Former::text('credit_date')->data_date_format(DEFAULT_DATE_PICKER_FORMAT) }}
diff --git a/app/views/invoices/edit.blade.php b/app/views/invoices/edit.blade.php index 4d0c650595b5..ff285441665a 100755 --- a/app/views/invoices/edit.blade.php +++ b/app/views/invoices/edit.blade.php @@ -43,15 +43,13 @@
{{ Former::text('invoice_number')->label('Invoice #')->data_bind("value: invoice_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') --}} + {{ Former::text('invoice_date')->data_bind("value: invoice_date, valueUpdate: 'afterkeydown'")->data_date_format(DEFAULT_DATE_PICKER_FORMAT) }} + {{ Former::text('due_date')->data_bind("value: due_date, valueUpdate: 'afterkeydown'")->data_date_format(DEFAULT_DATE_PICKER_FORMAT) }}
{{ 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'") }} + {{ Former::text('start_date')->data_bind("value: start_date, valueUpdate: 'afterkeydown'")->data_date_format(DEFAULT_DATE_PICKER_FORMAT) }} + {{ Former::text('end_date')->data_bind("value: end_date, valueUpdate: 'afterkeydown'")->data_date_format(DEFAULT_DATE_PICKER_FORMAT) }}
{{ Former::checkbox('recurring')->text('Enable | Learn more')->data_bind("checked: is_recurring") diff --git a/app/views/payments/edit.blade.php b/app/views/payments/edit.blade.php index 36f7bf287204..a96440495e42 100755 --- a/app/views/payments/edit.blade.php +++ b/app/views/payments/edit.blade.php @@ -30,7 +30,7 @@ {{ Former::select('client')->addOption('', '')->addGroupClass('client-select') }} {{ Former::select('invoice')->addOption('', '')->addGroupClass('invoice-select') }} {{ Former::text('amount') }} - {{ Former::text('payment_date') }} + {{ Former::text('payment_date')->data_date_format(DEFAULT_DATE_PICKER_FORMAT) }}
diff --git a/composer.json b/composer.json index 98cb3e0c852b..5e4af2b83dd8 100755 --- a/composer.json +++ b/composer.json @@ -23,9 +23,11 @@ "app/database/migrations", "app/database/seeds", "app/tests/TestCase.php", - "app/libraries", - "app/mailers" - ] + "app/libraries" + ], + "psr-0" : { + "ninja" : "app/" + } }, "scripts": { "post-install-cmd": [ diff --git a/public/css/style.css b/public/css/style.css old mode 100644 new mode 100755 diff --git a/public/js/bootstrap-datepicker.js b/public/js/bootstrap-datepicker.js index 78c346c9b213..ee2f53a522c3 100755 --- a/public/js/bootstrap-datepicker.js +++ b/public/js/bootstrap-datepicker.js @@ -1,9 +1,11 @@ /* ========================================================= * bootstrap-datepicker.js - * http://www.eyecon.ro/bootstrap-datepicker + * Repo: https://github.com/eternicode/bootstrap-datepicker/ + * Demo: http://eternicode.github.io/bootstrap-datepicker/ + * Docs: http://bootstrap-datepicker.readthedocs.org/ + * Forked from http://www.eyecon.ro/bootstrap-datepicker * ========================================================= - * Copyright 2012 Stefan Petre - * Improvements by Andrew Rowls + * Started by Stefan Petre; improvements by Andrew Rowls + contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +20,7 @@ * limitations under the License. * ========================================================= */ -(function( $ ) { +(function($, undefined) { var $window = $(window); @@ -27,21 +29,71 @@ } function UTCToday(){ var today = new Date(); - return UTCDate(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate()); + return UTCDate(today.getFullYear(), today.getMonth(), today.getDate()); } + function alias(method){ + return function(){ + return this[method].apply(this, arguments); + }; + } + + var DateArray = (function(){ + var extras = { + get: function(i){ + return this.slice(i)[0]; + }, + contains: function(d){ + // Array.indexOf is not cross-browser; + // $.inArray doesn't work with Dates + var val = d && d.valueOf(); + for (var i=0, l=this.length; i 0 + if (o.multidate !== true){ + o.multidate = Number(o.multidate) || false; + if (o.multidate !== false) + o.multidate = Math.max(0, o.multidate); + else + o.multidate = 1; + } + o.multidateSeparator = String(o.multidateSeparator); + o.weekStart %= 7; o.weekEnd = ((o.weekStart + 6) % 7); @@ -204,17 +263,31 @@ _events: [], _secondaryEvents: [], _applyEvents: function(evs){ - for (var i=0, el, ev; i this.o.endDate || + !date + ); + }, this), true); + this.dates.replace(dates); + + if (this.dates.length) + this.viewDate = new Date(this.dates.get(-1)); + else if (this.viewDate < this.o.startDate) + this.viewDate = new Date(this.o.startDate); + else if (this.viewDate > this.o.endDate) + this.viewDate = new Date(this.o.endDate); if (fromArgs) { // setting date by clicking this.setValue(); - } else if (date) { + } else if (dates.length) { // setting date by typing - if (oldDate.getTime() !== this.date.getTime()) + if (String(oldDates) !== String(this.dates)) this._trigger('changeDate'); - } else { - // clearing date + } + if (!this.dates.length && oldDates.length) this._trigger('clearDate'); - } - if (this.date < this.o.startDate) { - this.viewDate = new Date(this.o.startDate); - this.date = new Date(this.o.startDate); - } else if (this.date > this.o.endDate) { - this.viewDate = new Date(this.o.endDate); - this.date = new Date(this.o.endDate); - } else { - this.viewDate = new Date(this.date); - this.date = new Date(this.date); - } this.fill(); }, @@ -552,23 +685,23 @@ var cls = [], year = this.viewDate.getUTCFullYear(), month = this.viewDate.getUTCMonth(), - currentDate = this.date.valueOf(), today = new Date(); - if (date.getUTCFullYear() < year || (date.getUTCFullYear() == year && date.getUTCMonth() < month)) { + if (date.getUTCFullYear() < year || (date.getUTCFullYear() === year && date.getUTCMonth() < month)) { cls.push('old'); - } else if (date.getUTCFullYear() > year || (date.getUTCFullYear() == year && date.getUTCMonth() > month)) { + } else if (date.getUTCFullYear() > year || (date.getUTCFullYear() === year && date.getUTCMonth() > month)) { cls.push('new'); } + if (this.focusDate && date.valueOf() === this.focusDate.valueOf()) + cls.push('focused'); // Compare internal UTC date with local today, not UTC today if (this.o.todayHighlight && - date.getUTCFullYear() == today.getFullYear() && - date.getUTCMonth() == today.getMonth() && - date.getUTCDate() == today.getDate()) { + date.getUTCFullYear() === today.getFullYear() && + date.getUTCMonth() === today.getMonth() && + date.getUTCDate() === today.getDate()) { cls.push('today'); } - if (currentDate && date.valueOf() == currentDate) { + if (this.dates.contains(date) !== -1) cls.push('active'); - } if (date.valueOf() < this.o.startDate || date.valueOf() > this.o.endDate || $.inArray(date.getUTCDay(), this.o.daysOfWeekDisabled) !== -1) { cls.push('disabled'); @@ -577,7 +710,7 @@ if (date > this.range[0] && date < this.range[this.range.length-1]){ cls.push('range'); } - if ($.inArray(date.valueOf(), this.range) != -1){ + if ($.inArray(date.valueOf(), this.range) !== -1){ cls.push('selected'); } } @@ -592,7 +725,6 @@ startMonth = this.o.startDate !== -Infinity ? this.o.startDate.getUTCMonth() : -Infinity, endYear = this.o.endDate !== Infinity ? this.o.endDate.getUTCFullYear() : Infinity, endMonth = this.o.endDate !== Infinity ? this.o.endDate.getUTCMonth() : Infinity, - currentDate = this.date && this.date.valueOf(), tooltip; this.picker.find('.datepicker-days thead th.datepicker-switch') .text(dates[this.o.language].months[month]+' '+year); @@ -604,7 +736,7 @@ .toggle(this.o.clearBtn !== false); this.updateNavArrows(); this.fillMonths(); - var prevMonth = UTCDate(year, month-1, 28,0,0,0,0), + var prevMonth = UTCDate(year, month-1, 28), day = DPGlobal.getDaysInMonth(prevMonth.getUTCFullYear(), prevMonth.getUTCMonth()); prevMonth.setUTCDate(day); prevMonth.setUTCDate(day - (prevMonth.getUTCDay() - this.o.weekStart + 7)%7); @@ -614,7 +746,7 @@ var html = []; var clsName; while(prevMonth.valueOf() < nextMonth) { - if (prevMonth.getUTCDay() == this.o.weekStart) { + if (prevMonth.getUTCDay() === this.o.weekStart) { html.push(''); if(this.o.calendarWeeks){ // ISO 8601: First week contains first thursday. @@ -653,29 +785,31 @@ clsName = $.unique(clsName); html.push(''+prevMonth.getUTCDate() + ''); - if (prevMonth.getUTCDay() == this.o.weekEnd) { + if (prevMonth.getUTCDay() === this.o.weekEnd) { html.push(''); } prevMonth.setUTCDate(prevMonth.getUTCDate()+1); } this.picker.find('.datepicker-days tbody').empty().append(html.join('')); - var currentYear = this.date && this.date.getUTCFullYear(); var months = this.picker.find('.datepicker-months') .find('th:eq(1)') .text(year) .end() .find('span').removeClass('active'); - if (currentYear && currentYear == year) { - months.eq(this.date.getUTCMonth()).addClass('active'); - } + + $.each(this.dates, function(i, d){ + if (d.getUTCFullYear() === year) + months.eq(d.getUTCMonth()).addClass('active'); + }); + if (year < startYear || year > endYear) { months.addClass('disabled'); } - if (year == startYear) { + if (year === startYear) { months.slice(0, startMonth).addClass('disabled'); } - if (year == endYear) { + if (year === endYear) { months.slice(endMonth+1).addClass('disabled'); } @@ -687,8 +821,19 @@ .end() .find('td'); year -= 1; + var years = $.map(this.dates, function(d){ return d.getUTCFullYear(); }), + classes; for (var i = -1; i < 11; i++) { - html += ''+year+''; + classes = ['year']; + if (i === -1) + classes.push('old'); + else if (i === 10) + classes.push('new'); + if ($.inArray(year, years) !== -1) + classes.push('active'); + if (year < startYear || year > endYear) + classes.push('disabled'); + html += ''+year+''; year += 1; } yearCont.html(html); @@ -731,8 +876,9 @@ click: function(e) { e.preventDefault(); - var target = $(e.target).closest('span, td, th'); - if (target.length == 1) { + var target = $(e.target).closest('span, td, th'), + year, month, day; + if (target.length === 1) { switch(target[0].nodeName.toLowerCase()) { case 'th': switch(target[0].className) { @@ -741,7 +887,7 @@ break; case 'prev': case 'next': - var dir = DPGlobal.modes[this.viewMode].navStep * (target[0].className == 'prev' ? -1 : 1); + var dir = DPGlobal.modes[this.viewMode].navStep * (target[0].className === 'prev' ? -1 : 1); switch(this.viewMode){ case 0: this.viewDate = this.moveMonth(this.viewDate, dir); @@ -761,7 +907,7 @@ date = UTCDate(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0); this.showMode(-2); - var which = this.o.todayBtn == 'linked' ? null : 'view'; + var which = this.o.todayBtn === 'linked' ? null : 'view'; this._setDate(date, which); break; case 'clear': @@ -772,8 +918,8 @@ element = this.element.find('input'); if (element) element.val("").change(); - this._trigger('changeDate'); this.update(); + this._trigger('changeDate'); if (this.o.autoclose) this.hide(); break; @@ -783,22 +929,22 @@ if (!target.is('.disabled')) { this.viewDate.setUTCDate(1); if (target.is('.month')) { - var day = 1; - var month = target.parent().find('span').index(target); - var year = this.viewDate.getUTCFullYear(); + day = 1; + month = target.parent().find('span').index(target); + year = this.viewDate.getUTCFullYear(); this.viewDate.setUTCMonth(month); this._trigger('changeMonth', this.viewDate); if (this.o.minViewMode === 1) { - this._setDate(UTCDate(year, month, day,0,0,0,0)); + this._setDate(UTCDate(year, month, day)); } } else { - var year = parseInt(target.text(), 10)||0; - var day = 1; - var month = 0; + day = 1; + month = 0; + year = parseInt(target.text(), 10)||0; this.viewDate.setUTCFullYear(year); this._trigger('changeYear', this.viewDate); if (this.o.minViewMode === 2) { - this._setDate(UTCDate(year, month, day,0,0,0,0)); + this._setDate(UTCDate(year, month, day)); } } this.showMode(-1); @@ -807,9 +953,9 @@ break; case 'td': if (target.is('.day') && !target.is('.disabled')){ - var day = parseInt(target.text(), 10)||1; - var year = this.viewDate.getUTCFullYear(), - month = this.viewDate.getUTCMonth(); + day = parseInt(target.text(), 10)||1; + year = this.viewDate.getUTCFullYear(); + month = this.viewDate.getUTCMonth(); if (target.is('.old')) { if (month === 0) { month = 11; @@ -818,25 +964,46 @@ month -= 1; } } else if (target.is('.new')) { - if (month == 11) { + if (month === 11) { month = 0; year += 1; } else { month += 1; } } - this._setDate(UTCDate(year, month, day,0,0,0,0)); + this._setDate(UTCDate(year, month, day)); } break; } } + if (this.picker.is(':visible') && this._focused_from){ + $(this._focused_from).focus(); + } + delete this._focused_from; + }, + + _toggle_multidate: function( date ) { + var ix = this.dates.contains(date); + if (!date){ + this.dates.clear(); + } + else if (ix !== -1){ + this.dates.remove(ix); + } + else{ + this.dates.push(date); + } + if (typeof this.o.multidate === 'number') + while (this.dates.length > this.o.multidate) + this.dates.remove(0); }, _setDate: function(date, which){ - if (!which || which == 'date') - this.date = new Date(date); - if (!which || which == 'view') - this.viewDate = new Date(date); + if (!which || which === 'date') + this._toggle_multidate(date && new Date(date)); + if (!which || which === 'view') + this.viewDate = date && new Date(date); + this.fill(); this.setValue(); this._trigger('changeDate'); @@ -849,12 +1016,13 @@ if (element) { element.change(); } - if (this.o.autoclose && (!which || which == 'date')) { + if (this.o.autoclose && (!which || which === 'date')) { this.hide(); } }, moveMonth: function(date, dir){ + if (!date) return undefined; if (!dir) return date; var new_date = new Date(date.valueOf()), day = new_date.getUTCDate(), @@ -862,14 +1030,14 @@ mag = Math.abs(dir), new_month, test; dir = dir > 0 ? 1 : -1; - if (mag == 1){ - test = dir == -1 + if (mag === 1){ + test = dir === -1 // If going back one month, make sure month is not current month // (eg, Mar 31 -> Feb 31 == Feb 28, not Mar 02) - ? function(){ return new_date.getUTCMonth() == month; } + ? function(){ return new_date.getUTCMonth() === month; } // If going forward one month, make sure month is as expected // (eg, Jan 31 -> Feb 31 == Feb 28, not Mar 02) - : function(){ return new_date.getUTCMonth() != new_month; }; + : function(){ return new_date.getUTCMonth() !== new_month; }; new_month = month + dir; new_date.setUTCMonth(new_month); // Dec -> Jan (12) or Jan -> Dec (-1) -- limit expected date to 0-11 @@ -883,7 +1051,7 @@ // ...then reset the day, keeping it in the new month new_month = new_date.getUTCMonth(); new_date.setUTCDate(day); - test = function(){ return new_month != new_date.getUTCMonth(); }; + test = function(){ return new_month !== new_date.getUTCMonth(); }; } // Common date-resetting loop -- if date is beyond end of month, make it // end of month @@ -904,82 +1072,104 @@ keydown: function(e){ if (this.picker.is(':not(:visible)')){ - if (e.keyCode == 27) // allow escape to hide and re-show picker + if (e.keyCode === 27) // allow escape to hide and re-show picker this.show(); return; } var dateChanged = false, - dir, day, month, - newDate, newViewDate; + dir, newDate, newViewDate, + focusDate = this.focusDate || this.viewDate; switch(e.keyCode){ case 27: // escape - this.hide(); + if (this.focusDate){ + this.focusDate = null; + this.viewDate = this.dates.get(-1) || this.viewDate; + this.fill(); + } + else + this.hide(); e.preventDefault(); break; case 37: // left case 39: // right if (!this.o.keyboardNavigation) break; - dir = e.keyCode == 37 ? -1 : 1; + dir = e.keyCode === 37 ? -1 : 1; if (e.ctrlKey){ - newDate = this.moveYear(this.date, dir); - newViewDate = this.moveYear(this.viewDate, dir); + newDate = this.moveYear(this.dates.get(-1) || UTCToday(), dir); + newViewDate = this.moveYear(focusDate, dir); this._trigger('changeYear', this.viewDate); } else if (e.shiftKey){ - newDate = this.moveMonth(this.date, dir); - newViewDate = this.moveMonth(this.viewDate, dir); + newDate = this.moveMonth(this.dates.get(-1) || UTCToday(), dir); + newViewDate = this.moveMonth(focusDate, dir); this._trigger('changeMonth', this.viewDate); } else { - newDate = new Date(this.date); - newDate.setUTCDate(this.date.getUTCDate() + dir); - newViewDate = new Date(this.viewDate); - newViewDate.setUTCDate(this.viewDate.getUTCDate() + dir); + newDate = new Date(this.dates.get(-1) || UTCToday()); + newDate.setUTCDate(newDate.getUTCDate() + dir); + newViewDate = new Date(focusDate); + newViewDate.setUTCDate(focusDate.getUTCDate() + dir); } if (this.dateWithinRange(newDate)){ - this.date = newDate; - this.viewDate = newViewDate; + this.focusDate = this.viewDate = newViewDate; this.setValue(); - this.update(); + this.fill(); e.preventDefault(); - dateChanged = true; } break; case 38: // up case 40: // down if (!this.o.keyboardNavigation) break; - dir = e.keyCode == 38 ? -1 : 1; + dir = e.keyCode === 38 ? -1 : 1; if (e.ctrlKey){ - newDate = this.moveYear(this.date, dir); - newViewDate = this.moveYear(this.viewDate, dir); + newDate = this.moveYear(this.dates.get(-1) || UTCToday(), dir); + newViewDate = this.moveYear(focusDate, dir); this._trigger('changeYear', this.viewDate); } else if (e.shiftKey){ - newDate = this.moveMonth(this.date, dir); - newViewDate = this.moveMonth(this.viewDate, dir); + newDate = this.moveMonth(this.dates.get(-1) || UTCToday(), dir); + newViewDate = this.moveMonth(focusDate, dir); this._trigger('changeMonth', this.viewDate); } else { - newDate = new Date(this.date); - newDate.setUTCDate(this.date.getUTCDate() + dir * 7); - newViewDate = new Date(this.viewDate); - newViewDate.setUTCDate(this.viewDate.getUTCDate() + dir * 7); + newDate = new Date(this.dates.get(-1) || UTCToday()); + newDate.setUTCDate(newDate.getUTCDate() + dir * 7); + newViewDate = new Date(focusDate); + newViewDate.setUTCDate(focusDate.getUTCDate() + dir * 7); } if (this.dateWithinRange(newDate)){ - this.date = newDate; - this.viewDate = newViewDate; + this.focusDate = this.viewDate = newViewDate; this.setValue(); - this.update(); + this.fill(); e.preventDefault(); - dateChanged = true; } break; + case 32: // spacebar + // Spacebar is used in manually typing dates in some formats. + // As such, its behavior should not be hijacked. + break; case 13: // enter - this.hide(); - e.preventDefault(); + focusDate = this.focusDate || this.dates.get(-1) || this.viewDate; + this._toggle_multidate(focusDate); + dateChanged = true; + this.focusDate = null; + this.viewDate = this.dates.get(-1) || this.viewDate; + this.setValue(); + this.fill(); + if (this.picker.is(':visible')){ + e.preventDefault(); + if (this.o.autoclose) + this.hide(); + } break; case 9: // tab + this.focusDate = null; + this.viewDate = this.dates.get(-1) || this.viewDate; + this.fill(); this.hide(); break; } if (dateChanged){ - this._trigger('changeDate'); + if (this.dates.length) + this._trigger('changeDate'); + else + this._trigger('clearDate'); var element; if (this.isInput) { element = this.element; @@ -996,17 +1186,11 @@ if (dir) { this.viewMode = Math.max(this.o.minViewMode, Math.min(2, this.viewMode + dir)); } - /* - vitalets: fixing bug of very special conditions: - jquery 1.7.1 + webkit + show inline datepicker in bootstrap popover. - Method show() does not set display css correctly and datepicker is not shown. - Changed to .css('display', 'block') solve the problem. - See https://github.com/vitalets/x-editable/issues/37 - - In jquery 1.7.2+ everything works fine. - */ - //this.picker.find('>div').hide().filter('.datepicker-'+DPGlobal.modes[this.viewMode].clsName).show(); - this.picker.find('>div').hide().filter('.datepicker-'+DPGlobal.modes[this.viewMode].clsName).css('display', 'block'); + this.picker + .find('>div') + .hide() + .filter('.datepicker-'+DPGlobal.modes[this.viewMode].clsName) + .css('display', 'block'); this.updateNavArrows(); } }; @@ -1025,7 +1209,7 @@ }; DateRangePicker.prototype = { updateDates: function(){ - this.dates = $.map(this.pickers, function(i){ return i.date; }); + this.dates = $.map(this.pickers, function(i){ return i.getUTCDate(); }); this.updateRanges(); }, updateRanges: function(){ @@ -1035,11 +1219,24 @@ }); }, dateUpdated: function(e){ + // `this.updating` is a workaround for preventing infinite recursion + // between `changeDate` triggering and `setUTCDate` calling. Until + // there is a better mechanism. + if (this.updating) + return; + this.updating = true; + var dp = $(e.target).data('datepicker'), new_date = dp.getUTCDate(), i = $.inArray(e.target, this.inputs), l = this.inputs.length; - if (i == -1) return; + if (i === -1) + return; + + $.each(this.pickers, function(i, p){ + if (!p.getUTCDate()) + p.setUTCDate(new_date); + }); if (new_date < this.dates[i]){ // Date being moved earlier/left @@ -1054,6 +1251,8 @@ } } this.updateDates(); + + delete this.updating; }, remove: function(){ $.map(this.pickers, function(p){ p.remove(); }); @@ -1065,11 +1264,14 @@ // Derive options from element data-attrs var data = $(el).data(), out = {}, inkey, - replace = new RegExp('^' + prefix.toLowerCase() + '([A-Z])'), - prefix = new RegExp('^' + prefix.toLowerCase()); + replace = new RegExp('^' + prefix.toLowerCase() + '([A-Z])'); + prefix = new RegExp('^' + prefix.toLowerCase()); + function re_lower(_,a){ + return a.toLowerCase(); + } for (var key in data) if (prefix.test(key)){ - inkey = key.replace(replace, function(_,a){ return a.toLowerCase(); }); + inkey = key.replace(replace, re_lower); out[inkey] = data[key]; } return out; @@ -1081,7 +1283,7 @@ // Check if "de-DE" style date is available, if not language should // fallback to 2 letter code eg "de" if (!dates[lang]) { - lang = lang.split('-')[0] + lang = lang.split('-')[0]; if (!dates[lang]) return; } @@ -1097,12 +1299,11 @@ $.fn.datepicker = function ( option ) { var args = Array.apply(null, arguments); args.shift(); - var internal_return, - this_return; + var internal_return; this.each(function () { var $this = $(this), data = $this.data('datepicker'), - options = typeof option == 'object' && option; + options = typeof option === 'object' && option; if (!data) { var elopts = opts_from_el(this, 'date'), // Preliminary otions @@ -1120,7 +1321,7 @@ $this.data('datepicker', (data = new Datepicker(this, opts))); } } - if (typeof option == 'string' && typeof data[option] == 'function') { + if (typeof option === 'string' && typeof data[option] === 'function') { internal_return = data[option].apply(data, args); if (internal_return !== undefined) return false; @@ -1144,6 +1345,8 @@ keyboardNavigation: true, language: 'en', minViewMode: 0, + multidate: false, + multidateSeparator: ',', orientation: "auto", rtl: false, startDate: -Infinity, @@ -1206,15 +1409,17 @@ return {separators: separators, parts: parts}; }, parseDate: function(date, format, language) { + if (!date) + return undefined; if (date instanceof Date) return date; if (typeof format === 'string') format = DPGlobal.parseFormat(format); + var part_re = /([\-+]\d+)([dmwy])/, + parts = date.match(/([\-+]\d+)([dmwy])/g), + part, dir, i; if (/^[\-+]\d+[dmwy]([\s,]+[\-+]\d+[dmwy])*$/.test(date)) { - var part_re = /([\-+]\d+)([dmwy])/, - parts = date.match(/([\-+]\d+)([dmwy])/g), - part, dir; date = new Date(); - for (var i=0; i'+ '', contTemplate: '', - footTemplate: '' + footTemplate: ''+ + ''+ + ''+ + ''+ + ''+ + ''+ + ''+ + '' }; DPGlobal.template = '
'+ '
'+ diff --git a/public/js/script.js b/public/js/script.js index f77d5e362927..403d8cea46b7 100755 --- a/public/js/script.js +++ b/public/js/script.js @@ -602,6 +602,23 @@ ko.bindingHandlers.dropdown = { } }; + +/* +ko.bindingHandlers.datePicker = { + init: function (element, valueAccessor, allBindingsAccessor) { + var value = ko.utils.unwrapObservable(valueAccessor()); + if (value) $(element).datepicker('update', value); + console.log("datePicker-init: %s", value); + }, + update: function (element, valueAccessor) { + var value = ko.utils.unwrapObservable(valueAccessor()); + if (value) $(element).datepicker('update', value); + console.log("datePicker-init: %s", value); + } +}; +*/ + + function wordWrapText(value, width) { if (!width) width = 200;