1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-02-19 13:54:25 +01:00

[ongoing] spaces reservation calendar

This commit is contained in:
Sylvain 2017-02-23 17:45:55 +01:00
parent 1efd506a5d
commit 8e86c4b212
14 changed files with 549 additions and 38 deletions

View File

@ -171,3 +171,344 @@ Application.Controllers.controller 'ShowSpaceController', ['$scope', '$state', '
, (error)->
growl.warning(_t('space_show.the_space_cant_be_deleted_because_it_is_already_reserved_by_some_users'))
]
##
# Controller used in the spaces reservation agenda page.
# This controller is very similar to the machine reservation controller with one major difference: here, there is many places
# per slots.
##
Application.Controllers.controller "ReserveSpaceController", ["$scope", '$stateParams', 'Auth', '$timeout', 'Availability', 'Member', 'availabilitySpacesPromise', 'plansPromise', 'groupsPromise', 'settingsPromise', 'spacePromise', '_t', 'uiCalendarConfig', 'CalendarConfig'
($scope, $stateParams, Auth, $timeout, Availability, Member, availabilitySpacesPromise, plansPromise, groupsPromise, settingsPromise, spacePromise, _t, uiCalendarConfig, CalendarConfig) ->
### PRIVATE STATIC CONSTANTS ###
# Color of the selected event backgound
SELECTED_EVENT_BG_COLOR = '#ffdd00'
# Slot free to be booked
FREE_SLOT_BORDER_COLOR = '<%= AvailabilityHelper::SPACE_COLOR %>'
### PUBLIC SCOPE ###
## bind the spaces availabilities with full-Calendar events
$scope.eventSources = [ { events: availabilitySpacesPromise, textColor: 'black' } ]
## the user to deal with, ie. the current user for non-admins
$scope.ctrl =
member: {}
## list of plans, classified by group
$scope.plansClassifiedByGroup = []
for group in groupsPromise
groupObj = { id: group.id, name: group.name, plans: [] }
for plan in plansPromise
groupObj.plans.push(plan) if plan.group_id == group.id
$scope.plansClassifiedByGroup.push(groupObj)
## mapping of fullCalendar events.
$scope.events =
reserved: [] # Slots that the user wants to book
modifiable: null # Slot that the user wants to change
placable: null # Destination slot for the change
paid: [] # Slots that were just booked by the user (transaction ok)
moved: null # Slots that were just moved by the user (change done) -> {newSlot:* oldSlot: *}
## the moment when the slot selection changed for the last time, used to trigger changes in the cart
$scope.selectionTime = null
## the last clicked event in the calender
$scope.selectedEvent = null
## indicates the state of the current view : calendar or plans information
$scope.plansAreShown = false
## will store the user's plan if he choosed to buy one
$scope.selectedPlan = null
## the moment when the plan selection changed for the last time, used to trigger changes in the cart
$scope.planSelectionTime = null
## Selected space
$scope.space = spacePromise
## fullCalendar (v2) configuration
$scope.calendarConfig = CalendarConfig
minTime: moment.duration(moment(settingsPromise.booking_window_start).format('HH:mm:ss'))
maxTime: moment.duration(moment(settingsPromise.booking_window_end).format('HH:mm:ss'))
eventClick: (event, jsEvent, view) ->
calendarEventClickCb(event, jsEvent, view)
eventRender: (event, element, view) ->
eventRenderCb(event, element, view)
## Application global settings
$scope.settings = settingsPromise
## Global config: message to the end user concerning the subscriptions rules
$scope.subscriptionExplicationsAlert = settingsPromise.subscription_explications_alert
## Global config: message to the end user concerning the space reservation
$scope.spaceExplicationsAlert = settingsPromise.space_explications_alert
##
# Change the last selected slot's appearence to looks like 'added to cart'
##
$scope.markSlotAsAdded = ->
$scope.selectedEvent.backgroundColor = SELECTED_EVENT_BG_COLOR
updateCalendar()
##
# Change the last selected slot's appearence to looks like 'never added to cart'
##
$scope.markSlotAsRemoved = (slot) ->
slot.backgroundColor = 'white'
slot.title = ''
slot.borderColor = FREE_SLOT_BORDER_COLOR
slot.id = null
slot.isValid = false
slot.is_reserved = false
slot.can_modify = false
slot.offered = false
slot.is_completed = false if slot.is_completed
updateCalendar()
##
# Callback when a slot was successfully cancelled. Reset the slot style as 'ready to book'
##
$scope.slotCancelled = ->
$scope.markSlotAsRemoved($scope.selectedEvent)
##
# Change the last selected slot's appearence to looks like 'currently looking for a new destination to exchange'
##
$scope.markSlotAsModifying = ->
$scope.selectedEvent.backgroundColor = '#eee'
$scope.selectedEvent.title = _t('space_reserve.i_change')
updateCalendar()
##
# Change the last selected slot's appearence to looks like 'the slot being exchanged will take this place'
##
$scope.changeModifyTrainingSlot = ->
if $scope.events.placable
$scope.events.placable.backgroundColor = 'white'
$scope.events.placable.title = ''
if !$scope.events.placable or $scope.events.placable._id != $scope.selectedEvent._id
$scope.selectedEvent.backgroundColor = '#bbb'
$scope.selectedEvent.title = _t('space_reserve.i_shift')
updateCalendar()
##
# When modifying an already booked reservation, callback when the modification was successfully done.
##
$scope.modifyTrainingSlot = ->
$scope.events.placable.title = if $scope.currentUser.role isnt 'admin' then _t('space_reserve.i_ve_reserved') else ''
$scope.events.placable.backgroundColor = 'white'
$scope.events.placable.borderColor = $scope.events.modifiable.borderColor
$scope.events.placable.id = $scope.events.modifiable.id
$scope.events.placable.is_reserved = true
$scope.events.placable.can_modify = true
$scope.events.modifiable.backgroundColor = 'white'
$scope.events.modifiable.title = ''
$scope.events.modifiable.borderColor = FREE_SLOT_BORDER_COLOR
$scope.events.modifiable.id = null
$scope.events.modifiable.is_reserved = false
$scope.events.modifiable.can_modify = false
$scope.events.modifiable.is_completed = false if $scope.events.modifiable.is_completed
updateCalendar()
##
# Cancel the current booking modification, reseting the whole process
##
$scope.cancelModifyTrainingSlot = ->
if $scope.events.placable
$scope.events.placable.backgroundColor = 'white'
$scope.events.placable.title = $scope.events.placable.training.name
$scope.events.modifiable.title = if $scope.currentUser.role isnt 'admin' then _t('i_ve_reserved') else ''
$scope.events.modifiable.backgroundColor = 'white'
updateCalendar()
##
# Callback to deal with the reservations of the user selected in the dropdown list instead of the current user's
# reservations. (admins only)
##
$scope.updateMember = ->
if $scope.ctrl.member
Member.get {id: $scope.ctrl.member.id}, (member) ->
$scope.ctrl.member = member
Availability.spaces {spaceId: $scope.space.id, member_id: $scope.ctrl.member.id}, (spaces) ->
uiCalendarConfig.calendars.calendar.fullCalendar 'removeEvents'
$scope.eventSources.splice(0, 1,
events: spaces
textColor: 'black'
)
# as the events are re-fetched for the new user, we must re-init the cart
$scope.events.reserved = []
$scope.selectedPlan = null
$scope.plansAreShown = false
##
# Add the provided plan to the current shopping cart
# @param plan {Object} the plan to subscribe
##
$scope.selectPlan = (plan) ->
# toggle selected plan
if $scope.selectedPlan != plan
$scope.selectedPlan = plan
else
$scope.selectedPlan = null
$scope.planSelectionTime = new Date()
##
# Changes the user current view from the plan subsription screen to the machine reservation agenda
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
##
$scope.doNotSubscribePlan = (e)->
e.preventDefault()
$scope.plansAreShown = false
$scope.selectedPlan = null
$scope.planSelectionTime = new Date()
##
# Switch the user's view from the reservation agenda to the plan subscription
##
$scope.showPlans = ->
$scope.plansAreShown = true
##
# Once the reservation is booked (payment process successfully completed), change the event style
# in fullCalendar, update the user's subscription and free-credits if needed
# @param reservation {Object}
##
$scope.afterPayment = (reservation)->
$scope.events.paid[0].backgroundColor = 'white'
$scope.events.paid[0].is_reserved = true
$scope.events.paid[0].can_modify = true
updateSpaceSlotId($scope.events.paid[0], reservation)
$scope.events.paid[0].borderColor = '#b2e774'
$scope.events.paid[0].title = _t('space_reserve.i_ve_reserved')
if $scope.selectedPlan
$scope.ctrl.member.subscribed_plan = angular.copy($scope.selectedPlan)
Auth._currentUser.subscribed_plan = angular.copy($scope.selectedPlan)
$scope.plansAreShown = false
$scope.selectedPlan = null
$scope.ctrl.member.training_credits = angular.copy(reservation.user.training_credits)
$scope.ctrl.member.machine_credits = angular.copy(reservation.user.machine_credits)
Auth._currentUser.training_credits = angular.copy(reservation.user.training_credits)
Auth._currentUser.machine_credits = angular.copy(reservation.user.machine_credits)
refetchCalendar()
### PRIVATE SCOPE ###
##
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
if $scope.currentUser.role isnt 'admin'
Member.get id: $scope.currentUser.id, (member) ->
$scope.ctrl.member = member
##
# Triggered when the user clicks on a reservation slot in the agenda.
# Defines the behavior to adopt depending on the slot status (already booked, free, ready to be reserved ...),
# the user's subscription (current or about to be took) and the time (the user cannot modify a booked reservation
# if it's too late).
# @see http://fullcalendar.io/docs/mouse/eventClick/
##
calendarEventClickCb = (event, jsEvent, view) ->
$scope.selectedEvent = event
if $stateParams.id is 'all'
$scope.training = event.training
$scope.selectionTime = new Date()
##
# Triggered when fullCalendar tries to graphicaly render an event block.
# Append the event tag into the block, just after the event title.
# @see http://fullcalendar.io/docs/event_rendering/eventRender/
##
eventRenderCb = (event, element, view)->
if $scope.currentUser.role is 'admin' and event.tags.length > 0
html = ''
for tag in event.tags
html += "<span class='label label-success text-white' title='#{tag.name}'>#{tag.name}</span>"
element.find('.fc-time').append(html)
return
##
# After payment, update the id of the newly reserved slot with the id returned by the server.
# This will allow the user to modify the reservation he just booked.
# @param slot {Object}
# @param reservation {Object}
##
updateSpaceSlotId = (slot, reservation)->
angular.forEach reservation.slots, (s)->
if slot.start_at == slot.start_at
slot.id = s.id
##
# Update the calendar's display to render the new attributes of the events
##
updateCalendar = ->
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
##
# Asynchronously fetch the events from the API and refresh the calendar's view with these new events
##
refetchCalendar = ->
$timeout ->
uiCalendarConfig.calendars.calendar.fullCalendar 'refetchEvents'
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
## !!! MUST BE CALLED AT THE END of the controller
initialize()
]

