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

(feat) slots/availabilities status for pending reservations

This commit is contained in:
Sylvain 2023-01-11 17:32:16 +01:00
parent 6433767846
commit 9f9a2e616f
11 changed files with 154 additions and 38 deletions

View File

@ -37,7 +37,7 @@ class Availability < ApplicationRecord
scope :trainings, -> { includes(:trainings).where(available_type: 'training') }
scope :spaces, -> { includes(:spaces).where(available_type: 'space') }
attr_accessor :is_reserved, :current_user_slots_reservations_ids, :can_modify
attr_accessor :is_reserved, :current_user_slots_reservations_ids, :current_user_pending_reservations_ids, :can_modify
validates :start_at, :end_at, presence: true
validate :length_must_be_slot_multiple, unless: proc { end_at.blank? or start_at.blank? }

View File

@ -1,11 +1,6 @@
# frozen_string_literal: true
# Items that can be added to the shopping cart
module CartItem
def self.table_name_prefix
'cart_item_'
end
end
require_relative 'cart_item'
# This is an abstract class implemented by classes that can be added to the shopping cart
class CartItem::BaseItem < ApplicationRecord

View File

@ -0,0 +1,8 @@
# frozen_string_literal: true
# Items that can be added to the shopping cart
module CartItem
def self.table_name_prefix
'cart_item_'
end
end

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
require_relative 'cart_item'
# A discount coupon applied to the whole shopping cart
class CartItem::Coupon < ApplicationRecord
belongs_to :operator_profile, class_name: 'InvoicingProfile'

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
require_relative 'cart_item'
# A relation table between a pending event reservation and a special price for this event
class CartItem::EventReservationTicket < ApplicationRecord
belongs_to :cart_item_event_reservation, class_name: 'CartItem::EventReservation', inverse_of: :cart_item_event_reservation_tickets

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
require_relative 'cart_item'
# A payment schedule applied to plan in the shopping cart
class CartItem::PaymentSchedule < ApplicationRecord
belongs_to :customer_profile, class_name: 'InvoicingProfile'

View File

@ -1,8 +1,20 @@
# frozen_string_literal: true
require_relative 'cart_item'
# A relation table between a pending reservation and a slot
class CartItem::ReservationSlot < ApplicationRecord
self.table_name = 'cart_item_reservation_slots'
belongs_to :cart_item, polymorphic: true
belongs_to :cart_item_machine_reservation, foreign_type: 'CartItem::MachineReservation', foreign_key: 'cart_item_id',
inverse_of: :cart_item_reservation_slots, class_name: 'CartItem::MachineReservation'
belongs_to :cart_item_space_reservation, foreign_type: 'CartItem::SpaceReservation', foreign_key: 'cart_item_id',
inverse_of: :cart_item_reservation_slots, class_name: 'CartItem::SpaceReservation'
belongs_to :cart_item_training_reservation, foreign_type: 'CartItem::TrainingReservation', foreign_key: 'cart_item_id',
inverse_of: :cart_item_reservation_slots, class_name: 'CartItem::TrainingReservation'
belongs_to :cart_item_event_reservation, foreign_type: 'CartItem::EventReservation', foreign_key: 'cart_item_id',
inverse_of: :cart_item_reservation_slots, class_name: 'CartItem::EventReservation'
belongs_to :slot
belongs_to :slots_reservation

View File

@ -12,7 +12,7 @@ class Slot < ApplicationRecord
has_many :cart_item_reservation_slots, class_name: 'CartItem::ReservationSlot', dependent: :destroy
attr_accessor :is_reserved, :machine, :space, :title, :can_modify, :current_user_slots_reservations_ids
attr_accessor :is_reserved, :machine, :space, :title, :can_modify, :current_user_slots_reservations_ids, :current_user_pending_reservations_ids
def full?(reservable = nil)
availability_places = availability.available_places_per_slot(reservable)

View File

