1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2024-11-28 09:24:24 +01:00

[feature] ability to block new reservations (#78)

On a machine, space or training availability slot, if there's no current reservation or if all reservations were cancelled, the slot can be locked to prevent any new reservations on it. Once locked it will disapear from the members calendars. The admins can also to unlock these slots later.
This commit is contained in:
Sylvain 2017-09-07 10:48:21 +02:00
commit f4ddd92b40
19 changed files with 217 additions and 40 deletions

View File

@ -132,6 +132,40 @@ Application.Controllers.controller "AdminCalendarController", ["$scope", "$state
##
# Mark the selected slot as unavailable for new reservations or allow reservations again on it
##
$scope.toggleLockReservations = ->
# first, define a shortcut to the lock property
locked = $scope.availability.lock
# then check if we'll allow reservations locking
prevent = !locked # if currently locked, allow unlock anyway
if (!locked)
prevent = false
angular.forEach $scope.reservations, (r) ->
if r.canceled_at == null
prevent = true # if currently unlocked and has any non-cancelled reservation, disallow locking
if (!prevent)
# open a confirmation dialog
dialogs.confirm
resolve:
object: ->
title: _t('admin_calendar.confirmation_required')
msg: if locked then _t("admin_calendar.do_you_really_want_to_allow_reservations") else _t("admin_calendar.do_you_really_want_to_block_this_slot")
, ->
# the admin has confirmed, lock/unlock the slot
Availability.lock {id: $scope.availability.id}, {lock: !locked}
, (data) -> # success
$scope.availability = data
growl.success(if locked then _t('admin_calendar.unlocking_success') else _t('admin_calendar.locking_success') )
uiCalendarConfig.calendars.calendar.fullCalendar 'refetchEvents'
, (error) -> # failed
growl.error(if locked then _t('admin_calendar.unlocking_failed') else _t('admin_calendar.locking_failed'))
else
growl.error(_t('admin_calendar.unlockable_because_reservations'))
### PRIVATE SCOPE ###
##

View File

@ -256,6 +256,9 @@ Application.Controllers.controller "ShowMachineController", ['$scope', '$state',
$state.go('app.public.machines_list')
, (error)->
growl.warning(_t('the_machine_cant_be_deleted_because_it_is_already_reserved_by_some_users'))
##
# Callback to book a reservation for the current machine
##
@ -412,6 +415,7 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", '$stat
updateCalendar()
##
# When modifying an already booked reservation, callback when the modification was successfully done.
##

View File

@ -24,4 +24,7 @@ Application.Services.factory 'Availability', ["$resource", ($resource)->
isArray: true
update:
method: 'PUT'
lock:
method: 'PUT'
url: '/api/availabilities/:id/lock'
]

View File

@ -360,6 +360,10 @@ body.container{
}
.reservations-locked {
background-color: #f5f5f5;
}
.reservation-canceled {
color: #606060;
border-radius: 0.2em;

View File

@ -42,11 +42,12 @@
</a>
<iframe name="export-frame" height="0" width="0" class="none"></iframe>
</div>
<div class="widget panel b-a m m-t-lg">
<div class="widget panel b-a m m-t-lg" ng-if="availability">
<div class="panel-heading b-b small">
<h3 translate>{{ 'admin_calendar.ongoing_reservations' }}</h3>
</div>
<div class="widget-content no-bg auto wrapper">
<div class="widget-content no-bg auto wrapper" ng-class="{'reservations-locked': availability.lock}">
<ul class="list-unstyled" ng-if="reservations.length > 0">
<li ng-repeat="r in reservations" class="m-b-xs" ng-class="{'reservation-canceled':r.canceled_at}">
{{r.user.name}}
@ -55,25 +56,42 @@
<span class="btn btn-warning btn-xs" ng-click="cancelBooking(r)" ng-if="!r.canceled_at"><i class="fa fa-times red"></i></span>
</li>
</ul>
<div ng-if="reservations.length == 0" translate>{{ 'admin_calendar.no_reservations' }}</div>
<div ng-show="reservations.length == 0" translate>{{ 'admin_calendar.no_reservations' }}</div>
<div class="m-t" ng-show="availability.lock"><i class="fa fa-ban"/> <span class="m-l-xs" translate>{{ 'admin_calendar.reservations_locked' }}</span></div>
</div>
</div>
<div class="widget panel b-a m m-t-lg" ng-if="availability.machine_ids.length > 0">
<div class="panel-heading b-b small">
<h3 translate>{{ 'admin_calendar.machines' }}</h3>
</div>
<div class="widget-content no-bg auto wrapper">
<ul class="list-unstyled">
<li ng-repeat="m in machines" class="m-b-xs" ng-show="availability.machine_ids.indexOf(m.id) > -1">
{{m.name}}
<span class="btn btn-warning btn-xs" ng-click="removeMachine(m)" ng-if="availability.machine_ids.length > 1"><i class="fa fa-times red"></i></span>
</li>
</ul>
</div>
</div>
<div class="widget panel b-a m m-t-lg" ng-if="availability">
<div class="panel-heading b-b small">
<h3 translate>{{ 'admin_calendar.actions' }}</h3>
</div>
<div class="widget-content no-bg auto wrapper">
<button class="btn btn-default" ng-click="toggleLockReservations()">
<span ng-hide="availability.lock">
<i class="fa fa-stop" />
<span class="m-l-xs" translate>{{ 'admin_calendar.block_reservations' }}</span>
</span>
<span ng-show="availability.lock">
<i class="fa fa-play" />
<span class="m-l-xs" translate>{{ 'admin_calendar.allow_reservations' }}</span>
</span>
</button>
</div>
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-3" ng-if="availability.machine_ids.length > 0">
<div class="widget panel b-a m m-t-lg">
<div class="panel-heading b-b small">
<h3 translate>{{ 'admin_calendar.machines' }}</h3>
</div>
<div class="widget-content no-bg auto wrapper">
<ul class="list-unstyled">
<li ng-repeat="m in machines" class="m-b-xs" ng-show="availability.machine_ids.indexOf(m.id) > -1">
{{m.name}}
<span class="btn btn-warning btn-xs" ng-click="removeMachine(m)" ng-if="availability.machine_ids.length > 1"><i class="fa fa-times red"></i></span>
</li>
</ul>
</div>
</div>
</div>
</section>
</section>

View File

@ -2,7 +2,7 @@ class API::AvailabilitiesController < API::ApiController
include FablabConfiguration
before_action :authenticate_user!, except: [:public]
before_action :set_availability, only: [:show, :update, :destroy, :reservations]
before_action :set_availability, only: [:show, :update, :destroy, :reservations, :lock]
before_action :define_max_visibility, only: [:machine, :trainings, :spaces]
respond_to :json
@ -21,22 +21,32 @@ class API::AvailabilitiesController < API::ApiController
def public
start_date = ActiveSupport::TimeZone[params[:timezone]].parse(params[:start])
end_date = ActiveSupport::TimeZone[params[:timezone]].parse(params[:end]).end_of_day
@reservations = Reservation.includes(:slots, user: [:profile]).references(:slots, :user).where('slots.start_at >= ? AND slots.end_at <= ?', start_date, end_date)
@reservations = Reservation.includes(:slots, user: [:profile]).references(:slots, :user)
.where('slots.start_at >= ? AND slots.end_at <= ?', start_date, end_date)
# request for 1 single day
if in_same_day(start_date, end_date)
# trainings, events
@training_and_event_availabilities = Availability.includes(:tags, :trainings, :event, :slots).where(available_type: %w(training event))
.where('start_at >= ? AND end_at <= ?', start_date, end_date)
.where(lock: false)
# machines
@machine_availabilities = Availability.includes(:tags, :machines).where(available_type: 'machines')
.where('start_at >= ? AND end_at <= ?', start_date, end_date)
.where(lock: false)
@machine_slots = []
@machine_availabilities.each do |a|
a.machines.each do |machine|
if params[:m] and params[:m].include?(machine.id.to_s)
((a.end_at - a.start_at)/ApplicationHelper::SLOT_DURATION.minutes).to_i.times do |i|
slot = Slot.new(start_at: a.start_at + (i*ApplicationHelper::SLOT_DURATION).minutes, end_at: a.start_at + (i*ApplicationHelper::SLOT_DURATION).minutes + ApplicationHelper::SLOT_DURATION.minutes, availability_id: a.id, availability: a, machine: machine, title: machine.name)
slot = Slot.new(
start_at: a.start_at + (i*ApplicationHelper::SLOT_DURATION).minutes,
end_at: a.start_at + (i*ApplicationHelper::SLOT_DURATION).minutes + ApplicationHelper::SLOT_DURATION.minutes,
availability_id: a.id,
availability: a,
machine: machine,
title: machine.name
)
slot = verify_machine_is_reserved(slot, @reservations, current_user, '')
@machine_slots << slot
end
@ -47,6 +57,7 @@ class API::AvailabilitiesController < API::ApiController
# spaces
@space_availabilities = Availability.includes(:tags, :spaces).where(available_type: 'space')
.where('start_at >= ? AND end_at <= ?', start_date, end_date)
.where(lock: false)
if params[:s]
@space_availabilities.where(available_id: params[:s])
@ -57,7 +68,14 @@ class API::AvailabilitiesController < API::ApiController
space = a.spaces.first
((a.end_at - a.start_at)/ApplicationHelper::SLOT_DURATION.minutes).to_i.times do |i|
if (a.start_at + (i * ApplicationHelper::SLOT_DURATION).minutes) > Time.now
slot = Slot.new(start_at: a.start_at + (i*ApplicationHelper::SLOT_DURATION).minutes, end_at: a.start_at + (i*ApplicationHelper::SLOT_DURATION).minutes + ApplicationHelper::SLOT_DURATION.minutes, availability_id: a.id, availability: a, space: space, title: space.name)
slot = Slot.new(
start_at: a.start_at + (i*ApplicationHelper::SLOT_DURATION).minutes,
end_at: a.start_at + (i*ApplicationHelper::SLOT_DURATION).minutes + ApplicationHelper::SLOT_DURATION.minutes,
availability_id: a.id,
availability: a,
space: space,
title: space.name
)
slot = verify_space_is_reserved(slot, @reservations, current_user, '')
@space_slots << slot
end
@ -69,6 +87,7 @@ class API::AvailabilitiesController < API::ApiController
else
@availabilities = Availability.includes(:tags, :machines, :trainings, :spaces, :event, :slots)
.where('start_at >= ? AND end_at <= ?', start_date, end_date)
.where(lock: false)
@availabilities.each do |a|
if a.available_type == 'training' or a.available_type == 'event'
a = verify_training_event_is_reserved(a, @reservations, current_user)
@ -125,16 +144,27 @@ class API::AvailabilitiesController < API::ApiController
@slots = []
@reservations = Reservation.where('reservable_type = ? and reservable_id = ?', @machine.class.to_s, @machine.id).includes(:slots, user: [:profile]).references(:slots, :user).where('slots.start_at > ?', Time.now)
if @user.is_admin?
@availabilities = @machine.availabilities.includes(:tags).where("end_at > ? AND available_type = 'machines'", Time.now)
@availabilities = @machine.availabilities.includes(:tags)
.where("end_at > ? AND available_type = 'machines'", Time.now)
.where(lock: false)
else
end_at = @visi_max_other
end_at = @visi_max_year if is_subscription_year(@user)
@availabilities = @machine.availabilities.includes(:tags).where("end_at > ? AND end_at < ? AND available_type = 'machines'", Time.now, end_at).where('availability_tags.tag_id' => @user.tag_ids.concat([nil]))
@availabilities = @machine.availabilities.includes(:tags).where("end_at > ? AND end_at < ? AND available_type = 'machines'", Time.now, end_at)
.where('availability_tags.tag_id' => @user.tag_ids.concat([nil]))
.where(lock: false)
end
@availabilities.each do |a|
((a.end_at - a.start_at)/ApplicationHelper::SLOT_DURATION.minutes).to_i.times do |i|
if (a.start_at + (i * ApplicationHelper::SLOT_DURATION).minutes) > Time.now
slot = Slot.new(start_at: a.start_at + (i*ApplicationHelper::SLOT_DURATION).minutes, end_at: a.start_at + (i*ApplicationHelper::SLOT_DURATION).minutes + ApplicationHelper::SLOT_DURATION.minutes, availability_id: a.id, availability: a, machine: @machine, title: '')
slot = Slot.new(
start_at: a.start_at + (i*ApplicationHelper::SLOT_DURATION).minutes,
end_at: a.start_at + (i*ApplicationHelper::SLOT_DURATION).minutes + ApplicationHelper::SLOT_DURATION.minutes,
availability_id: a.id,
availability: a,
machine: @machine,
title: ''
)
slot = verify_machine_is_reserved(slot, @reservations, current_user, @current_user_role)
@slots << slot
end
@ -167,12 +197,17 @@ class API::AvailabilitiesController < API::ApiController
# who made the request?
# 1) an admin (he can see all future availabilities)
if current_user.is_admin?
@availabilities = @availabilities.includes(:tags, :slots, trainings: [:machines]).where('availabilities.start_at > ?', Time.now)
@availabilities = @availabilities.includes(:tags, :slots, trainings: [:machines])
.where('availabilities.start_at > ?', Time.now)
.where(lock: false)
# 2) an user (he cannot see availabilities further than 1 (or 3) months)
else
end_at = @visi_max_year
end_at = @visi_max_year if can_show_slot_plus_three_months(@user)
@availabilities = @availabilities.includes(:tags, :slots, :availability_tags, trainings: [:machines]).where('availabilities.start_at > ? AND availabilities.start_at < ?', Time.now, end_at).where('availability_tags.tag_id' => @user.tag_ids.concat([nil]))
@availabilities = @availabilities.includes(:tags, :slots, :availability_tags, trainings: [:machines])
.where('availabilities.start_at > ? AND availabilities.start_at < ?', Time.now, end_at)
.where('availability_tags.tag_id' => @user.tag_ids.concat([nil]))
.where(lock: false)
end
# finally, we merge the availabilities with the reservations
@ -190,18 +225,32 @@ class API::AvailabilitiesController < API::ApiController
@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)
@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 current_user.is_admin?
@availabilities = @space.availabilities.includes(:tags).where("end_at > ? AND available_type = 'space'", Time.now)
@availabilities = @space.availabilities.includes(:tags)
.where("end_at > ? AND available_type = 'space'", Time.now)
.where(lock: false)
else
end_at = @visi_max_other
end_at = @visi_max_year 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]))
@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]))
.where(lock: false)
end
@availabilities.each do |a|
((a.end_at - a.start_at)/ApplicationHelper::SLOT_DURATION.minutes).to_i.times do |i|
if (a.start_at + (i * ApplicationHelper::SLOT_DURATION).minutes) > Time.now
slot = Slot.new(start_at: a.start_at + (i*ApplicationHelper::SLOT_DURATION).minutes, end_at: a.start_at + (i*ApplicationHelper::SLOT_DURATION).minutes + ApplicationHelper::SLOT_DURATION.minutes, availability_id: a.id, availability: a, space: @space, title: '')
slot = Slot.new(
start_at: a.start_at + (i*ApplicationHelper::SLOT_DURATION).minutes,
end_at: a.start_at + (i*ApplicationHelper::SLOT_DURATION).minutes + ApplicationHelper::SLOT_DURATION.minutes,
availability_id: a.id,
availability: a,
space: @space,
title: ''
)
slot = verify_space_is_reserved(slot, @reservations, @user, @current_user_role)
@slots << slot
end
@ -235,6 +284,15 @@ class API::AvailabilitiesController < API::ApiController
end
end
def lock
authorize @availability
if @availability.update_attributes(lock: lock_params)
render :show, status: :ok, location: @availability
else
render json: @availability.errors, status: :unprocessable_entity
end
end
private
def set_availability
@availability = Availability.find(params[:id])
@ -245,6 +303,10 @@ class API::AvailabilitiesController < API::ApiController
:machines_attributes => [:id, :_destroy])
end
def lock_params
params.require(:lock)
end
def is_reserved_availability(availability, user_id)
reserved_slots = []
availability.slots.each do |s|