View File

@ -87,7 +87,7 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", '$sta
# Color of the selected event backgound
SELECTED_EVENT_BG_COLOR = '#ffdd00'
# Slot already booked by the current user
# Slot free to be booked
FREE_SLOT_BORDER_COLOR = '<%= AvailabilityHelper::TRAINING_COLOR %>'

View File

@ -445,6 +445,39 @@ angular.module('application.router', ['ui.router']).
]
.state 'app.logged.space_reserve',
url: '/spaces/:id/reserve'
abstract: Fablab.withoutSpaces
views:
'main@':
templateUrl: '<%= asset_path "spaces/reserve.html" %>'
controller: 'ReserveSpaceController'
resolve:
spacePromise: ['Space', '$stateParams', (Space, $stateParams)->
Space.get(id: $stateParams.id).$promise
]
availabilitySpacesPromise: ['Availability', '$stateParams', (Availability, $stateParams)->
Availability.spaces({spaceId: $stateParams.id}).$promise
]
plansPromise: ['Plan', (Plan)->
Plan.query().$promise
]
groupsPromise: ['Group', (Group)->
Group.query().$promise
]
settingsPromise: ['Setting', (Setting)->
Setting.query(names: "['booking_window_start',
'booking_window_end',
'booking_move_enable',
'booking_move_delay',
'booking_cancel_enable',
'booking_cancel_delay',
'subscription_explications_alert',
'space_explications_alert']").$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query(['app.logged.space_reserve', 'app.shared.plan_subscribe', 'app.shared.member_select',
'app.shared.stripe', 'app.shared.valid_reservation_modal', 'app.shared.confirm_modify_slot_modal',
'app.shared.wallet', 'app.shared.coupon_input', 'app.shared.cart']).$promise
]
# trainings
.state 'app.public.trainings_list',

