diff --git a/app/assets/javascripts/admin/orders/controllers/order_controller.js.coffee b/app/assets/javascripts/admin/orders/controllers/order_controller.js.coffee new file mode 100644 index 0000000000..822e81eb49 --- /dev/null +++ b/app/assets/javascripts/admin/orders/controllers/order_controller.js.coffee @@ -0,0 +1,26 @@ +angular.module("admin.orders").controller "orderCtrl", ($scope, shops, orderCycles, $compile, $attrs, Orders) -> + $scope.$compile = $compile + $scope.shops = shops + $scope.orderCycles = orderCycles + + $scope.distributor_id = parseInt($attrs.ofnDistributorId) + $scope.order_cycle_id = parseInt($attrs.ofnOrderCycleId) + + $scope.validOrderCycle = (oc) -> + $scope.orderCycleHasDistributor oc, parseInt($scope.distributor_id) + + $scope.distributorHasOrderCycles = (distributor) -> + (oc for oc in $scope.orderCycles when @orderCycleHasDistributor(oc, distributor.id)).length > 0 + + $scope.orderCycleHasDistributor = (oc, distributor_id) -> + distributor_ids = (d.id for d in oc.distributors) + distributor_ids.indexOf(distributor_id) != -1 + + $scope.distributionChosen = -> + $scope.distributor_id && $scope.order_cycle_id + + for oc in $scope.orderCycles + oc.name_and_status = "#{oc.name} (#{oc.status})" + + for shop in $scope.shops + shop.disabled = !$scope.distributorHasOrderCycles(shop) diff --git a/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee b/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee index 060b23bed9..96bfd63ab2 100644 --- a/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee +++ b/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee @@ -1,26 +1,41 @@ -angular.module("admin.orders").controller "ordersCtrl", ($scope, $compile, $attrs, shops, orderCycles) -> - $scope.$compile = $compile - $scope.shops = shops - $scope.orderCycles = orderCycles +angular.module("admin.orders").controller "ordersCtrl", ($scope, RequestMonitor, Orders, SortOptions) -> + $scope.RequestMonitor = RequestMonitor + $scope.pagination = Orders.pagination + $scope.orders = Orders.all + $scope.sortOptions = SortOptions - $scope.distributor_id = parseInt($attrs.ofnDistributorId) - $scope.order_cycle_id = parseInt($attrs.ofnOrderCycleId) + $scope.initialise = -> + $scope.q = { + completed_at_not_null: true + } + $scope.fetchResults() - $scope.validOrderCycle = (oc) -> - $scope.orderCycleHasDistributor oc, parseInt($scope.distributor_id) + $scope.fetchResults = (page=1) -> + Orders.index({ + 'q[created_at_lt]': $scope['q']['created_at_lt'], + 'q[created_at_gt]': $scope['q']['created_at_gt'], + 'q[state_eq]': $scope['q']['state_eq'], + 'q[number_cont]': $scope['q']['number_cont'], + 'q[email_cont]': $scope['q']['email_cont'], + 'q[bill_address_firstname_start]': $scope['q']['bill_address_firstname_start'], + 'q[bill_address_lastname_start]': $scope['q']['bill_address_lastname_start'], + 'q[completed_at_not_null]': $scope['q']['completed_at_not_null'], + 'q[inventory_units_shipment_id_null]': $scope['q']['inventory_units_shipment_id_null'], + 'q[distributor_id_in]': $scope['q']['distributor_id_in'], + 'q[order_cycle_id_in]': $scope['q']['order_cycle_id_in'], + 'q[order_cycle_id_in]': $scope['q']['order_cycle_id_in'], + 'q[s]': $scope.sorting || 'id desc', + per_page: $scope.per_page || 15, + page: page + }) - $scope.distributorHasOrderCycles = (distributor) -> - (oc for oc in orderCycles when @orderCycleHasDistributor(oc, distributor.id)).length > 0 + $scope.$watch 'sortOptions', (sort) -> + if sort && sort.predicate != "" + $scope.sorting = sort.predicate + ' desc' if sort.reverse + $scope.sorting = sort.predicate + ' asc' if !sort.reverse + $scope.fetchResults() + , true - $scope.orderCycleHasDistributor = (oc, distributor_id) -> - distributor_ids = (d.id for d in oc.distributors) - distributor_ids.indexOf(distributor_id) != -1 - - $scope.distributionChosen = -> - $scope.distributor_id && $scope.order_cycle_id - - for oc in $scope.orderCycles - oc.name_and_status = "#{oc.name} (#{oc.status})" - - for shop in $scope.shops - shop.disabled = !$scope.distributorHasOrderCycles(shop) + $scope.changePage = (newPage) -> + $scope.page = newPage + $scope.fetchResults(newPage) diff --git a/app/assets/javascripts/admin/resources/resources/order_resource.js.coffee b/app/assets/javascripts/admin/resources/resources/order_resource.js.coffee index 317b384485..d5679c629e 100644 --- a/app/assets/javascripts/admin/resources/resources/order_resource.js.coffee +++ b/app/assets/javascripts/admin/resources/resources/order_resource.js.coffee @@ -2,7 +2,6 @@ angular.module("admin.resources").factory 'OrderResource', ($resource) -> $resource('/admin/orders/:id/:action.json', {}, { 'index': method: 'GET' - isArray: true 'update': method: 'PUT' }) diff --git a/app/assets/javascripts/admin/resources/services/orders.js.coffee b/app/assets/javascripts/admin/resources/services/orders.js.coffee index da3f409149..5eef10eecb 100644 --- a/app/assets/javascripts/admin/resources/services/orders.js.coffee +++ b/app/assets/javascripts/admin/resources/services/orders.js.coffee @@ -1,18 +1,30 @@ -angular.module("admin.resources").factory 'Orders', ($q, OrderResource) -> +angular.module("admin.resources").factory 'Orders', ($q, OrderResource, RequestMonitor) -> new class Orders + all: [] byID: {} pristineByID: {} + pagination: {} index: (params={}, callback=null) -> - OrderResource.index params, (data) => + request = OrderResource.index params, (data) => @load(data) (callback || angular.noop)(data) + RequestMonitor.load(request.$promise) + @all - load: (orders) -> - for order in orders + load: (data) -> + angular.extend(@pagination, data.pagination) + @clearData() + for order in data.orders + @all.push order @byID[order.id] = order @pristineByID[order.id] = angular.copy(order) + clearData: -> + @all.length = 0 + @byID = {} + @pristineByID = {} + save: (order) -> deferred = $q.defer() order.$update({id: order.number}) diff --git a/app/assets/stylesheets/admin/components/pagination.scss b/app/assets/stylesheets/admin/components/pagination.scss new file mode 100644 index 0000000000..05cbf34ca7 --- /dev/null +++ b/app/assets/stylesheets/admin/components/pagination.scss @@ -0,0 +1,20 @@ +@import "admin/variables"; + +.pagination { + text-align: center; + margin: 2em 0 1em; + + button { + margin: 0 0.35em; + + &.active { + background-color: darken($spree-blue, 15%); + cursor: default; + } + + &.disabled { + background-color: $disabled_button; + cursor: default; + } + } +} diff --git a/app/assets/stylesheets/admin/orders.css.scss b/app/assets/stylesheets/admin/orders.css.scss index 33c5a4a3e8..0955d9fd46 100644 --- a/app/assets/stylesheets/admin/orders.css.scss +++ b/app/assets/stylesheets/admin/orders.css.scss @@ -91,3 +91,15 @@ th.actions { table.index td.actions { text-align: left; } + +.orders-loading { + margin-top: 1em; + + img { + width: 85px; + } + + span { + font-size: 1.2em; + } +} diff --git a/app/assets/stylesheets/admin/variables.css.scss b/app/assets/stylesheets/admin/variables.css.scss index 416daec5a9..72f806af7d 100644 --- a/app/assets/stylesheets/admin/variables.css.scss +++ b/app/assets/stylesheets/admin/variables.css.scss @@ -9,8 +9,10 @@ $warning-orange: #da7f52; $bright-orange: #ffa92e; $medium-grey: #919191; $pale-blue: #cee1f4; +$light-grey: #ccc; $admin-table-border: $pale-blue; $modal-close-button-color: #de6060; $modal-close-button-hover-color: #bf4545; +$disabled-button: $light-grey; diff --git a/app/controllers/spree/admin/orders_controller_decorator.rb b/app/controllers/spree/admin/orders_controller_decorator.rb index 49f017930d..6df8943966 100644 --- a/app/controllers/spree/admin/orders_controller_decorator.rb +++ b/app/controllers/spree/admin/orders_controller_decorator.rb @@ -61,7 +61,14 @@ Spree::Admin::OrdersController.class_eval do respond_with(@orders) do |format| format.html format.json do - render_as_json @orders + render json: { + orders: ActiveModel::ArraySerializer.new(@orders, each_serializer: Api::Admin::OrderSerializer), + pagination: { + results: @orders.total_count, + pages: @orders.num_pages, + page: params[:page].to_i + } + } end end end @@ -102,16 +109,14 @@ Spree::Admin::OrdersController.class_eval do def orders if json_request? @search = OpenFoodNetwork::Permissions.new(spree_current_user).editable_orders.ransack(params[:q]) - @search.result.reorder('id ASC') else @search = Spree::Order.accessible_by(current_ability, :index).ransack(params[:q]) # Replaced this search to filter orders to only show those distributed by current user (or all for admin user) - @search.result.includes([:user, :shipments, :payments]). - distributed_by_user(spree_current_user). - page(params[:page]). - per(params[:per_page] || Spree::Config[:orders_per_page]) + @search.result.includes([:user, :shipments, :payments]).distributed_by_user(spree_current_user) end + + @search.result.page(params[:page]).per(params[:per_page] || Spree::Config[:orders_per_page]) end def require_distributor_abn diff --git a/app/serializers/api/admin/order_serializer.rb b/app/serializers/api/admin/order_serializer.rb index 6f22ba1e94..e3207b30fd 100644 --- a/app/serializers/api/admin/order_serializer.rb +++ b/app/serializers/api/admin/order_serializer.rb @@ -1,5 +1,8 @@ class Api::Admin::OrderSerializer < ActiveModel::Serializer - attributes :id, :number, :full_name, :email, :phone, :completed_at + attributes :id, :number, :full_name, :email, :phone, :completed_at, :display_total + attributes :show_path, :edit_path, :state, :payment_state, :shipment_state + attributes :payments_path, :shipments_path, :ship_path, :ready_to_ship, :created_at + attributes :distributor_name, :special_instructions, :payment_capture_path has_one :distributor, serializer: Api::Admin::IdSerializer has_one :order_cycle, serializer: Api::Admin::IdSerializer @@ -8,6 +11,48 @@ class Api::Admin::OrderSerializer < ActiveModel::Serializer object.billing_address.nil? ? "" : ( object.billing_address.full_name || "" ) end + def distributor_name + object.distributor.andand.name + end + + def show_path + return '' unless object.id + spree_routes_helper.admin_order_path(object) + end + + def edit_path + return '' unless object.id + spree_routes_helper.edit_admin_order_path(object) + end + + def payments_path + return '' unless object.payment_state + spree_routes_helper.admin_order_payments_path(object) + end + + def shipments_path + return '' unless object.shipment_state + spree_routes_helper.admin_order_shipments_path(object) + end + + def ship_path + spree_routes_helper.fire_admin_order_path(object, e: 'ship') + end + + def payment_capture_path + pending_payment = object.pending_payments.first + return '' unless object.payment_required? && pending_payment + spree_routes_helper.fire_admin_order_payment_path(object, pending_payment.id, e: 'capture') + end + + def ready_to_ship + object.ready_to_ship? + end + + def display_total + object.display_total.to_html + end + def email object.email || "" end @@ -16,7 +61,17 @@ class Api::Admin::OrderSerializer < ActiveModel::Serializer object.billing_address.nil? ? "a" : ( object.billing_address.phone || "" ) end + def created_at + object.created_at.blank? ? "" : I18n.l(object.created_at, format: '%B %d, %Y') + end + def completed_at - object.completed_at.blank? ? "" : object.completed_at.strftime("%F %T") + object.completed_at.blank? ? "" : I18n.l(object.completed_at, format: '%B %d, %Y') + end + + private + + def spree_routes_helper + Spree::Core::Engine.routes_url_helpers end end diff --git a/app/views/admin/shared/_angular_pagination.html.haml b/app/views/admin/shared/_angular_pagination.html.haml new file mode 100644 index 0000000000..53a28af885 --- /dev/null +++ b/app/views/admin/shared/_angular_pagination.html.haml @@ -0,0 +1,17 @@ +.pagination + %button{'ng-click' => 'changePage(1)', 'ng-class' => "{'disabled': pagination.page == 1}", 'ng-disabled' => "pagination.page == 1"} + = "«".html_safe + = t(:first) + %button{'ng-click' => 'changePage((pagination.page)-1)', 'ng-class' => "{'disabled': pagination.page == 1}", 'ng-disabled' => "pagination.page == 1"} + = t(:previous) + %span{'ng-show' => 'pagination.page > 3'} + = "…".html_safe + %button{'ng-repeat' => 'i in [].constructor(pagination.pages) track by $index', 'ng-show' =>'($index+1 > pagination.page-3 || (pagination.page > pagination.pages-2 && $index+1 > pagination.pages-5)) && ($index+1 < pagination.page+3 || (pagination.page < 3 && $index+1 < 6))', 'ng-class' => "{'active': pagination.page == $index+1}", 'ng-click' => 'changePage($index+1)', 'ng-disabled' => "pagination.page == $index+1"} + {{$index+1}} + %span{'ng-show' => 'pagination.page < pagination.pages-2'} + = "…".html_safe + %button{'ng-click' => 'changePage((pagination.page)+1)', 'ng-class' => "{'disabled': pagination.page == pagination.pages}", 'ng-disabled' => "pagination.page == pagination.pages"} + = t(:next) + %button{'ng-click' => 'changePage(pagination.pages)', 'ng-class' => "{'disabled': pagination.page == pagination.pages}", 'ng-disabled' => "pagination.page == pagination.pages"} + = t(:last) + = "»".html_safe diff --git a/app/views/spree/admin/orders/_capture.html.haml b/app/views/spree/admin/orders/_capture.html.haml deleted file mode 100644 index b1267badbb..0000000000 --- a/app/views/spree/admin/orders/_capture.html.haml +++ /dev/null @@ -1,7 +0,0 @@ -- # Get the payment in 'checkout' state if any, and show capture button -- if order.payments.present? - - payment = order.payments.select{|p| p if p.state == 'checkout'}.first - - if !payment.nil? - - payment.actions.grep(/^capture$/).each do |action| - - # copied from backend/app/views/spree/admin/payments/_list.html.erb - = link_to_with_icon "icon-#{action}", t('admin.orders.index.capture'), fire_admin_order_payment_path(order, payment, :e => action), :method => :put, :no_text => true, :data => {:action => action} diff --git a/app/views/spree/admin/orders/_filters.html.haml b/app/views/spree/admin/orders/_filters.html.haml new file mode 100644 index 0000000000..5ebea2a4cd --- /dev/null +++ b/app/views/spree/admin/orders/_filters.html.haml @@ -0,0 +1,51 @@ +%div{"data-hook" => "admin_orders_index_search"} + = search_form_for [:admin, @search], html: { name: "orders_form", "ng-submit" => "fetchResults()"} do |f| + .field-block.alpha.four.columns + .date-range-filter.field + = label_tag nil, t(:date_range) + .date-range-fields + = f.text_field :created_at_gt, class: 'datepicker', datepicker: 'q.created_at_gt', 'ng-model' => 'q.created_at_gt', :value => params[:q][:created_at_gt], :placeholder => t(:start) + %span.range-divider + %i.icon-arrow-right + = f.text_field :created_at_lt, class: 'datepicker', datepicker: 'q.created_at_lt', 'ng-model' => 'q.created_at_lt', :value => params[:q][:created_at_lt], :placeholder => t(:stop) + .field + = label_tag nil, t(:status) + = f.select :state_eq, Spree::Order.state_machines[:state].states.collect {|s| [t("order_state.#{s.name}"), s.value]}, {:include_blank => true}, :class => 'select2', 'ng-model' => 'q.state_eq' + .four.columns + .field + = label_tag nil, t(:order_number) + = f.text_field :number_cont, 'ng-model' => 'q.number_cont' + .field + = label_tag nil, t(:email) + = f.email_field :email_cont, 'ng-model' => 'q.email_cont' + .four.columns + .field + = label_tag nil, t(:first_name_begins_with) + = f.text_field :bill_address_firstname_start, :size => 25, 'ng-model' => 'q.bill_address_firstname_start' + .field + = label_tag nil, t(:last_name_begins_with) + = f.text_field :bill_address_lastname_start, :size => 25, 'ng-model' => 'q.bill_address_lastname_start' + .omega.four.columns + .field.checkbox + %label + = f.check_box :completed_at_not_null, {:checked => @show_only_completed, 'ng-model' => 'q.completed_at_not_null'}, '1', '' + = t(:show_only_complete_orders) + .field.checkbox + %label + = f.check_box :inventory_units_shipment_id_null, {'ng-model' => 'q.inventory_units_shipment_id_null'}, '1', '0' + = t(:show_only_unfulfilled_orders) + .field-block.alpha.eight.columns + = label_tag nil, t(:distributors) + = select_tag("q[distributor_id_in]", + options_for_select(Enterprise.is_distributor.managed_by(spree_current_user).map {|e| [e.name, e.id]}, params[:distributor_ids]), + {class: "select2 fullwidth", multiple: true, 'ng-model' => 'q.distributor_id_in'}) + .field-block.omega.eight.columns + = label_tag nil, t(:order_cycles) + = select_tag("q[order_cycle_id_in]", + options_for_select(OrderCycle.managed_by(spree_current_user).where('order_cycles.orders_close_at is not null').order('order_cycles.orders_close_at DESC').map {|oc| [oc.name, oc.id]}, params[:order_cycle_ids]), + {class: "select2 fullwidth", multiple: true, 'ng-model' => 'q.order_cycle_id_in'}) + .clearfix + .actions.filter-actions + %div + %a.button.icon-search{'ng-click' => 'fetchResults()'} + = t(:filter_results) diff --git a/app/views/spree/admin/orders/_sortable_header.html.haml b/app/views/spree/admin/orders/_sortable_header.html.haml new file mode 100644 index 0000000000..97d24165c0 --- /dev/null +++ b/app/views/spree/admin/orders/_sortable_header.html.haml @@ -0,0 +1,4 @@ +%a{'ng-click' => "sortOptions.toggle('#{column_name}')"} + = t(column_name.to_s, scope: 'activerecord.attributes.spree/order') + %span{'ng-show' => "sorting == '#{column_name} asc'"}= "▲".html_safe + %span{'ng-show' => "sorting == '#{column_name} desc'"}= "▼".html_safe \ No newline at end of file diff --git a/app/views/spree/admin/orders/edit.html.haml b/app/views/spree/admin/orders/edit.html.haml index f53cca42ef..f897065224 100644 --- a/app/views/spree/admin/orders/edit.html.haml +++ b/app/views/spree/admin/orders/edit.html.haml @@ -13,7 +13,7 @@ %div{"data-hook" => "admin_order_edit_header"} = render 'spree/shared/error_messages', target: @order -%div{"ng-app" => "admin.orders", "ng-controller" => "ordersCtrl", "ofn-distributor-id" => @order.distributor_id, "ofn-order-cycle-id" => @order.order_cycle_id} +%div{"ng-app" => "admin.orders", "ng-controller" => "orderCtrl", "ofn-distributor-id" => @order.distributor_id, "ofn-order-cycle-id" => @order.order_cycle_id} = render 'add_product' %div{"data-hook" => "admin_order_edit_form"} diff --git a/app/views/spree/admin/orders/index.html.haml b/app/views/spree/admin/orders/index.html.haml index 3b233424cc..5c8f0e8d14 100644 --- a/app/views/spree/admin/orders/index.html.haml +++ b/app/views/spree/admin/orders/index.html.haml @@ -1,110 +1,86 @@ - content_for :page_title do - = t(:listing_orders) + = t('.listing_orders') + - content_for :page_actions do %li - = button_link_to t(:new_order), new_admin_order_url, :icon => 'icon-plus', :id => 'admin_new_order' + = button_link_to t('.new_order'), new_admin_order_url, icon: 'icon-plus', id: 'admin_new_order' + = render partial: 'spree/admin/shared/order_sub_menu' + +- content_for :app_wrapper_attrs do + = "ng-app='admin.orders' ng-controller='ordersCtrl'" + - content_for :table_filter_title do = t(:search) + - content_for :table_filter do - %div{"data-hook" => "admin_orders_index_search"} - = search_form_for [:admin, @search] do |f| - .field-block.alpha.four.columns - .date-range-filter.field - = label_tag nil, t(:date_range) - .date-range-fields - = f.text_field :created_at_gt, :class => 'datepicker datepicker-from', :value => params[:q][:created_at_gt], :placeholder => t(:start) - %span.range-divider - %i.icon-arrow-right - = f.text_field :created_at_lt, :class => 'datepicker datepicker-to', :value => params[:q][:created_at_lt], :placeholder => t(:stop) - .field - = label_tag nil, t(:status) - = f.select :state_eq, Spree::Order.state_machines[:state].states.collect {|s| [t("order_state.#{s.name}"), s.value]}, {:include_blank => true}, :class => 'select2' - .four.columns - .field - = label_tag nil, t(:order_number) - = f.text_field :number_cont - .field - = label_tag nil, t(:email) - = f.email_field :email_cont - .four.columns - .field - = label_tag nil, t(:first_name_begins_with) - = f.text_field :bill_address_firstname_start, :size => 25 - .field - = label_tag nil, t(:last_name_begins_with) - = f.text_field :bill_address_lastname_start, :size => 25 - .omega.four.columns - .field.checkbox - %label - = f.check_box :completed_at_not_null, {:checked => @show_only_completed}, '1', '' - = t(:show_only_complete_orders) - .field.checkbox - %label - = f.check_box :inventory_units_shipment_id_null, { }, '1', '0' - = t(:show_only_unfulfilled_orders) - .field-block.alpha.eight.columns - = label_tag nil, t(:distributors) - = select_tag("q[distributor_id_in]", - options_for_select(Enterprise.is_distributor.managed_by(spree_current_user).map {|e| [e.name, e.id]}, params[:distributor_ids]), - {class: "select2 fullwidth", multiple: true}) - .field-block.omega.eight.columns - = label_tag nil, t(:order_cycles) - = select_tag("q[order_cycle_id_in]", - options_for_select(OrderCycle.managed_by(spree_current_user).where('order_cycles.orders_close_at is not null').order('order_cycles.orders_close_at DESC').map {|oc| [oc.name, oc.id]}, params[:order_cycle_ids]), - {class: "select2 fullwidth", multiple: true}) - .clearfix - .actions.filter-actions - %div{"data-hook" => "admin_orders_index_search_buttons"} - = button t(:filter_results), 'icon-search' - -- unless @orders.empty? - %table#listing_orders.index.responsive{"data-hook" => "", width: "100%", "ng-app" => "ofn.admin"} - %colgroup - %col{style: "width: 10%"} - %thead - %tr{"data-hook" => "admin_orders_index_headers"} + = render partial: 'filters' + +%table#listing_orders.index.responsive{width: "100%", 'ng-init' => 'initialise()', 'ng-show' => "!RequestMonitor.loading && orders.length > 0" } + %colgroup + %col{style: "width: 10%"} + %thead + %tr + %th + = t(:products_distributor) + - if @show_only_completed %th - = t(:products_distributor) - - if @show_only_completed - %th= sort_link @search, :completed_at, t(:completed_at, :scope => 'activerecord.attributes.spree/order') - - else - %th= sort_link @search, :created_at, t(:created_at, :scope => 'activerecord.attributes.spree/order') - %th= sort_link @search, :number, t(:number, :scope => 'activerecord.attributes.spree/order') - %th= sort_link @search, :state, t(:state, :scope => 'activerecord.attributes.spree/order') - %th= sort_link @search, :payment_state, t(:payment_state, :scope => 'activerecord.attributes.spree/order') - %th= sort_link @search, :shipment_state, t(:shipment_state, :scope => 'activerecord.attributes.spree/order') - %th= sort_link @search, :email, t(:email, :scope => 'activerecord.attributes.spree/order') - %th= sort_link @search, :total, t(:total, :scope => 'activerecord.attributes.spree/order') - %th.actions{"data-hook" => "admin_orders_index_header_actions"} - %tbody - - @orders.each do |order| - %tr{class: "state-#{order.state.downcase} #{cycle('odd', 'even')}", "data-hook" => "admin_orders_index_rows"} - %td.align-center - = order.distributor.andand.name - %td.align-center= l (@show_only_completed ? order.completed_at : order.created_at).to_date - %td - = link_to order.number, admin_order_path(order) - - if order.special_instructions.present? - %br - %span{class: "icon-warning-sign", "ofn-with-tip" => order.special_instructions} - notes - %td.align-center - %span{class: "state #{order.state.downcase}"}= t("order_state.#{order.state.downcase}") - %td.align-center - %span{class: "state #{order.payment_state}"}= link_to t("payment_states.#{order.payment_state}"), admin_order_payments_path(order) if order.payment_state - %td.align-center - %span{class: "state #{order.shipment_state}"}= link_to t("shipment_states.#{order.shipment_state}"), admin_order_shipments_path(order) if order.shipment_state - %td= mail_to order.email - %td.align-center= order.display_total.to_html - %td.actions{"data-hook" => "admin_orders_index_row_actions"} - = link_to_edit_url edit_admin_order_path(order), :title => "admin_edit_#{dom_id(order)}", :no_text => true - - if order.ready_to_ship? - - # copied from backend/app/views/spree/admin/payments/_list.html.erb - = link_to_with_icon "icon-road", t('admin.orders.index.ship'), fire_admin_order_url(order, :e => 'ship'), :method => :put, :no_text => true, :data => {:action => 'ship', :confirm => t(:are_you_sure)} - = render partial: 'spree/admin/orders/capture', locals: {order: order} -- else - .no-objects-found - = t(:no_orders_found) + %a{'ng-click' => "sortOptions.toggle('completed_at')"} + = t(:completed_at, scope: 'activerecord.attributes.spree/order') + %span{'ng-show' => "sorting == 'completed_at asc'"}= "▲".html_safe + %span{'ng-show' => "sorting == 'completed_at desc' || sorting === undefined"}= "▼".html_safe + - else + %th + = render partial: 'sortable_header', locals: {column_name: 'created_at'} + - ['number', 'state', 'payment_state', 'shipment_state', 'email', 'total'].each do |column_name| + %th + = render partial: 'sortable_header', locals: {column_name: column_name} + %th.actions + %tbody + %tr{ng: {repeat: 'order in orders track by $index', class: {even: "'even'", odd: "'odd'"}}, 'ng-class' => "'state-{{order.state}}'"} + %td.align-center + {{::order.distributor_name}} + %td.align-center + = @show_only_completed ? '{{::order.completed_at}}' : '{{::order.created_at}}' + %td + %a{'ng-href' => '{{::order.show_path}}'} + {{order.number}} + %div{'ng-if' => 'order.special_instructions'} + %br + %span.icon-warning-sign{'ofn-with-tip' => "{{::order.special_instructions}}"} + = t('.note') + %td.align-center + %span.state{'ng-class' => 'order.state'} + {{'order_state.' + order.state | t}} + %td.align-center + %span.state{'ng-class' => 'order.payment_state', 'ng-if' => 'order.payment_state'} + %a{'ng-href' => '{{::order.payments_path}}' } + {{'payment_states.' + order.payment_state | t}} + %td.align-center + %span.state{'ng-class' => 'order.shipment_state', 'ng-if' => 'order.shipment_state'} + %a{'ng-href' => '{{::order.shipments_path}}' } + {{'shipment_states.' + order.shipment_state | t}} + %td + = mail_to "{{::order.email}}" + %td.align-center + %span{'ng-bind-html' => '::order.display_total'} + %td.actions + %a.icon_link.with-tip.icon-edit.no-text{'ng-href' => '{{::order.edit_path}}', 'data-action' => 'edit', 'ofn-with-tip' => t('.edit')} + %div{'ng-if' => 'order.ready_to_ship'} + %a.icon-road.icon_link.with-tip.no-text{'ng-href' => '{{::order.ship_path}}', 'data-action' => 'ship', 'data-confirm' => t(:are_you_sure), 'data-method' => 'put', rel: 'nofollow', 'ofn-with-tip' => t('.ship')} + %div{'ng-if' => 'order.payment_capture_path'} + %a.icon-capture.icon_link.no-text{'ng-href' => '{{::order.payment_capture_path}}', 'data-action' => 'capture', 'data-method' => 'put', rel: 'nofollow', 'ofn-with-tip' => t('.capture')} -= paginate @orders +.orders-loading{'ng-show' => 'RequestMonitor.loading'} + .row + .small-12.columns.fullwidth.text-center + %img.spinner{ src: "/assets/spinning-circles.svg" } + .row + .small-12.columns.fullwidth.text-center + %span= t('.loading') + +%div{'ng-show' => "!RequestMonitor.loading && orders.length > 0" } + = render partial: 'admin/shared/angular_pagination' + +.no-objects-found{'ng-show' => "!RequestMonitor.loading && orders.length == 0"} + = t('.no_orders_found') diff --git a/app/views/spree/admin/orders/new.html.haml b/app/views/spree/admin/orders/new.html.haml index 24190f3190..b653439230 100644 --- a/app/views/spree/admin/orders/new.html.haml +++ b/app/views/spree/admin/orders/new.html.haml @@ -14,7 +14,7 @@ %div{"data-hook" => "admin_order_new_header"} = render 'spree/shared/error_messages', :target => @order -%div{"ng-app" => "admin.orders", "ng-controller" => "ordersCtrl"} +%div{"ng-app" => "admin.orders", "ng-controller" => "orderCtrl"} %div{"ng-show" => "distributionChosen()"} = render 'add_product' diff --git a/app/views/spree/admin/orders/set_distribution.html.haml b/app/views/spree/admin/orders/set_distribution.html.haml index 51c7b97e8d..89d036b93a 100644 --- a/app/views/spree/admin/orders/set_distribution.html.haml +++ b/app/views/spree/admin/orders/set_distribution.html.haml @@ -14,7 +14,7 @@ %div{"data-hook" => "admin_order_new_header"} = render 'spree/shared/error_messages', :target => @order -%div{"ng-app" => "admin.orders", "ng-controller" => "ordersCtrl"} +%div{"ng-app" => "admin.orders", "ng-controller" => "orderCtrl"} = form_for @order, url: admin_order_url(@order), method: :put do |f| = render 'spree/admin/orders/_form/distribution_fields' -# This param passed to stop validation error in next page due to no line items in order yet: diff --git a/config/locales/en.yml b/config/locales/en.yml index a289a1e4b6..ea6a746d97 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -620,9 +620,6 @@ en: controls: back_to_my_inventory: Back to my inventory orders: - index: - capture: "Capture" - ship: "Ship" invoice_email_sent: 'Invoice email has been sent' order_email_resent: 'Order email has been resent' bulk_management: @@ -2657,6 +2654,19 @@ See the %{link} to find out more about %{sitename}'s features and to start using index: inherits_properties_checkbox_hint: "Inherit properties from %{supplier}? (unless overridden above)" orders: + index: + listing_orders: "Listing Orders" + new_order: "New Order" + capture: "Capture" + ship: "Ship" + edit: "Edit" + note: "Note" + first: "First" + last: "Last" + previous: "Previous" + next: "Next" + loading: "Loading" + no_orders_found: "No Orders Found" invoice: issued_on: Issued on tax_invoice: TAX INVOICE diff --git a/spec/controllers/spree/admin/orders_controller_spec.rb b/spec/controllers/spree/admin/orders_controller_spec.rb index 25c2683d4d..8d76ea3a8d 100644 --- a/spec/controllers/spree/admin/orders_controller_spec.rb +++ b/spec/controllers/spree/admin/orders_controller_spec.rb @@ -66,26 +66,27 @@ describe Spree::Admin::OrdersController, type: :controller do end it "retrieves a list of orders with appropriate attributes, including line items with appropriate attributes" do - keys = json_response.first.keys.map{ |key| key.to_sym } + keys = json_response['orders'].first.keys.map{ |key| key.to_sym } order_attributes.all?{ |attr| keys.include? attr }.should == true end - it "sorts orders in ascending id order" do - ids = json_response.map{ |order| order['id'] } - ids[0].should < ids[1] - ids[1].should < ids[2] + it "sorts orders in descending id order" do + ids = json_response['orders'].map{ |order| order['id'] } + ids[0].should > ids[1] + ids[1].should > ids[2] end it "formats completed_at to 'yyyy-mm-dd hh:mm'" do - json_response.map{ |order| order['completed_at'] }.all?{ |a| a.match("^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}$") }.should == true + pp json_response + json_response['orders'].map{ |order| order['completed_at'] }.all?{ |a| a == order1.completed_at.strftime('%B %d, %Y') }.should == true end it "returns distributor object with id key" do - json_response.map{ |order| order['distributor'] }.all?{ |d| d.has_key?('id') }.should == true + json_response['orders'].map{ |order| order['distributor'] }.all?{ |d| d.has_key?('id') }.should == true end it "retrieves the order number" do - json_response.map{ |order| order['number'] }.all?{ |number| number.match("^R\\d{5,10}$") }.should == true + json_response['orders'].map{ |order| order['number'] }.all?{ |number| number.match("^R\\d{5,10}$") }.should == true end end @@ -120,7 +121,7 @@ describe Spree::Admin::OrdersController, type: :controller do end it "retrieves a list of orders" do - keys = json_response.first.keys.map{ |key| key.to_sym } + keys = json_response['orders'].first.keys.map{ |key| key.to_sym } order_attributes.all?{ |attr| keys.include? attr }.should == true end end @@ -132,7 +133,7 @@ describe Spree::Admin::OrdersController, type: :controller do end it "retrieves a list of orders" do - keys = json_response.first.keys.map{ |key| key.to_sym } + keys = json_response['orders'].first.keys.map{ |key| key.to_sym } order_attributes.all?{ |attr| keys.include? attr }.should == true end end diff --git a/spec/features/admin/adjustments_spec.rb b/spec/features/admin/adjustments_spec.rb index a051d28b2b..992ee7c47c 100644 --- a/spec/features/admin/adjustments_spec.rb +++ b/spec/features/admin/adjustments_spec.rb @@ -3,7 +3,7 @@ require "spec_helper" feature %q{ As an administrator I want to manage adjustments on orders -} do +}, js: true do include AuthenticationWorkflow include WebHelper @@ -30,7 +30,7 @@ feature %q{ click_link 'New Adjustment' fill_in 'adjustment_amount', with: 110 fill_in 'adjustment_label', with: 'Late fee' - select 'GST', from: 'tax_rate_id' + select2_select 'GST', from: 'tax_rate_id' click_button 'Continue' # Then I should see the adjustment, with the correct tax @@ -51,11 +51,11 @@ feature %q{ page.find('tr', text: 'Shipping').find('a.icon-edit').click # Then I should see the uneditable included tax and our tax rate as the default - page.should have_field :adjustment_included_tax, with: '10.00', disabled: true - page.should have_select :tax_rate_id, selected: 'GST' + expect(page).to have_field :adjustment_included_tax, with: '10.00', disabled: true + expect(page).to have_select2 :tax_rate_id, selected: 'GST' # When I edit the adjustment, removing the tax - select 'Remove tax', from: :tax_rate_id + select2_select 'Remove tax', from: :tax_rate_id click_button 'Continue' # Then the adjustment tax should be cleared @@ -75,11 +75,11 @@ feature %q{ page.find('tr', text: 'Shipping').find('a.icon-edit').click # Then I should see the uneditable included tax and 'Remove tax' as the default tax rate - page.should have_field :adjustment_included_tax, with: '0.00', disabled: true - page.should have_select :tax_rate_id, selected: [] + expect(page).to have_field :adjustment_included_tax, with: '0.00', disabled: true + expect(page).to have_select2 :tax_rate_id, selected: [] # When I edit the adjustment, setting a tax rate - select 'GST', from: :tax_rate_id + select2_select 'GST', from: :tax_rate_id click_button 'Continue' # Then the adjustment tax should be recalculated diff --git a/spec/features/admin/bulk_order_management_spec.rb b/spec/features/admin/bulk_order_management_spec.rb index 602fb99f8c..0c436014b2 100644 --- a/spec/features/admin/bulk_order_management_spec.rb +++ b/spec/features/admin/bulk_order_management_spec.rb @@ -54,8 +54,8 @@ feature %q{ it "displays a column for order date" do expect(page).to have_selector "th.date", text: "ORDER DATE", :visible => true - expect(page).to have_selector "td.date", text: o1.completed_at.strftime("%F %T"), :visible => true - expect(page).to have_selector "td.date", text: o2.completed_at.strftime("%F %T"), :visible => true + expect(page).to have_selector "td.date", text: o1.completed_at.strftime('%B %d, %Y'), :visible => true + expect(page).to have_selector "td.date", text: o2.completed_at.strftime('%B %d, %Y'), :visible => true end it "displays a column for producer" do diff --git a/spec/features/admin/orders_spec.rb b/spec/features/admin/orders_spec.rb index b9bfd9ad6e..032ed3443a 100644 --- a/spec/features/admin/orders_spec.rb +++ b/spec/features/admin/orders_spec.rb @@ -109,7 +109,7 @@ feature %q{ quick_login_as_admin visit '/admin/orders' uncheck 'Only show complete orders' - click_button 'Filter Results' + page.find('a.icon-search').click click_edit diff --git a/spec/javascripts/unit/admin/line_items/controllers/line_items_controller_spec.js.coffee b/spec/javascripts/unit/admin/line_items/controllers/line_items_controller_spec.js.coffee index 7e63ba7284..1b23dea59d 100644 --- a/spec/javascripts/unit/admin/line_items/controllers/line_items_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/line_items/controllers/line_items_controller_spec.js.coffee @@ -37,7 +37,7 @@ describe "LineItemsCtrl", -> order = { id: 9, order_cycle: { id: 4 }, distributor: { id: 5 }, number: "R123456" } lineItem = { id: 7, quantity: 3, order: { id: 9 }, supplier: { id: 1 } } - httpBackend.expectGET("/admin/orders.json?q%5Bcompleted_at_gteq%5D=SomeDate&q%5Bcompleted_at_lt%5D=SomeDate&q%5Bcompleted_at_not_null%5D=true&q%5Bstate_not_eq%5D=canceled").respond [order] + httpBackend.expectGET("/admin/orders.json?q%5Bcompleted_at_gteq%5D=SomeDate&q%5Bcompleted_at_lt%5D=SomeDate&q%5Bcompleted_at_not_null%5D=true&q%5Bstate_not_eq%5D=canceled").respond {orders: [order], pagination: {page: 1, pages: 1, results: 1}} httpBackend.expectGET("/admin/bulk_line_items.json?q%5Border%5D%5Bcompleted_at_gteq%5D=SomeDate&q%5Border%5D%5Bcompleted_at_lt%5D=SomeDate&q%5Border%5D%5Bcompleted_at_not_null%5D=true&q%5Border%5D%5Bstate_not_eq%5D=canceled").respond [lineItem] httpBackend.expectGET("/admin/enterprises/visible.json?ams_prefix=basic&q%5Bsells_in%5D%5B%5D=own&q%5Bsells_in%5D%5B%5D=any").respond [distributor] httpBackend.expectGET("/admin/order_cycles.json?ams_prefix=basic&as=distributor&q%5Borders_close_at_gt%5D=SomeDate").respond [orderCycle] @@ -68,7 +68,7 @@ describe "LineItemsCtrl", -> describe "initialisation", -> it "gets suppliers", -> - expect(scope.suppliers).toDeepEqual [supplier ] + expect(scope.suppliers).toDeepEqual [ supplier ] it "gets distributors", -> expect(scope.distributors).toDeepEqual [ distributor ] diff --git a/spec/javascripts/unit/admin/orders/controllers/order_controller_spec.js.coffee b/spec/javascripts/unit/admin/orders/controllers/order_controller_spec.js.coffee new file mode 100644 index 0000000000..297eac32f6 --- /dev/null +++ b/spec/javascripts/unit/admin/orders/controllers/order_controller_spec.js.coffee @@ -0,0 +1,40 @@ +describe "orderCtrl", -> + ctrl = null + scope = {} + attrs = {} + shops = [] + orderCycles = [ + {id: 10, name: 'Ten', status: 'open', distributors: [{id: 1, name: 'One'}]} + {id: 20, name: 'Twenty', status: 'closed', distributors: [{id: 2, name: 'Two', status: 'closed'}]} + ] + + beforeEach -> + scope = {} + + module 'admin.orders' + inject ($controller) -> + ctrl = $controller 'orderCtrl', {$scope: scope, $attrs: attrs, shops: shops, orderCycles: orderCycles} + + it "initialises name_and_status", -> + expect(scope.orderCycles[0].name_and_status).toEqual "Ten (open)" + expect(scope.orderCycles[1].name_and_status).toEqual "Twenty (closed)" + + describe "finding valid order cycles for a distributor", -> + order_cycle = {id: 10, distributors: [{id: 1, name: 'One'}]} + + it "returns true when the order cycle includes the distributor", -> + scope.distributor_id = '1' + expect(scope.validOrderCycle(order_cycle, 1, [order_cycle])).toBe true + + it "returns false otherwise", -> + scope.distributor_id = '2' + expect(scope.validOrderCycle(order_cycle, 1, [order_cycle])).toBe false + + describe "checking if a distributor has order cycles", -> + it "returns true when it does", -> + distributor = {id: 1} + expect(scope.distributorHasOrderCycles(distributor)).toBe true + + it "returns false otherwise", -> + distributor = {id: 3} + expect(scope.distributorHasOrderCycles(distributor)).toBe false diff --git a/spec/javascripts/unit/admin/orders/controllers/orders_controller_spec.js.coffee b/spec/javascripts/unit/admin/orders/controllers/orders_controller_spec.js.coffee index 4ceeea277b..4aed599140 100644 --- a/spec/javascripts/unit/admin/orders/controllers/orders_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/orders/controllers/orders_controller_spec.js.coffee @@ -1,40 +1,37 @@ describe "ordersCtrl", -> ctrl = null - scope = {} - attrs = {} - shops = [] - orderCycles = [ - {id: 10, name: 'Ten', status: 'open', distributors: [{id: 1, name: 'One'}]} - {id: 20, name: 'Twenty', status: 'closed', distributors: [{id: 2, name: 'Two', status: 'closed'}]} + Orders = null + $scope = null + orders = [ + { id: 8, order_cycle: { id: 4 }, distributor: { id: 5 }, number: "R123456" } + { id: 9, order_cycle: { id: 5 }, distributor: { id: 7 }, number: "R213776" } ] + form = { + q: { + created_at_lt: '' + created_at_gt: '' + completed_at_not_null: true + } + } beforeEach -> - scope = {} + module 'admin.orders' + inject ($controller, $rootScope, RequestMonitor, SortOptions) -> + $scope = $rootScope.$new() + Orders = + index: jasmine.createSpy('index').and.returnValue(orders) + all: orders + ctrl = $controller 'ordersCtrl', { $scope: $scope, RequestMonitor: RequestMonitor, SortOptions: SortOptions, Orders: Orders } + $scope.q = form.q - module('admin.orders') - inject ($controller) -> - ctrl = $controller 'ordersCtrl', {$scope: scope, $attrs: attrs, shops: shops, orderCycles: orderCycles} + describe "initialising the controller", -> + it "fetches orders", -> + $scope.initialise() + expect(Orders.index).toHaveBeenCalled() + expect($scope.orders).toEqual orders - it "initialises name_and_status", -> - expect(scope.orderCycles[0].name_and_status).toEqual "Ten (open)" - expect(scope.orderCycles[1].name_and_status).toEqual "Twenty (closed)" - - describe "finding valid order cycles for a distributor", -> - order_cycle = {id: 10, distributors: [{id: 1, name: 'One'}]} - - it "returns true when the order cycle includes the distributor", -> - scope.distributor_id = '1' - expect(scope.validOrderCycle(order_cycle, 1, [order_cycle])).toBe true - - it "returns false otherwise", -> - scope.distributor_id = '2' - expect(scope.validOrderCycle(order_cycle, 1, [order_cycle])).toBe false - - describe "checking if a distributor has order cycles", -> - it "returns true when it does", -> - distributor = {id: 1} - expect(scope.distributorHasOrderCycles(distributor)).toBe true - - it "returns false otherwise", -> - distributor = {id: 3} - expect(scope.distributorHasOrderCycles(distributor)).toBe false + describe "using pagination", -> + it "changes the page", -> + $scope.changePage(2) + expect($scope.page).toEqual 2 + expect(Orders.index).toHaveBeenCalled() diff --git a/spec/javascripts/unit/admin/orders/services/orders_spec.js.coffee b/spec/javascripts/unit/admin/orders/services/orders_spec.js.coffee index 3d41ded40a..68bc3beb1f 100644 --- a/spec/javascripts/unit/admin/orders/services/orders_spec.js.coffee +++ b/spec/javascripts/unit/admin/orders/services/orders_spec.js.coffee @@ -18,20 +18,20 @@ describe "Orders service", -> result = response = null beforeEach -> - response = [{ id: 5, name: 'Order 1'}] + response = { orders: [{ id: 5, name: 'Order 1'}], pagination: {page: 1, pages: 1, results: 1} } $httpBackend.expectGET('/admin/orders.json').respond 200, response result = Orders.index() $httpBackend.flush() it "stores returned data in @byID, with ids as keys", -> # OrderResource returns instances of Resource rather than raw objects - expect(Orders.byID).toDeepEqual { 5: response[0] } + expect(Orders.byID).toDeepEqual { 5: response.orders[0] } it "stores returned data in @pristineByID, with ids as keys", -> - expect(Orders.pristineByID).toDeepEqual { 5: response[0] } + expect(Orders.pristineByID).toDeepEqual { 5: response.orders[0] } it "returns an array of orders", -> - expect(result).toDeepEqual response + expect(result).toDeepEqual response.orders describe "#save", ->