diff --git a/app/assets/javascripts/admin/order_cycles/controllers/order_cycles_controller.js.coffee b/app/assets/javascripts/admin/order_cycles/controllers/order_cycles_controller.js.coffee index 527bc06b08..2f5d6d9b5c 100644 --- a/app/assets/javascripts/admin/order_cycles/controllers/order_cycles_controller.js.coffee +++ b/app/assets/javascripts/admin/order_cycles/controllers/order_cycles_controller.js.coffee @@ -1,21 +1,25 @@ -angular.module("admin.orderCycles").controller "OrderCyclesCtrl", ($scope, $q, Columns, StatusMessage, RequestMonitor, OrderCycles, Enterprises) -> +angular.module("admin.orderCycles").controller "OrderCyclesCtrl", ($scope, $q, Columns, StatusMessage, RequestMonitor, OrderCycles, Enterprises, Schedules) -> $scope.RequestMonitor = RequestMonitor $scope.columns = Columns.columns $scope.saveAll = -> OrderCycles.saveChanges($scope.order_cycles_form) $scope.ordersCloseAtLimit = -31 # days $scope.involvingFilter = 0 - compileDataFor = (orderCycles) -> - for orderCycle in orderCycles + compileData = -> + for schedule in $scope.schedules + Schedules.linkToOrderCycles(schedule) + for orderCycle in $scope.orderCycles OrderCycles.linkToEnterprises(orderCycle) + OrderCycles.linkToSchedules(orderCycle) orderCycle.involvedEnterpriseIDs = [orderCycle.coordinator.id] orderCycle.producerNames = orderCycle.producers.map((producer) -> orderCycle.involvedEnterpriseIDs.push(producer.id); producer.name).join(", ") orderCycle.shopNames = orderCycle.shops.map((shop) -> orderCycle.involvedEnterpriseIDs.push(shop.id); shop.name).join(", ") # NOTE: this is using the Enterprises service from the admin.enterprises module RequestMonitor.load ($scope.enterprises = Enterprises.index(action: "visible", ams_prefix: "basic")).$promise + $scope.schedules = Schedules.index() RequestMonitor.load ($scope.orderCycles = OrderCycles.index(ams_prefix: "index", "q[orders_close_at_gt]": "#{daysFromToday($scope.ordersCloseAtLimit)}")).$promise - RequestMonitor.load $q.all([$scope.enterprises.$promise, $scope.orderCycles.$promise]).then -> compileDataFor($scope.orderCycles) + RequestMonitor.load $q.all([$scope.enterprises.$promise, $scope.schedules.$promise, $scope.orderCycles.$promise]).then -> compileData() $scope.$watch 'order_cycles_form.$dirty', (newVal, oldVal) -> StatusMessage.display 'notice', "You have unsaved changes" if newVal @@ -25,7 +29,7 @@ angular.module("admin.orderCycles").controller "OrderCyclesCtrl", ($scope, $q, C existingIDs = Object.keys(OrderCycles.orderCyclesByID) RequestMonitor.load (orderCycles = OrderCycles.index(ams_prefix: "index", "q[orders_close_at_gt]": "#{daysFromToday($scope.ordersCloseAtLimit)}", "q[id_not_in][]": existingIDs)).$promise orderCycles.$promise.then -> - compileDataFor(orderCycles) + compileData() $scope.orderCycles.push(orderCycle) for orderCycle in orderCycles daysFromToday = (days) -> diff --git a/app/assets/javascripts/admin/order_cycles/directives/order_cycles_selector.js.coffee b/app/assets/javascripts/admin/order_cycles/directives/order_cycles_selector.js.coffee new file mode 100644 index 0000000000..6ad521331f --- /dev/null +++ b/app/assets/javascripts/admin/order_cycles/directives/order_cycles_selector.js.coffee @@ -0,0 +1,22 @@ +angular.module("admin.orderCycles").directive 'orderCyclesSelector', (OrderCycles, Schedules) -> + restrict: 'C' + templateUrl: 'admin/order_cycles_selector.html' + link: (scope, element, attr) -> + if scope.scheduleID? + scope.selectedOrderCycles = Schedules.byID[scope.scheduleID].orderCycles + scope.orderCycleIDs = scope.selectedOrderCycles.map (i, orderCycle) -> orderCycle.id + else + scope.selectedOrderCycles = [] + + scope.availableOrderCycles = (orderCycle for id, orderCycle of OrderCycles.orderCyclesByID when orderCycle not in scope.selectedOrderCycles) + + + element.find('#available-order-cycles .order-cycles').sortable + connectWith: '#selected-order-cycles .order-cycles' + + element.find('#selected-order-cycles .order-cycles').sortable + connectWith: '#available-order-cycles .order-cycles' + receive: (event, ui) -> + scope.orderCycleIDs = $('#selected-order-cycles .order-cycles').children('.order-cycle').map((i, element) -> $(element).scope().orderCycle.id).get() + remove: (event, ui) -> + scope.orderCycleIDs = $('#selected-order-cycles .order-cycles').children('.order-cycle').map((i, element) -> $(element).scope().orderCycle.id).get() diff --git a/app/assets/javascripts/admin/order_cycles/directives/schedule_dialog.js.coffee b/app/assets/javascripts/admin/order_cycles/directives/schedule_dialog.js.coffee new file mode 100644 index 0000000000..4d09cfb80c --- /dev/null +++ b/app/assets/javascripts/admin/order_cycles/directives/schedule_dialog.js.coffee @@ -0,0 +1,40 @@ +angular.module("admin.orderCycles").directive 'scheduleDialog', ($compile, $injector, $templateCache, DialogDefaults, Schedules) -> + restrict: 'A' + scope: + scheduleID: '@' + link: (scope, element, attr) -> + scope.submitted = false + scope.name = "" + scope.orderCycleIDs = [] + scope.errors = [] + + scope.close = -> + scope.template.dialog('close') + return + + scope.addSchedule = -> + scope.schedule_form.$setPristine() + scope.submitted = true + scope.errors = [] + if scope.schedule_form.$valid + Schedules.add({name: scope.name, order_cycle_ids: scope.orderCycleIDs}).$promise.then (data) -> + if data.id + scope.name = "" + scope.orderCycleIDs = "" + scope.submitted = false + template.dialog('close') + , (response) -> + if response.data.errors + scope.errors.push(error) for error in response.data.errors + else + scope.errors.push("Sorry! Could not create '#{scope.name}'") + return + + # Link opening of dialog to click event on element + element.bind 'click', (e) -> + # Compile modal template + scope.template = $compile($templateCache.get('admin/schedule_dialog.html'))(scope) + # Set Dialog options + scope.template.dialog(DialogDefaults) + scope.template.dialog(close: -> scope.template.remove()) + scope.template.dialog('open') diff --git a/app/assets/javascripts/admin/order_cycles/services/schedule_resource.js.coffee b/app/assets/javascripts/admin/order_cycles/services/schedule_resource.js.coffee new file mode 100644 index 0000000000..8f1443d99d --- /dev/null +++ b/app/assets/javascripts/admin/order_cycles/services/schedule_resource.js.coffee @@ -0,0 +1,10 @@ +angular.module("admin.orderCycles").factory 'ScheduleResource', ($resource) -> + $resource('/admin/schedules/:id/:action.json', {}, { + 'index': + method: 'GET' + isArray: true + 'create': + method: 'POST' + 'update': + method: 'PUT' + }) diff --git a/app/assets/javascripts/admin/order_cycles/services/schedules.js.coffee b/app/assets/javascripts/admin/order_cycles/services/schedules.js.coffee new file mode 100644 index 0000000000..ed58981d66 --- /dev/null +++ b/app/assets/javascripts/admin/order_cycles/services/schedules.js.coffee @@ -0,0 +1,33 @@ +angular.module("admin.orderCycles").factory "Schedules", ($q, RequestMonitor, ScheduleResource) -> + new class Schedules + byID: {} + # all: [] + + add: (params) -> + ScheduleResource.create params, (schedule) => + @byID[schedule.id] = schedule if schedule.id + + # remove: (schedule) -> + # params = id: schedule.id + # ScheduleResource.destroy params, => + # i = @schedules.indexOf schedule + # @schedules.splice i, 1 unless i < 0 + # , (response) => + # errors = response.data.errors + # if errors? + # InfoDialog.open 'error', errors[0] + # else + # InfoDialog.open 'error', "Could not delete schedule: #{schedule.email}" + + index: -> + request = ScheduleResource.index (data) => + @byID[schedule.id] = schedule for schedule in data + data + # @all = data + RequestMonitor.load(request.$promise) + request + + linkToOrderCycles: (schedule) -> + for orderCycle, i in schedule.orderCycles + orderCycle = OrderCycles.orderCyclesByID[orderCycle.id] + schedule.orderCycles[i] = orderCycle if orderCycle? diff --git a/app/assets/javascripts/admin/resources/services/order_cycles.js.coffee b/app/assets/javascripts/admin/resources/services/order_cycles.js.coffee index 3216969408..9dab7ceb98 100644 --- a/app/assets/javascripts/admin/resources/services/order_cycles.js.coffee +++ b/app/assets/javascripts/admin/resources/services/order_cycles.js.coffee @@ -80,3 +80,8 @@ angular.module("admin.resources").factory 'OrderCycles', ($q, $injector, OrderCy for shop, i in orderCycle.shops shop = Enterprises.enterprisesByID[shop.id] orderCycle.shops[i] = shop if shop? + + linkToSchedules: (orderCycle) -> + for schedule, i in orderCycle.schedules + schedule = Schedules.byID[schedule.id] + orderCycle.schedules[i] = schedule if schedule? diff --git a/app/assets/javascripts/templates/admin/order_cycles_selector.html.haml b/app/assets/javascripts/templates/admin/order_cycles_selector.html.haml new file mode 100644 index 0000000000..746a2e3a2f --- /dev/null +++ b/app/assets/javascripts/templates/admin/order_cycles_selector.html.haml @@ -0,0 +1,10 @@ +#available-order-cycles + Available + .order-cycles + .order-cycle{ ng: { repeat: 'orderCycle in availableOrderCycles' } } + {{ orderCycle.name }} +#selected-order-cycles + Selected + .order-cycles + .order-cycle{ ng: { repeat: 'orderCycle in selectedOrderCycles' } } + {{ orderCycle.name }} diff --git a/app/assets/javascripts/templates/admin/schedule_dialog.html.haml b/app/assets/javascripts/templates/admin/schedule_dialog.html.haml new file mode 100644 index 0000000000..9f74c2493f --- /dev/null +++ b/app/assets/javascripts/templates/admin/schedule_dialog.html.haml @@ -0,0 +1,19 @@ +#schedule-dialog + .text-normal.margin-bottom-30.text-center + = t('admin.order_cycles.index.add_a_new_schedule') + + %form{ name: 'schedule_form', novalidate: true, ng: { submit: "addSchedule()" }} + + .text-center.margin-bottom-20 + %input.fullwidth{ type: 'text', name: 'name', required: true, placeholder: t('admin.order_cycles.index.schedule_name_placeholder'), ng: { model: "name" } } + %div{ ng: { show: "submitted && new_schedule_form.$pristine" } } + .error{ ng: { show: "(new_schedule_form.name.$error.required)" } } + = t('admin.order_cycles.index.name_required_error') + .error{ ng: { repeat: "error in errors", bind: "error" } } + + .order-cycles-selector.text-center.margin-bottom-30 + + .text-center + %input.button.icon-plus{ type: 'submit', value: t('admin.order_cycles.index.create_schedule') } + or + %input.button.red.icon-remove{ type: 'button', value: t('actions.cancel'), ng: { click: 'close()' } } diff --git a/app/assets/stylesheets/admin/order_cycles.scss b/app/assets/stylesheets/admin/order_cycles.scss new file mode 100644 index 0000000000..a649599ad5 --- /dev/null +++ b/app/assets/stylesheets/admin/order_cycles.scss @@ -0,0 +1,28 @@ +#schedule-dialog { + #available-order-cycles, #selected-order-cycles { + text-align: left; + display: inline-block; + width: 45%; + height: 200px; + max-height: 300px; + width: 45%; + + .order-cycles { + display: block; + border: 1px solid #dddddd; + height: 100%; + width: 100%; + overflow-x: hidden; + overflow-y: scroll; + + .order-cycle { + padding: 8px 5px; + cursor: pointer; + + &:hover { + background-color: #cee1f4; + } + } + } + } +} diff --git a/app/controllers/admin/order_cycles_controller.rb b/app/controllers/admin/order_cycles_controller.rb index 963b9195d0..39f306976e 100644 --- a/app/controllers/admin/order_cycles_controller.rb +++ b/app/controllers/admin/order_cycles_controller.rb @@ -105,13 +105,13 @@ module Admin def collection return Enterprise.where("1=0") unless json_request? ocs = if params[:as] == "distributor" - OrderCycle.ransack(params[:q]).result. + OrderCycle.preload(:schedules).ransack(params[:q]).result. involving_managed_distributors_of(spree_current_user).order('updated_at DESC') elsif params[:as] == "producer" - OrderCycle.ransack(params[:q]).result. + OrderCycle.preload(:schedules).ransack(params[:q]).result. involving_managed_producers_of(spree_current_user).order('updated_at DESC') else - OrderCycle.ransack(params[:q]).result.accessible_by(spree_current_user) + OrderCycle.preload(:schedules).ransack(params[:q]).result.accessible_by(spree_current_user) end ocs.undated + @@ -132,7 +132,7 @@ module Admin params[:q] = { g: [ params.delete(:q) || {}, { m: 'or', orders_close_at_gt: orders_close_at_gt, orders_close_at_null: true } ] } - @order_cycle_set = OrderCycleSet.new :collection => (@collection = collection) + @collection = collection end end diff --git a/app/controllers/admin/schedules_controller.rb b/app/controllers/admin/schedules_controller.rb new file mode 100644 index 0000000000..4b31004fb2 --- /dev/null +++ b/app/controllers/admin/schedules_controller.rb @@ -0,0 +1,38 @@ +require 'open_food_network/permissions' + +module Admin + class SchedulesController < ResourceController + + respond_override create: { json: { + success: lambda { + binding.pry + render_as_json @schedule, editable_schedule_ids: permissions.editable_schedules.pluck(:id) + }, + failure: lambda { render json: { errors: @schedule.errors.full_messages }, status: :unprocessable_entity } + } } + + + def index + respond_to do |format| + format.json do + render_as_json @collection, ams_prefix: params[:ams_prefix], editable_schedule_ids: permissions.editable_schedules.pluck(:id) + end + end + end + + private + def collection + return Schedule.where("1=0") unless json_request? + permissions.visible_schedules + end + + def collection_actions + [:index] + end + + def permissions + return @permissions unless @permission.nil? + @permissions = OpenFoodNetwork::Permissions.new(spree_current_user) + end + end +end diff --git a/app/models/schedule.rb b/app/models/schedule.rb index d6b505ba4d..3b46307a55 100644 --- a/app/models/schedule.rb +++ b/app/models/schedule.rb @@ -1,5 +1,5 @@ class Schedule < ActiveRecord::Base has_and_belongs_to_many :order_cycles, join_table: 'order_cycle_schedules' - attr_sccessible :name, :order_cycle_ids + attr_accessible :name, :order_cycle_ids end diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index cbcf086682..38eed4f408 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -185,6 +185,7 @@ class AbilityDecorator can [:admin, :index, :read, :edit, :update], OrderCycle do |order_cycle| OrderCycle.accessible_by(user).include? order_cycle end + can [:admin, :index, :create], Schedule can [:bulk_update, :clone, :destroy, :notify_producers], OrderCycle do |order_cycle| user.enterprises.include? order_cycle.coordinator end diff --git a/app/serializers/api/admin/index_order_cycle_serializer.rb b/app/serializers/api/admin/index_order_cycle_serializer.rb index 876c3d131b..16eb86b2d6 100644 --- a/app/serializers/api/admin/index_order_cycle_serializer.rb +++ b/app/serializers/api/admin/index_order_cycle_serializer.rb @@ -7,6 +7,8 @@ class Api::Admin::IndexOrderCycleSerializer < ActiveModel::Serializer attributes :coordinator, :producers, :shops, :viewing_as_coordinator attributes :edit_path, :clone_path, :delete_path + has_many :schedules, serializer: Api::Admin::IdNameSerializer + def deletable can_delete?(object) end diff --git a/app/serializers/api/admin/schedule_serializer.rb b/app/serializers/api/admin/schedule_serializer.rb new file mode 100644 index 0000000000..63010ca429 --- /dev/null +++ b/app/serializers/api/admin/schedule_serializer.rb @@ -0,0 +1,9 @@ +class Api::Admin::ScheduleSerializer < ActiveModel::Serializer + attributes :id, :name, :order_cycle_ids, :viewing_as_coordinator + + has_many :order_cycles, serializer: Api::Admin::IdSerializer + + def viewing_as_coordinator + options[:editable_schedule_ids].include? object.id + end +end diff --git a/app/views/admin/order_cycles/_header.html.haml b/app/views/admin/order_cycles/_header.html.haml index bf005bfea2..72e00577f8 100644 --- a/app/views/admin/order_cycles/_header.html.haml +++ b/app/views/admin/order_cycles/_header.html.haml @@ -1,5 +1,6 @@ %colgroup %col{ ng: { show: 'columns.name.visible' } } + %col{ ng: { show: 'columns.schedule.visible' } } %col{ ng: { show: 'columns.open.visible' }, style: 'width: 20%;' } %col{ ng: { show: 'columns.close.visible' }, style: 'width: 20%;' } - unless simple_index @@ -15,6 +16,8 @@ %tr %th{ ng: { show: 'columns.name.visible' } } =t :name + %th{ ng: { show: 'columns.schedule.visible' } } + =t('admin.order_cycles.index.schedules') %th{ ng: { show: 'columns.open.visible' } } =t :open %th{ ng: { show: 'columns.close.visible' } } diff --git a/app/views/admin/order_cycles/_row.html.haml b/app/views/admin/order_cycles/_row.html.haml index 97c0f0989b..c1e80f33ac 100644 --- a/app/views/admin/order_cycles/_row.html.haml +++ b/app/views/admin/order_cycles/_row.html.haml @@ -2,6 +2,9 @@ %td{ ng: { show: 'columns.name.visible' } } %a{ ng: { href: '{{orderCycle.edit_path}}' } } {{ orderCycle.name }} + %td{ ng: { show: 'columns.schedule.visible' } } + %a{ href: '#', 'schedule-dialog' => true, 'schedule-id' => 'schedule.id', ng: { repeat: 'schedule in orderCycle.schedules'} } + {{ schedule.name }} %td{ ng: { show: 'columns.open.visible' } } %input.datetimepicker{ id: 'oc{{::orderCycle.id}}_orders_open_at', name: 'oc{{::orderCycle.id}}[orders_open_at]', type: 'text', ng: { if: 'orderCycle.viewing_as_coordinator', model: 'orderCycle.orders_open_at' }, datetimepicker: 'orderCycle.orders_open_at' } %input{ id: 'oc{{::orderCycle.id}}_orders_open_at', name: 'oc{{::orderCycle.id}}[orders_open_at]', type: 'text', ng: { if: '!orderCycle.viewing_as_coordinator', model: 'orderCycle.orders_open_at'}, disabled: true } diff --git a/app/views/admin/order_cycles/index.html.haml b/app/views/admin/order_cycles/index.html.haml index 0fdc9d688f..84d98a7aeb 100644 --- a/app/views/admin/order_cycles/index.html.haml +++ b/app/views/admin/order_cycles/index.html.haml @@ -5,6 +5,9 @@ = "ng-app='admin.orderCycles'" = content_for :page_actions do + %li + %a.button.icon-plus#new-schedule{ href: "#", "schedule-dialog" => true } + = t('admin.order_cycles.index.new_schedule') %li#new_order_cycle_link = button_link_to t(:new_order_cycle), main_app.new_admin_order_cycle_path, icon: 'icon-plus', id: 'admin_new_order_cycle_link' diff --git a/config/locales/en.yml b/config/locales/en.yml index 5fbe2469ac..187ee534de 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -676,6 +676,11 @@ en: debug_info: Debug information index: involving: Involving + schedules: Schedules + add_a_new_schedule: Add a new schedule + create_schedule: Create Schedule + schedule_name_placeholder: Schedule Name + name_required_error: Please enter a name for this schedule name_and_timing_form: name: Name orders_open: Orders open at diff --git a/config/routes.rb b/config/routes.rb index 360d0af7c2..c8592da54f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -174,6 +174,8 @@ Openfoodnetwork::Application.routes.draw do get :connect, on: :collection get :status, on: :collection end + + resources :schedules, only: [:index, :create], format: :json end namespace :api do diff --git a/lib/open_food_network/column_preference_defaults.rb b/lib/open_food_network/column_preference_defaults.rb index 1e69c91934..4e089e6365 100644 --- a/lib/open_food_network/column_preference_defaults.rb +++ b/lib/open_food_network/column_preference_defaults.rb @@ -86,13 +86,14 @@ module OpenFoodNetwork def order_cycles_index_columns node = "admin.order_cycles.index" { - name: { name: I18n.t("admin.name"), visible: true }, - open: { name: I18n.t("open"), visible: true }, - close: { name: I18n.t("close"), visible: true }, - producers: { name: I18n.t("label_producers"), visible: true }, - coordinator: { name: I18n.t("coordinator"), visible: true }, - shops: { name: I18n.t("label_shops"), visible: true }, - products: { name: I18n.t("products"), visible: true } + name: { name: I18n.t("admin.name"), visible: true }, + schedules: { name: I18n.t("#{node}.schedules"), visible: true }, + open: { name: I18n.t("open"), visible: true }, + close: { name: I18n.t("close"), visible: true }, + producers: { name: I18n.t("label_producers"), visible: true }, + coordinator: { name: I18n.t("coordinator"), visible: true }, + shops: { name: I18n.t("label_shops"), visible: true }, + products: { name: I18n.t("products"), visible: true } } end end diff --git a/lib/open_food_network/permissions.rb b/lib/open_food_network/permissions.rb index 0747df17ad..84905385ce 100644 --- a/lib/open_food_network/permissions.rb +++ b/lib/open_food_network/permissions.rb @@ -127,6 +127,17 @@ module OpenFoodNetwork @user.enterprises.length == 1 end + def editable_schedules + Schedule.joins(:order_cycles).where(coordinator_id: managed_enterprises.pluck(:id)).select("DISTINCT schedules.*") + end + + def visible_schedules + managed_enterprise_ids = managed_enterprises.pluck(:id) + Schedule.joins(order_cycles: :exchanges) + .where('exchanges.sender_id IN (?) OR exchanges.receiver_id IN (?)', managed_enterprise_ids, managed_enterprise_ids) + .select("DISTINCT schedules.*") + end + private