View File

@ -17,6 +17,11 @@ Application.Services.factory 'Availability', ["$resource", ($resource)->
url: '/api/availabilities/trainings/:trainingId'
params: {trainingId: "@trainingId"}
isArray: true
spaces:
method: 'GET'
url: '/api/availabilities/spaces/:spaceId'
params: {spaceId: "@spaceId"}
isArray: true
update:
method: 'PUT'
]

View File

@ -7,17 +7,7 @@
</div>
<div class="col-xs-10 col-sm-10 col-md-8 b-l b-r-md">
<section class="heading-title">
<h1 ng-hide="training" translate>{{ 'trainings_planning' }}</h1>
<h1 ng-show="training"><span translate>{{ 'planning_of' }}</span> {{training.name}}</h1>
</section>
</div>
<div class="col-xs-12 col-sm-12 col-md-3 b-t hide-b-md">
<section class="heading-actions wrapper">
<a class="btn btn-lg btn-warning bg-white b-2x rounded m-t-xs"
ui-sref="app.logged.trainings_reserve({id:'all'})"
ng-show="training"
role="button"
translate>{{ 'all_trainings' }}</a>
<h1 translate translate-values="{NAME:space.name}">{{ 'space_reserve.planning_of_space_NAME' }}</h1>
</section>
</div>
</div>
@ -37,8 +27,6 @@
<select-member></select-member>
</div>
<cart slot="selectedEvent"
slot-selection-time="selectionTime"
events="events"
@ -55,27 +43,17 @@
on-slot-modify-unselect="changeModifyTrainingSlot"
on-slot-cancel-success="slotCancelled"
after-payment="afterPayment"
reservable-id="{{training.id}}"
reservable-type="Training"
reservable-name="{{training.name}}"
limit-to-one-slot="true"></cart>
reservable-id="{{space.id}}"
reservable-type="Space"
reservable-name="{{space.name}}"></cart>
<uib-alert type="info m">
<p class="text-sm font-bold">
<i class="fa fa-lightbulb-o"></i>
<span ng-bind-html="trainingInformationMessage"></span>
</p>
</uib-alert>
<uib-alert type="warning m">
<p class="text-sm">
<i class="fa fa-warning"></i>
<span ng-bind-html="trainingExplicationsAlert"></span>
<span ng-bind-html="spaceExplicationsAlert"></span>
</p>
</uib-alert>
</div>
</div>