View File

@ -0,0 +1,3 @@
# Raised when total of reservation isn't equal to the total of stripe's invoice
class InvoiceTotalDifferentError < StandardError
end

View File

@ -1,3 +0,0 @@
# Raised when total of reservation isnt equal total of strip's invoice
class InvoiceTotalDiffrentError < StandardError
end

View File

@ -0,0 +1,3 @@
# Raised when reserving on a locked availability
class LockedError < StandardError
end

View File

@ -47,6 +47,13 @@ class Reservation < ActiveRecord::Base
plan = nil
end
# check that none of the reserved availabilities was locked
slots.each do |slot|
if slot.availability.lock
raise LockedError
end
end
case reservable
@ -260,7 +267,7 @@ class Reservation < ActiveRecord::Base
# this function only check reservation total is equal strip invoice total when
# pay only reservation not reservation + subscription
#if !is_equal_reservation_total_and_stp_invoice_total(stp_invoice, coupon_code)
#raise InvoiceTotalDiffrentError
#raise InvoiceTotalDifferentError
#end
stp_invoice.pay
card.delete if card

View File

@ -1,5 +1,5 @@
class AvailabilityPolicy < ApplicationPolicy
%w(index? show? create? update? destroy? reservations? export?).each do |action|
%w(index? show? create? update? destroy? reservations? export? lock?).each do |action|
define_method action do
user.is_admin?
end

