From c45ec2495926632551ed7794125b9d356298126e Mon Sep 17 00:00:00 2001 From: Paul Kilmurray Date: Thu, 29 Oct 2015 22:41:06 +0800 Subject: [PATCH 001/169] fix admin settings --- includes/admin/class-wc-pos-settings.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/includes/admin/class-wc-pos-settings.php b/includes/admin/class-wc-pos-settings.php index e77a3aab..1bfdc694 100644 --- a/includes/admin/class-wc-pos-settings.php +++ b/includes/admin/class-wc-pos-settings.php @@ -109,10 +109,12 @@ public function enqueue_admin_scripts() { // deregister scripts wp_deregister_script( 'underscore' ); wp_deregister_script( 'select2' ); + wp_deregister_script( 'backbone' ); // register $external_libs = WC_POS_Template::get_external_js_libraries(); wp_register_script( 'underscore', $external_libs[ 'lodash' ], array( 'jquery' ), null, true ); + wp_register_script( 'backbone', $external_libs[ 'backbone' ], array( 'jquery', 'underscore' ), null, true ); wp_register_script( 'backbone.radio', $external_libs[ 'radio' ], array( 'jquery', 'backbone', 'underscore' ), null, true ); wp_register_script( 'marionette', $external_libs[ 'marionette' ], array( 'jquery', 'backbone', 'underscore' ), null, true ); wp_register_script( 'handlebars', $external_libs[ 'handlebars' ], false, null, true ); From ce1d988ec43aff3825b6b2cf406327ba2ad2f7ca Mon Sep 17 00:00:00 2001 From: Paul Kilmurray Date: Tue, 10 Nov 2015 12:05:39 +0800 Subject: [PATCH 002/169] fix: bug causing issue with some variations, e.g.: 000, 00 and 0 --- includes/api/class-wc-pos-products.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/api/class-wc-pos-products.php b/includes/api/class-wc-pos-products.php index 9d8ffaee..fb3517e2 100644 --- a/includes/api/class-wc-pos-products.php +++ b/includes/api/class-wc-pos-products.php @@ -191,7 +191,7 @@ private function get_variation_option( $product, $attribute, $option ){ if ( $attribute['is_taxonomy'] ) { $terms = wp_get_post_terms( $product->parent->id, $attribute['name'] ); if( !is_wp_error($terms) ) : foreach( $terms as $term ) : - if( $option == $term->slug ) $name = $term->name; + if( $option === $term->slug ) $name = $term->name; endforeach; endif; // piped attributes From 95401fe033af25a86ade4de34df71b6ea4de3957 Mon Sep 17 00:00:00 2001 From: Paul Kilmurray Date: Fri, 13 Nov 2015 20:00:10 +0800 Subject: [PATCH 003/169] Fix: display of print receipt path in settings --- assets/js/src/apps/print/receipt/route.js | 11 +++-- includes/admin/settings/views/tools.php | 5 +- includes/api/class-wc-pos-orders.php | 59 ++++++++++++++--------- includes/class-wc-pos-template.php | 23 ++++++--- includes/gateways/class-wc-pos-cash.php | 5 +- readme.txt | 15 ++++-- 6 files changed, 76 insertions(+), 42 deletions(-) diff --git a/assets/js/src/apps/print/receipt/route.js b/assets/js/src/apps/print/receipt/route.js index c4e075a7..0a501481 100644 --- a/assets/js/src/apps/print/receipt/route.js +++ b/assets/js/src/apps/print/receipt/route.js @@ -14,9 +14,14 @@ var ReceiptRoute = Route.extend({ }, fetch: function() { - if (this.collection.length === 0) { - return this.collection.fetch({ remote: true }); - } + return this.collection.fetch({ + remote: true, + data: { + filter: { + limit: 1 + } + } + }); }, render: function() { diff --git a/includes/admin/settings/views/tools.php b/includes/admin/settings/views/tools.php index d35e8de1..8095c961 100644 --- a/includes/admin/settings/views/tools.php +++ b/includes/admin/settings/views/tools.php @@ -35,7 +35,10 @@ class="button" - Template path: %s', 'woocommerce-pos' ), ''. WC_POS_Template::locate_template_file('print/tmpl-receipt.php') .'' ); ?> + Template path: %s', 'woocommerce-pos' ), ''. $template_path .'' ); + ?> diff --git a/includes/api/class-wc-pos-orders.php b/includes/api/class-wc-pos-orders.php index 84e927a4..1f6b144f 100644 --- a/includes/api/class-wc-pos-orders.php +++ b/includes/api/class-wc-pos-orders.php @@ -527,8 +527,7 @@ public function payment_complete( $order_id ) { public function order_response( $order_data, $order, $fields, $server ) { // add cashier data - $cashier_details = isset( $order_data['cashier'] ) ? $order_data['cashier'] : array(); - $order_data['cashier'] = $this->add_cashier_details( $order, $cashier_details ); + $order_data['cashier'] = $this->add_cashier_details( $order ); // add pos payment info $payment_details = isset( $order_data['payment_details'] ) ? $order_data['payment_details'] : array(); @@ -569,36 +568,48 @@ public function order_response( $order_data, $order, $fields, $server ) { /** * @param $order - * @param array $cashier * @return array */ - private function add_cashier_details( $order, array $cashier = array() ){ - $cashier['id'] = get_post_meta( $order->id, '_pos_user', true); - $first_name = get_post_meta( $order->id, '_pos_user_first_name', true); - $last_name = get_post_meta( $order->id, '_pos_user_last_name', true); - if( !$first_name && !$last_name ) { - $user_info = get_userdata( $cashier['id'] ); - $first_name = $user_info->first_name; - $last_name = $user_info->last_name; - } - $cashier['first_name'] = $first_name; - $cashier['last_name'] = $last_name; - return apply_filters( 'woocommerce_pos_order_response_cashier', $cashier, $order ); - } + private function add_cashier_details( $order ) { + + if ( !$cashier_id = get_post_meta( $order->id, '_pos_user', true ) ) { + return; + } + + $first_name = get_post_meta( $order->id, '_pos_user_first_name', true ); + $last_name = get_post_meta( $order->id, '_pos_user_last_name', true ); + if ( !$first_name && !$last_name && $user_info = get_userdata( $cashier_id ) ) { + $first_name = $user_info->first_name; + $last_name = $user_info->last_name; + } + + $cashier = array( + 'id' => $cashier_id, + 'first_name' => $first_name, + 'last_name' => $last_name + ); + + return apply_filters( 'woocommerce_pos_order_response_cashier', $cashier, $order ); + + } /** * @param $order - * @param array $payment + * @param array $payment_details * @return array */ - private function add_payment_details( $order, array $payment = array() ){ - $payment['result'] = get_post_meta( $order->id, '_pos_payment_result', true ); - $payment['message'] = get_post_meta( $order->id, '_pos_payment_message', true ); - $payment['redirect'] = get_post_meta( $order->id, '_pos_payment_redirect', true ); - if( isset( $payment['method_id'] ) && $payment['method_id'] == 'pos_cash' ){ - $payment = WC_POS_Gateways_Cash::payment_details( $payment, $order ); + private function add_payment_details( $order, array $payment_details ){ + + $payment_details['result'] = get_post_meta( $order->id, '_pos_payment_result', true ); + $payment_details['message'] = get_post_meta( $order->id, '_pos_payment_message', true ); + $payment_details['redirect'] = get_post_meta( $order->id, '_pos_payment_redirect', true ); + + if( isset( $payment_details['method_id'] ) && $payment_details['method_id'] == 'pos_cash' ){ + $payment_details['method_pos_cash'] = WC_POS_Gateways_Cash::payment_details( $order ); } - return apply_filters( 'woocommerce_pos_order_response_payment_details', $payment, $order ); + + return apply_filters( 'woocommerce_pos_order_response_payment_details', $payment_details, $order ); + } /** diff --git a/includes/class-wc-pos-template.php b/includes/class-wc-pos-template.php index f02f5e53..ca08fb03 100644 --- a/includes/class-wc-pos-template.php +++ b/includes/class-wc-pos-template.php @@ -296,17 +296,20 @@ static public function locate_default_template_files( $partials_dir = '' ) { if ( empty( $partials_dir ) ) $partials_dir = self::get_template_dir(); - $iterator = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator( $partials_dir ), + $Directory = new RecursiveDirectoryIterator( $partials_dir ); + + $Iterator = new RecursiveIteratorIterator( + $Directory, RecursiveIteratorIterator::SELF_FIRST ); - $regex = new RegexIterator( - $iterator, '/^.+tmpl-[a-z-]+\.php$/i', + $Regex = new RegexIterator( + $Iterator, + '/^.+tmpl-[a-z-]+\.php$/i', RecursiveRegexIterator::GET_MATCH ); - $paths = array_keys( iterator_to_array( $regex ) ); + $paths = array_keys( iterator_to_array( $Regex ) ); $templates = array(); foreach ( $paths as $path ) { @@ -352,7 +355,7 @@ static public function locate_template_file( $default_path = '' ) { * @return string */ static public function get_template_dir() { - return realpath( WC_POS_PLUGIN_PATH . 'includes/views' ); + return WC_POS_PLUGIN_PATH . 'includes/views'; } /** @@ -448,4 +451,12 @@ public function templates_payload() { return apply_filters( 'woocommerce_pos_templates', $templates ); } + /** + * Returns path of print receipt template + */ + static public function locate_print_receipt_template() { + $receipt_path = self::locate_template_file( WC_POS_PLUGIN_PATH . 'includes/views/print/tmpl-receipt.php' ); + return apply_filters( 'woocommerce_pos_print_receipt_path', $receipt_path ); + } + } \ No newline at end of file diff --git a/includes/gateways/class-wc-pos-cash.php b/includes/gateways/class-wc-pos-cash.php index 87eaa8f7..11d85f4e 100644 --- a/includes/gateways/class-wc-pos-cash.php +++ b/includes/gateways/class-wc-pos-cash.php @@ -97,12 +97,11 @@ public function calculate_change( $order_id ) { echo $message; } - static public function payment_details( $payment_details, $order ) { - $payment_details['method_pos_cash'] = array( + static public function payment_details( $order ) { + return array( 'tendered' => get_post_meta( $order->id, '_pos_cash_amount_tendered', true ), 'change' => get_post_meta( $order->id, '_pos_cash_change', true ) ); - return $payment_details; } } \ No newline at end of file diff --git a/readme.txt b/readme.txt index c74e4f32..31d83863 100644 --- a/readme.txt +++ b/readme.txt @@ -79,7 +79,12 @@ Bugs can be reported on the [WooCommerce POS GitHub repository](https://github.c == Changelog == -= 0.4.5 - 2015/10/** = += 0.4.6 - 2015/11/** = +* Important: Pro users should update [WooCommerce POS Pro](http://woopos.com.au/pro) to version 0.4.6 +* Fix: issue with some product variation names, e.g.: 000, 00 and 0 +* Fix: display of print receipt path in settings + += 0.4.5 - 2015/10/28 = * Important: WooCommerce POS now requires PHP 5.4 or higher * Important: WooCommerce POS now requires WooCommerce 2.3.7 or higher * Important: Pro users should update [WooCommerce POS Pro](http://woopos.com.au/pro) to version 0.4.5 @@ -167,10 +172,10 @@ Bugs can be reported on the [WooCommerce POS GitHub repository](https://github.c == Upgrade Notice == += 0.4.6 = +* Pro users should update [WooCommerce POS Pro](http://woopos.com.au/pro) to version 0.4.6. + = 0.4.5 = * WooCommerce POS now requires PHP 5.4 or higher. * WooCommerce POS now requires WooCommerce 2.3.7 or higher. -* Pro users should update [WooCommerce POS Pro](http://woopos.com.au/pro) to version 0.4.5. - -= 0.4.4 = -* Pro users should update [WooCommerce POS Pro](http://woopos.com.au/pro) to version 0.4.4. \ No newline at end of file +* Pro users should update [WooCommerce POS Pro](http://woopos.com.au/pro) to version 0.4.5. \ No newline at end of file From 91eea863c8b44347e13b57add0babe08bf540245 Mon Sep 17 00:00:00 2001 From: Paul Kilmurray Date: Fri, 13 Nov 2015 21:54:25 +0800 Subject: [PATCH 004/169] Fix: POS not showing for some users --- includes/class-wc-pos-ajax.php | 2 ++ includes/class-wc-pos-template.php | 19 +++++++++++++------ readme.txt | 2 ++ 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/includes/class-wc-pos-ajax.php b/includes/class-wc-pos-ajax.php index 72ebe67b..41401292 100644 --- a/includes/class-wc-pos-ajax.php +++ b/includes/class-wc-pos-ajax.php @@ -121,8 +121,10 @@ public function email_receipt() { */ public function send_support_email() { $headers[] = 'From: '. $_POST['name'] .' <'. $_POST['email'] .'>'; + $headers[] = 'Reply-To: '. $_POST['name'] .' <'. $_POST['email'] .'>'; $message = $_POST['message'] . "\n\n" . $_POST['status']; $support = apply_filters( 'woocommerce_pos_support_email', 'support@woopos.com.au' ); + if( wp_mail( $support, 'WooCommerce POS Support', $message, $headers ) ) { $response = array( 'result' => 'success', diff --git a/includes/class-wc-pos-template.php b/includes/class-wc-pos-template.php index ca08fb03..04ebbc72 100644 --- a/includes/class-wc-pos-template.php +++ b/includes/class-wc-pos-template.php @@ -367,18 +367,25 @@ static public function create_templates_array( $partials_dir = '' ) { foreach ( self::locate_template_files( $partials_dir ) as $slug => $file ) { $keys = explode( substr( $slug, 0, 1 ), substr( $slug, 1 ) ); - $template = array_reduce( array_reverse( $keys ), function ( $result, $key ) { - if ( is_string( $result ) ) - $key = preg_replace( '/^tmpl-/i', '', $key ); - - return array( $key => $result ); - }, self::template_output( $file ) ); + $template = array_reduce( array_reverse( $keys ), 'self::reduce_templates_array', self::template_output( $file ) ); $templates = array_merge_recursive( $templates, $template ); } return $templates; } + + /** + * @param $result + * @param $key + * @return array + */ + static private function reduce_templates_array( $result, $key ) { + if ( is_string( $result ) ) + $key = preg_replace( '/^tmpl-/i', '', $key ); + return array( $key => $result ); + } + /** * Output template partial as string * diff --git a/readme.txt b/readme.txt index 31d83863..55cd6211 100644 --- a/readme.txt +++ b/readme.txt @@ -81,8 +81,10 @@ Bugs can be reported on the [WooCommerce POS GitHub repository](https://github.c = 0.4.6 - 2015/11/** = * Important: Pro users should update [WooCommerce POS Pro](http://woopos.com.au/pro) to version 0.4.6 +* Fix: POS not showing for some users * Fix: issue with some product variation names, e.g.: 000, 00 and 0 * Fix: display of print receipt path in settings +* Tweak: Add Reply-To header to support emails = 0.4.5 - 2015/10/28 = * Important: WooCommerce POS now requires PHP 5.4 or higher From 3fc7ebd86e2a3e198448189ac2fa01490af41dd4 Mon Sep 17 00:00:00 2001 From: Paul Kilmurray Date: Fri, 13 Nov 2015 22:03:38 +0800 Subject: [PATCH 005/169] Tweak: remove hard dependence for unique customer email --- assets/js/src/entities/customers/collection.js | 5 +---- assets/js/src/lib/config/idb-collection.js | 5 ++--- readme.txt | 1 + 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/assets/js/src/entities/customers/collection.js b/assets/js/src/entities/customers/collection.js index d0571d09..fc1dbed6 100644 --- a/assets/js/src/entities/customers/collection.js +++ b/assets/js/src/entities/customers/collection.js @@ -7,10 +7,7 @@ module.exports = DualCollection.extend({ name: 'customers', indexes: [ {name: 'local_id', keyPath: 'local_id', unique: true}, - {name: 'id', keyPath: 'id', unique: true}, - {name: 'status', keyPath: 'status', unique: false}, - {name: 'email', keyPath: 'email', unique: true}, - {name: 'username', keyPath: 'username', unique: true} + {name: 'id', keyPath: 'id', unique: true} ], initialize: function(){ diff --git a/assets/js/src/lib/config/idb-collection.js b/assets/js/src/lib/config/idb-collection.js index e7034676..2bd75f1d 100644 --- a/assets/js/src/lib/config/idb-collection.js +++ b/assets/js/src/lib/config/idb-collection.js @@ -11,13 +11,12 @@ var Radio = require('backbone.radio'); module.exports = app.prototype.IndexedDBCollection = Collection.extend({ name : 'store', storePrefix : 'wc_pos_', - dbVersion : 4005, + dbVersion : 4006, keyPath : 'local_id', autoIncrement : true, indexes : [ {name: 'local_id', keyPath: 'local_id', unique: true}, - {name: 'id', keyPath: 'id', unique: true}, - {name: 'status', keyPath: 'status', unique: false} + {name: 'id', keyPath: 'id', unique: true} ], constructor: function() { diff --git a/readme.txt b/readme.txt index 55cd6211..14fd2f16 100644 --- a/readme.txt +++ b/readme.txt @@ -85,6 +85,7 @@ Bugs can be reported on the [WooCommerce POS GitHub repository](https://github.c * Fix: issue with some product variation names, e.g.: 000, 00 and 0 * Fix: display of print receipt path in settings * Tweak: Add Reply-To header to support emails +* Tweak: remove hard dependence for unique customer email = 0.4.5 - 2015/10/28 = * Important: WooCommerce POS now requires PHP 5.4 or higher From 45369249169332fdee1a49fa690cdbfd222093e0 Mon Sep 17 00:00:00 2001 From: Paul Kilmurray Date: Wed, 2 Dec 2015 22:28:21 +0800 Subject: [PATCH 006/169] remove Bootstrap modals --- Gruntfile.js | 21 +- assets/css/src/scss/partials/_images.scss | 5 + assets/css/src/scss/partials/_main.scss | 2 +- assets/css/src/scss/partials/_modal.scss | 312 +++++------------- assets/css/src/scss/pos.scss | 1 + assets/js/src/app.js | 5 +- assets/js/src/apps/app/layout-view.js | 6 +- assets/js/src/apps/header/service.js | 11 +- .../src/apps/header/views/modals/browser.js | 7 +- .../js/src/apps/header/views/modals/help.js | 4 +- assets/js/src/apps/pos/cart/route.js | 2 +- .../apps/pos/receipt/views/modals/email.js | 7 +- .../checkout/modals/gateway-settings.js | 9 - assets/js/src/apps/settings/checkout/route.js | 29 +- .../tools/modal/translation-update.js | 4 +- assets/js/src/apps/settings/tools/route.js | 7 +- .../js/src/lib/components/buttons/behavior.js | 6 +- assets/js/src/lib/components/buttons/view.js | 4 +- .../modal/{views/error.hbs => body.hbs} | 4 +- assets/js/src/lib/components/modal/body.js | 28 ++ assets/js/src/lib/components/modal/header.hbs | 6 + .../components/modal/{views => }/header.js | 25 +- assets/js/src/lib/components/modal/layout.js | 91 +++++ assets/js/src/lib/components/modal/modal.hbs | 3 + assets/js/src/lib/components/modal/service.js | 184 +++++------ .../src/lib/components/modal/views/alert.js | 47 --- .../src/lib/components/modal/views/header.hbs | 6 - .../src/lib/components/modal/views/layout.js | 136 -------- .../src/lib/components/modal/views/modal.hbs | 7 - assets/js/src/lib/config/application.js | 1 + assets/js/src/lib/config/idb-collection.js | 39 ++- includes/class-wc-pos-i18n.php | 3 +- includes/class-wc-pos-template.php | 12 +- includes/views/modals/browser.php | 2 +- package.json | 43 ++- woocommerce-pos.php | 4 +- 36 files changed, 432 insertions(+), 651 deletions(-) create mode 100644 assets/css/src/scss/partials/_images.scss rename assets/js/src/lib/components/modal/{views/error.hbs => body.hbs} (64%) create mode 100644 assets/js/src/lib/components/modal/body.js create mode 100644 assets/js/src/lib/components/modal/header.hbs rename assets/js/src/lib/components/modal/{views => }/header.js (60%) create mode 100644 assets/js/src/lib/components/modal/layout.js create mode 100644 assets/js/src/lib/components/modal/modal.hbs delete mode 100644 assets/js/src/lib/components/modal/views/alert.js delete mode 100644 assets/js/src/lib/components/modal/views/header.hbs delete mode 100644 assets/js/src/lib/components/modal/views/layout.js delete mode 100644 assets/js/src/lib/components/modal/views/modal.hbs diff --git a/Gruntfile.js b/Gruntfile.js index cfe4abd0..7704a626 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -170,7 +170,7 @@ module.exports = function(grunt) { accounting: 'accounting', moment: 'moment', select2: 'select2', - 'idb-wrapper': 'IDBStore' + //'idb-wrapper': 'IDBStore' }, cache: true, watch: true @@ -206,25 +206,6 @@ module.exports = function(grunt) { 'assets/js/app.min.js': 'assets/js/app.build.js', 'assets/js/admin.min.js': 'assets/js/admin.build.js' } - }, - // not used in production - // only provided as a convenience if internet is unavailable - vendor: { - files: { - 'assets/js/vendor.bundle.js': [ - 'node_modules/jquery/dist/jquery.js', - 'node_modules/lodash/index.js', - 'node_modules/backbone/backbone.js', - 'node_modules/backbone.radio/build/backbone.radio.js', - 'node_modules/backbone.marionette/lib/backbone.marionette.js', - 'node_modules/handlebars/dist/handlebars.js', - //'node_modules/idb-wrapper/idbstore.js', - 'node_modules/select2/select2.js', - 'node_modules/moment/moment.js', - 'node_modules/accounting/accounting.js', - 'node_modules/jquery-color/jquery.color.js' - ] - } } }, diff --git a/assets/css/src/scss/partials/_images.scss b/assets/css/src/scss/partials/_images.scss new file mode 100644 index 00000000..f69af45c --- /dev/null +++ b/assets/css/src/scss/partials/_images.scss @@ -0,0 +1,5 @@ +.responsive img { + max-width: 100% !important; + height: auto; + display: block; +} \ No newline at end of file diff --git a/assets/css/src/scss/partials/_main.scss b/assets/css/src/scss/partials/_main.scss index 206a0643..d257d7e6 100644 --- a/assets/css/src/scss/partials/_main.scss +++ b/assets/css/src/scss/partials/_main.scss @@ -41,7 +41,7 @@ main { padding: $section-padding-y $section-padding-x; } - > div { + > div:not(.loading) { @include display-flex; height: 100%; diff --git a/assets/css/src/scss/partials/_modal.scss b/assets/css/src/scss/partials/_modal.scss index b1c3c3cd..5b1ad9b4 100644 --- a/assets/css/src/scss/partials/_modal.scss +++ b/assets/css/src/scss/partials/_modal.scss @@ -1,23 +1,25 @@ -$zindex-modal-bg : 1040; -$zindex-modal : 1050; +$zindex-modal : 1040; +$modal-fade-in-name : namespace('modal-fade-in'); +$modal-fade-out-name : namespace('modal-fade-out'); -$modal-inner-padding : 15px !default; +$modal-header-border-color : #e5e5e5 !default; +$modal-footer-border-color : $modal-header-border-color !default; + +$modal-content-bg : #fff !default; +$modal-content-border-color : rgba(0,0,0,.2) !default; +$modal-border-radius : $border-radius-base; + +$modal-padding-y : $padding-base-vertical; +$modal-padding-x : $padding-base-horizontal; $modal-title-padding-y : $padding-base-vertical; $modal-title-padding-x : $padding-base-horizontal; $modal-title-line-height : $line-height-base !default; - $modal-close-padding-y : $padding-base-vertical; $modal-close-padding-x : $padding-base-horizontal; -$modal-content-bg : #fff !default; -$modal-content-border-color : rgba(0,0,0,.2) !default; -$modal-border-radius : 0; - -$modal-backdrop-bg : #000 !default; -$modal-backdrop-opacity : .5 !default; -$modal-header-border-color : #e5e5e5 !default; -$modal-footer-border-color : $modal-header-border-color !default; +$modal-footer-padding-y : $padding-base-vertical; +$modal-footer-padding-x : $padding-base-horizontal; $modal-body-padding-y : $padding-base-vertical; $modal-body-padding-x : $padding-base-horizontal; @@ -29,6 +31,8 @@ $modal-lg : 900px !default; $modal-md : 600px !default; $modal-sm : 300px !default; +$raw-bg : $input-bg; +$raw-border-color : $modal-header-border-color; /** * @name Modal @@ -36,106 +40,77 @@ $modal-sm : 300px !default; * @state .error - Error Modal * * @markup -
{{/if}} diff --git a/includes/views/pos/cart/tmpl-totals.php b/includes/views/pos/cart/tmpl-totals.php index a149d1bc..bd885f14 100644 --- a/includes/views/pos/cart/tmpl-totals.php +++ b/includes/views/pos/cart/tmpl-totals.php @@ -21,7 +21,7 @@
{{{money total}}}
diff --git a/tests/unit/js/spec/entities/cart/models/abstract.spec.js b/tests/unit/js/spec/entities/cart/models/abstract.spec.js index 60f9badd..e4b72cc8 100644 --- a/tests/unit/js/spec/entities/cart/models/abstract.spec.js +++ b/tests/unit/js/spec/entities/cart/models/abstract.spec.js @@ -44,13 +44,18 @@ describe('entities/cart/models/abstract.js', function () { var model = new Model(); expect( model.taxes ).to.be.undefined; - var model = new Model( { taxable: true, tax_class: '' }, { + var model = new Model( { taxable: true }, { collection: { order : { tax: { calc_taxes: 'yes' }, - tax_rates: dummy_tax_US + getTaxRates: function(){ + return dummy_tax_US['']; + }, + taxRateEnabled: function(){ + return true; + } } } }); @@ -68,7 +73,12 @@ describe('entities/cart/models/abstract.js', function () { tax: { calc_taxes: 'yes' }, - tax_rates: dummy_tax_GB + getTaxRates: function( tax_class ){ + return dummy_tax_GB[tax_class]; + }, + taxRateEnabled: function(){ + return true; + } } } }); @@ -85,13 +95,18 @@ describe('entities/cart/models/abstract.js', function () { it('should reset tax rates on taxable change', function(){ - var model = new Model( { taxable: false, tax_class: '' }, { + var model = new Model( { taxable: false }, { collection: { order : { tax: { calc_taxes: 'yes' }, - tax_rates: dummy_tax_US + getTaxRates: function(){ + return dummy_tax_US['']; + }, + taxRateEnabled: function(){ + return true; + } } } }); diff --git a/tests/unit/js/spec/entities/cart/models/fee.spec.js b/tests/unit/js/spec/entities/cart/models/fee.spec.js index adbe96f1..6569fb65 100644 --- a/tests/unit/js/spec/entities/cart/models/fee.spec.js +++ b/tests/unit/js/spec/entities/cart/models/fee.spec.js @@ -46,7 +46,6 @@ describe('entities/cart/models/fee.js', function () { var model = new Model({ taxable: true, - tax_class: '', price: 10 }, { collection: { @@ -55,7 +54,12 @@ describe('entities/cart/models/fee.js', function () { calc_taxes: 'yes', prices_include_tax: 'no' }, - tax_rates: dummy_tax_GB + getTaxRates: function(){ + return dummy_tax_GB['']; + }, + taxRateEnabled: function(){ + return true; + } } } }); @@ -70,7 +74,6 @@ describe('entities/cart/models/fee.js', function () { var model = new Model({ taxable: true, - tax_class: '', price: 10 }, { collection: { @@ -79,7 +82,12 @@ describe('entities/cart/models/fee.js', function () { calc_taxes: 'yes', prices_include_tax: 'yes' }, - tax_rates: dummy_tax_GB + getTaxRates: function(){ + return dummy_tax_GB['']; + }, + taxRateEnabled: function(){ + return true; + } } } }); @@ -96,7 +104,6 @@ describe('entities/cart/models/fee.js', function () { var model = new Model({ taxable: true, - tax_class: '', price: 10 }, { collection: { @@ -105,7 +112,12 @@ describe('entities/cart/models/fee.js', function () { calc_taxes: 'yes', prices_include_tax: 'no' }, - tax_rates: dummy_tax_GB + getTaxRates: function(){ + return dummy_tax_GB['']; + }, + taxRateEnabled: function(){ + return true; + } } } }); @@ -117,7 +129,6 @@ describe('entities/cart/models/fee.js', function () { var model = new Model({ taxable: true, - tax_class: '', price: 10 }, { collection: { @@ -126,7 +137,12 @@ describe('entities/cart/models/fee.js', function () { calc_taxes: 'yes', prices_include_tax: 'no' }, - tax_rates: dummy_tax_GB + getTaxRates: function(){ + return dummy_tax_GB['']; + }, + taxRateEnabled: function(){ + return true; + } } } }); diff --git a/tests/unit/js/spec/entities/cart/models/product.spec.js b/tests/unit/js/spec/entities/cart/models/product.spec.js index 1566defb..aa36cc00 100644 --- a/tests/unit/js/spec/entities/cart/models/product.spec.js +++ b/tests/unit/js/spec/entities/cart/models/product.spec.js @@ -105,7 +105,6 @@ describe('entities/cart/models/product.js', function () { it("should match the Woo unit test for inclusive tax", function() { var model = new Model({ taxable: true, - tax_class: '', price: 9.99 }, { collection: { @@ -114,7 +113,12 @@ describe('entities/cart/models/product.js', function () { calc_taxes: 'yes', prices_include_tax: 'yes' }, - tax_rates: dummy_tax_GB + getTaxRates: function(){ + return dummy_tax_GB['']; + }, + taxRateEnabled: function(){ + return true; + } } } }); @@ -127,7 +131,6 @@ describe('entities/cart/models/product.js', function () { var model = new Model({ taxable: true, - tax_class: '', price: 9.99 }, { collection: { @@ -136,7 +139,12 @@ describe('entities/cart/models/product.js', function () { calc_taxes: 'yes', prices_include_tax: 'no' }, - tax_rates: dummy_tax_GB + getTaxRates: function(){ + return dummy_tax_GB['']; + }, + taxRateEnabled: function(){ + return true; + } } } }); @@ -149,7 +157,6 @@ describe('entities/cart/models/product.js', function () { var model = new Model({ taxable: true, - tax_class: '', price: 100 }, { collection: { @@ -158,11 +165,14 @@ describe('entities/cart/models/product.js', function () { calc_taxes: 'yes', prices_include_tax: 'no' }, - tax_rates: { - '': { + getTaxRates: function(){ + return { 1: {rate: '5.0000', label: 'GST', shipping: 'yes', compound: 'no'}, 2: {rate: '8.5000', label: 'PST', shipping: 'yes', compound: 'yes'} - } + }; + }, + taxRateEnabled: function(){ + return true; } } } @@ -181,7 +191,6 @@ describe('entities/cart/models/product.js', function () { var model = new Model({ taxable: true, - tax_class: '', price: 100 }, { collection: { @@ -190,11 +199,14 @@ describe('entities/cart/models/product.js', function () { calc_taxes: 'yes', prices_include_tax: 'yes' }, - tax_rates: { - '': { + getTaxRates: function(){ + return { 1: {rate: '5.0000', label: 'GST', shipping: 'yes', compound: 'no'}, 2: {rate: '8.5000', label: 'PST', shipping: 'yes', compound: 'yes'} - } + }; + }, + taxRateEnabled: function(){ + return true; } } } @@ -225,13 +237,18 @@ describe('entities/cart/models/product.js', function () { calc_taxes: 'yes', prices_include_tax: 'no' }, - tax_rates: dummy_tax_GB + getTaxRates: function(){ + return dummy_tax_GB['']; + }, + taxRateEnabled: function(){ + return true; + } } } }); // dummy product id 99, regular price $3, on sale for $2 - model.set({ 'taxable': true, 'tax_class': '' }); + model.set({ 'taxable': true }); expect(model.get('item_price')).equal(2); expect(model.get('subtotal')).equal(3); @@ -255,13 +272,18 @@ describe('entities/cart/models/product.js', function () { calc_taxes: 'yes', prices_include_tax: 'no' }, - tax_rates: dummy_tax_GB + getTaxRates: function(){ + return dummy_tax_GB['']; + }, + taxRateEnabled: function(){ + return true; + } } } }); var quantity = _.random(10, true); - model.set( { 'taxable': true, 'tax_class': '', quantity: quantity } ); + model.set( { 'taxable': true, quantity: quantity } ); expect(model.get('item_price')).equal(2); expect(model.get('subtotal')).equal( Utils.round( 3 * quantity, 4 ) ); @@ -280,13 +302,18 @@ describe('entities/cart/models/product.js', function () { calc_taxes: 'yes', prices_include_tax: 'yes' }, - tax_rates: dummy_tax_GB + getTaxRates: function(){ + return dummy_tax_GB['']; + }, + taxRateEnabled: function(){ + return true; + } } } }); // dummy product id 99, regular price $3, on sale for $2 - model.set({ 'taxable': true, 'tax_class': '' }); + model.set({ 'taxable': true }); expect(model.get('item_price')).equal(2); expect(model.get('subtotal')).equal(2.5); @@ -310,13 +337,18 @@ describe('entities/cart/models/product.js', function () { calc_taxes: 'yes', prices_include_tax: 'yes' }, - tax_rates: dummy_tax_GB + getTaxRates: function(){ + return dummy_tax_GB['']; + }, + taxRateEnabled: function(){ + return true; + } } } }); var quantity = _.random(10, true); - model.set( { 'taxable': true, 'tax_class': '', quantity: quantity } ); + model.set( { 'taxable': true, quantity: quantity } ); expect(model.get('item_price')).equal(2); expect(model.get('subtotal')).equal( Utils.round( 2.5 * quantity, 4 ) ); @@ -335,14 +367,18 @@ describe('entities/cart/models/product.js', function () { calc_taxes: 'yes', prices_include_tax: 'no' }, - tax_rates: dummy_tax_GB + getTaxRates: function(tax_class){ + return dummy_tax_GB[tax_class]; + }, + taxRateEnabled: function(){ + return true; + } } } }); // dummy product id 99, regular price $3, on sale for $2 - model.set({ 'taxable': true, 'tax_class': '' }); - model.set({ 'tax_class': 'reduced-rate' }); + model.set({ 'taxable': true, 'tax_class': 'reduced-rate' }); expect(model.get('item_price')).equal(2); expect(model.get('subtotal')).equal(3); @@ -367,14 +403,18 @@ describe('entities/cart/models/product.js', function () { calc_taxes: 'yes', prices_include_tax: 'yes' }, - tax_rates: dummy_tax_GB + getTaxRates: function(tax_class){ + return dummy_tax_GB[tax_class]; + }, + taxRateEnabled: function(){ + return true; + } } } }); // dummy product id 99, regular price $3, on sale for $2 - model.set({ 'taxable': true, 'tax_class': '' }); - model.set({ 'tax_class': 'reduced-rate' }); + model.set({ 'taxable': true, 'tax_class': 'reduced-rate' }); expect(model.get('item_price')).equal(2); expect(model.get('subtotal')).equal(2.8571); @@ -399,14 +439,18 @@ describe('entities/cart/models/product.js', function () { calc_taxes: 'yes', prices_include_tax: 'no' }, - tax_rates: dummy_tax_GB + getTaxRates: function(tax_class){ + return dummy_tax_GB[tax_class]; + }, + taxRateEnabled: function(){ + return true; + } } } }); // dummy product id 99, regular price $3, on sale for $2 - model.set({ 'taxable': true, 'tax_class': '' }); - model.set({ 'tax_class': 'zero-rate' }); + model.set({ 'taxable': true, 'tax_class': 'zero-rate' }); expect(model.get('item_price')).equal(2); expect(model.get('subtotal')).equal(3); @@ -430,14 +474,18 @@ describe('entities/cart/models/product.js', function () { calc_taxes: 'yes', prices_include_tax: 'yes' }, - tax_rates: dummy_tax_GB + getTaxRates: function( tax_class ){ + return dummy_tax_GB[tax_class]; + }, + taxRateEnabled: function(){ + return true; + } } } }); // dummy product id 99, regular price $3, on sale for $2 - model.set({ 'taxable': true, 'tax_class': '' }); - model.set({ 'tax_class': 'zero-rate' }); + model.set({ 'taxable': true, 'tax_class': 'zero-rate' }); expect(model.get('item_price')).equal(2); expect(model.get('subtotal')).equal(3); @@ -467,13 +515,18 @@ describe('entities/cart/models/product.js', function () { calc_taxes: 'yes', prices_include_tax: 'no' }, - tax_rates: dummy_tax_US + getTaxRates: function(){ + return dummy_tax_US['']; + }, + taxRateEnabled: function(){ + return true; + } } } }); // dummy product id 99, regular price $3, on sale for $2 - model.set({ 'taxable': true, 'tax_class': '' }); + model.set({ 'taxable': true }); expect(model.get('item_price')).equal(2); expect(model.get('subtotal')).equal(3); @@ -499,13 +552,18 @@ describe('entities/cart/models/product.js', function () { calc_taxes: 'yes', prices_include_tax: 'no' }, - tax_rates: dummy_tax_US + getTaxRates: function(){ + return dummy_tax_US['']; + }, + taxRateEnabled: function(){ + return true; + } } } }); var quantity = _.random(10, true); - model.set( { 'taxable': true, 'tax_class': '', quantity: quantity } ); + model.set( { 'taxable': true, quantity: quantity } ); expect(model.get('item_price')).equal(2); expect(model.get('subtotal')).equal( Utils.round( 3 * quantity, 4 ) ); @@ -524,13 +582,18 @@ describe('entities/cart/models/product.js', function () { calc_taxes: 'yes', prices_include_tax: 'yes' }, - tax_rates: dummy_tax_US + getTaxRates: function(){ + return dummy_tax_US['']; + }, + taxRateEnabled: function(){ + return true; + } } } }); // dummy product id 99, regular price $3, on sale for $2 - model.set({ 'taxable': true, 'tax_class': '' }); + model.set({ 'taxable': true }); expect(model.get('item_price')).equal(2); expect(model.get('subtotal')).equal(2.6738); @@ -556,18 +619,21 @@ describe('entities/cart/models/product.js', function () { calc_taxes: 'yes', prices_include_tax: 'no' }, - tax_rates: { - '': { + getTaxRates: function(){ + return { 4: {rate: '10.0000', label: 'VAT', shipping: 'yes', compound: 'no'}, 5: {rate: '2.0000', label: 'VAT', shipping: 'yes', compound: 'no'} - } + }; + }, + taxRateEnabled: function(){ + return true; } } } }); // dummy product id 99, regular price $3, on sale for $2 - model.set({ 'taxable': true, 'tax_class': '' }); + model.set({ 'taxable': true }); expect(model.get('item_price')).equal(2); expect(model.get('subtotal')).equal(3); @@ -593,18 +659,21 @@ describe('entities/cart/models/product.js', function () { calc_taxes: 'yes', prices_include_tax: 'yes' }, - tax_rates: { - '': { + getTaxRates: function(){ + return { 4: {rate: '10.0000', label: 'VAT', shipping: 'yes', compound: 'no'}, 5: {rate: '2.0000', label: 'VAT', shipping: 'yes', compound: 'no'} - } + }; + }, + taxRateEnabled: function(){ + return true; } } } }); // dummy product id 99, regular price $3, on sale for $2 - model.set({ 'taxable': true, 'tax_class': '' }); + model.set({ 'taxable': true }); expect(model.get('item_price')).equal(2); expect(model.get('subtotal')).equal(2.6786); @@ -630,14 +699,18 @@ describe('entities/cart/models/product.js', function () { calc_taxes: 'yes', prices_include_tax: 'no' }, - tax_rates: dummy_tax_US + getTaxRates: function( tax_class ){ + return dummy_tax_US[tax_class]; + }, + taxRateEnabled: function(){ + return true; + } } } }); // dummy product id 99, regular price $3, on sale for $2 - model.set({ 'taxable': true, 'tax_class': '' }); - model.set({ 'tax_class': 'reduced-rate' }); + model.set({ 'taxable': true, 'tax_class': 'reduced-rate' }); expect(model.get('item_price')).equal(2); expect(model.get('subtotal')).equal(3); diff --git a/tests/unit/js/spec/entities/cart/models/shipping.spec.js b/tests/unit/js/spec/entities/cart/models/shipping.spec.js index 0a59fe02..ea9fbb10 100644 --- a/tests/unit/js/spec/entities/cart/models/shipping.spec.js +++ b/tests/unit/js/spec/entities/cart/models/shipping.spec.js @@ -45,7 +45,6 @@ describe('entities/cart/models/shipping.js', function () { var model = new Model({ taxable: true, - tax_class: '', price: 10 }, { collection: { @@ -54,7 +53,12 @@ describe('entities/cart/models/shipping.js', function () { calc_taxes: 'yes', prices_include_tax: 'no' }, - tax_rates: dummy_tax_GB + getTaxRates: function(){ + return dummy_tax_GB['']; + }, + taxRateEnabled: function(){ + return true; + } } } }); @@ -69,7 +73,6 @@ describe('entities/cart/models/shipping.js', function () { var model = new Model({ taxable: true, - tax_class: '', price: 10 }, { collection: { @@ -78,7 +81,12 @@ describe('entities/cart/models/shipping.js', function () { calc_taxes: 'yes', prices_include_tax: 'yes' }, - tax_rates: dummy_tax_GB + getTaxRates: function(){ + return dummy_tax_GB['']; + }, + taxRateEnabled: function(){ + return true; + } } } }); @@ -93,7 +101,6 @@ describe('entities/cart/models/shipping.js', function () { var model = new Model({ taxable: true, - tax_class: '', price: 10 }, { collection: { @@ -102,10 +109,13 @@ describe('entities/cart/models/shipping.js', function () { calc_taxes: 'yes', prices_include_tax: 'no' }, - tax_rates: { - '': { + getTaxRates: function(){ + return { 1: {rate: '20.0000', label: 'VAT', shipping: 'no', compound: 'yes'} - } + }; + }, + taxRateEnabled: function(){ + return true; } } } @@ -121,7 +131,6 @@ describe('entities/cart/models/shipping.js', function () { var model = new Model({ taxable: true, - tax_class: '', price: 10 }, { collection: { @@ -130,10 +139,13 @@ describe('entities/cart/models/shipping.js', function () { calc_taxes: 'yes', prices_include_tax: 'yes' }, - tax_rates: { - '': { + getTaxRates: function(){ + return { 1: {rate: '20.0000', label: 'VAT', shipping: 'no', compound: 'yes'} - } + }; + }, + taxRateEnabled: function(){ + return true; } } } @@ -153,7 +165,6 @@ describe('entities/cart/models/shipping.js', function () { var model = new Model({ taxable: true, - tax_class: '', price: 10 }, { collection: { @@ -162,7 +173,12 @@ describe('entities/cart/models/shipping.js', function () { calc_taxes: 'yes', prices_include_tax: 'no' }, - tax_rates: dummy_tax_US + getTaxRates: function(){ + return dummy_tax_US['']; + }, + taxRateEnabled: function(){ + return true; + } } } }); @@ -177,7 +193,6 @@ describe('entities/cart/models/shipping.js', function () { var model = new Model({ taxable: true, - tax_class: '', price: 10 }, { collection: { @@ -186,7 +201,12 @@ describe('entities/cart/models/shipping.js', function () { calc_taxes: 'yes', prices_include_tax: 'yes' }, - tax_rates: dummy_tax_US + getTaxRates: function(){ + return dummy_tax_US['']; + }, + taxRateEnabled: function(){ + return true; + } } } }); @@ -201,7 +221,6 @@ describe('entities/cart/models/shipping.js', function () { var model = new Model({ taxable: true, - tax_class: '', price: 10 }, { collection: { @@ -210,11 +229,14 @@ describe('entities/cart/models/shipping.js', function () { calc_taxes: 'yes', prices_include_tax: 'no' }, - tax_rates: { - '': { + getTaxRates: function(){ + return { 4: {rate: '10.0000', label: 'VAT', shipping: 'yes', compound: 'no'}, 5: {rate: '2.0000', label: 'VAT', shipping: 'no', compound: 'yes'} - } + }; + }, + taxRateEnabled: function(){ + return true; } } } @@ -229,7 +251,6 @@ describe('entities/cart/models/shipping.js', function () { var model = new Model({ taxable: true, - tax_class: '', price: 10 }, { collection: { @@ -238,11 +259,14 @@ describe('entities/cart/models/shipping.js', function () { calc_taxes: 'yes', prices_include_tax: 'yes' }, - tax_rates: { - '': { + getTaxRates: function(){ + return { 4: {rate: '10.0000', label: 'VAT', shipping: 'no', compound: 'no'}, 5: {rate: '2.0000', label: 'VAT', shipping: 'yes', compound: 'yes'} - } + }; + }, + taxRateEnabled: function(){ + return true; } } } diff --git a/tests/unit/js/spec/entities/orders/model.spec.js b/tests/unit/js/spec/entities/orders/model.spec.js index b170385a..3648e61d 100644 --- a/tests/unit/js/spec/entities/orders/model.spec.js +++ b/tests/unit/js/spec/entities/orders/model.spec.js @@ -4,6 +4,25 @@ describe('entities/orders/model.js', function () { var OrderModel = require('entities/orders/model'); var Utils = require('lib/utilities/utils'); + var dummy_tax_GB = { + '': { + 1: {rate: '20.0000', label: 'VAT', shipping: 'yes', compound: 'yes'} + }, + 'reduced-rate': { + 2: {rate: '5.0000', label: 'VAT', shipping: 'yes', compound: 'yes'} + }, + 'zero-rate': { + 3: {rate: '0.0000', label: 'VAT', shipping: 'yes', compound: 'yes'} + } + } + + var dummy_tax_US = { + '': { + 4: {rate: '10.0000', label: 'VAT', shipping: 'yes', compound: 'no'}, + 5: {rate: '2.0000', label: 'VAT', shipping: 'yes', compound: 'yes'} + } + } + it('should be in a valid state', function () { var model = new OrderModel(); expect(model).to.be.ok; @@ -227,22 +246,29 @@ describe('entities/orders/model.js', function () { it('should be match the dummy order', function(){ - var model = new OrderModel(null, { parse: true }); - model.sync = sinon.stub(); - - model.tax = { - calc_taxes: 'yes', - prices_include_tax: 'no' - }; - - model.tax_rates = { - '': { - 4: {rate: '5.0000', label: 'Standard', shipping: 'yes', compound: 'no'}, + var Model = OrderModel.extend({ + getSettings: function(name){ + if( name === 'tax' ){ + return { + calc_taxes: 'yes', + prices_include_tax: 'no' + }; + } + if( name === 'tax_rates' ){ + return { + '': { + 4: {rate: '5.0000', label: 'Standard', shipping: 'yes', compound: 'no'}, + }, + 'reduced-rate': { + 5: {rate: '10.0000', label: 'Tax', shipping: 'yes', compound: 'no'} + } + }; + } }, - 'reduced-rate': { - 5: {rate: '10.0000', label: 'Tax', shipping: 'yes', compound: 'no'} - } - }; + sync: stub() + }); + + var model = new Model(null, { parse: true }); expect( model.cart ).to.be.instanceOf( Backbone.Collection ); @@ -298,4 +324,50 @@ describe('entities/orders/model.js', function () { }); + it('should parse tax rates', function(){ + var model = new OrderModel(); + model.getSettings = function(name){ + if( name === 'tax_rates' ){ + return dummy_tax_GB; + } + }; + expect( model.parseTaxRates() ).eqls({ + all: true, + rate_1: true, + rate_2: true, + rate_3: true + }); + + }); + + it('should return an array of tax rates for a given tax_class', function(){ + var model = new OrderModel(); + model.getSettings = function(name){ + if( name === 'tax_rates' ){ + return dummy_tax_GB; + } + }; + model.parseTaxRates(); + expect( model.getTaxRates('') ).eqls([{ + rate_id: '1', + rate: '20.0000', + title: 'VAT', + shipping: true, + compound: true, + enabled: true, + total: 0, + subtotal: 0 + }]); + expect( model.getTaxRates('reduced-rate') ).eqls([{ + rate_id: '2', + rate: '5.0000', + title: 'VAT', + shipping: true, + compound: true, + enabled: true, + total: 0, + subtotal: 0 + }]); + }); + }); \ No newline at end of file diff --git a/tests/unit/js/spec/entities/tax/collection.spec.js b/tests/unit/js/spec/entities/tax/collection.spec.js index 9f475994..e62d96a9 100644 --- a/tests/unit/js/spec/entities/tax/collection.spec.js +++ b/tests/unit/js/spec/entities/tax/collection.spec.js @@ -27,15 +27,34 @@ describe('entities/tax/collection.js', function () { }); it('should parse tax rates from POS params', function(){ - var collection = new Collection( dummy_tax_US[''], { parse: true } ); + var collection = new Collection( dummy_tax_US[''] ); expect(collection).to.have.length(2); expect(collection.pluck('rate_id')).to.eql(['4', '5']); }); it('update rate collection on tax_class change', function(){ - var collection = new Collection( dummy_tax_US[''], { parse: true } ); - expect(collection).to.have.length(2); - expect(collection.pluck('rate_id')).to.eql(['4', '5']); + var collection = new Collection( dummy_tax_GB[''] ); + collection.reset( dummy_tax_GB['reduced-rate'] ); + expect(collection).to.have.length(1); + expect(collection.pluck('rate_id')).to.eql(['2']); + }); + + it('toggle taxes enabled true/false', function(){ + var collection = new Collection( dummy_tax_US[''] ); + expect(collection.pluck('enabled')).to.eql([true, true]); + + collection.toggleTaxes({ all: false }); + expect(collection.pluck('enabled')).to.eql([false, false]); + + collection.toggleTaxes({ + all: true, + rate_1: true, + rate_2: true, + rate_3: true, + rate_4: true, + rate_5: false + }); + expect(collection.pluck('enabled')).to.eql([true, false]); }); }); \ No newline at end of file From ccdc6d25656a4179319c8b1ef3064cf88d8db98d Mon Sep 17 00:00:00 2001 From: Paul Kilmurray Date: Mon, 1 Feb 2016 15:28:14 +0800 Subject: [PATCH 024/169] refactor product entities --- .../css/src/scss/partials/_button_group.scss | 1 + assets/css/src/scss/partials/_cart.scss | 5 + assets/css/src/scss/partials/_icons.scss | 23 ++- .../css/src/scss/partials/_input-group.scss | 5 + .../apps/pos/cart/views/line/views/item.js | 7 +- assets/js/src/apps/pos/cart/views/totals.js | 8 + .../views/item/views/drawer/variation.js | 3 +- .../pos/products/views/item/views/product.js | 21 +- assets/js/src/apps/support/form/views/form.js | 2 +- .../js/src/entities/cart/models/abstract.js | 4 + assets/js/src/entities/orders/model.js | 12 +- assets/js/src/entities/products/collection.js | 18 +- assets/js/src/entities/products/model.js | 178 ---------------- .../src/entities/products/models/abstract.js | 79 +++++++ .../js/src/entities/products/models/simple.js | 5 + .../src/entities/products/models/variable.js | 172 ++++++++++++++++ assets/js/src/entities/tax/collection.js | 12 ++ .../js/src/entities/variations/collection.js | 47 ++++- assets/js/src/entities/variations/model.js | 26 +-- .../js/src/lib/utilities/stickit-handlers.js | 18 ++ includes/views/pos/cart/tmpl-customer.php | 2 +- includes/views/pos/cart/tmpl-item-drawer.php | 4 +- includes/views/pos/cart/tmpl-item.php | 4 +- includes/views/pos/cart/tmpl-totals.php | 8 +- includes/views/support/tmpl-form.php | 2 +- readme.txt | 1 + .../spec/entities/products/collection.spec.js | 29 +++ .../js/spec/entities/products/model.spec.js | 193 ------------------ .../entities/products/models/abstract.spec.js | 106 ++++++++++ .../entities/products/models/simple.spec.js | 11 + .../entities/products/models/variable.spec.js | 109 ++++++++++ .../entities/variations/collection.spec.js | 7 + .../js/spec/entities/variations/model.spec.js | 25 +++ 33 files changed, 711 insertions(+), 436 deletions(-) delete mode 100644 assets/js/src/entities/products/model.js create mode 100644 assets/js/src/entities/products/models/abstract.js create mode 100644 assets/js/src/entities/products/models/simple.js create mode 100644 assets/js/src/entities/products/models/variable.js create mode 100644 tests/unit/js/spec/entities/products/collection.spec.js delete mode 100644 tests/unit/js/spec/entities/products/model.spec.js create mode 100644 tests/unit/js/spec/entities/products/models/abstract.spec.js create mode 100644 tests/unit/js/spec/entities/products/models/simple.spec.js create mode 100644 tests/unit/js/spec/entities/products/models/variable.spec.js create mode 100644 tests/unit/js/spec/entities/variations/model.spec.js diff --git a/assets/css/src/scss/partials/_button_group.scss b/assets/css/src/scss/partials/_button_group.scss index 682d1c1b..d39eb47a 100644 --- a/assets/css/src/scss/partials/_button_group.scss +++ b/assets/css/src/scss/partials/_button_group.scss @@ -33,6 +33,7 @@ btn-group { &:last-child:not(:first-child) { @include border-left-radius(0); } + } .btn + .btn, diff --git a/assets/css/src/scss/partials/_cart.scss b/assets/css/src/scss/partials/_cart.scss index bdf0a51c..9e9d58aa 100644 --- a/assets/css/src/scss/partials/_cart.scss +++ b/assets/css/src/scss/partials/_cart.scss @@ -253,6 +253,11 @@ cart { font-size: 1.4em; } + .disabled { + text-decoration: line-through; + color: $secondary-text-color; + } + } diff --git a/assets/css/src/scss/partials/_icons.scss b/assets/css/src/scss/partials/_icons.scss index caa92a4a..5ff74c20 100644 --- a/assets/css/src/scss/partials/_icons.scss +++ b/assets/css/src/scss/partials/_icons.scss @@ -146,10 +146,29 @@ icon { } + // common icon colors + .#{current-module()}-secondary { + a & { + color: $secondary-text-color; + &:hover { + color: $brand-primary; + } + } + } + + .#{current-module()}-tertiary { + a & { + color: $tertiary-text-color; + &:hover { + color: $brand-primary; + } + } + } + // special cases .#{current-module()}-add { a & { - color: $tertiary-text-color; + //color: $tertiary-text-color; &:hover { color: $brand-success; } @@ -158,7 +177,7 @@ icon { .#{current-module()}-remove { a & { - color: $tertiary-text-color; + //color: $tertiary-text-color; &:hover { color: $brand-danger; } diff --git a/assets/css/src/scss/partials/_input-group.scss b/assets/css/src/scss/partials/_input-group.scss index 7632003d..6d790bff 100644 --- a/assets/css/src/scss/partials/_input-group.scss +++ b/assets/css/src/scss/partials/_input-group.scss @@ -121,6 +121,11 @@ input-group { font-size: 0; white-space: nowrap; + // give icons the same line-height at the input + i { + line-height: 1.5; + } + // Negative margin for spacing, position for bringing hovered/focused/actived // element above the siblings. > .btn { diff --git a/assets/js/src/apps/pos/cart/views/line/views/item.js b/assets/js/src/apps/pos/cart/views/line/views/item.js index 1607b77d..904e6b1c 100644 --- a/assets/js/src/apps/pos/cart/views/line/views/item.js +++ b/assets/js/src/apps/pos/cart/views/line/views/item.js @@ -75,9 +75,10 @@ module.exports = FormView.extend({ }, // product: name, shipping: method_title, fee: title - '*[data-name="name"]' : { observe: 'name', events: ['blur'] }, - '*[data-name="method_title"]' : {observe: 'method_title', events: ['blur']}, - '*[data-name="title"]' : { observe: 'title', events: ['blur'] }, + // note: contenteditable=true comes with filter and events: ['blur'] + '*[data-name="name"]' : { observe: 'name' }, + '*[data-name="method_title"]' : {observe: 'method_title' }, + '*[data-name="title"]' : { observe: 'title' }, 'dl.meta': { observe: 'meta', diff --git a/assets/js/src/apps/pos/cart/views/totals.js b/assets/js/src/apps/pos/cart/views/totals.js index fa360ee2..c4e44846 100644 --- a/assets/js/src/apps/pos/cart/views/totals.js +++ b/assets/js/src/apps/pos/cart/views/totals.js @@ -32,9 +32,17 @@ module.exports = ItemView.extend({ data.cart_discount = this.model.getDisplayCartDiscount(); } + // sum for disabled taxes + data.total_tax = this.model.sumItemizedTaxes( + this.model.get('tax_lines'), 'total' + ); + return data; }, + /** + * + */ toggleTax: function(e){ if(e){ e.preventDefault(); } var rate_id = $(e.currentTarget).data('rate_id'); diff --git a/assets/js/src/apps/pos/products/views/item/views/drawer/variation.js b/assets/js/src/apps/pos/products/views/item/views/drawer/variation.js index ddbf47d0..e9ac781b 100644 --- a/assets/js/src/apps/pos/products/views/item/views/drawer/variation.js +++ b/assets/js/src/apps/pos/products/views/item/views/drawer/variation.js @@ -27,8 +27,7 @@ var Item = ItemView.extend({ templateHelpers: function(){ return { - variation: true, - title: this.options.model.parent.get('title') + variation: true }; } diff --git a/assets/js/src/apps/pos/products/views/item/views/product.js b/assets/js/src/apps/pos/products/views/item/views/product.js index f40b2577..c90cecdb 100644 --- a/assets/js/src/apps/pos/products/views/item/views/product.js +++ b/assets/js/src/apps/pos/products/views/item/views/product.js @@ -77,14 +77,19 @@ var Item = ItemView.extend({ }, templateHelpers: function(){ - var variations = this.model.getVariations(); - var data = variations ? { - price: variations.superset().range('price'), - sale_price: variations.superset().range('sale_price'), - regular_price: variations.superset().range('regular_price'), - product_variations: this.model.getVariationOptions() - } : {} ; - data.product_attributes = this.model.productAttributes(); + var data = { + product_attributes: this.model.getProductAttributes() + }; + + if( this.model.get('type') === 'variable' ){ + _.extend( data, { + price: this.model.getRange('price'), + sale_price: this.model.getRange('sale_price'), + regular_price: this.model.getRange('regular_price'), + product_variations: this.model.getVariationOptions() + }); + } + return data; } diff --git a/assets/js/src/apps/support/form/views/form.js b/assets/js/src/apps/support/form/views/form.js index 73bd4b0c..5cc657c8 100644 --- a/assets/js/src/apps/support/form/views/form.js +++ b/assets/js/src/apps/support/form/views/form.js @@ -6,7 +6,7 @@ module.exports = FormView.extend({ template: 'support.form', ui: { - toggle : '.toggle' + toggle : '[data-action="toggle"]' }, events: { diff --git a/assets/js/src/entities/cart/models/abstract.js b/assets/js/src/entities/cart/models/abstract.js index d6800616..abe7b727 100644 --- a/assets/js/src/entities/cart/models/abstract.js +++ b/assets/js/src/entities/cart/models/abstract.js @@ -42,6 +42,10 @@ module.exports = bb.Model.extend({ line_item: this }); + // listen to tax toggle on order + this.listenTo( this.taxes, 'change:enabled', this.updateTotals ); + + // on change in line item tax options this.on( 'change:taxable change:tax_class', function(){ this.taxes.reset( this.getTaxRates() ); this.updateTotals(); diff --git a/assets/js/src/entities/orders/model.js b/assets/js/src/entities/orders/model.js index 224adc69..9f954627 100644 --- a/assets/js/src/entities/orders/model.js +++ b/assets/js/src/entities/orders/model.js @@ -283,14 +283,6 @@ module.exports = DualModel.extend({ } else { this.set( 'taxes.all', ! this.get( 'taxes.all' ) ); } - - // - if( this.tax_rates ){ - var enabled_taxes = this.get('taxes'); - _.each( this.tax_rates, function( taxes ){ - taxes.toggleTaxes( enabled_taxes ); - }); - } }, /** @@ -304,7 +296,7 @@ module.exports = DualModel.extend({ // parse tax_rates _.each( tax_rates, function( tax_rate, tax_class ){ - var taxes = new Taxes( tax_rate ); + var taxes = new Taxes( tax_rate, { order: this } ); taxes.toggleTaxes( enabled_taxes ); this.tax_rates[ tax_class ] = taxes; }, this); @@ -334,7 +326,7 @@ module.exports = DualModel.extend({ }, /** - * Returns this.tax_rates.toJSON() for a given tax_rate + * Returns parsed tax rates for a given tax_rate */ getTaxRates: function( tax_class ){ if( this.tax_rates ){ diff --git a/assets/js/src/entities/products/collection.js b/assets/js/src/entities/products/collection.js index 200d363b..a1efbba3 100644 --- a/assets/js/src/entities/products/collection.js +++ b/assets/js/src/entities/products/collection.js @@ -1,7 +1,21 @@ var DualCollection = require('lib/config/dual-collection'); -var Model = require('./model'); + +var subclasses = { + simple : require('./models/simple'), + variable : require('./models/variable') +}; module.exports = DualCollection.extend({ - model: Model, + + /** + * Init model based on product type + */ + model: function(attributes, options){ + attributes = attributes || {}; + var Subclass = subclasses[attributes.type] || subclasses['simple']; + return new Subclass(attributes, options); + }, + name: 'products' + }); \ No newline at end of file diff --git a/assets/js/src/entities/products/model.js b/assets/js/src/entities/products/model.js deleted file mode 100644 index faf803a9..00000000 --- a/assets/js/src/entities/products/model.js +++ /dev/null @@ -1,178 +0,0 @@ -var DualModel = require('lib/config/dual-model'); -var _ = require('lodash'); -var Variations = require('../variations/collection'); -var FilteredCollection = require('lib/config/obscura'); - -module.exports = DualModel.extend({ - name: 'product', - - // this is an array of fields used by FilterCollection.matchmaker() - fields: ['title'], - - // data types - schema: { - price : 'number', - regular_price : 'number', - sale_price : 'number', - stock_quantity: 'number' - }, - - initialize: function(){ - this.on({ - 'change:updated_at': this.onUpdate - }); - }, - - onUpdate: function(){ - // update stock - if( this.get('type') === 'variable' ){ - var variations = this.getVariations().superset(); - variations.set( this.get('variations') ); - } - }, - - /** - * Helper functions to display attributes vs variations - */ - productAttributes: function(){ - return _.chain(this.get('attributes')) - .where({variation: false}) - .where({visible: true}) - .value(); - }, - - productVariations: function(){ - return _.where(this.get('attributes'), {variation: true}); - }, - - /** - * Special cases for product model filter - * @param {Array} tokens An array of query tokens, see QParser - * @param {Object} methods Helper match methods - * @param {Function} callback - */ - matchMaker: function(tokens, methods, callback){ - - var match = _.all(tokens, function(token){ - - // barcode - if( token.type === 'prefix' && token.prefix === 'barcode' ){ - if(token.query){ return this.barcodeMatch(token.query); } - } - - // cat - if( token.type === 'prefix' && token.prefix === 'cat' ){ - token.prefix = 'categories'; - return methods.prefix(token, this); - } - - }, this); - - //if(match){ - // return match; - //} - - // the original matchMaker - return match ? match : callback(tokens, this); - - }, - - barcodeMatch: function(barcode){ - var type = this.get('type'), - test = this.get('barcode').toLowerCase(), - value = barcode.toString().toLowerCase(); - - if(test === value) { - if(type !== 'variable'){ - this.trigger('match:barcode', this); - } - return true; - } - - if(type !== 'variable'){ - return this.partialBarcodeMatch(test, value); - } - - return this.variableBarcodeMatch(test, value); - }, - - partialBarcodeMatch: function(test, value){ - if(test.indexOf( value ) !== -1) { - return true; - } - return false; - }, - - variableBarcodeMatch: function(test, value){ - var match; - - this.getVariations().superset().each(function(variation){ - var vtest = variation.get('barcode').toLowerCase(); - if(vtest === value){ - match = variation; - return; - } - if(vtest.indexOf( value ) !== -1) { - match = 'partial'; - return; - } - }); - - if(match){ - if(match !== 'partial'){ - this.trigger('match:barcode', match, this); - } - return true; - } - - return this.partialBarcodeMatch(test, value); - }, - - /** - * Construct variable options from variation array - * - variable.attributes includes all options, including those not used - */ - getVariationOptions: function(){ - if( this._variationOptions ) { - return this._variationOptions; - } - - var variations = this.get('variations'); - - // pluck all options, eg: - // { Color: ['Black', 'Blue'], Size: ['Small', 'Large'] } - var result = _.pluck(variations, 'attributes') - .reduce(function(result, attrs){ - _.each(attrs, function(attr){ - if(result[attr.name]){ - return result[attr.name].push(attr.option); - } - result[attr.name] = [attr.option]; - }); - return result; - }, {}); - - // map options with consistent keys - this._variationOptions = _.map(result, function(options, name){ - return { - 'name': name, - 'options': _.uniq( options ) - }; - }); - - return this._variationOptions; - }, - - /** - * - */ - getVariations: function(){ - if( this.get('type') !== 'variable' ){ return false; } - if( ! this._variations ){ - var variations = new Variations(this.get('variations'), { parent: this }); - this._variations = new FilteredCollection(variations); - } - return this._variations; - } - -}); \ No newline at end of file diff --git a/assets/js/src/entities/products/models/abstract.js b/assets/js/src/entities/products/models/abstract.js new file mode 100644 index 00000000..f362c2c7 --- /dev/null +++ b/assets/js/src/entities/products/models/abstract.js @@ -0,0 +1,79 @@ +var DualModel = require('lib/config/dual-model'); +var _ = require('lodash'); + +module.exports = DualModel.extend({ + + name: 'product', + + // this is an array of fields used by FilterCollection.matchmaker() + fields: ['title'], + + // data types + schema: { + price : 'number', + regular_price : 'number', + sale_price : 'number', + stock_quantity: 'number' + }, + + /** + * Get the product attributes + * - not to be confused with Backbone Model attributes + * - visible product attributes without variations + */ + getProductAttributes: function(){ + return _.chain(this.get('attributes')) + .where({variation: false}) + .where({visible: true}) + .value(); + }, + + /** + * Special cases for product model filter + */ + matchMaker: function(tokens, methods, callback){ + + var match = _.all(tokens, function(token){ + + // barcode + if( token.type === 'prefix' && token.prefix === 'barcode' ){ + if(token.query){ return this.barcodeMatch(token.query); } + } + + // cat + if( token.type === 'prefix' && token.prefix === 'cat' ){ + token.prefix = 'categories'; + return methods.prefix(token, this); + } + + }, this); + + //if(match){ + // return match; + //} + + // the original matchMaker + return match ? match : callback(tokens, this); + + }, + + barcodeMatch: function(barcode){ + var type = this.get('type'), + test = this.get('barcode').toLowerCase(), + value = barcode.toString().toLowerCase(); + + if(test === value) { + if(type !== 'variable'){ + this.trigger('match:barcode', this); + } + return true; + } + + if(type !== 'variable'){ + return this.partialBarcodeMatch(test, value); + } + + return this.variableBarcodeMatch(test, value); + } + +}); \ No newline at end of file diff --git a/assets/js/src/entities/products/models/simple.js b/assets/js/src/entities/products/models/simple.js new file mode 100644 index 00000000..eed0b085 --- /dev/null +++ b/assets/js/src/entities/products/models/simple.js @@ -0,0 +1,5 @@ +var Model = require('./abstract'); + +module.exports = Model.extend({ + +}); \ No newline at end of file diff --git a/assets/js/src/entities/products/models/variable.js b/assets/js/src/entities/products/models/variable.js new file mode 100644 index 00000000..2ceff3ca --- /dev/null +++ b/assets/js/src/entities/products/models/variable.js @@ -0,0 +1,172 @@ +var Model = require('./abstract'); +var Variations = require('../../variations/collection'); +var FilteredCollection = require('lib/config/obscura'); +var _ = require('lodash'); + +module.exports = Model.extend({ + + /** + * attach variations during parse + */ + parse: function( attributes ){ + attributes = Model.prototype.parse.call( this, attributes ); + + if( attributes.variations && ! this.variations ){ + this.attachVariations( attributes ); + } + + return attributes; + }, + + /** + * attach Filtered Collection of variations + */ + attachVariations: function( parent ){ + var attributes = _.defaults( parent.variations ); + var variations = new Variations(attributes, { + title: parent.title, + parent: this // a reference to the parent model + }); + this.variations = new FilteredCollection( variations ); + }, + + /** + * return attached variations, returns Filtered Collection + */ + getVariations: function(){ + return this.variations; + }, + + /** + * Wrapper for variation options + */ + getVariationOptions: function(){ + return this.variations.superset().getVariationOptions(); + }, + + /** + * Wrapper for variations range + */ + getRange: function( attribute ){ + return this.variations.superset().range( attribute ); + } + + /** + * Special cases for product model filter + * @param {Array} tokens An array of query tokens, see QParser + * @param {Object} methods Helper match methods + * @param {Function} callback + */ + //matchMaker: function(tokens, methods, callback){ + // + // var match = _.all(tokens, function(token){ + // + // // barcode + // if( token.type === 'prefix' && token.prefix === 'barcode' ){ + // if(token.query){ return this.barcodeMatch(token.query); } + // } + // + // // cat + // if( token.type === 'prefix' && token.prefix === 'cat' ){ + // token.prefix = 'categories'; + // return methods.prefix(token, this); + // } + // + // }, this); + // + // //if(match){ + // // return match; + // //} + // + // // the original matchMaker + // return match ? match : callback(tokens, this); + // + //}, + // + //barcodeMatch: function(barcode){ + // var type = this.get('type'), + // test = this.get('barcode').toLowerCase(), + // value = barcode.toString().toLowerCase(); + // + // if(test === value) { + // if(type !== 'variable'){ + // this.trigger('match:barcode', this); + // } + // return true; + // } + // + // if(type !== 'variable'){ + // return this.partialBarcodeMatch(test, value); + // } + // + // return this.variableBarcodeMatch(test, value); + //}, + // + //partialBarcodeMatch: function(test, value){ + // if(test.indexOf( value ) !== -1) { + // return true; + // } + // return false; + //}, + // + //variableBarcodeMatch: function(test, value){ + // var match; + // + // this.getVariations().superset().each(function(variation){ + // var vtest = variation.get('barcode').toLowerCase(); + // if(vtest === value){ + // match = variation; + // return; + // } + // if(vtest.indexOf( value ) !== -1) { + // match = 'partial'; + // return; + // } + // }); + // + // if(match){ + // if(match !== 'partial'){ + // this.trigger('match:barcode', match, this); + // } + // return true; + // } + // + // return this.partialBarcodeMatch(test, value); + //}, + + /** + * Construct variable options from variation array + * - variable.attributes includes all options, including those not used + */ + //getVariationOptions: function(){ + // if( this._variationOptions ) { + // return this._variationOptions; + // } + // + // var variations = this.get('variations'); + // + // // pluck all options, eg: + // // { Color: ['Black', 'Blue'], Size: ['Small', 'Large'] } + // var result = _.pluck(variations, 'attributes') + // .reduce(function(result, attrs){ + // _.each(attrs, function(attr){ + // if(result[attr.name]){ + // return result[attr.name].push(attr.option); + // } + // result[attr.name] = [attr.option]; + // }); + // return result; + // }, {}); + // + // // map options with consistent keys + // this._variationOptions = _.map(result, function(options, name){ + // return { + // 'name': name, + // 'options': _.uniq( options ) + // }; + // }); + // + // return this._variationOptions; + //}, + +}); \ No newline at end of file diff --git a/assets/js/src/entities/tax/collection.js b/assets/js/src/entities/tax/collection.js index 13203576..1fd058ee 100644 --- a/assets/js/src/entities/tax/collection.js +++ b/assets/js/src/entities/tax/collection.js @@ -24,6 +24,18 @@ module.exports = bb.Collection.extend({ bb.Collection.call(this, models, options); }, + /** + * Listen to the order model for tax toggle + */ + initialize: function( models, options ){ + var order = options.order || _.get(this.line_item, ['collection', 'order']); + + // listen for order tax toggle + this.listenTo( order, 'change:taxes', function(){ + this.toggleTaxes( order.get('taxes') ); + }); + }, + /** * Calculate the line item tax total * based on the calc_exclusive_tax function in diff --git a/assets/js/src/entities/variations/collection.js b/assets/js/src/entities/variations/collection.js index 5559c8d7..4d80b88b 100644 --- a/assets/js/src/entities/variations/collection.js +++ b/assets/js/src/entities/variations/collection.js @@ -1,26 +1,48 @@ var Collection = require('lib/config/collection'); var Model = require('./model'); -var Radio = require('backbone.radio'); var _ = require('lodash'); module.exports = Collection.extend({ + model: Model, - url: function(){ - var wc_api = Radio.request('entities', 'get', { - type: 'option', - name: 'wc_api' + /** + * Returns an array of options for the variation collection + * - caches on first run + */ + getVariationOptions: function(){ + if( this._variationOptions ) { + return this._variationOptions; + } + + // pluck all options, eg: + // { Color: ['Black', 'Blue'], Size: ['Small', 'Large'] } + var result = this.pluck('attributes') + .reduce(function(result, attrs){ + _.each(attrs, function(attr){ + if(result[attr.name]){ + return result[attr.name].push(attr.option); + } + result[attr.name] = [attr.option]; + }); + return result; + }, {}); + + // map options with consistent keys + // { name: 'Color', options: ['Black', 'Blue'] }, + // { name: 'Size', options: ['Small', 'Large'] } + this._variationOptions = _.map(result, function(options, name){ + return { + 'name': name, + 'options': _.uniq( options ) + }; }); - return wc_api + 'products'; - }, - initialize: function() { - this._isNew = false; + return this._variationOptions; }, /** - * same as _.compact - * except allows 0 + * same as _.compact, except allows 0 */ /* jshint -W074 */ compact: function(array) { @@ -39,6 +61,9 @@ module.exports = Collection.extend({ }, /* jshint +W074 */ + /** + * + */ range: function(attr){ var attrs = this.compact( this.pluck(attr) ), min = 0, max = 0; if( !_.isEmpty(attrs) ) { diff --git a/assets/js/src/entities/variations/model.js b/assets/js/src/entities/variations/model.js index 5277447d..ef97fc91 100644 --- a/assets/js/src/entities/variations/model.js +++ b/assets/js/src/entities/variations/model.js @@ -1,11 +1,6 @@ -//var Model = require('lib/config/model'); var Model = require('lib/config/deep-model'); module.exports = Model.extend({ - name: 'product', - defaults: { - type: 'variation' - }, // data types schema: { @@ -15,20 +10,19 @@ module.exports = Model.extend({ stock_quantity: 'number' }, - initialize: function(attributes, options){ + /** + * Attach parent product, add defaults + */ + constructor: function(attributes, options){ options = options || {}; this.parent = options.parent; - this.set({ title: options.parent.get('title') }); - }, - // copy variation to parent - save: function(attributes, options){ - var self = this; - return Model.prototype.save.call(this, attributes, options) - .then(function(){ - self.parent.set({ variations: self.collection.toJSON() }); - self.parent.merge(); - }); + this.defaults = { + type : 'variation', + title : options.title + } + + Model.apply( this, arguments ); } }); \ No newline at end of file diff --git a/assets/js/src/lib/utilities/stickit-handlers.js b/assets/js/src/lib/utilities/stickit-handlers.js index a413241a..bf2cc02e 100644 --- a/assets/js/src/lib/utilities/stickit-handlers.js +++ b/assets/js/src/lib/utilities/stickit-handlers.js @@ -71,4 +71,22 @@ bb.Stickit.addHandler({ } return val; } +}); + +/** + * Default stickit updateMethod = 'html' for contenteditable + * - change updateMethod to 'text' + * - update on 'blur' + * - filter html input + */ +bb.Stickit.addHandler({ + selector: '[contenteditable=true]', + updateMethod: 'text', + events: ['blur'], + onSet: function(val, obj){ + if( val !== this.$(obj.selector).html() ){ + this.$(obj.selector).text( val ); + } + return val; + } }); \ No newline at end of file diff --git a/includes/views/pos/cart/tmpl-customer.php b/includes/views/pos/cart/tmpl-customer.php index e089d19f..5e1bd5e7 100644 --- a/includes/views/pos/cart/tmpl-customer.php +++ b/includes/views/pos/cart/tmpl-customer.php @@ -2,6 +2,6 @@
- {{#if customer_id}} {{/if}} + {{#if customer_id}} {{/if}} {{formatCustomerName customer}}
\ No newline at end of file diff --git a/includes/views/pos/cart/tmpl-item-drawer.php b/includes/views/pos/cart/tmpl-item-drawer.php index a2d253f7..45819982 100644 --- a/includes/views/pos/cart/tmpl-item-drawer.php +++ b/includes/views/pos/cart/tmpl-item-drawer.php @@ -35,12 +35,12 @@ - + {{/each}} - + diff --git a/includes/views/pos/cart/tmpl-item.php b/includes/views/pos/cart/tmpl-item.php index c46c0559..67487ae1 100644 --- a/includes/views/pos/cart/tmpl-item.php +++ b/includes/views/pos/cart/tmpl-item.php @@ -5,7 +5,7 @@
{{name}}
- +
{{else}} @@ -17,7 +17,7 @@ {{#if fee}} {{title}} {{/if}} - +
{{/if}} diff --git a/includes/views/pos/cart/tmpl-totals.php b/includes/views/pos/cart/tmpl-totals.php index bd885f14..9d6a38e9 100644 --- a/includes/views/pos/cart/tmpl-totals.php +++ b/includes/views/pos/cart/tmpl-totals.php @@ -14,7 +14,7 @@ {{#if itemized}} {{#each tax_lines}} {{#compare total '!==' 0}} -
  • +
  • {{#if ../../incl_tax}}(){{/if}} {{title}}: @@ -22,14 +22,14 @@
    {{{money total}}}
  • {{/compare}} {{/each}} {{else}} -
  • +
  • {{#if incl_tax}}(){{/if}} countries->tax_or_vat() ); ?>: @@ -37,7 +37,7 @@
    {{{money total_tax}}}
  • diff --git a/includes/views/support/tmpl-form.php b/includes/views/support/tmpl-form.php index 7f1a673c..6d96e8bd 100644 --- a/includes/views/support/tmpl-form.php +++ b/includes/views/support/tmpl-form.php @@ -17,7 +17,7 @@ - + \ No newline at end of file diff --git a/readme.txt b/readme.txt index 5d2298be..e236e817 100644 --- a/readme.txt +++ b/readme.txt @@ -89,6 +89,7 @@ Bugs can be reported on the [WooCommerce POS GitHub repository](https://github.c * New: Settings for default Shipping and Fee line items * New: Setting for which customer roles to display in the POS * New: Split products into multiple lines in the cart +* New: Toggle order taxes * Fix: Checkout settings not showing for some users * Fix: issue with some product variation names, e.g.: 000, 00 and 0 * Tweak: Add Reply-To header to support emails diff --git a/tests/unit/js/spec/entities/products/collection.spec.js b/tests/unit/js/spec/entities/products/collection.spec.js new file mode 100644 index 00000000..e71be7bc --- /dev/null +++ b/tests/unit/js/spec/entities/products/collection.spec.js @@ -0,0 +1,29 @@ +describe('entities/products/collection.js', function () { + + // @todo idb-wrapper throws an error + var Collection = proxyquire('entities/products/collection',{ + 'lib/config/dual-collection': require('lib/config/collection') + }); + + var dummy_products = require('../../../data/products.json'); + + it('should be in a valid state', function() { + var collection = new Collection(); + expect(collection).to.be.ok; + }); + + it('should parse products, with or without \'products\' node', function() { + + // with node + var collection = new Collection(); + collection.set( dummy_products, { parse: true } ); + expect(collection.length).to.equal( dummy_products.products.length ); + + // without node + var collection = new Collection(); + collection.set( dummy_products.products, { parse: true } ); + expect(collection.length).to.equal( dummy_products.products.length ); + + }); + +}); \ No newline at end of file diff --git a/tests/unit/js/spec/entities/products/model.spec.js b/tests/unit/js/spec/entities/products/model.spec.js deleted file mode 100644 index 4c536324..00000000 --- a/tests/unit/js/spec/entities/products/model.spec.js +++ /dev/null @@ -1,193 +0,0 @@ -describe('entities/products/model.js', function () { - - var Model = require('entities/products/model'); - var _ = require('lodash'); - var dummy = require('../../../data/products.json'); - - var dummy_data = { - id: 2, - title: 'Variable Product', - type: 'variable', - on_sale: true, - price: '2.00', - sale_price: null, - regular_price: '2.00', - barcode: 'SKU12345', - variations: [{ - id: 3, - on_sale: false, - price: '6.00', - sale_price: null, - regular_price: '6.00', - barcode: 'SKU67890' - },{ - id: 4, - on_sale: true, - price: '2.50', - sale_price: '2.50', - regular_price: '5.00' - },{ - id: 5, - on_sale: false, - price: '5.00', - sale_price: '2.00', - regular_price: '5.00' - }], - attributes: [{ - name: 'Ingredients', - visible: true, - variation: false, - options: ['Milk', 'Flour', 'Eggs', 'Butter'] - },{ - name: 'States', - visible: false, - variation: false, - options: ['WA', 'VIC', 'NSW'] - },{ - name: 'OS', - visible: true, - variation: false, - options: ['Mac', 'PC', 'Linux'] - },{ - name: 'Color', - visible: true, - variation: true, - options: ['Black', 'Blue'] - },{ - name: 'Size', - visible: true, - variation: true, - options: ['S', 'M', 'L'] - }] - }; - - beforeEach(function () { - this.product = new Model(dummy_data); - }); - - it('should be in a valid state', function() { - expect(this.product).to.be.ok; - }); - - describe('product attributes', function () { - - it('should return the visible product attributes (without variations)', function() { - expect(this.product.productAttributes()).eql([{ - name: 'Ingredients', - visible: true, - variation: false, - options: ['Milk', 'Flour', 'Eggs', 'Butter'] - },{ - name: 'OS', - visible: true, - variation: false, - options: ['Mac', 'PC', 'Linux'] - }]); - }); - - it('should return the product variations', function() { - expect(this.product.productVariations()).eql([{ - name: 'Color', - visible: true, - variation: true, - options: ['Black', 'Blue'] - },{ - name: 'Size', - visible: true, - variation: true, - options: ['S', 'M', 'L'] - }]); - }); - - }); - - describe('barcode match', function () { - - it('should return true for a simple barcode match', function() { - this.product.set({ type: 'simple' }); - this.product.barcodeMatch('SKU12345').should.be.true; - }); - - it('should return false for a simple barcode mismatch', function() { - this.product.set({ type: 'simple' }); - this.product.barcodeMatch('FOO12345').should.be.false; - }); - - it('should trigger "match:barcode" for exact barcode match', function() { - var trigger = stub(); - this.product.set({ type: 'simple' }); - this.product.on('match:barcode', trigger); - this.product.barcodeMatch('SKU12345'); - trigger.should.have.been.calledWith(this.product); - }); - - //it('should return true for a variable barcode match', function() { - // this.product.barcodeMatch('SKU12345').should.be.true; - //}); - // - //it('should return true for a variation barcode match', function() { - // this.product.barcodeMatch('SKU67890').should.be.true; - //}); - // - //it('should trigger "match:barcode" for exact variation match', function() { - // var trigger = stub(); - // this.product.on('match:barcode', trigger); - // this.product.barcodeMatch('SKU67890'); - // trigger.should.have.been.calledWith(dummy_data.variations[0], this.product); - //}); - // - //it('should return true for a partial barcode match', function() { - // this.product.barcodeMatch('12345').should.be.true; - //}); - // - //it('should return true for a partial variation barcode match', function() { - // this.product.barcodeMatch('67890').should.be.true; - //}); - - }); - - describe('variable products', function () { - - before(function () { - var variableAttributes = _.findWhere(dummy.products, { id: 40 }); - this.variableProduct = new Model(variableAttributes); - }); - - it('getVariationOptions should return variation options', function() { - this.variableProduct.should.respondTo('getVariationOptions'); - this.variableProduct.getVariationOptions().should.eql([{ - name: 'Color', - options: [ 'Black', 'Blue' ] - }]); - }); - - it('getVariations should return variation collection', function(){ - this.variableProduct.should.respondTo('getVariations'); - var variations = this.variableProduct.getVariations(); - variations.should.be.instanceOf( require('lib/config/obscura') ); - variations.length.should.eql(2); - }); - - // changes to variations to made to parent.variations?? - // - // it('getVariationOptions should return multiple variation options', function() { - // var filteredVariations = this.variableProduct.getVariations(); - // var variations = filteredVariations.superset(); - // variations.each(function(variation) { - // var variant = variation.get('attributes'); - // variant.push({name: 'Size', option: 'Small'}); - // variation.set('attributes', variant); - // }); - // - // this.variableProduct.getVariationOptions().should.eql([{ - // name: 'Color', - // options: [ 'Black', 'Blue' ] - // }, { - // name: 'Size', - // options: [ 'Small' ] - // }]); - //}); - - }); - -}); \ No newline at end of file diff --git a/tests/unit/js/spec/entities/products/models/abstract.spec.js b/tests/unit/js/spec/entities/products/models/abstract.spec.js new file mode 100644 index 00000000..54c8c696 --- /dev/null +++ b/tests/unit/js/spec/entities/products/models/abstract.spec.js @@ -0,0 +1,106 @@ +describe('entities/products/models/abstract.js', function () { + + var Model = require('entities/products/models/abstract'); + + var dummy_products = require('../../../../data/products.json'); + + it('should be in a valid state', function() { + var product = new Model(dummy_products.products[0]); + expect(product).to.be.ok; + }); + + it('should return Product attributes (ie: visible no variations)', function(){ + + var dummy_product = { + attributes: [{ + name: 'Ingredients', + visible: true, + variation: false, + options: ['Milk', 'Flour', 'Eggs', 'Butter'] + },{ + name: 'States', + visible: false, + variation: false, + options: ['WA', 'VIC', 'NSW'] + },{ + name: 'OS', + visible: true, + variation: false, + options: ['Mac', 'PC', 'Linux'] + },{ + name: 'Color', + visible: true, + variation: true, + options: ['Black', 'Blue'] + },{ + name: 'Size', + visible: true, + variation: true, + options: ['S', 'M', 'L'] + }] + }; + + var product = new Model(dummy_product); + + expect( product.getProductAttributes() ).eqls([{ + name: 'Ingredients', + visible: true, + variation: false, + options: ['Milk', 'Flour', 'Eggs', 'Butter'] + },{ + name: 'OS', + visible: true, + variation: false, + options: ['Mac', 'PC', 'Linux'] + }]); + + }); + + + describe('barcode match', function () { + + //it('should return true for a simple barcode match', function() { + // this.product.set({ type: 'simple' }); + // this.product.barcodeMatch('SKU12345').should.be.true; + //}); + // + //it('should return false for a simple barcode mismatch', function() { + // this.product.set({ type: 'simple' }); + // this.product.barcodeMatch('FOO12345').should.be.false; + //}); + // + //it('should trigger "match:barcode" for exact barcode match', function() { + // var trigger = stub(); + // this.product.set({ type: 'simple' }); + // this.product.on('match:barcode', trigger); + // this.product.barcodeMatch('SKU12345'); + // trigger.should.have.been.calledWith(this.product); + //}); + + //it('should return true for a variable barcode match', function() { + // this.product.barcodeMatch('SKU12345').should.be.true; + //}); + // + //it('should return true for a variation barcode match', function() { + // this.product.barcodeMatch('SKU67890').should.be.true; + //}); + // + //it('should trigger "match:barcode" for exact variation match', function() { + // var trigger = stub(); + // this.product.on('match:barcode', trigger); + // this.product.barcodeMatch('SKU67890'); + // trigger.should.have.been.calledWith(dummy_data.variations[0], this.product); + //}); + // + //it('should return true for a partial barcode match', function() { + // this.product.barcodeMatch('12345').should.be.true; + //}); + // + //it('should return true for a partial variation barcode match', function() { + // this.product.barcodeMatch('67890').should.be.true; + //}); + + }); + + +}); \ No newline at end of file diff --git a/tests/unit/js/spec/entities/products/models/simple.spec.js b/tests/unit/js/spec/entities/products/models/simple.spec.js new file mode 100644 index 00000000..514e9f2a --- /dev/null +++ b/tests/unit/js/spec/entities/products/models/simple.spec.js @@ -0,0 +1,11 @@ +describe('entities/products/models/simple.js', function () { + + var Model = require('entities/products/models/simple'); + var dummy_products = require('../../../../data/products.json'); + + it('should be in a valid state', function() { + var product = new Model( dummy_products.products[0] ); + expect(product).to.be.ok; + }); + +}); \ No newline at end of file diff --git a/tests/unit/js/spec/entities/products/models/variable.spec.js b/tests/unit/js/spec/entities/products/models/variable.spec.js new file mode 100644 index 00000000..d670b509 --- /dev/null +++ b/tests/unit/js/spec/entities/products/models/variable.spec.js @@ -0,0 +1,109 @@ +describe('entities/products/models/variable.js', function () { + + var Model = require('entities/products/models/variable'); + + var dummy_products = require('../../../../data/products.json'); + + var dummy_product = { + id: 2, + title: 'Variable Product', + type: 'variable', + on_sale: true, + price: '2.00', + sale_price: null, + regular_price: '2.00', + barcode: 'SKU12345', + variations: [{ + id: 3, + on_sale: false, + price: '6.00', + sale_price: null, + regular_price: '6.00', + barcode: 'SKU67890', + attributes: [{ + name: 'Color', + option: 'Black' + },{ + name: 'Size', + option: 'S' + }] + },{ + id: 4, + on_sale: true, + price: '2.50', + sale_price: '2.50', + regular_price: '5.00', + attributes: [{ + name: 'Color', + option: 'Black' + },{ + name: 'Size', + option: 'M' + }] + },{ + id: 5, + on_sale: false, + price: '5.00', + sale_price: '2.00', + regular_price: '5.00', + attributes: [{ + name: 'Color', + option: 'Blue' + },{ + name: 'Size', + option: 'S' + }] + }], + attributes: [{ + name: 'Ingredients', + visible: true, + variation: false, + options: ['Milk', 'Flour', 'Eggs', 'Butter'] + },{ + name: 'States', + visible: false, + variation: false, + options: ['WA', 'VIC', 'NSW'] + },{ + name: 'OS', + visible: true, + variation: false, + options: ['Mac', 'PC', 'Linux'] + },{ + name: 'Color', + visible: true, + variation: true, + options: ['Black', 'Blue'] + },{ + name: 'Size', + visible: true, + variation: true, + options: ['S', 'M', 'L'] + }] + }; + + it('should be in a valid state', function() { + var variableProduct = _.findWhere(dummy_products.products, { id: 40 }); + var product = new Model( variableProduct ); + expect(product).to.be.ok; + }); + + it('should attach a collection of variations', function() { + var variableProduct = _.findWhere(dummy_products.products, { id: 40 }); + var product = new Model( variableProduct, { parse: true } ); + expect( product.variations ).to.be.an.instanceof( Backbone.Collection ); + expect( product.variations.length).equals( variableProduct.variations.length ); + }); + + it('should return Product variations', function(){ + var product = new Model( dummy_product, { parse: true } ); + expect( product.getVariationOptions() ).eqls([{ + name: 'Color', + options: ['Black', 'Blue'] + },{ + name: 'Size', + options: ['S', 'M'] // note: L variation not preset in dummy_product + }]); + }); + +}); \ No newline at end of file diff --git a/tests/unit/js/spec/entities/variations/collection.spec.js b/tests/unit/js/spec/entities/variations/collection.spec.js index f6603cb5..6c00d099 100644 --- a/tests/unit/js/spec/entities/variations/collection.spec.js +++ b/tests/unit/js/spec/entities/variations/collection.spec.js @@ -30,4 +30,11 @@ describe('entities/variations/collection.js', function () { variations.range('sale_price').should.eql([ 30 ]); }); + it('should return an array of the available variation options', function(){ + variations.getVariationOptions().should.eql([{ + name: 'Color', + options: [ "Black", "Blue" ] + }]); + }); + }); \ No newline at end of file diff --git a/tests/unit/js/spec/entities/variations/model.spec.js b/tests/unit/js/spec/entities/variations/model.spec.js new file mode 100644 index 00000000..617dd93c --- /dev/null +++ b/tests/unit/js/spec/entities/variations/model.spec.js @@ -0,0 +1,25 @@ +describe('entities/variations/model.js', function () { + + var dummy_products = require('../../../data/products.json'); + + var Model = require('entities/variations/model'); + + it('should be in a valid state', function(){ + var variable = _.find( dummy_products.products, { id: 40 } ); + var model = new Model( variable.variations[0] ); + model.should.be.ok; + }); + + it('should init with type = variation', function(){ + var variable = _.find( dummy_products.products, { id: 40 } ); + var model = new Model( variable.variations[0] ); + model.get('type').should.equal('variation'); + }); + + it('should parse the title from the parent product', function(){ + var variable = _.find( dummy_products.products, { id: 40 } ); + var model = new Model( variable.variations[0], { title: variable.title } ); + model.get('title').should.equal(variable.title); + }); + +}); \ No newline at end of file From 3c2aefc2dbdfa45092b795f12166c4167f3d387b Mon Sep 17 00:00:00 2001 From: Paul Kilmurray Date: Sun, 7 Feb 2016 02:54:19 +0800 Subject: [PATCH 025/169] refactor cart --- .jshintrc | 4 +- assets/css/src/scss/partials/_checkout.scss | 12 +- assets/css/src/scss/partials/_header.scss | 4 + assets/css/src/scss/partials/_loading.scss | 3 +- assets/css/src/scss/partials/_main.scss | 8 +- assets/css/src/scss/partials/_panel.scss | 8 +- assets/css/src/scss/partials/_tabs.scss | 45 +++-- assets/js/src/apps/app/layout-view.js | 32 ++-- assets/js/src/apps/pos/cart/layout.js | 13 +- assets/js/src/apps/pos/cart/route.js | 148 +++++++++------- assets/js/src/apps/pos/cart/views/items.js | 3 + .../js/src/apps/pos/cart/views/line/layout.js | 18 +- .../apps/pos/cart/views/line/views/drawer.js | 14 +- .../apps/pos/cart/views/line/views/item.js | 14 +- assets/js/src/apps/pos/cart/views/notes.js | 71 ++++---- assets/js/src/apps/pos/checkout/route.js | 14 +- .../pos/checkout/views/gateways/drawer.js | 48 ++++- assets/js/src/apps/pos/layout.js | 16 -- assets/js/src/apps/pos/products/route.js | 28 ++- .../pos/products/views/item/views/product.js | 2 +- assets/js/src/apps/pos/receipt/route.js | 5 +- assets/js/src/apps/pos/router.js | 77 +++++--- assets/js/src/apps/support/form/route.js | 5 +- assets/js/src/apps/support/layout.js | 18 -- assets/js/src/apps/support/router.js | 10 +- assets/js/src/apps/support/status/route.js | 5 +- assets/js/src/entities/cart/collection.js | 4 +- assets/js/src/entities/gateways/collection.js | 33 ++-- assets/js/src/entities/gateways/model.js | 24 +++ assets/js/src/entities/orders/collection.js | 81 +-------- assets/js/src/entities/orders/model.js | 114 +++++++++--- assets/js/src/entities/variations/model.js | 2 +- .../js/src/lib/components/logger/service.js | 2 + assets/js/src/lib/components/modal/service.js | 1 + .../components/tabs/entities/collection.js | 49 +++--- .../src/lib/components/tabs/entities/model.js | 5 +- .../src/lib/components/tabs/old_views/tab.hbs | 6 + .../src/lib/components/tabs/old_views/tab.js | 46 +++++ .../src/lib/components/tabs/old_views/tabs.js | 32 ++++ assets/js/src/lib/components/tabs/service.js | 2 - .../js/src/lib/components/tabs/views/tab.hbs | 5 - .../js/src/lib/components/tabs/views/tab.js | 57 +++--- .../js/src/lib/components/tabs/views/tabs.hbs | 1 + .../js/src/lib/components/tabs/views/tabs.js | 89 ++++++++-- assets/js/src/lib/config/application.js | 2 + assets/js/src/lib/config/composite-view.js | 2 +- assets/js/src/lib/config/dual-model.js | 25 ++- assets/js/src/lib/config/obscura.js | 5 +- assets/js/src/lib/config/route.js | 15 +- assets/js/src/lib/config/router.js | 35 ++++ .../src/lib/utilities/handlebars-helpers.js | 2 + .../js/vendor/jquery.scrollIntoView.build.js | 134 ++++++++++++++ assets/js/vendor/jquery.scrollIntoView.min.js | 2 +- .../admin/settings/class-wc-pos-checkout.php | 29 +-- includes/api/class-wc-pos-gateways.php | 12 +- includes/class-wc-pos-gateways.php | 12 +- includes/class-wc-pos-template.php | 2 +- tests/unit/js/setup/helpers.js | 3 +- .../spec/entities/gateways/collection.spec.js | 28 +++ .../js/spec/entities/gateways/model.spec.js | 40 +++++ .../js/spec/entities/orders/model.spec.js | 166 +++++++++++++----- 61 files changed, 1148 insertions(+), 544 deletions(-) delete mode 100644 assets/js/src/apps/pos/layout.js delete mode 100644 assets/js/src/apps/support/layout.js create mode 100644 assets/js/src/lib/components/tabs/old_views/tab.hbs create mode 100644 assets/js/src/lib/components/tabs/old_views/tab.js create mode 100644 assets/js/src/lib/components/tabs/old_views/tabs.js create mode 100644 assets/js/src/lib/components/tabs/views/tabs.hbs create mode 100644 assets/js/vendor/jquery.scrollIntoView.build.js create mode 100644 tests/unit/js/spec/entities/gateways/collection.spec.js create mode 100644 tests/unit/js/spec/entities/gateways/model.spec.js diff --git a/.jshintrc b/.jshintrc index 642261a2..30bd08b5 100644 --- a/.jshintrc +++ b/.jshintrc @@ -3,9 +3,7 @@ "require" : true, "module" : true, "exports" : true, - "console" : true, - "global" : true, - "__VERSION__": true + "global" : true }, "predef": ["-Promise"], diff --git a/assets/css/src/scss/partials/_checkout.scss b/assets/css/src/scss/partials/_checkout.scss index 476b94f2..0776ba50 100644 --- a/assets/css/src/scss/partials/_checkout.scss +++ b/assets/css/src/scss/partials/_checkout.scss @@ -53,8 +53,9 @@ checkout { } .drawer { - > div { + > form { padding: $gateway-padding-y $gateway-padding-x; + margin: 0; fieldset { border: 0; padding: 0; @@ -63,11 +64,12 @@ checkout { .form-row { @include clearfix(); label { - float: left; - margin-right: $gateway-padding-x; - line-height: 2.4; + //float: left; + //margin-right: $gateway-padding-x; + //line-height: 2.4; } - input { + // special case for SV Braintree, uses div instead of input + input, .wc-braintree-hosted-field { @extend .form-control } } diff --git a/assets/css/src/scss/partials/_header.scss b/assets/css/src/scss/partials/_header.scss index a5e15332..12fdeb93 100644 --- a/assets/css/src/scss/partials/_header.scss +++ b/assets/css/src/scss/partials/_header.scss @@ -89,6 +89,10 @@ header { } } + i { + line-height: 1.5; + } + } } diff --git a/assets/css/src/scss/partials/_loading.scss b/assets/css/src/scss/partials/_loading.scss index 47bd39b1..26109017 100644 --- a/assets/css/src/scss/partials/_loading.scss +++ b/assets/css/src/scss/partials/_loading.scss @@ -39,7 +39,8 @@ $pulse-name : namespace('pulse'); #page > i.icon-pos { font-size: 40em; - font-size: 40vw; + font-size: 50vh; + line-height: 100vh; color: nth( $primaryPalette, 10 ); width: 100%; text-align: center; diff --git a/assets/css/src/scss/partials/_main.scss b/assets/css/src/scss/partials/_main.scss index d257d7e6..7ddd3f41 100644 --- a/assets/css/src/scss/partials/_main.scss +++ b/assets/css/src/scss/partials/_main.scss @@ -58,12 +58,12 @@ main { .two-column & { top: 68px; - &.left-active { + .left-active { #right { display: none; } } - &.right-active { + .right-active { #left { display: none; } @@ -76,8 +76,8 @@ main { .two-column & { top: 38px; - &.left-active #right, - &.right-active #left { + .left-active #right, + .right-active #left { @include display-flex; } diff --git a/assets/css/src/scss/partials/_panel.scss b/assets/css/src/scss/partials/_panel.scss index 46963c0b..da0226e6 100644 --- a/assets/css/src/scss/partials/_panel.scss +++ b/assets/css/src/scss/partials/_panel.scss @@ -76,6 +76,12 @@ panel { background: $panel-bg; overflow-y: auto; -webkit-overflow-scrolling: touch; + + /** + * prevent body from collapsing completely + * @todo: panel-body can be 1 row, so anything over 20px is too much + */ + min-height: 20px; } /** @@ -94,7 +100,6 @@ panel { > div { background: $panel-footer-bg; @include clearfix(); - line-height: 1; } .info { @@ -102,6 +107,7 @@ panel { color: $panel-footer-color; float: right; font-style: italic; + line-height: 1; } } diff --git a/assets/css/src/scss/partials/_tabs.scss b/assets/css/src/scss/partials/_tabs.scss index 7b4c0745..d544e9cc 100644 --- a/assets/css/src/scss/partials/_tabs.scss +++ b/assets/css/src/scss/partials/_tabs.scss @@ -12,22 +12,27 @@ tabs { * @name Tabs Spec * * @markup - *
      - *
    • Item 1
    • - *
    • Item 2
    • - *
    • Item 3
    • - *
    • - *
    + *
    + *
      + *
    • Item 1
    • + *
    • Item 2
    • + *
    • Item 3
    • + *
    • + *
    + *
    */ .#{current-module()} { background: $tabs-background; - list-style: none; - margin: 0; - padding: 0; - @include clearfix(); - > li { + ul { + list-style: none; + margin: 0; + padding: 0; + @include clearfix(); + } + + li { //display: inline-block; float: left; background: $tab-item-background; @@ -37,17 +42,25 @@ tabs { &.active { background: $tab-active-background; + cursor: default; + } + + // icons + i { + line-height: 1.5; } /** * @name Removable Tabs Spec * * @markup - *
      - *
    • Item 1
    • - *
    • Item 2
    • - *
    • Item 3
    • - *
    + *
    + *
      + *
    • Item 1
    • + *
    • Item 2
    • + *
    • Item 3
    • + *
    + *
    */ > .tab-remove { color: $secondary-text-color; diff --git a/assets/js/src/apps/app/layout-view.js b/assets/js/src/apps/app/layout-view.js index a0fe32bf..bbe9e4dd 100644 --- a/assets/js/src/apps/app/layout-view.js +++ b/assets/js/src/apps/app/layout-view.js @@ -1,5 +1,4 @@ var LayoutView = require('lib/config/layout-view'); -var $ = require('jquery'); var _ = require('lodash'); var Radio = require('backbone.radio'); var globalChannel = Radio.channel('global'); @@ -13,7 +12,7 @@ module.exports = LayoutView.extend({ return '' + '' + '' + - '
    ' + + '
    ' + '
    '; }, @@ -34,7 +33,7 @@ module.exports = LayoutView.extend({ }, onMainShow: function(layout){ - if(layout.columns && layout.columns === 2){ + if(layout.columns && layout.columns.length === 2){ this.$el.addClass('two-column'); this.showTabs(); } else { @@ -62,23 +61,30 @@ module.exports = LayoutView.extend({ }, showTabs: function(){ - var tabs = this.getRegion('main').tabs = Radio.request('tabs', 'view', { - tabs: [ - {id: 'left'}, - {id: 'right'} - ] + var tabs = Radio.request('tabs', 'view', { + collection : [ + { id: 'left', active: true }, + { id: 'right' } + ], + activeId: 'left' }); - this.listenTo(tabs.collection, 'active:tab', this.toggleLayout); + + this.listenTo( tabs, 'childview:click', function(tab){ + this.toggleLayout( tab.model.id ); + }); + + this.getRegion('main').tabs = tabs; this.getRegion('tabs').show(tabs); }, - toggleLayout: function(model){ - $('#main').removeClass('left-active right-active'); - $('#main').addClass(model.id + '-active'); + toggleLayout: function(id){ + this.getRegion('main').currentView.$el + .removeClass('left-active right-active') + .addClass(id + '-active'); }, updateTabLabel: function(options){ - this.getRegion('main').tabs.setLabel(options); + this.getRegion('tabs').currentView.setLabel(options); }, showHelpModal: function() { diff --git a/assets/js/src/apps/pos/cart/layout.js b/assets/js/src/apps/pos/cart/layout.js index 25b146ce..57bbabf8 100644 --- a/assets/js/src/apps/pos/cart/layout.js +++ b/assets/js/src/apps/pos/cart/layout.js @@ -1,12 +1,8 @@ var LayoutView = require('lib/config/layout-view'); module.exports = LayoutView.extend({ - template: 'pos.cart.panel', - initialize: function(options){ - options = options || {}; - this.order = options.order; - }, + template: 'pos.cart.panel', regions: { list : '.cart-list', @@ -19,13 +15,6 @@ module.exports = LayoutView.extend({ attributes: { 'class' : 'panel cart' - }, - - /** - * add/remove cart-empty class - */ - onShow: function(){ - this.$el.toggleClass('cart-empty', !this.order); } }); \ No newline at end of file diff --git a/assets/js/src/apps/pos/cart/route.js b/assets/js/src/apps/pos/cart/route.js index 64497283..1a57cc15 100644 --- a/assets/js/src/apps/pos/cart/route.js +++ b/assets/js/src/apps/pos/cart/route.js @@ -6,9 +6,7 @@ var NotesView = require('./views/notes'); var CustomerView = require('./views/customer'); var Buttons = require('lib/components/buttons/view'); //var debug = require('debug')('cart'); -var _ = require('lodash'); var App = require('lib/config/application'); -var Utils = require('lib/utilities/utils'); var polyglot = require('lib/utilities/polyglot'); var CartRoute = Route.extend({ @@ -16,77 +14,103 @@ var CartRoute = Route.extend({ initialize: function( options ) { options = options || {}; this.container = options.container; - this.collection = options.collection; - this.setTabLabel({ - tab : 'right', - label : polyglot.t('titles.cart') + this.filtered = options.filtered; + this.collection = options.filtered.superset(); + this.setTabLabel( polyglot.t('titles.cart') ); + + this.listenTo( this.filtered, { + destroy: function(){ + this.render(); + } }); }, + /** + * + */ + addToCart: function( products ){ + if( this.activeOrder ){ + + // if new, wait for save to complete, then render + if( this.activeOrder.id === 'new' ) { + this.listenToOnce(this.activeOrder, { + 'sync': function ( model ) { + this.render( model.id ); + } + }); + } + + this.activeOrder.cart.add( products ); + } + }, + + /** + * Fetch orders from idb if new + */ fetch: function() { if (this.collection.isNew()) { - return this.collection.fetch({ silent: true }); + return this.collection.fetch(); } }, - onFetch: function(id){ - this.order = this.collection.setActiveOrder(id); - - if(this.order){ - this.setTabLabel({ - tab : 'right', - label : this.tabLabel(this.order) - }); + /** + * + */ + render: function(id) { + this.setActiveOrder( id ); + this.updateTabLabel(); + this.layout = new LayoutView(); + this.listenTo( this.layout, 'show', this.onShow ); + this.container.show( this.layout ); + }, - this.listenTo( this.order, 'change:total', function(model){ - this.setTabLabel({ - tab : 'right', - label : this.tabLabel(model) - }); - }); + /** + * + */ + setActiveOrder: function(id){ + if( ! this.collection.get('new') ){ + this.collection.add({ local_id: 'new' }); } - }, - render: function() { - this.layout = new LayoutView({ - order: this.order - }); + this.activeOrder = this.collection.get(id); - this.listenTo( this.layout, 'show', this.onShow ); + if( ! this.activeOrder ){ + this.activeOrder = this.filtered.first(); + } - this.container.show( this.layout ); + if( ! this.activeOrder.isEditable() ){ + this.navigate('receipt/' + this.activeOrder.id, { trigger: true }); + } }, - onRender: function(){ - if( this.order && !this.order.isEditable() ){ - this.navigate('receipt/' + this.order.id, { trigger: true }); - } + /** + * + */ + updateTabLabel: function(){ + this.setTabLabel( this.activeOrder.getLabel() ); + this.listenTo( this.activeOrder, 'change:total', function(model){ + this.setTabLabel( model.getLabel() ); + }); }, + /** + * + */ onShow: function(){ - if(!this.order){ - return this.noActiveOrder(); - } - this.showCart(); - this.showTotals(); - this.showCustomer(); - this.showActions(); - this.showNotes(); + this._showCart(); }, /** - * Empty Cart + * */ - noActiveOrder: function(){ - var view = new ItemsView(); - this.layout.getRegion('list').show(view); - _.invoke([ - this.layout.getRegion('totals'), - this.layout.getRegion('customer'), - this.layout.getRegion('actions'), - this.layout.getRegion('note') - ], 'empty'); + _showCart: function(){ + if( this.activeOrder.cart.length > 0 ){ + this.showTotals(); + this.showCustomer(); + this.showActions(); + this.showNotes(); + } }, /** @@ -94,7 +118,7 @@ var CartRoute = Route.extend({ */ showCart: function() { var view = new ItemsView({ - collection: this.order.cart + collection: this.activeOrder.cart }); this.layout.getRegion('list').show(view); }, @@ -104,15 +128,17 @@ var CartRoute = Route.extend({ */ showTotals: function() { var view = new TotalsView({ - model: this.order + model: this.activeOrder }); - this.on('discount:clicked', view.showDiscountRow); this.layout.getRegion('totals').show(view); }, + /** + * + */ showCustomer: function(){ var view = new CustomerView({ - model: this.order + model: this.activeOrder }); this.layout.getRegion('customer').show( view ); }, @@ -135,7 +161,7 @@ var CartRoute = Route.extend({ this.listenTo(view, { 'action:void': function(){ - var order = this.order; + var order = this.activeOrder; view.triggerMethod('disableButtons'); this.layout.getRegion('list').currentView.fadeCart().done(function(){ order.destroy(); @@ -154,7 +180,7 @@ var CartRoute = Route.extend({ this.order.cart.add( {}, { type: 'shipping' }); }, 'action:checkout': function(){ - this.navigate('checkout/' + this.order.id, { trigger: true }); + this.navigate('checkout/' + this.activeOrder.id, { trigger: true }); } }); @@ -166,18 +192,10 @@ var CartRoute = Route.extend({ */ showNotes: function() { var view = new NotesView({ - model: this.order + model: this.activeOrder }); this.on( 'note:clicked', view.showNoteField ); this.layout.getRegion('note').show( view ); - }, - - tabLabel: function(order){ - var prefix = polyglot.t('titles.cart'), - total = order.get('total'), - formatTotal = Utils.formatMoney(total); - - return prefix + ': ' + formatTotal; } }); diff --git a/assets/js/src/apps/pos/cart/views/items.js b/assets/js/src/apps/pos/cart/views/items.js index 8e1198c4..c518f8fe 100644 --- a/assets/js/src/apps/pos/cart/views/items.js +++ b/assets/js/src/apps/pos/cart/views/items.js @@ -19,6 +19,9 @@ var View = CollectionView.extend({ emptyView: Empty, fadeCart: function(){ var fadeAll = this.children.map( function( child ){ + if( child instanceof Empty ){ + return; + } return child.fadeOut(); } ); return $.when.apply( $, fadeAll ); diff --git a/assets/js/src/apps/pos/cart/views/line/layout.js b/assets/js/src/apps/pos/cart/views/line/layout.js index 8a4db7d0..09ad5e60 100644 --- a/assets/js/src/apps/pos/cart/views/line/layout.js +++ b/assets/js/src/apps/pos/cart/views/line/layout.js @@ -79,16 +79,16 @@ module.exports = LayoutView.extend({ pulse: function() { var el = this.$el, type = this.model.get( 'type' ); - // scroll to row + // @todo: do a better scrollTo for multiple pulses + el.scrollIntoView(); + el.addClass('pulse-in') - .scrollIntoView({ complete: function(){ - el.animate({backgroundColor: 'transparent'}, 500, function() { - $(this).removeClass('pulse-in').removeAttr('style'); - if( type === 'fee' || type === 'shipping' ) { - $('.title strong', this).focus().selectText(); - } - }); - }}); + .animate({backgroundColor: 'transparent'}, 500, function() { + $(this).removeClass('pulse-in').removeAttr('style'); + if( type === 'fee' || type === 'shipping' ) { + $('.title strong', this).focus().selectText(); + } + }); }, diff --git a/assets/js/src/apps/pos/cart/views/line/views/drawer.js b/assets/js/src/apps/pos/cart/views/line/views/drawer.js index 14b59929..008ebfe0 100644 --- a/assets/js/src/apps/pos/cart/views/line/views/drawer.js +++ b/assets/js/src/apps/pos/cart/views/line/views/drawer.js @@ -5,11 +5,16 @@ var Numpad = require('lib/components/numpad/behavior'); var Radio = require('backbone.radio'); var _ = require('lodash'); var $ = require('jquery'); +var App = require('lib/config/application'); -module.exports = FormView.extend({ +var View = FormView.extend({ template: 'pos.cart.item-drawer', + attributes: { + style: 'display:none' // start drawer closed + }, + templateHelpers: function(){ return { product : this.model.type === 'product', @@ -98,7 +103,7 @@ module.exports = FormView.extend({ }, onShow: function() { - this.$el.hide().slideDown(250); + this.$el.slideDown(250); }, remove: function() { @@ -130,4 +135,7 @@ module.exports = FormView.extend({ this.model.set({ meta: meta }); } -}); \ No newline at end of file +}); + +module.exports = View; +App.prototype.set('POSApp.Cart.Views.Line.Drawer', View); \ No newline at end of file diff --git a/assets/js/src/apps/pos/cart/views/line/views/item.js b/assets/js/src/apps/pos/cart/views/line/views/item.js index 904e6b1c..55502fa2 100644 --- a/assets/js/src/apps/pos/cart/views/line/views/item.js +++ b/assets/js/src/apps/pos/cart/views/line/views/item.js @@ -37,14 +37,7 @@ module.exports = FormView.extend({ events: { 'click @ui.title' : 'focusTitle', - 'click @ui.split' : function(e){ - if(e) { e.preventDefault(); } - this.model.trigger( 'split', this.model ); - }, - 'click @ui.combine' : function(e){ - if(e) { e.preventDefault(); } - this.model.trigger( 'combine', this.model ); - } + 'click @ui.split' : 'split' }, triggers: { @@ -120,6 +113,11 @@ module.exports = FormView.extend({ focusTitle: function(){ this.ui.title.find('strong').focus(); + }, + + split: function(e){ + if(e) { e.preventDefault(); } + this.model.trigger( 'split', this.model ); } }); \ No newline at end of file diff --git a/assets/js/src/apps/pos/cart/views/notes.js b/assets/js/src/apps/pos/cart/views/notes.js index fffe4708..6ccde7ef 100644 --- a/assets/js/src/apps/pos/cart/views/notes.js +++ b/assets/js/src/apps/pos/cart/views/notes.js @@ -1,40 +1,55 @@ -var ItemView = require('lib/config/item-view'); +var FormView = require('lib/config/form-view'); +var hbs = require('handlebars'); var _ = require('lodash'); -module.exports = ItemView.extend({ - template: _.template( '<%= note %>' ), +module.exports = FormView.extend({ - modelEvents: { - 'change:note': 'render' - }, - - events: { - 'click' : 'edit', - 'keypress': 'saveOnEnter', - 'blur' : 'save' - }, + template: hbs.compile( '{{note}}' ), - onShow: function() { - this.showOrHide(); + attributes: { + contenteditable: true, + style: 'display:none' }, - showOrHide: function() { - if( this.model.get('note') === '' ) { - this.$el.hide(); + modelEvents: { + 'change:note': function(){ + this.model.save(); } }, - edit: function() { - this.$el.attr('contenteditable','true').focus(); + events: { + 'keypress': 'saveOnEnter' }, - save: function() { - var value = this.$el.text(); + bindings: { + ':el': { + observe: 'note', + onGet: function (val) { + val = _.isString(val) ? val.trim() : ''; + if (val) { + this.$el.show(); + } else { + this.$el.hide(); + } + return val; + }, + onSet: function (val) { + val = _.isString(val) ? val.trim() : ''; + if (val !== this.$el.html()) { + this.$el.text(val); + } + if (val) { + this.$el.show(); + } else { + this.$el.hide(); + } + return val; + } + } + }, - // validate and save - this.model.save({ note: value }); - this.$el.attr('contenteditable','false'); - this.showOrHide(); + showNoteField: function(){ + this.$el.show().focus(); }, saveOnEnter: function(e) { @@ -43,10 +58,6 @@ module.exports = ItemView.extend({ e.preventDefault(); this.$el.blur(); } - }, - - showNoteField: function() { - this.$el.show(); - this.$el.attr('contenteditable','true').focus(); } + }); \ No newline at end of file diff --git a/assets/js/src/apps/pos/checkout/route.js b/assets/js/src/apps/pos/checkout/route.js index bac5b4fe..bf594496 100644 --- a/assets/js/src/apps/pos/checkout/route.js +++ b/assets/js/src/apps/pos/checkout/route.js @@ -13,10 +13,7 @@ var CheckoutRoute = Route.extend({ options = options || {}; this.container = options.container; this.collection = options.collection; - this.setTabLabel({ - tab : 'right', - label : polyglot.t('titles.checkout') - }); + this.setTabLabel( polyglot.t('titles.checkout') ); }, fetch: function(){ @@ -88,7 +85,14 @@ var CheckoutRoute = Route.extend({ }, 'action:process-payment': function(btn){ btn.trigger('state', 'loading'); - this.order.process() + + // alert third party plugins that gateway has init + if( this.order.gateways ){ + var gateway = this.order.gateways.getActiveGateway(); + Radio.trigger('checkout', 'order:payment:' + gateway.id, this.order); + } + + this.order.save({}, { remote: true }) .always(function(){ btn.trigger('state', 'reset'); }); diff --git a/assets/js/src/apps/pos/checkout/views/gateways/drawer.js b/assets/js/src/apps/pos/checkout/views/gateways/drawer.js index 6064d691..8aadd500 100644 --- a/assets/js/src/apps/pos/checkout/views/gateways/drawer.js +++ b/assets/js/src/apps/pos/checkout/views/gateways/drawer.js @@ -5,13 +5,24 @@ var Numpad = require('lib/components/numpad/behavior'); var Utils = require('lib/utilities/utils'); var polyglot = require('lib/utilities/polyglot'); var hbs = require('handlebars'); +var App = require('lib/config/application'); +var Radio = require('backbone.radio'); -module.exports = FormView.extend({ +var View = FormView.extend({ + + tagName: 'form', + + attributes: { + style: 'display:none' // start drawer closed + }, initialize: function() { - this.template = hbs.compile( this.model.get('payment_fields') ); + this.template = hbs.compile( this.model.getPaymentFields() ); this.order_total = this.model.collection.order.get('total'); this.updateStatusMessage(); + + // alert third party plugins that gateway has init + Radio.trigger( 'checkout', 'gateway:init:' + this.model.id, this ); }, templateHelpers: function(){ @@ -100,7 +111,9 @@ module.exports = FormView.extend({ }, onShow: function() { - this.$el.hide().slideDown(250); + // allow third party to setup before show + $.when( this.deferShow.call( this ) ) + .then( this.slideDown.bind(this) ); if(window.Modernizr.touch){ this.$('#pos-cash-tendered').attr('readonly', true); @@ -108,10 +121,20 @@ module.exports = FormView.extend({ } }, + slideDown: function(){ + this.$el.slideDown(250); + }, + remove: function() { - this.$el.slideUp( 250, function() { - FormView.prototype.remove.call(this); - }.bind(this)); + // allow third party to setup before remove + this.slideUp(); + + $.when( this.deferRemove.call( this ) ) + .then( FormView.prototype.remove.bind(this) ); + }, + + slideUp: function(){ + return this.$el.slideUp( 250 ); }, calcChange: function(tendered){ @@ -124,6 +147,15 @@ module.exports = FormView.extend({ this.model.collection.order.set({ 'payment_details.message': this.model.get('message') }); - } + }, + + /** + * Allow third party gateways to setup, teardown and submit payment details + */ + deferShow: function(){}, + deferRemove: function(){} + +}); -}); \ No newline at end of file +module.exports = View; +App.prototype.set('POSApp.Checkout.Views.Gateways.Drawer', View); \ No newline at end of file diff --git a/assets/js/src/apps/pos/layout.js b/assets/js/src/apps/pos/layout.js deleted file mode 100644 index 0718697d..00000000 --- a/assets/js/src/apps/pos/layout.js +++ /dev/null @@ -1,16 +0,0 @@ -var LayoutView = require('lib/config/layout-view'); - -module.exports = LayoutView.extend({ - columns: 2, - - template: function(){ - return '' + - '
    ' + - ''; - }, - - regions: { - left: '#left', - right: '#right' - } -}); \ No newline at end of file diff --git a/assets/js/src/apps/pos/products/route.js b/assets/js/src/apps/pos/products/route.js index 0f8084a6..8246a0c1 100644 --- a/assets/js/src/apps/pos/products/route.js +++ b/assets/js/src/apps/pos/products/route.js @@ -16,10 +16,7 @@ module.exports = Route.extend({ name : 'products', perPage : 10 }); - this.setTabLabel({ - tab : 'left', - label : polyglot.t('titles.products') - }); + this.setTabLabel( polyglot.t('titles.products') ); }, fetch: function() { @@ -59,17 +56,22 @@ module.exports = Route.extend({ }, showTabs: function() { + var tabSettings = Radio.request('entities', 'get', { + type: 'option', + name: 'tabs' + }); + var view = Radio.request('tabs', 'view', { - tabs: this.tabsArray() + collection: _.map( tabSettings, function( obj ){ + return obj; + }) }); - this.listenTo(view.collection, 'active:tab', function(model) { - this.filtered.query('tab', model.id); + this.listenTo(view, 'childview:click', function(tab) { + this.filtered.query('tab', tab.model.id); }); - // show tabs component this.layout.getRegion('tabs').show(view); - this.tabs = view; }, showProducts: function() { @@ -79,14 +81,6 @@ module.exports = Route.extend({ this.layout.getRegion('list').show(view); }, - tabsArray: function(){ - var tabs = Radio.request('entities', 'get', { - type: 'option', - name: 'tabs' - }); - return _.map(tabs); - }, - showPagination: function(){ var view = new Pagination({ collection: this.filtered diff --git a/assets/js/src/apps/pos/products/views/item/views/product.js b/assets/js/src/apps/pos/products/views/item/views/product.js index c90cecdb..258ec3dd 100644 --- a/assets/js/src/apps/pos/products/views/item/views/product.js +++ b/assets/js/src/apps/pos/products/views/item/views/product.js @@ -18,7 +18,7 @@ var Item = ItemView.extend({ }, events: { - 'click @ui.add' : 'addToCart', + 'click @ui.add' : 'addToCart', 'click @ui.vpopover' : 'variationsPopover', 'click @ui.vdrawer' : 'variationsDrawer' }, diff --git a/assets/js/src/apps/pos/receipt/route.js b/assets/js/src/apps/pos/receipt/route.js index 9dfe1745..6b0589aa 100644 --- a/assets/js/src/apps/pos/receipt/route.js +++ b/assets/js/src/apps/pos/receipt/route.js @@ -18,10 +18,7 @@ var ReceiptRoute = Route.extend({ this.container = options.container; this.collection = options.collection; this.autoPrint = options.autoPrint; - this.setTabLabel({ - tab : 'right', - label : polyglot.t('titles.receipt') - }); + this.setTabLabel( polyglot.t('titles.receipt') ); this.tax = Radio.request('entities', 'get', { type: 'option', name: 'tax' diff --git a/assets/js/src/apps/pos/router.js b/assets/js/src/apps/pos/router.js index 8997d2d8..82728fba 100644 --- a/assets/js/src/apps/pos/router.js +++ b/assets/js/src/apps/pos/router.js @@ -1,14 +1,17 @@ var App = require('lib/config/application'); var Router = require('lib/config/router'); -var LayoutView = require('./layout'); var Products = require('./products/route'); var CartRoute = require('./cart/route'); var CheckoutRoute = require('./checkout/route'); var ReceiptRoute = require('./receipt/route'); var Radio = require('backbone.radio'); var bb = require('backbone'); +var _ = require('lodash'); var POSRouter = Router.extend({ + + columns: ['left', 'right'], + routes: { '(cart)(/:id)(/)' : 'showCart', 'checkout(/:id)(/)': 'showCheckout', @@ -23,62 +26,89 @@ var POSRouter = Router.extend({ 'show:receipt' : this.showReceipt, 'add:to:cart' : this.addToCart }, this); + + this.initOrders(); }, onBeforeEnter: function() { - this.layout = new LayoutView(); this.container.show(this.layout); - this.initOrders(); }, + /** + * init a filtered collection of open orders + */ initOrders: function(){ - if(this.orders){ return; } + if(this.openOrders){ + return; + } // attach orders - this.orders = Radio.request('entities', 'get', { - type: 'collection', - name: 'orders' + this.openOrders = Radio.request('entities', 'get', { + type: 'filtered', + name: 'orders', + perPage : 10 // max open carts }); - // listen to order collection - this.listenTo(this.orders, { - 'add' : this.addOrder, - 'remove' : this.removeOrder + // show only open orders + this.openOrders.filterBy('openOrders', function(model) { + return model.isEditable(); }); + + // listen to order collection + //this.listenTo(this.openOrders, { + // add : this.addOrder, + // destroy : this.removeOrder + //}); }, onBeforeRoute: function(){ - //this.setActiveTab(); + this.setActiveTab(); this.showProducts(); Radio.request('header', 'update:title', ''); }, + /** + * todo: refactor + * hacky way to set the right tab + */ setActiveTab: function(){ - var tab = bb.history.getFragment() === '' ? 'left' : 'right'; - this.container.tabs.setActive(tab); + if( bb.history.getFragment() ){ + var rightTab, tabViews = _.get(this, ['container', 'tabs', 'children']); + if( tabViews ){ + rightTab = tabViews.findByIndex(1); + } + if( rightTab ){ + rightTab.$el.click(); + } + } }, showProducts: function(){ if( this.layout.getRegion('left').hasView() ){ return; } + var products = new Products({ - container : this.layout.getRegion('left') + container : this.layout.getRegion('left'), + column : this.columns[0] }); + products.enter(); }, showCart: function() { return new CartRoute({ container : this.layout.getRegion('right'), - collection: this.orders + filtered : this.openOrders, + column : this.columns[1] }); }, showCheckout: function() { return new CheckoutRoute({ container : this.layout.getRegion('right'), - collection: this.orders + collection: this.openOrders.superset(), + column : this.columns[1] }); }, @@ -90,26 +120,25 @@ var POSRouter = Router.extend({ return new ReceiptRoute({ container : this.layout.getRegion('right'), - collection: this.orders, - autoPrint : autoPrint + collection: this.openOrders.superset(), + autoPrint : autoPrint, + column : this.columns[1] }); }, /** * Add to cart only when cart route is active */ - addToCart: function(options){ + addToCart: function(products){ if(this._currentRoute instanceof CartRoute){ - this.orders.addToCart(options); + this._currentRoute.addToCart(products); } }, addOrder: function(order){ if(this._currentRoute instanceof CartRoute){ //bb.history.navigate('cart/' + order.id); - if(order.isEditable()){ - this.execute(this.showCart, [order.id]); - } + this.execute(this.showCart, [order.id]); } }, diff --git a/assets/js/src/apps/support/form/route.js b/assets/js/src/apps/support/form/route.js index 92b6c09d..3a07d654 100644 --- a/assets/js/src/apps/support/form/route.js +++ b/assets/js/src/apps/support/form/route.js @@ -10,10 +10,7 @@ var FormRoute = Route.extend({ initialize: function(options){ this.container = options.container; - this.setTabLabel({ - tab : 'left', - label : polyglot.t('titles.support-form') - }); + this.setTabLabel( polyglot.t('titles.support-form') ); }, render: function(){ diff --git a/assets/js/src/apps/support/layout.js b/assets/js/src/apps/support/layout.js deleted file mode 100644 index 5f9a8396..00000000 --- a/assets/js/src/apps/support/layout.js +++ /dev/null @@ -1,18 +0,0 @@ -var LayoutView = require('lib/config/layout-view'); - -module.exports = LayoutView.extend({ - - columns: 2, - - template: function(){ - return '' + - '
    ' + - ''; - }, - - regions: { - left: '#left', - right: '#right' - } - -}); \ No newline at end of file diff --git a/assets/js/src/apps/support/router.js b/assets/js/src/apps/support/router.js index 438ff7b0..ad1689aa 100644 --- a/assets/js/src/apps/support/router.js +++ b/assets/js/src/apps/support/router.js @@ -1,5 +1,4 @@ var Router = require('lib/config/router'); -var LayoutView = require('./layout'); var FormRoute = require('./form/route'); var StatusRoute = require('./status/route'); var Radio = require('backbone.radio'); @@ -8,6 +7,8 @@ var $ = require('jquery'); module.exports = Router.extend({ + columns: ['left', 'right'], + routes: { 'support' : 'showStatus' }, @@ -18,7 +19,6 @@ module.exports = Router.extend({ }, onBeforeEnter: function() { - this.layout = new LayoutView(); this.container.show(this.layout); this.updateTitle(); this.showForm(); @@ -32,7 +32,8 @@ module.exports = Router.extend({ showForm: function(){ var route = new FormRoute({ - container : this.layout.getRegion('left') + container : this.layout.getRegion('left'), + column : this.columns[0] }); route.enter(); }, @@ -40,7 +41,8 @@ module.exports = Router.extend({ showStatus: function(){ return new StatusRoute({ container : this.layout.getRegion('right'), - collection : this.collection + collection : this.collection, + column : this.columns[1] }); } diff --git a/assets/js/src/apps/support/status/route.js b/assets/js/src/apps/support/status/route.js index 829a99ca..1c909563 100644 --- a/assets/js/src/apps/support/status/route.js +++ b/assets/js/src/apps/support/status/route.js @@ -14,10 +14,7 @@ var StatusRoute = Route.extend({ initialize: function(options){ this.container = options.container; this.collection = options.collection; - this.setTabLabel({ - tab : 'right', - label : polyglot.t('titles.system-status') - }); + this.setTabLabel( polyglot.t('titles.system-status') ); }, fetch: function(){ diff --git a/assets/js/src/entities/cart/collection.js b/assets/js/src/entities/cart/collection.js index fb1af49a..a0b6bb48 100644 --- a/assets/js/src/entities/cart/collection.js +++ b/assets/js/src/entities/cart/collection.js @@ -94,11 +94,9 @@ module.exports = bb.Collection.extend({ } models = this.add(models, { at: ( model.collection.indexOf( model ) + 1 ), - split: true, - silent: true + split: true }); model.set({ quantity: qty - duplicate }); - model.collection.trigger('reset'); // re-render the cart this.pulseModels(models); } diff --git a/assets/js/src/entities/gateways/collection.js b/assets/js/src/entities/gateways/collection.js index a14ed696..6e005c99 100644 --- a/assets/js/src/entities/gateways/collection.js +++ b/assets/js/src/entities/gateways/collection.js @@ -1,25 +1,29 @@ var Collection = require('lib/config/collection'); var Model = require('./model'); var Radio = require('backbone.radio'); -var _ = require('lodash'); +/** + * @todo: abstract a tabbed collection + */ module.exports = Collection.extend({ model: Model, - constructor: function(models, options) { - models = Radio.request('entities', 'get', { + /** + * Construct with gateway settings, mix in saved attributes + */ + constructor: function( models, options ) { + var settings = Radio.request('entities', 'get', { type: 'option', name: 'gateways' }) || []; - if( models.length > 0 && _(models).pluck('active').compact().isEmpty() ){ - models[0].active = true; - } - return Collection.prototype.constructor.call(this, models, options); - }, - initialize: function() { - this._isNew = false; + Collection.prototype.constructor.call(this, settings, options); + this.on( 'change:active', this.onChangeActive ); + + if( models ){ + this.set( models, { remove: false } ); + } }, onChangeActive: function(model, active) { @@ -29,5 +33,14 @@ module.exports = Collection.extend({ tab.set({ active: false }); } }); + }, + + getActiveGateway: function(){ + return this.findWhere({ active: true }); + }, + + getPaymentDetails: function(){ + return this.getActiveGateway().toJSON(); } + }); \ No newline at end of file diff --git a/assets/js/src/entities/gateways/model.js b/assets/js/src/entities/gateways/model.js index 19efe4df..6eafd442 100644 --- a/assets/js/src/entities/gateways/model.js +++ b/assets/js/src/entities/gateways/model.js @@ -1,9 +1,33 @@ var Model = require('lib/config/model'); +var _ = require('lodash'); module.exports = Model.extend({ + idAttribute: 'method_id', defaults: { active: false + }, + + constructor: function( attributes, options ){ + attributes = attributes || {}; + this.payment_fields = attributes.payment_fields; + this.params = attributes.params; + + attributes = _.pick( + attributes, + ['active', 'icon', 'method_id', 'method_title'] + ); + + Model.call( this, attributes, options ); + }, + + getPaymentFields: function(){ + return this.payment_fields; + }, + + getParams: function(){ + return this.params; } + }); \ No newline at end of file diff --git a/assets/js/src/entities/orders/collection.js b/assets/js/src/entities/orders/collection.js index 76787e5f..e2f9aaa7 100644 --- a/assets/js/src/entities/orders/collection.js +++ b/assets/js/src/entities/orders/collection.js @@ -1,8 +1,5 @@ var DualCollection = require('lib/config/dual-collection'); var Model = require('./model'); -var _ = require('lodash'); -var $ = require('jquery'); -var bb = require('backbone'); module.exports = DualCollection.extend({ @@ -10,82 +7,6 @@ module.exports = DualCollection.extend({ name: 'orders', - _syncDelayed: false, - - /** - * - */ - setActiveOrder: function(id){ - var order = this.get(id); - - if( !order && id !== 'new' ){ - order = _.first( this.getOpenOrders() ); - } - - this.active = order; - return order; - }, - - /** - * - */ - getOpenOrders: function(){ - return this.filter(function(model){ - return model.isEditable(); - }); - }, - - /** - * - */ - addToCart: function( product ){ - this.getActiveOrder() - .then(function(order){ - order.cart.add( product, { parse: true } ); - }); - }, - - /** - * Promise of an active order - */ - getActiveOrder: function(){ - var self = this; - var deferred = new $.Deferred(); - - if(!this.active){ - this.create().then(function(order){ - self.active = order; - if(bb.history.getHash() === 'cart/new') { - bb.history.navigate('cart/' + order.id); - } - deferred.resolve(order); - }); - } else { - deferred.resolve(this.active); - } - - return deferred.promise(); - }, - - /** - * - */ - create: function(){ - var deferred = new $.Deferred(); - - // Safari has a problem with create, perhaps an autoincrement problem? - // Set local_id as timestamp milliseconds - DualCollection.prototype.create.call(this, { - local_id: Date.now(), - status: 'CREATE_FAILED' - }, { - wait: true, - parse: true, - success: deferred.resolve, - error: deferred.reject - }); - - return deferred.promise(); - } + _syncDelayed: false }); \ No newline at end of file diff --git a/assets/js/src/entities/orders/model.js b/assets/js/src/entities/orders/model.js index 9f954627..1c11cca3 100644 --- a/assets/js/src/entities/orders/model.js +++ b/assets/js/src/entities/orders/model.js @@ -1,13 +1,16 @@ var DualModel = require('lib/config/dual-model'); var Cart = require('../cart/collection'); +var Gateways = require('../gateways/collection'); var _ = require('lodash'); var debug = require('debug')('order'); var Radio = require('backbone.radio'); -var Utils = require('lib/utilities/utils'); var App = require('lib/config/application'); var Taxes = require('../tax/collection'); +var $ = require('jquery'); +var polyglot = require('lib/utilities/polyglot'); +var Utils = require('lib/utilities/utils'); -module.exports = DualModel.extend({ +var OrderModel = DualModel.extend({ name: 'order', fields: [ @@ -16,19 +19,14 @@ module.exports = DualModel.extend({ 'customer.email' ], - defaults: { - note : '' - }, - /** * add tax settings early for use by cart - * - cart set up during parse, parse called by constructor on new Order + * - always be parsin' */ - constructor: function(){ + constructor: function( attributes, options ){ // clone tax settings this.tax = _.clone( this.getSettings( 'tax' ) ); - - DualModel.apply( this, arguments ); + DualModel.call( this, attributes, _.extend( { parse: true }, options ) ); }, /** @@ -53,15 +51,51 @@ module.exports = DualModel.extend({ /** * */ - save: function(attributes, options){ - if( this.cart && this.cart.length > 0 ){ + save: _.debounce( function(){ + + // Safari doesn't like empty keyPath, perhaps an autoincrement problem? + // Set local_id as timestamp milliseconds + if( this.id === 'new' ){ + //this.set({ local_id: Date.now() }); + this.unset('local_id'); + } + + if( this.cart ){ this.updateTotals(); } - debug('save order', this); + var self = this; - return DualModel.prototype.save.call(this, attributes, options); - }, + // if remote save, let gateway do it's thing + + return $.when( this.deferSave.call(this) ) + .then( function(){ + debug('save order', self); + return DualModel.prototype.save.apply(self, arguments); + }); + + }, 10), + + //save: function(){ + // if( this.cart && this.cart.length > 0 ){ + // this.updateTotals(); + // } + // + // var self = this; + // var args = arguments; + // + // return $.when( this.deferSave.call(this) ) + // .then( function(){ + // debug('save order', self); + // return DualModel.prototype.save.apply(self, args); + // }); + // + //}, + + /** + * Allow third party plugins to defer the order save + */ + deferSave: function(){}, /** * Attach cart during parse @@ -70,9 +104,10 @@ module.exports = DualModel.extend({ parse: function(resp) { resp = DualModel.prototype.parse.call(this, resp); - if( resp && this.isEditable( resp.status ) && !this.cart ){ - this.attachCart( resp ); + if( resp && this.isEditable( resp.status ) ){ resp.taxes = this.parseTaxRates( resp.taxes ); + this.attachCart( resp ); + this.attachGateways( resp ); } return resp; @@ -82,10 +117,16 @@ module.exports = DualModel.extend({ * Attach cart during parse, allows updates from server */ attachCart: function( attributes ){ + if( this.cart ){ + return; + } + this.cart = new Cart( null, { order: this } ); // add line_items, shipping_lines and fee_lines - this.cart.set( attributes.line_items, { parse: true, remove: false } ); + this.cart.set( attributes.line_items, + { parse: true, remove: false } + ); this.cart.set( attributes.shipping_lines, { parse: true, remove: false, type: 'shipping' } ); @@ -93,8 +134,6 @@ module.exports = DualModel.extend({ { parse: true, remove: false, type: 'fee' } ); - //delete attributes.line_items; ? - this.cart.on( 'change add remove', function(){ if( this.cart && this.cart.length === 0 ){ this.destroy(); @@ -104,6 +143,21 @@ module.exports = DualModel.extend({ }, this ); }, + /** + * Attach gateways + */ + attachGateways: function( attributes ){ + if( this.gateways ){ + return; + } + + this.gateways = new Gateways( attributes.payment_details, { order: this } ); + + this.gateways.on( 'change', function(){ + this.save(); + }, this ); + }, + /** * */ @@ -111,6 +165,7 @@ module.exports = DualModel.extend({ toJSON: function(options) { var attrs = _.clone(this.attributes); + // process cart collection if( this.isEditable() && this.cart ){ var taxes = [], shipping_taxes = []; @@ -150,6 +205,11 @@ module.exports = DualModel.extend({ ); } + // process gateways + if( this.isEditable() && this.gateways ){ + attrs.payment_details = this.gateways.getPaymentDetails(); + } + return attrs; }, /* jshint +W071 */ @@ -333,6 +393,18 @@ module.exports = DualModel.extend({ var taxes = this.tax_rates[tax_class]; return taxes.toJSON(); } + }, + + /** + * Helper function to construct the tab label + * @todo: give users options on what to display, eg: customer name + */ + getLabel: function(){ + return polyglot.t('titles.cart') + ': ' + + Utils.formatMoney(this.get('total')); } -}); \ No newline at end of file +}); + +module.exports = OrderModel; +App.prototype.set('Entities.Order.Model', OrderModel); \ No newline at end of file diff --git a/assets/js/src/entities/variations/model.js b/assets/js/src/entities/variations/model.js index ef97fc91..6f5dbff4 100644 --- a/assets/js/src/entities/variations/model.js +++ b/assets/js/src/entities/variations/model.js @@ -20,7 +20,7 @@ module.exports = Model.extend({ this.defaults = { type : 'variation', title : options.title - } + }; Model.apply( this, arguments ); } diff --git a/assets/js/src/lib/components/logger/service.js b/assets/js/src/lib/components/logger/service.js index de86fc30..70c7e49a 100644 --- a/assets/js/src/lib/components/logger/service.js +++ b/assets/js/src/lib/components/logger/service.js @@ -21,8 +21,10 @@ module.exports = Service.extend({ /** * eg: jqXHR.fail */ + /* jshint -W117 */ error: function(options){ console.log(options); } + /* jshint +W117 */ }); \ No newline at end of file diff --git a/assets/js/src/lib/components/modal/service.js b/assets/js/src/lib/components/modal/service.js index 906fb83f..4344411a 100644 --- a/assets/js/src/lib/components/modal/service.js +++ b/assets/js/src/lib/components/modal/service.js @@ -73,6 +73,7 @@ module.exports = Service.extend({ var afterOpen = options.afterOpen; options.afterOpen = function( $el, opts ){ opts.el = $el; + // @todo pass smaller options object to Layout opts.layout = new Layout( opts ); if(afterOpen){ diff --git a/assets/js/src/lib/components/tabs/entities/collection.js b/assets/js/src/lib/components/tabs/entities/collection.js index 8ffa514d..265eadb7 100644 --- a/assets/js/src/lib/components/tabs/entities/collection.js +++ b/assets/js/src/lib/components/tabs/entities/collection.js @@ -1,31 +1,34 @@ var Collection = require('lib/config/collection'); var Model = require('./model'); +var App = require('lib/config/application'); -var TabsCollection = Collection.extend({ - model: Model, +var Tabs = Collection.extend({ - initialize: function(){ - this.on({ - 'change:active' : this.onChangeActive, - 'remove' : this.ensureActiveTab - }); - }, + model: Model - onChangeActive: function(model, active){ - if(!active){ return; } - this.each(function(m) { - m.set({active: m === model}); - }); - this.trigger('active:tab', model); - }, - - ensureActiveTab: function() { - var activeTabs = this.where({ 'active': true }); - if( this.length > 0 && activeTabs.length === 0 ) { - this.at(0).set({ active: true }); - } - } + //initialize: function(){ + // this.on({ + // 'change:active' : this.onChangeActive, + // 'remove' : this.ensureActiveTab + // }); + //}, + // + //onChangeActive: function(model, active){ + // if(!active){ return; } + // this.each(function(m) { + // m.set({active: m === model}); + // }); + // this.trigger('active:tab', model); + //}, + // + //ensureActiveTab: function() { + // var activeTabs = this.where({ 'active': true }); + // if( this.length > 0 && activeTabs.length === 0 ) { + // this.at(0).set({ active: true }); + // } + //} }); -module.exports = TabsCollection; \ No newline at end of file +module.exports = Tabs; +App.prototype.set('Components.Tabs.Collection', Tabs); \ No newline at end of file diff --git a/assets/js/src/lib/components/tabs/entities/model.js b/assets/js/src/lib/components/tabs/entities/model.js index 3107e2f5..9aefe631 100644 --- a/assets/js/src/lib/components/tabs/entities/model.js +++ b/assets/js/src/lib/components/tabs/entities/model.js @@ -3,9 +3,8 @@ var Model = require('lib/config/model'); var TabModel = Model.extend({ defaults: { id: '', - label: 'Tab', - active: false, - fixed: true + label: 'Tab', // space + active: false } }); diff --git a/assets/js/src/lib/components/tabs/old_views/tab.hbs b/assets/js/src/lib/components/tabs/old_views/tab.hbs new file mode 100644 index 00000000..c5ed397d --- /dev/null +++ b/assets/js/src/lib/components/tabs/old_views/tab.hbs @@ -0,0 +1,6 @@ +{{#unless fixed}} + + + +{{/unless}} +{{{ label }}} \ No newline at end of file diff --git a/assets/js/src/lib/components/tabs/old_views/tab.js b/assets/js/src/lib/components/tabs/old_views/tab.js new file mode 100644 index 00000000..b794181a --- /dev/null +++ b/assets/js/src/lib/components/tabs/old_views/tab.js @@ -0,0 +1,46 @@ +var hbs = require('handlebars'); +var ItemView = require('lib/config/item-view'); +var Tmpl = require('./tab.hbs'); + +var View = ItemView.extend({ + + tagName: 'li', + + template: hbs.compile(Tmpl), + + activeClassName: 'active', + + initialize: function( options ){ + this.mergeOptions( options, ['activeClassName'] ); + + // add active className + if( this.model.get('active') ){ + this.$el.addClass( this.activeClassName ); + } + }, + + modelEvents: { + 'change:active': 'toggleActive', + 'change:label' : 'render' // why does this not auto render?! + }, + + toggleActive: function(){ + this.$el.toggleClass(this.activeClassName, this.model.get('active')); + }, + + triggers: { + 'click': 'tab:clicked', + 'click *[data-action="remove"]': 'remove:tab' + }, + + onTabClicked: function () { + this.model.set({active: true}); + }, + + onRemoveTab: function(){ + this.model.collection.remove(this.model); + } + +}); + +module.exports = View; \ No newline at end of file diff --git a/assets/js/src/lib/components/tabs/old_views/tabs.js b/assets/js/src/lib/components/tabs/old_views/tabs.js new file mode 100644 index 00000000..f91c4184 --- /dev/null +++ b/assets/js/src/lib/components/tabs/old_views/tabs.js @@ -0,0 +1,32 @@ +var CollectionView = require('lib/config/collection-view'); +var Tab = require('./tab'); + +var View = CollectionView.extend({ + + tagName: 'ul', + + childView: Tab, + + attributes: { + 'class' : 'tabs', + 'role' : 'tablist' + }, + + setActive: function(id){ + var model = this.collection.get(id); + model.set({active: true}); + }, + + setLabel: function(options){ + options = options || {}; + var model = this.collection.get(options.tab); + model.set({label: options.label}); + }, + + onShow: function(){ + // last call for active tabs + this.collection.ensureActiveTab(); + } +}); + +module.exports = View; \ No newline at end of file diff --git a/assets/js/src/lib/components/tabs/service.js b/assets/js/src/lib/components/tabs/service.js index 9561a2c1..1a547a32 100644 --- a/assets/js/src/lib/components/tabs/service.js +++ b/assets/js/src/lib/components/tabs/service.js @@ -1,6 +1,5 @@ var Service = require('lib/config/service'); var TabsView = require('./views/tabs'); -var TabsCollection = require('./entities/collection'); var _ = require('lodash'); module.exports = Service.extend({ @@ -17,7 +16,6 @@ module.exports = Service.extend({ */ tabsView: function(options){ options = options || {}; - _.defaults( options, { collection: new TabsCollection(options.tabs) } ); if( options.adminTabs ){ _.defaults( options, { diff --git a/assets/js/src/lib/components/tabs/views/tab.hbs b/assets/js/src/lib/components/tabs/views/tab.hbs index c5ed397d..875ac0f4 100644 --- a/assets/js/src/lib/components/tabs/views/tab.hbs +++ b/assets/js/src/lib/components/tabs/views/tab.hbs @@ -1,6 +1 @@ -{{#unless fixed}} - - - -{{/unless}} {{{ label }}} \ No newline at end of file diff --git a/assets/js/src/lib/components/tabs/views/tab.js b/assets/js/src/lib/components/tabs/views/tab.js index b794181a..c396a5f6 100644 --- a/assets/js/src/lib/components/tabs/views/tab.js +++ b/assets/js/src/lib/components/tabs/views/tab.js @@ -1,46 +1,49 @@ -var hbs = require('handlebars'); var ItemView = require('lib/config/item-view'); var Tmpl = require('./tab.hbs'); +var hbs = require('handlebars'); +var _ = require('lodash'); -var View = ItemView.extend({ +module.exports = ItemView.extend({ tagName: 'li', template: hbs.compile(Tmpl), - activeClassName: 'active', - - initialize: function( options ){ - this.mergeOptions( options, ['activeClassName'] ); - - // add active className - if( this.model.get('active') ){ - this.$el.addClass( this.activeClassName ); + events: { + click: function(e){ + e.preventDefault(); + if( ! this.$el.hasClass(this.activeClassName) ){ + this.trigger('click', this); + } } }, - modelEvents: { - 'change:active': 'toggleActive', - 'change:label' : 'render' // why does this not auto render?! + label: { + observe: 'label' }, - toggleActive: function(){ - this.$el.toggleClass(this.activeClassName, this.model.get('active')); - }, + initialize: function(options){ + this.mergeOptions(options, ['label', 'activeClassName']); - triggers: { - 'click': 'tab:clicked', - 'click *[data-action="remove"]': 'remove:tab' + // re-render on change to observed attribute + if( this.label.observe ){ + this.listenTo( this.model, 'change:' + this.label.observe, this.render ); + } }, - onTabClicked: function () { - this.model.set({active: true}); - }, + /** + * Poor man's stickit + */ + templateHelpers: function(){ + var label = this.model.get( this.label.observe ); - onRemoveTab: function(){ - this.model.collection.remove(this.model); - } + if( this.label && this.label.onGet ){ + label = this.label.onGet( label, this.model ); + } -}); + return { + label: label || _.result( this, 'label' ) + }; + } -module.exports = View; \ No newline at end of file +}); \ No newline at end of file diff --git a/assets/js/src/lib/components/tabs/views/tabs.hbs b/assets/js/src/lib/components/tabs/views/tabs.hbs new file mode 100644 index 00000000..5487c3f1 --- /dev/null +++ b/assets/js/src/lib/components/tabs/views/tabs.hbs @@ -0,0 +1 @@ +
      \ No newline at end of file diff --git a/assets/js/src/lib/components/tabs/views/tabs.js b/assets/js/src/lib/components/tabs/views/tabs.js index f91c4184..ab1612c2 100644 --- a/assets/js/src/lib/components/tabs/views/tabs.js +++ b/assets/js/src/lib/components/tabs/views/tabs.js @@ -1,32 +1,89 @@ -var CollectionView = require('lib/config/collection-view'); +var CompositeView = require('lib/config/composite-view'); +var hbs = require('handlebars'); +var Tmpl = require('./tabs.hbs'); var Tab = require('./tab'); +var App = require('lib/config/application'); +var bb = require('backbone'); +var TabsCollection = require('../entities/collection'); -var View = CollectionView.extend({ +var Tabs = CompositeView.extend({ - tagName: 'ul', + template: hbs.compile(Tmpl), + + childViewContainer: '[role="tablist"]', childView: Tab, attributes: { - 'class' : 'tabs', - 'role' : 'tablist' + 'class' : 'tabs' }, - setActive: function(id){ - var model = this.collection.get(id); - model.set({active: true}); - }, + activeClassName: 'active', - setLabel: function(options){ + initialize: function(options){ options = options || {}; - var model = this.collection.get(options.tab); - model.set({label: options.label}); + + // store options + this.mergeOptions(options, [ + 'activeId', // current active model id + 'activeClassName', // className for active tab + 'label' // label string or function + ]); + + // + if( options.collection instanceof bb.Collection ){ + return; + } + + this.collection = new TabsCollection( options.collection ); + }, + + /** + * Pick options to pass to childview + */ + childViewOptions: function( model ){ + // pass default options + var options = { + activeClassName : 'active' + }; + + // pass label options + if( this.label ){ + options.label = this.label; + } + + // init with active class + if( this.activeId === model.id ){ + options.className = this.activeClassName; + } + + return options; + }, + + /** + * On child click, toggle active class + */ + childEvents: { + click: function( activeView ){ + this.children.each(function( childView ){ + if( childView === activeView ){ + return childView.$el.addClass( this.activeClassName ); + } + childView.$el.removeClass( this.activeClassName ); + }, this); + } }, - onShow: function(){ - // last call for active tabs - this.collection.ensureActiveTab(); + /** + * Helper function to set label + */ + setLabel: function(options){ + options = options || {}; + var model = this.collection.get( options.tab ); + model.set({ label: options.label }); } + }); -module.exports = View; \ No newline at end of file +module.exports = Tabs; +App.prototype.set('Components.Tabs.View', Tabs); \ No newline at end of file diff --git a/assets/js/src/lib/config/application.js b/assets/js/src/lib/config/application.js index 1b60de8e..d1c7d33e 100644 --- a/assets/js/src/lib/config/application.js +++ b/assets/js/src/lib/config/application.js @@ -14,11 +14,13 @@ var initDebug = function( debug ){ debugFunction.enable('*'); } Radio.DEBUG = debug; + /* jshint -W117 */ console.info( 'Debugging is ' + ( debug ? 'on' : 'off' ) + ', visit http://woopos.com.au/docs/debugging' ); + /* jshint +W117 */ }; var Application = Mn.Application.extend({ diff --git a/assets/js/src/lib/config/composite-view.js b/assets/js/src/lib/config/composite-view.js index 3fc9ff68..f80ef06c 100644 --- a/assets/js/src/lib/config/composite-view.js +++ b/assets/js/src/lib/config/composite-view.js @@ -1,4 +1,4 @@ var Mn = require('backbone.marionette'); var app = require('./application'); -module.exports = app.prototype.CollectionView = Mn.CompositeView; \ No newline at end of file +module.exports = app.prototype.CompositeView = Mn.CompositeView; \ No newline at end of file diff --git a/assets/js/src/lib/config/dual-model.js b/assets/js/src/lib/config/dual-model.js index 527161a0..aedcfcd7 100644 --- a/assets/js/src/lib/config/dual-model.js +++ b/assets/js/src/lib/config/dual-model.js @@ -4,21 +4,28 @@ var _ = require('lodash'); var debug = require('debug')('dualModel'); module.exports = app.prototype.DualModel = DeepModel.extend({ + idAttribute: 'local_id', + remoteIdAttribute: 'id', + fields: ['title'], - validate: function(attrs){ - var obj = {}; - if(attrs[this.idAttribute]) { - obj[this.idAttribute] = parseInt(attrs[this.idAttribute], 10); - } - if(attrs[this.remoteIdAttribute]){ - obj[this.remoteIdAttribute] = parseInt(attrs[this.remoteIdAttribute], 10); - } - this.set(obj, {silent: true}); + defaults: { + status: 'CREATE_FAILED' }, +//validate: function(attrs){ +// var obj = {}; +// if(attrs[this.idAttribute]) { +// obj[this.idAttribute] = parseInt(attrs[this.idAttribute], 10); +// } +// if(attrs[this.remoteIdAttribute]){ +// obj[this.remoteIdAttribute] = parseInt(attrs[this.remoteIdAttribute], 10); +// } +// this.set(obj, {silent: true}); +//}, + url: function(){ var remoteId = this.get(this.remoteIdAttribute), urlRoot = _.result(this.collection, 'url'); diff --git a/assets/js/src/lib/config/obscura.js b/assets/js/src/lib/config/obscura.js index 8e6497e4..c238c9a0 100644 --- a/assets/js/src/lib/config/obscura.js +++ b/assets/js/src/lib/config/obscura.js @@ -7,6 +7,7 @@ var PaginatedCollection = require('./obscura/paginated'); var proxyCollection = require('backbone-collection-proxy'); var proxyEvents = require('./obscura/proxy-events'); var query = require('./obscura/query'); +var app = require('./application'); // extend FilteredCollection with query methods _.extend(FilteredCollection.prototype, query); @@ -134,7 +135,7 @@ _.each(unsupportedMethods, function (method) { methods[method] = function () { throw new Error( 'Backbone.Obscura: Unsupported method: ' + - method + 'called on read-only proxy' + method + ' called on read-only proxy' ); }; }); @@ -144,5 +145,5 @@ Obscura = Backbone.Collection.extend(Obscura.prototype); Obscura.FilteredCollection = FilteredCollection; Obscura.SortedCollection = SortedCollection; Obscura.PaginatedCollection = PaginatedCollection; -module.exports = Obscura; +module.exports = app.prototype.Obscura = Obscura; /* jshint +W071, +W003, +W021 */ \ No newline at end of file diff --git a/assets/js/src/lib/config/route.js b/assets/js/src/lib/config/route.js index 7c6afc63..c8416604 100644 --- a/assets/js/src/lib/config/route.js +++ b/assets/js/src/lib/config/route.js @@ -8,7 +8,8 @@ var Radio = require('backbone.radio'); var globalChannel = Radio.channel('global'); module.exports = app.prototype.Route = Mn.Object.extend({ - constructor: function() { + constructor: function( options ) { + this.mergeOptions( options, ['column'] ); this.initialize.apply(this, arguments); }, @@ -57,8 +58,16 @@ module.exports = app.prototype.Route = Mn.Object.extend({ fetch : function() {}, render : function() {}, - setTabLabel: function(options){ - globalChannel.trigger('tab:label', options); + /** + * Helper function to set the columned layout tabs + */ + setTabLabel: function(label){ + if( this.column ){ + globalChannel.trigger('tab:label', { + tab : this.column, + label : label + }); + } } }); \ No newline at end of file diff --git a/assets/js/src/lib/config/router.js b/assets/js/src/lib/config/router.js index 70de9bac..49652025 100644 --- a/assets/js/src/lib/config/router.js +++ b/assets/js/src/lib/config/router.js @@ -4,6 +4,32 @@ var $ = require('jquery'); var _ = require('lodash'); var Route = require('./route'); var app = require('./application'); +var LayoutView = require('./layout-view'); + +/** + * Columned layout + * - fix at two col for now + */ +var ColumnLayout = LayoutView.extend({ + + className: 'left-active', + + initialize: function(options){ + this.mergeOptions(options, ['columns']); + }, + + template: function(){ + return '' + + '
      ' + + ''; + }, + + regions: { + left: '#left', + right: '#right' + } + +}); module.exports = app.prototype.Router = Mn.AppRouter.extend({ @@ -28,10 +54,19 @@ module.exports = app.prototype.Router = Mn.AppRouter.extend({ } }, + /** + * + */ execute: function(callback, args) { var self = this; if (!this.active) { + + // attach layout if columns + if( this.columns ){ + this.layout = new ColumnLayout({ columns: this.columns }); + } + this.triggerMethod.apply(this, ['before:enter'].concat(args)); } diff --git a/assets/js/src/lib/utilities/handlebars-helpers.js b/assets/js/src/lib/utilities/handlebars-helpers.js index bdcb3e45..944154e2 100644 --- a/assets/js/src/lib/utilities/handlebars-helpers.js +++ b/assets/js/src/lib/utilities/handlebars-helpers.js @@ -159,6 +159,7 @@ hbs.registerHelper('formatDay', function(day, options){ return moment().isoWeekday(idx).format(f); }); +/* jshint -W117 */ hbs.registerHelper('debug', function(optionalValue) { console.log('Current Context'); console.log('===================='); @@ -170,6 +171,7 @@ hbs.registerHelper('debug', function(optionalValue) { console.log(optionalValue); } }); +/* jshint +W117 */ hbs.registerHelper('formatCustomerName', function(customer) { var name = _(customer).pick(['first_name','last_name']) diff --git a/assets/js/vendor/jquery.scrollIntoView.build.js b/assets/js/vendor/jquery.scrollIntoView.build.js new file mode 100644 index 00000000..499e04e4 --- /dev/null +++ b/assets/js/vendor/jquery.scrollIntoView.build.js @@ -0,0 +1,134 @@ +/*! + * $.fn.scrollIntoView - similar to the default browser scrollIntoView + * The default browser behavior always places the element at the top or bottom of its container. + * This override is smart enough to not scroll if the element is already visible. + * + * Copyright 2011 Arwid Bancewicz + * Licensed under the MIT license + * http://www.opensource.org/licenses/mit-license.php + * + * @date 8 Jan 2013 + * @author Arwid Bancewicz http://arwid.ca + * @version 0.3 + */ +(function($) { + $.fn.scrollIntoView = function(duration, easing, complete) { + // The arguments are optional. + // The first argment can be false for no animation or a duration. + // The first argment could also be a map of options. + // Refer to http://api.jquery.com/animate/. + var opts = $.extend({}, + $.fn.scrollIntoView.defaults); + + // Get options + if ($.type(duration) == "object") { + $.extend(opts, duration); + } else if ($.type(duration) == "number") { + $.extend(opts, { duration: duration, easing: easing, complete: complete }); + } else if (duration == false) { + opts.smooth = false; + } + + // get enclosing offsets + var elY = Infinity, elH = 0; + if (this.size()==1)((elY=this.get(0).offsetTop)==null||(elH=elY+this.get(0).offsetHeight)); + else this.each(function(i,el){(el.offsetTopelH?elH=el.offsetTop+el.offsetHeight:null)}); + elH -= elY; + + // start from the common ancester + var pEl = this.commonAncestor().get(0); + + var wH = $(window).height(); + + // go up parents until we find one that scrolls + while (pEl) { + var pY = pEl.scrollTop, pH = pEl.clientHeight; + if (pH > wH) pH = wH; + + // case: if body's elements are all absolutely/fixed positioned, use window height + if (pH == 0 && pEl.tagName == "BODY") pH = wH; + + if (pEl.scrollHeight > pEl.clientHeight) { + if (elY <= pY) scrollTo(pEl, elY); // scroll up + else if ((elY + elH) > (pY + pH)) scrollTo(pEl, elY + elH - pH); // scroll down + else scrollTo(pEl, undefined) // no scroll + return; + } + + // try next parent + pEl = pEl.parentNode; + } + + function scrollTo(el, scrollTo) { + if (scrollTo === undefined) { + if ($.isFunction(opts.complete)) opts.complete.call(el); + } else if (opts.smooth) { + $(el).stop().animate({ scrollTop: scrollTo }, opts); + } else { + el.scrollTop = scrollTo; + if ($.isFunction(opts.complete)) opts.complete.call(el); + } + } + return this; + }; + + $.fn.scrollIntoView.defaults = { + smooth: true, + duration: null, + easing: $.easing && $.easing.easeOutExpo ? 'easeOutExpo': null, + // Note: easeOutExpo requires jquery.effects.core.js + // otherwise jQuery will default to use 'swing' + complete: $.noop(), + step: null, + specialEasing: {} // cannot be null in jQuery 1.8.3 + }; + + /* + Returns whether the elements are in view + */ + $.fn.isOutOfView = function(completely) { + // completely? whether element is out of view completely + var outOfView = true; + this.each(function() { + var pEl = this.parentNode, pY = pEl.scrollTop, pH = pEl.clientHeight, elY = this.offsetTop, elH = this.offsetHeight; + if (completely ? (elY) > (pY + pH) : (elY + elH) > (pY + pH)) {} + else if (completely ? (elY + elH) < pY: elY < pY) {} + else outOfView = false; + }); + return outOfView; + }; + + /* + Returns the common ancestor of the elements. + It was taken from http://stackoverflow.com/questions/3217147/jquery-first-parent-containing-all-children + It has received minimal testing. + */ + $.fn.commonAncestor = function() { + var parents = []; + var minlen = Infinity; + + $(this).each(function() { + var curparents = $(this).parents(); + parents.push(curparents); + minlen = Math.min(minlen, curparents.length); + }); + + for (var i = 0; i < parents.length; i++) { + parents[i] = parents[i].slice(parents[i].length - minlen); + } + + // Iterate until equality is found + for (var i = 0; i < parents[0].length; i++) { + var equal = true; + for (var j in parents) { + if (parents[j][i] != parents[0][i]) { + equal = false; + break; + } + } + if (equal) return $(parents[0][i]); + } + return $([]); + } + +})(jQuery); \ No newline at end of file diff --git a/assets/js/vendor/jquery.scrollIntoView.min.js b/assets/js/vendor/jquery.scrollIntoView.min.js index 43aec2ac..eb34ff5d 100644 --- a/assets/js/vendor/jquery.scrollIntoView.min.js +++ b/assets/js/vendor/jquery.scrollIntoView.min.js @@ -11,4 +11,4 @@ * @author Arwid Bancewicz http://arwid.ca * @version 0.3 */ -(function(a){a.fn.scrollIntoView=function(f,j,c){var b=a.extend({},a.fn.scrollIntoView.defaults);if(a.type(f)=="object"){a.extend(b,f)}else{if(a.type(f)=="number"){a.extend(b,{duration:f,easing:j,complete:c})}else{if(f==false){b.smooth=false}}}var h=Infinity,e=0;if(this.size()==1){((h=this.get(0).offsetTop)==null||(e=h+this.get(0).offsetHeight))}else{this.each(function(m,n){(n.offsetTope?e=n.offsetTop+n.offsetHeight:null)})}e-=h;var k=this.commonAncestor().get(0);var g=a(window).height();while(k){var d=k.scrollTop,l=k.clientHeight;if(l>g){l=g}if(l==0&&k.tagName=="BODY"){l=g}if((k.scrollTop!=((k.scrollTop+=1)==null||k.scrollTop)&&(k.scrollTop-=1)!=null)||(k.scrollTop!=((k.scrollTop-=1)==null||k.scrollTop)&&(k.scrollTop+=1)!=null)){if(h<=d){i(k,h)}else{if((h+e)>(d+l)){i(k,h+e-l)}else{i(k,undefined)}}return}k=k.parentNode}function i(n,m){if(m===undefined){if(a.isFunction(b.complete)){b.complete.call(n)}}else{if(b.smooth){a(n).stop().animate({scrollTop:m},b)}else{n.scrollTop=m;if(a.isFunction(b.complete)){b.complete.call(n)}}}}return this};a.fn.scrollIntoView.defaults={smooth:true,duration:null,easing:a.easing&&a.easing.easeOutExpo?"easeOutExpo":null,complete:a.noop(),step:null,specialEasing:{}};a.fn.isOutOfView=function(b){var c=true;this.each(function(){var h=this.parentNode,d=h.scrollTop,g=h.clientHeight,f=this.offsetTop,e=this.offsetHeight;if(b?(f)>(d+g):(f+e)>(d+g)){}else{if(b?(f+e)e?e=n.offsetTop+n.offsetHeight:null)})}e-=h;var k=this.commonAncestor().get(0);var g=a(window).height();while(k){var d=k.scrollTop,l=k.clientHeight;if(l>g){l=g}if(l==0&&k.tagName=="BODY"){l=g}if(k.scrollHeight>k.clientHeight){if(h<=d){i(k,h)}else{if((h+e)>(d+l)){i(k,h+e-l)}else{i(k,undefined)}}return}k=k.parentNode}function i(n,m){if(m===undefined){if(a.isFunction(b.complete)){b.complete.call(n)}}else{if(b.smooth){a(n).stop().animate({scrollTop:m},b)}else{n.scrollTop=m;if(a.isFunction(b.complete)){b.complete.call(n)}}}}return this};a.fn.scrollIntoView.defaults={smooth:true,duration:null,easing:a.easing&&a.easing.easeOutExpo?"easeOutExpo":null,complete:a.noop(),step:null,specialEasing:{}};a.fn.isOutOfView=function(b){var c=true;this.each(function(){var h=this.parentNode,d=h.scrollTop,g=h.clientHeight,f=this.offsetTop,e=this.offsetHeight;if(b?(f)>(d+g):(f+e)>(d+g)){}else{if(b?(f+e)payment_gateways; $order = $this->get('gateway_order'); @@ -53,31 +56,37 @@ public function load_gateways() { } $settings = new WC_POS_Admin_Settings_Gateways($gateway->id); $settings->merge_settings($gateway); // @todo remove this, use get_title() etc - apply_filters( 'woocommerce_pos_load_gateway', $gateway ); } ksort( $ordered_gateways, SORT_NUMERIC ); - return $ordered_gateways; + return apply_filters( 'woocommerce_pos_load_gateways', $ordered_gateways ); } - public function load_enabled_gateways(){ + /** + * @return mixed|void + */ + public function load_enabled_gateways() { $gateways = $this->load_gateways(); $enabled = $this->get_enabled_gateway_ids(); - $default = $this->get('default_gateway'); - $_gateways = array(); + $default = $this->get( 'default_gateway' ); + $enabled_gateways = array(); - if($gateways): foreach($gateways as $gateway): + if ( $gateways ): foreach ( $gateways as $gateway ): $id = $gateway->id; - if(in_array($id, $enabled) && isset($gateway->pos) && $gateway->pos){ + if ( in_array( $id, $enabled ) && isset( $gateway->pos ) && $gateway->pos ) { $gateway->default = $id == $default; -// $gateway->enabled = 'yes'; // gets stomped later by init_settings() - $_gateways[$id] = $gateway; + // $gateway->enabled = 'yes'; // gets stomped later by init_settings() + $enabled_gateways[ $id ] = $gateway; } endforeach; endif; - return $_gateways; + return apply_filters( 'woocommerce_pos_load_enabled_gateways', $enabled_gateways ); } + /** + * Convenience function, returns POS enabled gateway ids + * @return array + */ public function get_enabled_gateway_ids(){ return array_keys( (array) $this->get('enabled'), true); } diff --git a/includes/api/class-wc-pos-gateways.php b/includes/api/class-wc-pos-gateways.php index 71e7fbd7..b7dda844 100644 --- a/includes/api/class-wc-pos-gateways.php +++ b/includes/api/class-wc-pos-gateways.php @@ -55,7 +55,8 @@ public function get_gateways( $wc_pos_admin = null ){ 'method_title' => esc_html( $gateway->get_title() ), 'icon' => $this->sanitize_icon( $gateway ), 'active' => $gateway->default, - 'payment_fields' => $this->sanitize_payment_fields( $gateway ) + 'payment_fields' => $this->sanitize_payment_fields( $gateway ), + 'params' => apply_filters('woocommerce_pos_gateway_' . strtolower( $gateway->id ) . '_params', array(), $gateway, $this ) ); endforeach; endif; @@ -117,14 +118,17 @@ private function removeDomNodes( $html, $xpathString ) { $dom = new DOMDocument(); + // suppress warnings for malformed HTML + libxml_use_internal_errors(true); + // Libxml constants not available on all servers (Libxml < 2.7.8) // $html->loadHTML($content, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); $dom->loadHtml( '' . '
      ' . $html . '
      ' ); - # remove removeChild( $dom->doctype ); - # remove + // remove $dom->removeChild( $dom->firstChild ); - # + // $dom->replaceChild( $dom->firstChild->firstChild->firstChild, $dom->firstChild ); // remove the required node diff --git a/includes/class-wc-pos-gateways.php b/includes/class-wc-pos-gateways.php index ea048c73..28786daa 100644 --- a/includes/class-wc-pos-gateways.php +++ b/includes/class-wc-pos-gateways.php @@ -18,7 +18,7 @@ class WC_POS_Gateways { */ public function __construct() { add_action( 'woocommerce_payment_gateways', array( $this, 'payment_gateways' ) ); - add_action( 'woocommerce_pos_load_gateway', array( $this, 'load_gateway' ) ); + add_action( 'woocommerce_pos_load_gateways', array( $this, 'load_gateways' ) ); static::$instance = $this; } @@ -56,12 +56,14 @@ public function payment_gateways( array $gateways ) { /** * Enable POS gateways - * @param $gateway + * @param $gateways * @return bool */ - public function load_gateway( WC_Payment_Gateway $gateway ) { - $gateway->pos = in_array( $gateway->id, array( 'pos_cash', 'pos_card', 'paypal' ) ); - return $gateway; + public function load_gateways( $gateways ) { + foreach( $gateways as $gateway ){ + $gateway->pos = in_array( $gateway->id, array( 'pos_cash', 'pos_card', 'paypal' ) ); + } + return $gateways; } } \ No newline at end of file diff --git a/includes/class-wc-pos-template.php b/includes/class-wc-pos-template.php index 4c14b024..560ed70d 100644 --- a/includes/class-wc-pos-template.php +++ b/includes/class-wc-pos-template.php @@ -171,7 +171,7 @@ public function footer() { $build = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? 'build' : 'min'; $js = self::get_external_js_libraries(); - $js[ 'scrollIntoView' ] = WC_POS_PLUGIN_URL . 'assets/js/vendor/jquery.scrollIntoView.min.js?ver=' . WC_POS_VERSION; + $js[ 'scrollIntoView' ] = WC_POS_PLUGIN_URL . 'assets/js/vendor/jquery.scrollIntoView.' . $build . '.js?ver=' . WC_POS_VERSION; $js[ 'app' ] = WC_POS_PLUGIN_URL . 'assets/js/app.' . $build . '.js?ver=' . WC_POS_VERSION; $scripts = apply_filters( 'woocommerce_pos_enqueue_footer_js', $js ); diff --git a/tests/unit/js/setup/helpers.js b/tests/unit/js/setup/helpers.js index 78636c37..de363712 100644 --- a/tests/unit/js/setup/helpers.js +++ b/tests/unit/js/setup/helpers.js @@ -13,7 +13,7 @@ before(function() { beforeEach(function() { this.sinon = sinon.sandbox.create(); //this.server = sinon.fakeServer.create(); - //this.clock = this.sandbox.useFakeTimers(); + //this.clock = sinon.useFakeTimers(); global.stub = this.sinon.stub.bind(this.sinon); global.spy = this.sinon.spy.bind(this.sinon); }); @@ -21,4 +21,5 @@ beforeEach(function() { afterEach(function() { this.sinon.restore(); //this.server.restore(); + //this.clock.restore(); }); \ No newline at end of file diff --git a/tests/unit/js/spec/entities/gateways/collection.spec.js b/tests/unit/js/spec/entities/gateways/collection.spec.js new file mode 100644 index 00000000..5fb08a6d --- /dev/null +++ b/tests/unit/js/spec/entities/gateways/collection.spec.js @@ -0,0 +1,28 @@ +describe('entities/gateways/collection.js', function () { + + var Gateways = require('entities/gateways/collection'); + + var gateway_settings = [{ + active: false, + method_id: 'inactive_gateway' + }, { + active: true, + method_id: 'active_gateway' + }]; + + it('should be in a valid state', function() { + var gateways = new Gateways(); + expect(gateways).to.be.ok; + }); + + it('should return the active gateway', function() { + var gateways = new Gateways(gateway_settings); + expect( gateways.getActiveGateway()).to.be.instanceof( Backbone.Model ); + }); + + it('should return the active gateway payment details', function() { + var gateways = new Gateways(gateway_settings); + expect( gateways.getPaymentDetails() ).eqls( gateway_settings[1] ); + }); + +}); \ No newline at end of file diff --git a/tests/unit/js/spec/entities/gateways/model.spec.js b/tests/unit/js/spec/entities/gateways/model.spec.js new file mode 100644 index 00000000..d0e3c1eb --- /dev/null +++ b/tests/unit/js/spec/entities/gateways/model.spec.js @@ -0,0 +1,40 @@ +describe('entities/gateways/model.js', function () { + + var Gateway = require('entities/gateways/model'); + + var settings = { + active: false, + icon: 'http://example.com/credit-card.gif', + method_id: 'example_gateway', + method_title: 'Example Gateway', + payment_fields: '
      Hello World
      ', + params: { + client_token: '123456789', + flag: true + } + }; + + it('should be in a valid state', function() { + var gateway = new Gateway(); + expect(gateway).to.be.ok; + }); + + it('should parse the gateway settings', function() { + var gateway = new Gateway(settings); + + // only store necessary data, ie: no need to save templates + var whitelist = ['active', 'icon', 'method_id', 'method_title']; + expect( gateway.toJSON() ).eqls( _.pick( settings, whitelist ) ); + }); + + it('should have a function to return the payment_fields', function() { + var gateway = new Gateway(settings); + expect( gateway.getPaymentFields() ).equals( settings.payment_fields ); + }); + + it('should have a function to return the gateway params', function() { + var gateway = new Gateway(settings); + expect( gateway.getParams() ).equals( settings.params ); + }); + +}); \ No newline at end of file diff --git a/tests/unit/js/spec/entities/orders/model.spec.js b/tests/unit/js/spec/entities/orders/model.spec.js index 3648e61d..1d5c9047 100644 --- a/tests/unit/js/spec/entities/orders/model.spec.js +++ b/tests/unit/js/spec/entities/orders/model.spec.js @@ -69,74 +69,97 @@ describe('entities/orders/model.js', function () { // //}); - it('should save on change to the cart', function(){ + it('should save on change to the cart', function( done ){ var order = _.defaults( { status: 'UPDATE_FAILED', local_id: 1 }, dummy_order.order ); var model = new OrderModel( order, { parse: true } ); - model.sync = sinon.stub(); + + // Note: stubbing w/ my own function because sinon is weird with debounce + model.sync = function( method ){ + expect( method ).equals( 'update' ); + done(); + }; var cart_item = model.cart.at(0); cart_item.set({ quantity: 3 }); - expect( model.sync ).to.have.been.calledWith( 'update' ); - }); - it('should save on add item to cart', function(){ + it('should save on add item to cart', function( done ){ var order = _.defaults( { status: 'UPDATE_FAILED', local_id: 1 }, dummy_order.order ); var model = new OrderModel( order, { parse: true } ); - model.sync = sinon.stub(); + // Note: stubbing w/ my own function because sinon is weird with debounce + model.sync = function( method ){ + expect( method ).equals( 'update' ); + done(); + }; model.cart.add({ id: 123 }); - expect( model.sync ).to.have.been.calledWith( 'update' ); + }); + + it('should debounce save on add items to cart', function( done ){ + + // need to add editable order to attach gateways + var order = _.defaults( { status: 'UPDATE_FAILED', local_id: 1 }, dummy_order.order ); + var model = new OrderModel( order, { parse: true } ); + + var callCount = 0; + + // Note: stubbing w/ my own function because sinon is weird with debounce + model.sync = function( method ){ + callCount++; + expect( method ).equals( 'update' ); + }; + + // trigger change + for( var i = 0; i < 10; i++ ){ + model.cart.add({ id: i }); + } + + expect( callCount ).equals( 0 ); + + setTimeout(function() { + expect( callCount ).equals( 1 ); + done(); + }, 150); }); - it('should save on remove item to cart', function(){ + it('should save on remove item to cart', function( done ){ var order = _.defaults( { status: 'UPDATE_FAILED', local_id: 1 }, dummy_order.order ); var model = new OrderModel( order, { parse: true } ); - model.sync = sinon.stub(); + // Note: stubbing w/ my own function because sinon is weird with debounce + model.sync = function( method ){ + expect( method ).equals( 'update' ); + done(); + }; var cart_item = model.cart.at(0); model.cart.remove(cart_item); - expect( model.sync ).to.have.been.calledWith( 'update' ); - }); - it('should destroy order if cart is empty', function(){ + it('should destroy order if cart is empty', function( done ){ var order = _.defaults( { status: 'UPDATE_FAILED', local_id: 1 }, dummy_order.order ); var model = new OrderModel( order, { parse: true } ); - model.sync = sinon.stub(); - - // http://stackoverflow.com/questions/10858935/cleanest-way-to-destroy-every-model-in-a-collection-in-backbone + // Note: stubbing w/ my own function because sinon is weird with debounce + model.sync = function( method ){ + expect( method ).equals( 'delete' ); + done(); + }; - //_.each( model.cart.toArray(), function(cart_item) { - // if( model.cart.length > 1 ){ - // model.cart.remove(cart_item, { silent: true }); - // } else { - // model.cart.remove(cart_item); - // } - //}); -// OR while (cart_item = model.cart.first()) { - if( model.cart.length > 1 ){ - model.cart.remove(cart_item, { silent: true }); - } else { - model.cart.remove(cart_item); - } + model.cart.remove(cart_item); } - expect( model.sync ).to.have.been.calledWith( 'delete' ); - }); it('should have a convience method to combine itemized taxes', function(){ @@ -244,7 +267,7 @@ describe('entities/orders/model.js', function () { }); - it('should be match the dummy order', function(){ + it('should be match the dummy order', function( done ){ var Model = OrderModel.extend({ getSettings: function(name){ @@ -264,14 +287,23 @@ describe('entities/orders/model.js', function () { } }; } - }, - sync: stub() + } }); var model = new Model(null, { parse: true }); expect( model.cart ).to.be.instanceOf( Backbone.Collection ); + // Note: stubbing w/ my own function because sinon is weird with debounce + model.sync = function(){ + expect( Utils.round( model.get('total'), 2 ) ).equals( parseFloat( dummy_order.order.total ) ); + // POS subtotal includes shipping and fees + var subtotal = parseFloat( dummy_order.order.subtotal ) + parseFloat( dummy_order.order.total_shipping ); + expect( Utils.round( model.get('subtotal'), 2 ) ).equals( subtotal ); + expect( Utils.round( model.get('total_tax'), 2 ) ).equals( parseFloat( dummy_order.order.total_tax ) ); + done(); + }; + // 2 x 546 model.cart.add( { id: 546, @@ -314,14 +346,6 @@ describe('entities/orders/model.js', function () { method_title: 'Flat Rate' }, { parse: true, type: 'shipping' } ); - expect( Utils.round( model.get('total'), 2 ) ).equals( parseFloat( dummy_order.order.total ) ); - - // POS subtotal includes shipping and fees - var subtotal = parseFloat( dummy_order.order.subtotal ) + parseFloat( dummy_order.order.total_shipping ); - expect( Utils.round( model.get('subtotal'), 2 ) ).equals( subtotal ); - - expect( Utils.round( model.get('total_tax'), 2 ) ).equals( parseFloat( dummy_order.order.total_tax ) ); - }); it('should parse tax rates', function(){ @@ -370,4 +394,64 @@ describe('entities/orders/model.js', function () { }]); }); + it('should attach and init gateways (if Editable)', function(){ + + // need to add editable order to attach gateways + var order = _.defaults( { status: 'UPDATE_FAILED', local_id: 1 }, dummy_order.order ); + var model = new OrderModel( order, { parse: true } ); + + model.sync = sinon.stub(); + + expect( model.gateways ).to.be.instanceOf( Backbone.Collection ); + // expect one gateway parsed from payment_details + expect( model.gateways.length ).equals(1); + expect( model.gateways.at( 0).id ).equals( dummy_order.order.payment_details.method_id ); + + }); + + it('should save on change to the gateways', function( done ){ + + // need to add editable order to attach gateways + var order = _.defaults( { status: 'UPDATE_FAILED', local_id: 1 }, dummy_order.order ); + var model = new OrderModel( order, { parse: true } ); + + // Note: stubbing w/ my own function because sinon is weird with debounce + model.sync = function( method ){ + expect( method ).equals( 'update' ); + done(); + }; + + // trigger change + model.gateways.trigger('change'); + + }); + + it('should debounce save on changes to the gateways', function( done ){ + + // need to add editable order to attach gateways + var order = _.defaults( { status: 'UPDATE_FAILED', local_id: 1 }, dummy_order.order ); + var model = new OrderModel( order, { parse: true } ); + + var callCount = 0; + + // Note: stubbing w/ my own function because sinon is weird with debounce + model.sync = function( method ){ + callCount++; + expect( method ).equals( 'update' ); + }; + + // trigger change + for( var i = 0; i < 10; i++ ){ + model.gateways.trigger('change'); + } + + expect( callCount ).equals( 0 ); + + setTimeout(function() { + expect( callCount ).equals( 1 ); + done(); + }, 150); + + }); + }); \ No newline at end of file From 67f64d031b042e14f6c9b13e572298464a4c4d1f Mon Sep 17 00:00:00 2001 From: Paul Kilmurray Date: Mon, 8 Feb 2016 23:19:32 +0800 Subject: [PATCH 026/169] refactor modals --- assets/css/src/scss/partials/_cart.scss | 4 +- assets/css/src/scss/partials/_header.scss | 2 +- .../css/src/scss/partials/_input-group.scss | 2 +- assets/css/src/scss/partials/_list.scss | 7 +- assets/css/src/scss/partials/_modal.scss | 13 +- assets/css/src/scss/partials/_tabs.scss | 2 +- assets/js/src/apps/pos/cart/route.js | 9 +- assets/js/src/apps/pos/checkout/route.js | 44 +++-- assets/js/src/apps/pos/router.js | 14 -- assets/js/src/entities/gateways/collection.js | 5 +- assets/js/src/entities/orders/model.js | 31 +-- assets/js/src/entities/service.js | 39 +++- assets/js/src/lib/components/modal/body.js | 30 --- .../src/lib/components/modal/dialog/view.js | 18 ++ .../modal/{body.hbs => error/error.hbs} | 6 +- .../src/lib/components/modal/error/parser.js | 104 ++++++++++ .../js/src/lib/components/modal/error/view.js | 46 +++++ .../components/modal/{ => header}/header.hbs | 4 +- .../modal/{header.js => header/view.js} | 14 +- assets/js/src/lib/components/modal/layout.js | 138 ++++++------- assets/js/src/lib/components/modal/modal.hbs | 4 - assets/js/src/lib/components/modal/service.js | 184 +++++------------- .../src/lib/components/tabs/old_views/tab.hbs | 6 - .../src/lib/components/tabs/old_views/tab.js | 46 ----- .../src/lib/components/tabs/old_views/tabs.js | 32 --- assets/js/src/lib/config/ajax.js | 9 +- assets/js/src/lib/config/idb-collection.js | 33 +--- assets/js/src/lib/config/module.js | 24 --- assets/js/src/lib/config/sync.js | 6 +- includes/api/class-wc-pos-i18n.php | 2 + includes/api/class-wc-pos-payload.php | 2 +- .../lib/components/modal/error/parser.spec.js | 83 ++++++++ 32 files changed, 494 insertions(+), 469 deletions(-) delete mode 100644 assets/js/src/lib/components/modal/body.js create mode 100644 assets/js/src/lib/components/modal/dialog/view.js rename assets/js/src/lib/components/modal/{body.hbs => error/error.hbs} (59%) create mode 100644 assets/js/src/lib/components/modal/error/parser.js create mode 100644 assets/js/src/lib/components/modal/error/view.js rename assets/js/src/lib/components/modal/{ => header}/header.hbs (87%) rename assets/js/src/lib/components/modal/{header.js => header/view.js} (69%) delete mode 100644 assets/js/src/lib/components/modal/modal.hbs delete mode 100644 assets/js/src/lib/components/tabs/old_views/tab.hbs delete mode 100644 assets/js/src/lib/components/tabs/old_views/tab.js delete mode 100644 assets/js/src/lib/components/tabs/old_views/tabs.js delete mode 100644 assets/js/src/lib/config/module.js create mode 100644 tests/unit/js/spec/lib/components/modal/error/parser.spec.js diff --git a/assets/css/src/scss/partials/_cart.scss b/assets/css/src/scss/partials/_cart.scss index 9e9d58aa..513d2d44 100644 --- a/assets/css/src/scss/partials/_cart.scss +++ b/assets/css/src/scss/partials/_cart.scss @@ -267,8 +267,10 @@ cart { .#{current-module()}-customer { background: $cart-customer-background; - border-top: 2px solid $input-border-color; + > div { + border-top: 2px solid $input-border-color; + } } .#{current-module()}-notes { diff --git a/assets/css/src/scss/partials/_header.scss b/assets/css/src/scss/partials/_header.scss index 12fdeb93..1f1253a7 100644 --- a/assets/css/src/scss/partials/_header.scss +++ b/assets/css/src/scss/partials/_header.scss @@ -90,7 +90,7 @@ header { } i { - line-height: 1.5; + line-height: inherit; } } diff --git a/assets/css/src/scss/partials/_input-group.scss b/assets/css/src/scss/partials/_input-group.scss index 6d790bff..baad6a95 100644 --- a/assets/css/src/scss/partials/_input-group.scss +++ b/assets/css/src/scss/partials/_input-group.scss @@ -123,7 +123,7 @@ input-group { // give icons the same line-height at the input i { - line-height: 1.5; + line-height: inherit; } // Negative margin for spacing, position for bringing hovered/focused/actived diff --git a/assets/css/src/scss/partials/_list.scss b/assets/css/src/scss/partials/_list.scss index 65dfde58..83c28cad 100644 --- a/assets/css/src/scss/partials/_list.scss +++ b/assets/css/src/scss/partials/_list.scss @@ -299,7 +299,10 @@ list { text-align: right; background: $list-totals-background; - border-top: 2px solid $input-border-color; + + > ul { + border-top: 2px solid $input-border-color; + } li { @@ -327,9 +330,9 @@ list { .#{current-module()}-actions { background: $list-actions-background; - border-top: 2px solid $input-border-color; > div { + border-top: 2px solid $input-border-color; padding: $list-actions-padding-y $list-actions-padding-x; text-align: right; diff --git a/assets/css/src/scss/partials/_modal.scss b/assets/css/src/scss/partials/_modal.scss index fbf51e61..716624d3 100644 --- a/assets/css/src/scss/partials/_modal.scss +++ b/assets/css/src/scss/partials/_modal.scss @@ -37,7 +37,7 @@ $raw-border-color : $modal-header-border-color; /** * @name Modal * - * @state .error - Error Modal + * @state .modal-error - Error Modal * * @markup