@ -2,6 +2,8 @@
# List all Availability's slots for the given resources
class Availabilities::AvailabilitiesService
# @param current_user [User]
# @param level [String]
def initialize(current_user, level = 'slot')
@current_user = current_user
@maximum_visibility = {
@ -13,6 +15,11 @@ class Availabilities::AvailabilitiesService
@level = level
end
# @param window [Hash] the time window the look through: {start: xxx, end: xxx}
# @option window [ActiveSupport::TimeWithZone] :start
# @option window [ActiveSupport::TimeWithZone] :end
# @param ids [Array<Integer>]
# @param events [Boolean] should events be included in the results?
def index(window, ids, events: false)
machines_availabilities = Setting.get('machines_module') ? machines(Machine.where(id: ids[:machines]), @current_user, window) : []
spaces_availabilities = Setting.get('spaces_module') ? spaces(Space.where(id: ids[:spaces]), @current_user, window) : []
@ -27,6 +34,11 @@ class Availabilities::AvailabilitiesService
end
# list all slots for the given machines, with visibility relative to the given user
# @param machines [ActiveRecord::Relation<Machine>]
# @param user [User]
# @param window [Hash] the time window the look through: {start: xxx, end: xxx}
# @option window [ActiveSupport::TimeWithZone] :start the beginning of the time window
# @option window [ActiveSupport::TimeWithZone] :end the end of the time window
def machines(machines, user, window)
ma_availabilities = Availability.includes(:machines_availabilities, :availability_tags, :machines, :slots_reservations,
slots: [:slots_reservations])
@ -41,6 +53,11 @@ class Availabilities::AvailabilitiesService
end
# list all slots for the given space, with visibility relative to the given user
# @param spaces [ActiveRecord::Relation<Space>]
# @param user [User]
# @param window [Hash] the time window the look through: {start: xxx, end: xxx}
# @option window [ActiveSupport::TimeWithZone] :start
# @option window [ActiveSupport::TimeWithZone] :end
def spaces(spaces, user, window)
sp_availabilities = Availability.includes('spaces_availabilities')
.where('spaces_availabilities.space_id': spaces.map(&:id))
@ -54,6 +71,11 @@ class Availabilities::AvailabilitiesService
end
# list all slots for the given training(s), with visibility relative to the given user
# @param trainings [ActiveRecord::Relation<Training>]
# @param user [User]
# @param window [Hash] the time window the look through: {start: xxx, end: xxx}
# @option window [ActiveSupport::TimeWithZone] :start
# @option window [ActiveSupport::TimeWithZone] :end
def trainings(trainings, user, window)
tr_availabilities = Availability.includes('trainings_availabilities')
.where('trainings_availabilities.training_id': trainings.map(&:id))
@ -67,6 +89,11 @@ class Availabilities::AvailabilitiesService
end
# list all slots for the given event(s), with visibility relative to the given user
# @param events [ActiveRecord::Relation<Event>]
# @param user [User]
# @param window [Hash] the time window the look through: {start: xxx, end: xxx}
# @option window [ActiveSupport::TimeWithZone] :start
# @option window [ActiveSupport::TimeWithZone] :end
def events(events, user, window)
ev_availabilities = Availability.includes('event').where('events.id': events.map(&:id))
availabilities = availabilities(ev_availabilities, 'event', user, window[:start], window[:end])
@ -80,6 +107,7 @@ class Availabilities::AvailabilitiesService
private
# @param user [User]
def subscription_year?(user)
user&.subscription && user.subscription.plan.interval == 'year' && user.subscription.expired_at >= Time.current
end
@ -87,10 +115,17 @@ class Availabilities::AvailabilitiesService
# members must have validated at least 1 training and must have a valid yearly subscription to view
# the trainings further in the futur. This is used to prevent users with a rolling subscription to take
# their first training in a very long delay.
# @param user [User]
def show_more_trainings?(user)
user&.trainings&.size&.positive? && subscription_year?(user)
end
# @param availabilities [ActiveRecord::Relation<Availability>]
# @param type [String]
# @param user [User]
# @param range_start [ActiveSupport::TimeWithZone]
# @param range_end [ActiveSupport::TimeWithZone]
# @return ActiveRecord::Relation<Availability>
def availabilities(availabilities, type, user, range_start, range_end)
# who made the request?
# 1) an admin (he can see all availabilities from 1 month ago to anytime in the future)

View File

@ -9,62 +9,121 @@ class Availabilities::StatusService
# check that the provided slot is reserved for the given reservable (machine, training or space).
# Mark it accordingly for display in the calendar
# @param slot [Slot]
# @param user [User]
# @param reservables [Array<Machine, Space, Training, Event>]
# @return [Slot]
def slot_reserved_status(slot, user, reservables)
if reservables.map(&:class).map(&:name).uniq.size > 1
raise TypeError('[Availabilities::StatusService#slot_reserved_status] reservables have differents types')
raise TypeError('[Availabilities::StatusService#slot_reserved_status] reservables have differents types: ' \
"#{reservables.map(&:class).map(&:name).uniq} , with slot #{slot.id}")
end
statistic_profile_id = user&.statistic_profile&.id
slots_reservations, user_slots_reservations = slots_reservations(slot.slots_reservations, reservables, user)
slots_reservations = slot.slots_reservations
.includes(:reservation)
.where('reservations.reservable_type': reservables.map(&:class).map(&:name))
.where('reservations.reservable_id': reservables.map(&:id))
.where('slots_reservations.canceled_at': nil)
pending_reserv_slot_ids = slot.cart_item_reservation_slots.select('id').map(&:id)
pending_reservations, user_pending_reservations = pending_reservations(pending_reserv_slot_ids, reservables, user)
user_slots_reservations = slots_reservations.where('reservations.statistic_profile_id': statistic_profile_id)
slot.is_reserved = !slots_reservations.empty?
slot.title = slot_title(slots_reservations, user_slots_reservations, reservables)
slot.can_modify = true if %w[admin manager].include?(@current_user_role) || !user_slots_reservations.empty?
slot.current_user_slots_reservations_ids = user_slots_reservations.map(&:id)
is_reserved = slots_reservations.count.positive? || pending_reservations.count.positive?
slot.is_reserved = is_reserved
slot.title = slot_title(slots_reservations, user_slots_reservations, user_pending_reservations, reservables)
slot.can_modify = true if %w[admin manager].include?(@current_user_role) || is_reserved
slot.current_user_slots_reservations_ids = user_slots_reservations.select('id').map(&:id)
slot.current_user_pending_reservations_ids = user_pending_reservations.select('id').map(&:id)
slot
end
# check that the provided ability is reserved by the given user
# @param availability [Availability]
# @param user [User]
# @param reservables [Array<Machine, Space, Training, Event>]
# @return [Availability]
def availability_reserved_status(availability, user, reservables)
if reservables.map(&:class).map(&:name).uniq.size > 1
raise TypeError('[Availabilities::StatusService#availability_reserved_status] reservables have differents types')
raise TypeError('[Availabilities::StatusService#availability_reserved_status] reservables have differents types: ' \
"#{reservables.map(&:class).map(&:name).uniq}, with availability #{availability.id}")
end
slots_reservations = availability.slots_reservations
.includes(:reservation)
.where('reservations.reservable_type': reservables.map(&:class).map(&:name))
.where('reservations.reservable_id': reservables.map(&:id))
.where('slots_reservations.canceled_at': nil)
slots_reservations, user_slots_reservations = slots_reservations(availability.slots_reservations, reservables, user)
user_slots_reservations = slots_reservations.where('reservations.statistic_profile_id': user&.statistic_profile&.id)
pending_reserv_slot_ids = availability.joins(slots: :cart_item_reservation_slots)
.select('cart_item_reservation_slots.id as cirs_id')
pending_reservations, user_pending_reservations = pending_reservations(pending_reserv_slot_ids, reservables, user)
availability.is_reserved = !slots_reservations.empty?
availability.current_user_slots_reservations_ids = user_slots_reservations.map(&:id)
availability.is_reserved = slots_reservations.count.positive? || pending_reservations.count.positive?
availability.current_user_slots_reservations_ids = user_slots_reservations.select('id').map(&:id)
availability.current_user_pending_reservations_ids = user_pending_reservations.select('id').map(&:id)
availability
end
private
def slot_title(slots_reservations, user_slots_reservations, reservables)
# @param slots_reservations [ActiveRecord::Relation<SlotsReservation>]
# @param user_slots_reservations [ActiveRecord::Relation<SlotsReservation>] same as slots_reservations but filtered by the current user
# @param user_pending_reservations [ActiveRecord::Relation<CartItem::ReservationSlot>]
# @param reservables [Array<Machine, Space, Training, Event>]
def slot_title(slots_reservations, user_slots_reservations, user_pending_reservations, reservables)
name = reservables.map(&:name).join(', ')
if user_slots_reservations.empty? && slots_reservations.empty?
if user_slots_reservations.count.zero? && slots_reservations.count.zero?
name
elsif user_slots_reservations.empty? && !slots_reservations.empty?
user_names = slots_reservations.map(&:reservation)
.map(&:user)
.map { |u| u&.profile&.full_name || I18n.t('availabilities.deleted_user') }
.join(', ')
"#{name} #{@show_name ? "- #{user_names}" : ''}"
elsif user_slots_reservations.count.zero? && slots_reservations.count.positive?
"#{name} #{@show_name ? "- #{slot_users_names(slots_reservations)}" : ''}"
elsif user_pending_reservations.count.positive?
"#{name} - #{I18n.t('availabilities.reserving')}"
else
"#{name} - #{I18n.t('availabilities.i_ve_reserved')}"
end
end
# @param slots_reservations [ActiveRecord::Relation<SlotsReservation>]
# @return [String]
def slot_users_names(slots_reservations)
slots_reservations.map(&:reservation)
.map(&:user)
.map { |u| u&.profile&.full_name || I18n.t('availabilities.deleted_user') }
.join(', ')
end
# @param slot_ids [Array<number>]
# @param reservables [Array<Machine, Space, Training, Event>]
# @param user [User]
# @return [Array<ActiveRecord::Relation<CartItem::ReservationSlot>>]
def pending_reservations(slot_ids, reservables, user)
reservable_types = reservables.map(&:class).map(&:name).uniq
if reservable_types.size > 1
raise TypeError("[Availabilities::StatusService#pending_reservations] reservables have differents types: #{reservable_types}")
end
relation = "cart_item_#{reservable_types.first&.downcase}_reservation"
table = reservable_types.first == 'Event' ? 'cart_item_event_reservations' : 'cart_item_reservations'
pending_reservations = CartItem::ReservationSlot.where(id: slot_ids)
.includes(relation.to_sym)
.where(table => { reservable_type: reservable_types })
.where(table => { reservable_id: reservables.map(&:id) })
user_pending_reservations = pending_reservations.where(table => { customer_profile_id: user&.invoicing_profile&.id })
[pending_reservations, user_pending_reservations]
end
# @param slots_reservations [ActiveRecord::Relation<SlotsReservation>]
# @param reservables [Array<Machine, Space, Training, Event>]
# @param user [User]
# @return [Array<ActiveRecord::Relation<SlotsReservation>>]
def slots_reservations(slots_reservations, reservables, user)
reservable_types = reservables.map(&:class).map(&:name).uniq
if reservable_types.size > 1
raise TypeError("[Availabilities::StatusService#slot_reservations] reservables have differents types: #{reservable_types}")
end
reservations = slots_reservations.includes(:reservation)
.where('reservations.reservable_type': reservable_types)
.where('reservations.reservable_id': reservables.map(&:id))
.where('slots_reservations.canceled_at': nil)
user_slots_reservations = reservations.where('reservations.statistic_profile_id': user&.statistic_profile&.id)
[reservations, user_slots_reservations]
end
end

View File

@ -63,6 +63,7 @@ en:
#availability slots in the calendar
availabilities:
not_available: "Not available"
reserving: "I'm reserving"
i_ve_reserved: "I've reserved"
length_must_be_slot_multiple: "must be at least %{MIN} minutes after the start date"
must_be_associated_with_at_least_1_machine: "must be associated with at least 1 machine"