View File

@ -6,11 +6,12 @@ json.array!(@availabilities) do |availability|
json.available_type availability.available_type
json.machine_ids availability.machine_ids
json.training_ids availability.training_ids
json.backgroundColor 'white'
json.backgroundColor !availability.lock ? 'white' : '#f5f5f5'
json.borderColor availability_border_color(availability)
json.tag_ids availability.tag_ids
json.tags availability.tags do |t|
json.id t.id
json.name t.name
end
json.lock availability.lock
end

View File

@ -11,3 +11,4 @@ json.tags @availability.tags do |t|
json.id t.id
json.name t.name
end
json.lock @availability.lock

View File

@ -48,6 +48,17 @@ en:
unable_to_delete_the_slot_START-END_because_it_s_already_reserved_by_a_member: "Unable to delete the slot {{START}} - {{END}} because it's already reserved by a member" # angular interpolation
you_should_select_at_least_a_machine: "You should select at least one machine on this slot."
export_is_running_you_ll_be_notified_when_its_ready: "Export is running. You'll be notified when it's ready."
actions: "Actions"
block_reservations: "Block reservations"
do_you_really_want_to_block_this_slot: "Do you really want to block new reservations on this slot? It will become invisible to users."
locking_success: "Slot successfully locked, it won't appear any longer in the user calendar"
locking_failed: "An error occurred. Slot locking has failed"
allow_reservations: "Allow reservations"
do_you_really_want_to_allow_reservations: "Do you really want to allow booking again on this slot? It will become visible for the users."
unlocking_success: "Slot successfully unlocked, it will appear again in the user calendar"
unlocking_failed: "An error occurred. Slot unlocking has failed"
reservations_locked: "Booking is blocked"
unlockable_because_reservations: "Unable to block booking on this slot because some uncancelled reservations exist on it."
project_elements:
# management of the projects' components

