1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-01-20 09:52:19 +01:00

Ability to define, per availability, a custom duration for the reservation slots

This commit is contained in:
Sylvain 2020-04-15 18:08:02 +02:00
parent 9735bd298e
commit ff75a96ecc
16 changed files with 124 additions and 70 deletions

View File

@ -1,5 +1,7 @@
# Changelog Fab-manager
- Ability to define, per availability, a custom duration for the reservation slots
## v4.3.4 2020 April 14
- Improved version check

View File

@ -419,6 +419,7 @@ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state
resolve: {
start() { return start; },
end() { return end; },
slots() { return Math.ceil(slots); },
machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }],
trainingsPromise: ['Training', function (Training) { return Training.query().$promise; }],
spacesPromise: ['Space', function (Space) { return Space.query().$promise; }],
@ -526,8 +527,8 @@ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state
/**
* Controller used in the slot creation modal window
*/
Application.Controllers.controller('CreateEventModalController', ['$scope', '$uibModalInstance', '$sce', 'moment', 'start', 'end', 'machinesPromise', 'Availability', 'trainingsPromise', 'spacesPromise', 'tagsPromise', 'plansPromise', 'groupsPromise', 'growl', '_t',
function ($scope, $uibModalInstance, $sce, moment, start, end, machinesPromise, Availability, trainingsPromise, spacesPromise, tagsPromise, plansPromise, groupsPromise, growl, _t) {
Application.Controllers.controller('CreateEventModalController', ['$scope', '$uibModalInstance', '$sce', 'moment', 'start', 'end', 'slots', 'machinesPromise', 'Availability', 'trainingsPromise', 'spacesPromise', 'tagsPromise', 'plansPromise', 'groupsPromise', 'growl', '_t',
function ($scope, $uibModalInstance, $sce, moment, start, end, slots, machinesPromise, Availability, trainingsPromise, spacesPromise, tagsPromise, plansPromise, groupsPromise, growl, _t) {
// $uibModal parameter
$scope.start = start;
@ -551,15 +552,6 @@ Application.Controllers.controller('CreateEventModalController', ['$scope', '$ui
$scope.selectedPlansBinding = {};
// list of plans, classified by group
$scope.plansClassifiedByGroup = [];
for (let group of Array.from(groupsPromise)) {
const groupObj = { id: group.id, name: group.name, plans: [] };
for (let plan of Array.from(plansPromise)) {
if (plan.group_id === group.id) { groupObj.plans.push(plan); }
}
if (groupObj.plans.length > 0) {
$scope.plansClassifiedByGroup.push(groupObj);
}
}
// machines associated with the created slot
$scope.selectedMachines = [];
@ -598,7 +590,8 @@ Application.Controllers.controller('CreateEventModalController', ['$scope', '$ui
is_recurrent: false,
period: 'week',
nb_periods: 1,
end_date: undefined // recurrence end
end_date: undefined, // recurrence end
slot_duration: Fablab.slotDuration
};
// recurrent slots
@ -613,9 +606,6 @@ Application.Controllers.controller('CreateEventModalController', ['$scope', '$ui
// localized name(s) of the selected plan(s)
$scope.plansName = '';
// make the duration available for display
$scope.slotDuration = Fablab.slotDuration;
/**
* Adds or removes the provided machine from the current slot
* @param machine {Object}
@ -731,6 +721,13 @@ Application.Controllers.controller('CreateEventModalController', ['$scope', '$ui
}
};
/*
* Test if the current availability type is divided in slots
*/
$scope.isTypeDivided = function () {
return isTypeDivided($scope.availability.available_type);
}
/* PRIVATE SCOPE */
/**
@ -752,21 +749,39 @@ Application.Controllers.controller('CreateEventModalController', ['$scope', '$ui
}
});
// group plans by Group
for (let group of Array.from(groupsPromise)) {
const groupObj = { id: group.id, name: group.name, plans: [] };
for (let plan of Array.from(plansPromise)) {
if (plan.group_id === group.id) { groupObj.plans.push(plan); }
}
if (groupObj.plans.length > 0) {
$scope.plansClassifiedByGroup.push(groupObj);
}
}
// When the slot duration changes, we increment the availability to match the value
$scope.$watch('availability.slot_duration', function (newValue, oldValue, scope) {
start = moment($scope.start);
start.add(newValue * slots, 'minutes');
$scope.end = start.toDate();
});
// When we configure a machine/space availability, do not let the user change the end time, as the total
// time must be dividable by Fablab.slotDuration minutes (base slot duration). For training availabilities, the user
// time must be dividable by $scope.availability.slot_duration minutes (base slot duration). For training availabilities, the user
// can configure any duration as it does not matters.
$scope.$watch('availability.available_type', function (newValue, oldValue, scope) {
if ((newValue === 'machines') || (newValue === 'space')) {
if (isTypeDivided(newValue)) {
$scope.endDateReadOnly = true;
const slots = Math.trunc(($scope.end.valueOf() - $scope.start.valueOf()) / (60 * 1000)) / Fablab.slotDuration;
const slots = Math.trunc(($scope.end.valueOf() - $scope.start.valueOf()) / (60 * 1000)) / $scope.availability.slot_duration;
if (!Number.isInteger(slots)) {
// otherwise, round it to upper decimal
const upper = Math.ceil(slots) * Fablab.slotDuration;
const upper = Math.ceil(slots) * $scope.availability.slot_duration;
$scope.end = moment($scope.start).add(upper, 'minutes').toDate();
}
return $scope.availability.end_at = $scope.end;
$scope.availability.end_at = $scope.end;
} else {
return $scope.endDateReadOnly = false;
$scope.endDateReadOnly = false;
}
});
@ -774,13 +789,13 @@ Application.Controllers.controller('CreateEventModalController', ['$scope', '$ui
// maintain the relative length of the slot (ie. change the end time accordingly)
$scope.$watch('start', function (newValue, oldValue, scope) {
// for machine or space availabilities, adjust the end time
if (($scope.availability.available_type === 'machines') || ($scope.availability.available_type === 'space')) {
if ($scope.isTypeDivided()) {
end = moment($scope.end);
end.add(moment(newValue).diff(oldValue), 'milliseconds');
$scope.end = end.toDate();
} else { // for training availabilities
// prevent the admin from setting the beginning after the end
if (moment(newValue).add(Fablab.slotDuration, 'minutes').isAfter($scope.end)) {
if (moment(newValue).add($scope.availability.slot_duration, 'minutes').isAfter($scope.end)) {
$scope.start = oldValue;
}
}
@ -791,7 +806,7 @@ Application.Controllers.controller('CreateEventModalController', ['$scope', '$ui
// Maintain consistency between the end time and the date object in the availability object
$scope.$watch('end', function (newValue, oldValue, scope) {
// we prevent the admin from setting the end of the availability before its beginning
if (moment($scope.start).add(Fablab.slotDuration, 'minutes').isAfter(newValue)) {
if (moment($scope.start).add($scope.availability.slot_duration, 'minutes').isAfter(newValue)) {
$scope.end = oldValue;
}
// update availability object
@ -799,6 +814,13 @@ Application.Controllers.controller('CreateEventModalController', ['$scope', '$ui
});
};
/*
* Test if the provided availability type is divided in slots
*/
const isTypeDivided = function (type) {
return ((type === 'machines') || (type === 'space'));
}
/**
* Validates that a machine or more was/were selected before continuing to step 3 (adjust time + tags)
*/

View File

@ -75,6 +75,13 @@
</div>
</div>
<div class="modal-body" ng-show="step === 3">
<div id="slotDuration" class="m-t-sm" ng-show="isTypeDivided()">
<p class="text-center font-sbold" translate>{{ 'app.admin.calendar.divide_this_availability' }}</p>
<div class="input-group">
<input type="number" class="form-control" ng-model="availability.slot_duration" step="5" />
<span class="input-group-addon" translate>{{ 'app.admin.calendar.minutes' }}</span>
</div>
</div>
<div id="timeAdjust" class="m-t-sm">
<p class="text-center font-sbold" translate>{{ 'app.admin.calendar.adjust_the_opening_hours' }}</p>
<div class="row">
@ -204,7 +211,7 @@
</ul>
<div class="alert alert-info text-xs">
<i class="fa fa-lightbulb-o m-r" aria-hidden="true"></i>
<span translate translate-values="{DURATION: slotDuration, COUNT: occurrences.length}"> {{ 'app.admin.calendar.divided_in_slots' }}</span>
<span translate translate-values="{DURATION: availability.slot_duration, COUNT: occurrences.length}"> {{ 'app.admin.calendar.divided_in_slots' }}</span>
</div>
<div>
<span class="underline" translate>{{ 'app.admin.calendar.reservable' }}</span>

View File

@ -147,7 +147,7 @@ class API::AvailabilitiesController < API::ApiController
def availability_params
params.require(:availability).permit(:start_at, :end_at, :available_type, :machine_ids, :training_ids, :nb_total_places,
:is_recurrent, :period, :nb_periods, :end_date,
:is_recurrent, :period, :nb_periods, :end_date, :slot_duration,
machine_ids: [], training_ids: [], space_ids: [], tag_ids: [], plan_ids: [],
machines_attributes: %i[id _destroy], plans_attributes: %i[id _destroy])
end

View File

@ -1,7 +1,8 @@
# frozen_string_literal: true
# API Controller for resources of type Slot
# Slots are used to cut Availabilities into reservable slots of ApplicationHelper::SLOT_DURATION minutes
# Slots are used to cut Availabilities into reservable slots. The duration of these slots is configured per
# availability by Availability.slot_duration, or otherwise globally by ApplicationHelper::SLOT_DURATION minutes
class API::SlotsController < API::ApiController
before_action :authenticate_user!
before_action :set_slot, only: %i[update cancel]

View File

@ -89,7 +89,8 @@ class Availability < ApplicationRecord
def available_space_places
return unless available_type == 'space'
((end_at - start_at) / ApplicationHelper::SLOT_DURATION.minutes).to_i * nb_total_places
duration = slot_duration || ApplicationHelper::SLOT_DURATION
((end_at - start_at) / duration.minutes).to_i * nb_total_places
end
def title(filter = {})
@ -159,9 +160,10 @@ class Availability < ApplicationRecord
private
def length_must_be_slot_multiple
return unless end_at < (start_at + ApplicationHelper::SLOT_DURATION.minutes)
duration = slot_duration || ApplicationHelper::SLOT_DURATION
return unless end_at < (start_at + duration.minutes)
errors.add(:end_at, I18n.t('availabilities.length_must_be_slot_multiple', MIN: ApplicationHelper::SLOT_DURATION))
errors.add(:end_at, I18n.t('availabilities.length_must_be_slot_multiple', MIN: duration))
end
def should_be_associated

View File

@ -1,6 +1,7 @@
# frozen_string_literal: true
# Time range of duration defined by ApplicationHelper::SLOT_DURATION, slicing an Availability.
# Time range, slicing an Availability.
# Its duration is defined by globally by ApplicationHelper::SLOT_DURATION but can be overridden per availability
# During a slot a Reservation is possible
# Only reserved slots are persisted in DB, others are instantiated on the fly
class Slot < ApplicationRecord

View File

@ -17,12 +17,13 @@ class Availabilities::AvailabilitiesService
slots = []
availabilities.each do |a|
((a.end_at - a.start_at) / ApplicationHelper::SLOT_DURATION.minutes).to_i.times do |i|
next unless (a.start_at + (i * ApplicationHelper::SLOT_DURATION).minutes) > DateTime.current || user.admin?
slot_duration = a.slot_duration || ApplicationHelper::SLOT_DURATION
((a.end_at - a.start_at) / slot_duration.minutes).to_i.times do |i|
next unless (a.start_at + (i * slot_duration).minutes) > DateTime.current || user.admin?
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,
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,
@ -43,12 +44,13 @@ class Availabilities::AvailabilitiesService
slots = []
availabilities.each do |a|
((a.end_at - a.start_at) / ApplicationHelper::SLOT_DURATION.minutes).to_i.times do |i|
next unless (a.start_at + (i * ApplicationHelper::SLOT_DURATION).minutes) > DateTime.current || user.admin?
slot_duration = a.slot_duration || ApplicationHelper::SLOT_DURATION
((a.end_at - a.start_at) / slot_duration.minutes).to_i.times do |i|
next unless (a.start_at + (i * slot_duration).minutes) > DateTime.current || user.admin?
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,
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,
space: space,

View File

@ -15,13 +15,14 @@ class Availabilities::PublicAvailabilitiesService
.where(lock: false)
slots = []
availabilities.each do |a|
slot_duration = a.slot_duration || ApplicationHelper::SLOT_DURATION
a.machines.each do |machine|
next unless machine_ids&.include?(machine.id.to_s)
((a.end_at - a.start_at) / ApplicationHelper::SLOT_DURATION.minutes).to_i.times do |i|
((a.end_at - a.start_at) / 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,
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,
@ -45,13 +46,14 @@ class Availabilities::PublicAvailabilitiesService
slots = []
availabilities.each do |a|
slot_duration = a.slot_duration || ApplicationHelper::SLOT_DURATION
space = a.spaces.first
((a.end_at - a.start_at) / ApplicationHelper::SLOT_DURATION.minutes).to_i.times do |i|
next unless (a.start_at + (i * ApplicationHelper::SLOT_DURATION).minutes) > DateTime.current
((a.end_at - a.start_at) / slot_duration.minutes).to_i.times do |i|
next unless (a.start_at + (i * slot_duration).minutes) > DateTime.current
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,
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,
space: space,

View File

@ -5,6 +5,7 @@ json.array!(@availabilities) do |availability|
json.title availability.title
json.start availability.start_at.iso8601
json.end availability.end_at.iso8601
json.slot_duration availability.slot_duration
json.available_type availability.available_type
json.machine_ids availability.machine_ids
json.training_ids availability.training_ids

View File

@ -3,6 +3,7 @@
json.extract! @availability, :id, :title, :lock, :is_recurrent, :occurrence_id, :period, :nb_periods, :end_date
json.start_at @availability.start_at.iso8601
json.end_at @availability.end_at.iso8601
json.slot_duration @availability.slot_duration
json.available_type @availability.available_type
json.machine_ids @availability.machine_ids
json.plan_ids @availability.plan_ids

View File

@ -16,10 +16,11 @@ wb.add_worksheet(name: t('export_availabilities.machines')) do |sheet|
# data rows
@availabilities.where(available_type: 'machines').order(:start_at).each do |a|
slot_duration = a.slot_duration || ApplicationHelper::SLOT_DURATION
a.machines.each do |m|
((a.end_at - a.start_at) / ApplicationHelper::SLOT_DURATION.minutes).to_i.times do |i|
start_at = a.start_at + (i * ApplicationHelper::SLOT_DURATION).minutes
end_at = a.start_at + (i * ApplicationHelper::SLOT_DURATION).minutes + ApplicationHelper::SLOT_DURATION.minutes
((a.end_at - a.start_at) / slot_duration.minutes).to_i.times do |i|
start_at = a.start_at + (i * slot_duration).minutes
end_at = a.start_at + (i * slot_duration).minutes + slot_duration.minutes
reservations = 0
if a.slots&.map(&:start_at)&.include? start_at
reservations = Reservation.where(reservable: m).includes(:slots).where('slots.id' => a.slots, 'slots.start_at' => start_at).count
@ -83,9 +84,10 @@ if Rails.application.secrets.fablab_without_spaces != 'false'
# data rows
@availabilities.where(available_type: 'space').order(:start_at).each do |a|
((a.end_at - a.start_at) / ApplicationHelper::SLOT_DURATION.minutes).to_i.times do |i|
start_at = a.start_at + (i * ApplicationHelper::SLOT_DURATION).minutes
end_at = a.start_at + (i * ApplicationHelper::SLOT_DURATION).minutes + ApplicationHelper::SLOT_DURATION.minutes
slot_duration = a.slot_duration || ApplicationHelper::SLOT_DURATION
((a.end_at - a.start_at) / slot_duration.minutes).to_i.times do |i|
start_at = a.start_at + (i * slot_duration).minutes
end_at = a.start_at + (i * slot_duration).minutes + slot_duration.minutes
reservations = a.slots.where(start_at: start_at).count
data = [

View File

@ -65,7 +65,7 @@ en:
select_nb_period: "Please select a number of periods for the recurrence"
select_end_date: "Please select the date of the last occurrence"
about_to_create: "You are about to create the following {TYPE, select, machines{machine} training{training} space{space} other{other}} {NUMBER, plural, one{slot} other{slots}}:"
divided_in_slots: "{COUNT, plural, =1{This slot} other{These slots}} will be open for booking in {DURATION}-minutes increments. Contact your system administrator to change this setting."
divided_in_slots: "{COUNT, plural, =1{This slot} other{These slots}} will be open for booking in {DURATION}-minutes increments."
reservable: "Reservable(s):"
labels: "Label(s):"
none: "None"
@ -98,6 +98,8 @@ en:
legend: "Legend"
and: "and"
external_sync: "Calendar synchronization"
divide_this_availability: "Divide this availability in slots of"
minutes: "minutes"
# import external iCal calendar
icalendar:
icalendar_import: "iCalendar import"

View File

@ -65,7 +65,7 @@ fr:
select_nb_period: "Veuillez choisir un nombre de périodes pour la récurrence"
select_end_date: "Veuillez choisir la date de dernière occurrence"
about_to_create: "Vous vous apprêtez à créer {NUMBER, plural, one{le créneau} other{les créneaux}} {TYPE, select, machines{machine} training{formation} space{espace} other{autre}} suivant :"
divided_in_slots: "{COUNT, plural, =1{Ce créneau sera proposé} other{Ces créneaux seront proposés}} à la réservation par tranches de {DURATION} minutes. Contactez votre administrateur système pour modifier ce paramètre."
divided_in_slots: "{COUNT, plural, =1{Ce créneau sera proposé} other{Ces créneaux seront proposés}} à la réservation par tranches de {DURATION} minutes."
reservable: "Réservable(s) :"
labels: "Étiquette(s) :"
none: "Aucune"

View File

@ -0,0 +1,8 @@
# frozen_string_literal:true
# From this migration any availability can override the default SLOT_DURATION value for its own slots
class AddSlotDurationToAvailability < ActiveRecord::Migration[5.2]
def change
add_column :availabilities, :slot_duration, :integer
end
end

View File

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2020_04_08_101654) do
ActiveRecord::Schema.define(version: 2020_04_15_141809) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_trgm"
@ -18,8 +18,8 @@ ActiveRecord::Schema.define(version: 2020_04_08_101654) do
enable_extension "unaccent"
create_table "abuses", id: :serial, force: :cascade do |t|
t.integer "signaled_id"
t.string "signaled_type"
t.integer "signaled_id"
t.string "first_name"
t.string "last_name"
t.string "email"
@ -48,8 +48,8 @@ ActiveRecord::Schema.define(version: 2020_04_08_101654) do
t.string "locality"
t.string "country"
t.string "postal_code"
t.integer "placeable_id"
t.string "placeable_type"
t.integer "placeable_id"
t.datetime "created_at"
t.datetime "updated_at"
end
@ -63,8 +63,8 @@ ActiveRecord::Schema.define(version: 2020_04_08_101654) do
end
create_table "assets", id: :serial, force: :cascade do |t|
t.integer "viewable_id"
t.string "viewable_type"
t.integer "viewable_id"
t.string "attachment"
t.string "type"
t.datetime "created_at"
@ -94,6 +94,7 @@ ActiveRecord::Schema.define(version: 2020_04_08_101654) do
t.string "period"
t.integer "nb_periods"
t.datetime "end_date"
t.integer "slot_duration"
end
create_table "availability_tags", id: :serial, force: :cascade do |t|
@ -131,8 +132,8 @@ ActiveRecord::Schema.define(version: 2020_04_08_101654) do
end
create_table "credits", id: :serial, force: :cascade do |t|
t.integer "creditable_id"
t.string "creditable_type"
t.integer "creditable_id"
t.integer "plan_id"
t.integer "hours"
t.datetime "created_at"
@ -284,8 +285,8 @@ ActiveRecord::Schema.define(version: 2020_04_08_101654) do
end
create_table "invoices", id: :serial, force: :cascade do |t|
t.integer "invoiced_id"
t.string "invoiced_type"
t.integer "invoiced_id"
t.string "stp_invoice_id"
t.integer "total"
t.datetime "created_at"
@ -348,15 +349,15 @@ ActiveRecord::Schema.define(version: 2020_04_08_101654) do
create_table "notifications", id: :serial, force: :cascade do |t|
t.integer "receiver_id"
t.integer "attached_object_id"
t.string "attached_object_type"
t.integer "attached_object_id"
t.integer "notification_type_id"
t.boolean "is_read", default: false
t.datetime "created_at"
t.datetime "updated_at"
t.string "receiver_type"
t.boolean "is_send", default: false
t.jsonb "meta_data", default: {}
t.jsonb "meta_data", default: "{}"
t.index ["notification_type_id"], name: "index_notifications_on_notification_type_id"
t.index ["receiver_id"], name: "index_notifications_on_receiver_id"
end
@ -456,8 +457,8 @@ ActiveRecord::Schema.define(version: 2020_04_08_101654) do
create_table "prices", id: :serial, force: :cascade do |t|
t.integer "group_id"
t.integer "plan_id"
t.integer "priceable_id"
t.string "priceable_type"
t.integer "priceable_id"
t.integer "amount"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
@ -564,8 +565,8 @@ ActiveRecord::Schema.define(version: 2020_04_08_101654) do
t.text "message"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "reservable_id"
t.string "reservable_type"
t.integer "reservable_id"
t.integer "nb_reserve_places"
t.integer "statistic_profile_id"
t.index ["reservable_type", "reservable_id"], name: "index_reservations_on_reservable_type_and_reservable_id"
@ -574,8 +575,8 @@ ActiveRecord::Schema.define(version: 2020_04_08_101654) do
create_table "roles", id: :serial, force: :cascade do |t|
t.string "name"
t.integer "resource_id"
t.string "resource_type"
t.integer "resource_id"
t.datetime "created_at"
t.datetime "updated_at"
t.index ["name", "resource_type", "resource_id"], name: "index_roles_on_name_and_resource_type_and_resource_id"
@ -866,8 +867,8 @@ ActiveRecord::Schema.define(version: 2020_04_08_101654) do
create_table "wallet_transactions", id: :serial, force: :cascade do |t|
t.integer "wallet_id"
t.integer "transactable_id"
t.string "transactable_type"
t.integer "transactable_id"
t.string "transaction_type"
t.integer "amount"
t.datetime "created_at", null: false