View File

@ -156,6 +156,34 @@ class API::AvailabilitiesController < API::ApiController
end
end
def spaces
if params[:member_id]
@user = User.find(params[:member_id])
else
@user = current_user
end
@current_user_role = current_user.is_admin? ? 'admin' : 'user'
@space = Space.friendly.find(params[:space_id])
@slots = []
@reservations = Reservation.where('reservable_type = ? and reservable_id = ?', @space.class.to_s, @space.id).includes(:slots, user: [:profile]).references(:slots, :user).where('slots.start_at > ?', Time.now)
if @user.is_admin?
@availabilities = @space.availabilities.includes(:tags).where("end_at > ? AND available_type = 'space'", Time.now)
else
end_at = 1.month.since
end_at = 3.months.since if is_subscription_year(@user)
@availabilities = @space.availabilities.includes(:tags).where("end_at > ? AND end_at < ? AND available_type = 'space'", Time.now, end_at).where('availability_tags.tag_id' => @user.tag_ids.concat([nil]))
end
@availabilities.each do |a|
((a.end_at - a.start_at)/SLOT_DURATION.minutes).to_i.times do |i|
if (a.start_at + (i * SLOT_DURATION).minutes) > Time.now
slot = Slot.new(start_at: a.start_at + (i*SLOT_DURATION).minutes, end_at: a.start_at + (i*SLOT_DURATION).minutes + SLOT_DURATION.minutes, availability_id: a.id, availability: a, machine: @machine, title: '')
slot = verify_space_is_reserved(slot, @reservations, current_user, @current_user_role)
@slots << slot
end
end
end
end
def reservations
authorize Availability
@reservation_slots = @availability.slots.includes(reservation: [user: [:profile]]).order('slots.start_at ASC')
@ -203,6 +231,28 @@ class API::AvailabilitiesController < API::ApiController
slot
end
def verify_space_is_reserved(slot, reservations, user, user_role)
reservations.each do |r|
r.slots.each do |s|
if slot.availability.spaces.first.id == r.reservable_id
if s.start_at == slot.start_at and s.canceled_at == nil
slot.id = s.id
slot.is_reserved = true
slot.title = t('availabilities.not_available')
slot.can_modify = true if user_role === 'admin'
slot.reservation = r
end
if s.start_at == slot.start_at and r.user == user and s.canceled_at == nil
slot.title = t('availabilities.i_ve_reserved')
slot.can_modify = true
slot.is_reserved_by_current_user = true
end
end
end
end
slot
end
def verify_training_event_is_reserved(availability, reservations, user)
reservations.each do |r|
r.slots.each do |s|