View File

@ -48,6 +48,17 @@ fr:
unable_to_delete_the_slot_START-END_because_it_s_already_reserved_by_a_member: "Le créneau {{START}} - {{END}} n'a pu être supprimé car il est déjà réservé par un membre" # angular interpolation
you_should_select_at_least_a_machine: "Vous devriez sélectionne au moins une machine pour ce créneau."
export_is_running_you_ll_be_notified_when_its_ready: "L'export est en cours. Vous serez notifié lorsqu'il sera prêt."
actions: "Actions"
block_reservations: "Bloquer les réservations"
do_you_really_want_to_block_this_slot: "Êtes vous sur de vouloir bloquer les nouvelles réservations sur ce créneau ? Il deviendra alors invisible pour les utilisateurs."
locking_success: "Le créneau a bien été verrouillé, il n'apparaitra plus dans le calendrier utilisateur"
locking_failed: "Une erreur est survenue. Le verrouillage du créneau a échoué"
allow_reservations: "Autoriser les réservations"
do_you_really_want_to_allow_reservations: "Êtes vous sur de vouloir autoriser de nouveau la prise de réservations sur ce créneau ? Il deviendra alors visible pour les utilisateurs."
unlocking_success: "Le créneau a bien été déverrouillé, il apparaîtra de nouveau dans le calendrier utilisateur"
unlocking_failed: "Une erreur est survenue. Le déverrouillage du créneau a échoué"
reservations_locked: "Réservations bloquées"
unlockable_because_reservations: "Impossible de bloquer les réservations sur ce créneau car il existe des réservations non annulées sur celui-ci."
project_elements:
# gestion des éléments constituant les projets

