Adding schedules to Order Cycles interface

This commit is contained in:
Rob Harrington
2016-07-08 14:34:03 +10:00
parent a25f2141a5
commit 920f52e112
22 changed files with 266 additions and 17 deletions

View File

@@ -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) ->

View File

@@ -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()

View File

@@ -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')

View File

@@ -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'
})

View File

@@ -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?

View File

@@ -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?

View File

@@ -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 }}

View File

@@ -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()' } }

View File

@@ -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;
}
}
}
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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' } }

View File

@@ -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 }

View File

@@ -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'

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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