View File

@ -21,7 +21,23 @@ module AvailabilityHelper
end
def machines_slot_border_color(slot)
slot.is_reserved ? (slot.is_reserved_by_current_user ? IS_RESERVED_BY_CURRENT_USER : IS_COMPLETED) : MACHINE_COLOR
if slot.is_reserved
slot.is_reserved_by_current_user ? IS_RESERVED_BY_CURRENT_USER : IS_COMPLETED
else
MACHINE_COLOR
end
end
def space_slot_border_color(slot)
if slot.is_reserved
if slot.is_reserved_by_current_user
IS_RESERVED_BY_CURRENT_USER
elsif slot.availability.is_completed
IS_COMPLETED
end
else
SPACE_COLOR
end
end
def trainings_events_border_color(availability)
@ -30,10 +46,15 @@ module AvailabilityHelper
elsif availability.is_completed
IS_COMPLETED
else
if availability.available_type == 'training'
TRAINING_COLOR
else
EVENT_COLOR
case availability.available_type
when 'training'
TRAINING_COLOR
when 'event'
EVENT_COLOR
when 'space'
SPACE_COLOR
else
'#000'
end
end
end

View File

@ -99,6 +99,13 @@ class Price < ActiveRecord::Base
_amount += get_slot_price(amount, slot, admin, _elements)
end
# Space reservation
when Space
amount = reservable.prices.find_by(group_id: user.group_id, plan_id: plan.try(:id)).amount
slots.each do |slot|
_amount += get_slot_price(amount, slot, admin, _elements)
end
# Unknown reservation type
else
raise NotImplementedError

View File