View File

@ -48,6 +48,17 @@ pt:
unable_to_delete_the_slot_START-END_because_it_s_already_reserved_by_a_member: "Não é possível deletar o slot {{START}} - {{END}} porque já foi reservado por um membro" # angular interpolation
you_should_select_at_least_a_machine: "Você deveria selecionar a última máquina neste slot."
export_is_running_you_ll_be_notified_when_its_ready: "A Exportação está em andamento. Você será notificado quando terminar."
actions: "Ações"
block_reservations: "Impedir reservas"
do_you_really_want_to_block_this_slot: "Você realmente quer bloquear novas reservas neste slot? Isso se tornará invisível para os usuários."
locking_success: "Slot bloqueado com êxito, ela não aparecerá mais tempo no calendário do usuário"
locking_failed: "Um erro ocorreu. O bloqueio do slot falhou"
allow_reservations: "Permitir a reserva"
do_you_really_want_to_allow_reservations: "Você realmente quer permitir a reserva novamente neste slot? Será visível para os usuários."
unlocking_success: "Slot desbloqueado com sucesso, ele aparecerá novamente no calendário do usuário"
unlocking_failed: "Um erro ocorreu. O desbloqueio do slot falhou"
reservations_locked: "Reserva é bloqueado"
unlockable_because_reservations: "Não é possível bloquear a reserva neste slot porque existem algumas reservas não cancelados nele."
project_elements:
# management of the projects' components

View File

@ -85,6 +85,7 @@ Rails.application.routes.draw do
get 'reservations', on: :member
get 'public', on: :collection
get '/export_index', action: 'export_availabilities', on: :collection
put ':id/lock', action: 'lock', on: :collection
end
resources :groups, only: [:index, :create, :update, :destroy]

View File

@ -0,0 +1,5 @@
class AddLockToAvailability < ActiveRecord::Migration
def change
add_column :availabilities, :lock, :boolean, default: false
end
end

View File

@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20170227114634) do
ActiveRecord::Schema.define(version: 20170906100906) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -79,6 +79,7 @@ ActiveRecord::Schema.define(version: 20170227114634) do
t.datetime "updated_at"
t.integer "nb_total_places"
t.boolean "destroying", default: false
t.boolean "lock", default: false
end
create_table "availability_tags", force: :cascade do |t|