mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-02-15 23:57:48 +00:00
Adding schedules to Order Cycles interface
This commit is contained in:
@@ -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) ->
|
||||
|
||||
@@ -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()
|
||||
@@ -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')
|
||||
@@ -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'
|
||||
})
|
||||
@@ -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?
|
||||
@@ -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?
|
||||
|
||||
@@ -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 }}
|
||||
@@ -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()' } }
|
||||
28
app/assets/stylesheets/admin/order_cycles.scss
Normal file
28
app/assets/stylesheets/admin/order_cycles.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
38
app/controllers/admin/schedules_controller.rb
Normal file
38
app/controllers/admin/schedules_controller.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
9
app/serializers/api/admin/schedule_serializer.rb
Normal file
9
app/serializers/api/admin/schedule_serializer.rb
Normal 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
|
||||
@@ -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' } }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user