@ -126,6 +126,28 @@ class Reservation < ActiveRecord::Base
self.invoice.invoice_items.push InvoiceItem.new(amount: ii_amount, stp_invoice_item_id: (ii.id if ii), description: description)
end
# === Space reservation ===
when Space
base_amount = reservable.prices.find_by(group_id: user.group_id, plan_id: plan.try(:id)).amount
slots.each_with_index do |slot, index|
description = reservable.name + " #{I18n.l slot.start_at, format: :long} - #{I18n.l slot.end_at, format: :hour_minute}"
ii_amount = base_amount # ii_amount default to base_amount
ii_amount = 0 if slot.offered and on_site # if it's a local payment and slot is offered free
unless on_site # if it's local payment then do not create Stripe::InvoiceItem
ii = Stripe::InvoiceItem.create(
customer: user.stp_customer_id,
amount: ii_amount,
currency: Rails.application.secrets.stripe_currency,
description: description
)
invoice_items << ii
end
self.invoice.invoice_items.push InvoiceItem.new(amount: ii_amount, stp_invoice_item_id: (ii.id if ii), description: description)
end
# === Unknown reservation type ===
else
raise NotImplementedError

View File

@ -11,17 +11,19 @@ module UsersCredits
if user
@manager = Managers::User.new(user)
elsif reservation
if reservation.reservable_type == "Training"
if reservation.reservable_type == 'Training'
@manager = Managers::Training.new(reservation, plan)
elsif reservation.reservable_type == "Machine"
elsif reservation.reservable_type == 'Machine'
@manager = Managers::Machine.new(reservation, plan)
elsif reservation.reservable_type == "Event"
elsif reservation.reservable_type == 'Event'
@manager = Managers::Event.new(reservation, plan)
elsif reservation.reservable_type == 'Space'
@manager = Managers::Space.new(reservation, plan)
else
raise ArgumentError, "reservation.reservable_type must be Training, Machine or Event"
raise ArgumentError, 'reservation.reservable_type must be Training, Machine, Space or Event'
end
else
raise ArgumentError, "you have to pass either a reservation or a user to initialize a UsersCredits::Manager"
raise ArgumentError, 'you have to pass either a reservation or a user to initialize a UsersCredits::Manager'
end
end
@ -152,5 +154,14 @@ module UsersCredits
def update_credits
end
end
class Space < Reservation
def will_use_credits?
false
end
def update_credits
end
end
end
end

View File

@ -0,0 +1,26 @@
json.array!(@slots) do |slot|
json.id slot.id if slot.id
json.can_modify slot.can_modify
json.title slot.title
json.start slot.start_at.iso8601
json.end slot.end_at.iso8601
json.is_reserved slot.is_reserved
json.backgroundColor 'white'
json.borderColor space_slot_border_color(slot)
json.availability_id slot.availability_id
json.space do
json.id slot.availability.spaces.first.id
json.name slot.availability.spaces.first.name
end
# the user who booked the slot ...
json.user do
json.id slot.reservation.user.id
json.name slot.reservation.user.profile.full_name
end if @current_user_role == 'admin' and slot.reservation # ... if the slot was reserved
json.tag_ids slot.availability.tag_ids
json.tags slot.availability.tags do |t|
json.id t.id
json.name t.name
end
end

View File

@ -112,6 +112,14 @@ en:
cancel_my_selection: "Cancel my selection"
i_ve_reserved: "I've reserved"
space_reserve:
# book a space
space_reserve:
planning_of_space_NAME: "Planning of the {{NAME}} space" # angular interpolation
i_ve_reserved: "I've reserved"
i_shift: "I shift"
i_change: "I change"
notifications:
notifications_center: "Notifications center"
mark_all_as_read: "Mark all as read"

View File

@ -112,6 +112,14 @@ fr:
cancel_my_selection: "Annuler ma sélection"
i_ve_reserved: "J'ai réservé"
space_reserve:
# réserver un espace
space_reserve:
planning_of_space_NAME: "Planning de l'espace {{NAME}}" # angular interpolation
i_ve_reserved: "J'ai réservé"
i_shift: "Je déplace"
i_change: "Je change"
notifications:
notifications_center: "Centre de notifications"
mark_all_as_read: "Tout marquer comme lu"

View File

@ -81,6 +81,7 @@ Rails.application.routes.draw do
resources :availabilities do
get 'machines/:machine_id', action: 'machine', on: :collection
get 'trainings/:training_id', action: 'trainings', on: :collection
get 'spaces/:space_id', action: 'spaces', on: :collection
get 'reservations', on: :member
get 'public', on: :collection
end