1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-02-17 11:54:22 +01:00

Ability to configure allowing or preventing member book a machine/formation/event slot if he already have a reservation the same day at the same time

This commit is contained in:
Du Peng 2020-02-05 16:53:47 +01:00
parent 767a8cd332
commit 7c86adde4b
16 changed files with 193 additions and 16 deletions

View File

@ -1,5 +1,6 @@
# Changelog Fab Manager
- Ability to configure allowing or preventing member book a machine/formation/event slot if he already have a reservation the same day at the same time
- Ability to create and delete periodic calendar availabilities (recurrence)
- Ability to fully customize the home page
- Automated setup assistant
@ -45,6 +46,7 @@
- [TODO DEPLOY] add the `PHONE_REQUIRED` environment variable (see [doc/environment.md](doc/environment.md#PHONE_REQUIRED) for configuration details)
- [TODO DEPLOY] add the `EVENTS_IN_CALENDAR` environment variable (see [doc/environment.md](doc/environment.md#EVENTS_IN_CALENDAR) for configuration details)
- [TODO DEPLOY] add the `USER_CONFIRMATION_NEEDED_TO_SIGN_IN` environment variable (see [doc/environment.md](doc/environment.md#USER_CONFIRMATION_NEEDED_TO_SIGN_IN) for configuration details)
- [TODO DEPLOY] add the `BOOK_SLOT_AT_SAME_TIME` environment variable (see [doc/environment.md](doc/environment.md#BOOK_SLOT_AT_SAME_TIME) for configuration details)
- [TODO DEPLOY] -> (only dev) `bundle install && yarn install`
- [TODO DEPLOY] `rake db:migrate && rake db:seed`
- [TODO DEPLOY] `rake fablab:fix:name_stylesheet`
@ -140,7 +142,7 @@
## v4.0.4 2019 August 14
- Fix a bug: #140 VAT rate is erroneous in invoices.
Note: this bug was introduced in v4.0.3 and requires (if you are on v4.0.3) to regenerate the invoices since August 1st (if
Note: this bug was introduced in v4.0.3 and requires (if you are on v4.0.3) to regenerate the invoices since August 1st (if
- [TODO DEPLOY] `rake fablab:maintenance:regenerate_invoices[2019,8]`
## v4.0.3 2019 August 01
@ -184,7 +186,7 @@
- Refactored user's profile to keep invoicing data after an user was deleted
- Refactored user's profile to keep statistical data after an user was deleted
- Ability to delete an user (fixes #129 and #120)
- Ask user acceptance before deposing analytics cookies
- Ask user acceptance before deposing analytics cookies
- Fix a bug: (spanish) some translations are not loaded correctly
- Fix a bug: some users may not appear in the admin's general listing
- Fix a bug: Availabilities export report an erroneous number of reservations for machine availabilities (#131)
@ -381,8 +383,8 @@
- Fix a security issue: sprockets < 2.12.5 has a security vulnerability as described in [CVE-2018-3760](https://nvd.nist.gov/vuln/detail/CVE-2018-3760)
- Ensure elasticSearch indices are started with green status on new installations
- Refactored User.to_json to remove code duplication
- Fixed syntax and typos in README
- [TODO DEPLOY] **IMPORTANT** Please read [elastic_upgrade.md](doc/elastic_upgrade.md) for instructions on upgrading ElasticSearch.
- Fixed syntax and typos in README
- [TODO DEPLOY] **IMPORTANT** Please read [elastic_upgrade.md](doc/elastic_upgrade.md) for instructions on upgrading ElasticSearch.
- [TODO DEPLOY] `rake fablab:fix:categories_slugs`
- [TODO DEPLOY] -> (only dev) `bundle install`
- [TODO DEPLOY] `rake db:seed`
@ -395,7 +397,7 @@
- Set Stripe API version, all fab-managers has to use this version because codebase relies on it
- Fix a security issue: OmniAuth < 1.3.2 has a security vulnerability described in [CVE-2017-18076](https://nvd.nist.gov/vuln/detail/CVE-2017-18076)
- Fix a security issue: rack-protection < 1.5.5 has a security vulnerability described in [CVE-2018-1000119](https://nvd.nist.gov/vuln/detail/CVE-2018-1000119)
- Fix a security issue: http gem < 0.7.3 has a security vulnerability described in [CVE-2015-1828](https://nvd.nist.gov/vuln/detail/CVE-2015-1828), updates twitter gem as a dependency
- Fix a security issue: http gem < 0.7.3 has a security vulnerability described in [CVE-2015-1828](https://nvd.nist.gov/vuln/detail/CVE-2015-1828), updates twitter gem as a dependency
## v2.6.3 2018 January 2
@ -451,12 +453,12 @@
## v2.5.13 2017 September 11
- Fix a bug: ActiveRecord::RecordNotFound when running rake task fix:recursive_events_over_DST with recursive events which the initial event was deleted
- Fix a bug: ActiveRecord::RecordNotFound when running rake task fix:recursive_events_over_DST with recursive events which the initial event was deleted
## v2.5.12 2017 September 11
- Fix a bug: Long words overflow from homepage's events blocks
- Fix a bug: ActiveRecord::RecordNotFound when running rake task fix:recursive_events_over_DST with non-recursive events
- Fix a bug: ActiveRecord::RecordNotFound when running rake task fix:recursive_events_over_DST with non-recursive events
## v2.5.11 2017 September 7

View File

@ -246,13 +246,34 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
$scope.reserveSuccess = false;
if (!$scope.isAuthenticated()) {
return $scope.login(null, function (user) {
$scope.reserve.toReserve = !$scope.reserve.toReserve;
if (user.role !== 'admin') {
return $scope.ctrl.member = user;
}
const sameTimeReservations = findReservationsAtSameTime();
if (sameTimeReservations.length > 0) {
showReserveSlotSameTimeModal(sameTimeReservations, function(res) {
return $scope.reserve.toReserve = !$scope.reserve.toReserve;
});
} else {
return $scope.reserve.toReserve = !$scope.reserve.toReserve;
}
});
} else {
return $scope.reserve.toReserve = !$scope.reserve.toReserve;
if ($scope.currentUser.role === 'admin') {
return $scope.reserve.toReserve = !$scope.reserve.toReserve;
} else {
Member.get({ id: $scope.currentUser.id }, function (member) {
$scope.ctrl.member = member;
const sameTimeReservations = findReservationsAtSameTime();
if (sameTimeReservations.length > 0) {
showReserveSlotSameTimeModal(sameTimeReservations, function(res) {
return $scope.reserve.toReserve = !$scope.reserve.toReserve;
});
} else {
return $scope.reserve.toReserve = !$scope.reserve.toReserve;
}
});
}
}
}
};
@ -798,6 +819,49 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
}
};
/**
* find user reservations the same date at the same time with event
*
*/
var findReservationsAtSameTime = function () {
let sameTimeReservations = [
'training_reservations',
'machine_reservations',
'space_reservations',
'events_reservations'
].map(k => {
return _.filter($scope.ctrl.member[k], r => {
if (r.reservable_type === 'Event' && r.reservable.id === $scope.event.id) {
return false;
};
return moment($scope.event.start_time).isSame(r.start_at) ||
(moment($scope.event.end_time).isAfter(r.start_at) && moment($scope.event.end_time).isBefore(r.end_at)) ||
(moment($scope.event.start_time).isAfter(r.start_at) && moment($scope.event.start_time).isBefore(r.end_at)) ||
(moment($scope.event.start_time).isBefore(r.start_at) && moment($scope.event.end_time).isAfter(r.end_at));
});
});
return _.union(...sameTimeReservations);
};
/**
* A modal for show reservations the same date at the same time
*
* @param sameTimeReservations {Array} reservations the same date at the same time
* @param callback {function} callback will invoke when user confirm
*/
var showReserveSlotSameTimeModal = function(sameTimeReservations, callback) {
const modalInstance = $uibModal.open({
animation: true,
templateUrl: '<%= asset_path "shared/_reserve_slot_same_time.html" %>',
size: 'md',
controller: 'ReserveSlotSameTimeController',
resolve: {
sameTimeReservations: function() { return sameTimeReservations; },
}
});
modalInstance.result.then(callback);
};
// !!! MUST BE CALLED AT THE END of the controller
return initialize();
}
@ -879,4 +943,3 @@ Application.Controllers.controller('DeleteRecurrentEventController', ['$scope',
}
}
]);

View File

@ -579,7 +579,7 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$stat
});
if ($scope.currentUser.role !== 'admin') {
return $scope.ctrl.member = $scope.currentUser;
return Member.get({ id: $scope.currentUser.id }, function (member) { $scope.ctrl.member = member; });
}
};

View File

@ -10,8 +10,8 @@
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs', 'growl', 'Auth', 'Price', 'Wallet', 'CustomAsset', 'Slot', 'helpers', '_t',
function ($rootScope, $uibModal, dialogs, growl, Auth, Price, Wallet, CustomAsset, Slot, helpers, _t) {
Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs', 'growl', 'Auth', 'Price', 'Wallet', 'CustomAsset', 'Slot', 'helpers', '_t', '$uibModal',
function ($rootScope, $uibModal, dialogs, growl, Auth, Price, Wallet, CustomAsset, Slot, helpers, _t, $uibModal) {
return ({
restrict: 'E',
scope: {
@ -72,8 +72,38 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs',
* @param slot {Object} fullCalendar event object
*/
$scope.validateSlot = function (slot) {
slot.isValid = true;
return updateCartPrice();
let sameTimeReservations = [
'training_reservations',
'machine_reservations',
'space_reservations',
'events_reservations'
].map(k => {
return _.filter($scope.user[k], r => {
return slot.start.isSame(r.start_at) ||
(slot.end.isAfter(r.start_at) && slot.end.isBefore(r.end_at)) ||
(slot.start.isAfter(r.start_at) && slot.start.isBefore(r.end_at)) ||
(slot.start.isBefore(r.start_at) && slot.end.isAfter(r.end_at));
});
});
sameTimeReservations = _.union(...sameTimeReservations);
if (sameTimeReservations.length > 0) {
const modalInstance = $uibModal.open({
animation: true,
templateUrl: '<%= asset_path "shared/_reserve_slot_same_time.html" %>',
size: 'md',
controller: 'ReserveSlotSameTimeController',
resolve: {
sameTimeReservations: function() { return sameTimeReservations; },
}
});
modalInstance.result.then(function(res) {
slot.isValid = true;
return updateCartPrice();
});
} else {
slot.isValid = true;
return updateCartPrice();
}
};
/**
@ -614,3 +644,25 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs',
});
}
]);
/**
* Controller of modal for show reservations the same date at the same time
*/
Application.Controllers.controller('ReserveSlotSameTimeController', ['$scope', '$uibModalInstance', 'sameTimeReservations', 'growl', '_t',
function ($scope, $uibModalInstance, sameTimeReservations, growl, _t) {
$scope.sameTimeReservations = sameTimeReservations;
$scope.bookSlotAtSameTime = Fablab.bookSlotAtSameTime;
/**
* Confirmation callback
*/
$scope.ok = function () {
$uibModalInstance.close({});
}
/**
* Cancellation callback
*/
$scope.cancel = function () {
$uibModalInstance.dismiss('cancel');
}
}
]);

View File

@ -0,0 +1,18 @@
<div class="modal-header">
<img ng-src="{{logoBlack.custom_asset_file_attributes.attachment_url}}" alt="{{logo.custom_asset_file_attributes.attachment}}" class="modal-logo"/>
<h1 translate>{{ 'app.shared.cart.slot_at_same_time' }}</h1>
</div>
<div class="modal-body">
<p ng-if="bookSlotAtSameTime" translate>{{ 'app.shared.cart.do_you_really_want_to_book_slot_at_same_time' }}</p>
<p ng-if="!bookSlotAtSameTime" translate>{{ 'app.shared.cart.unable_to_book_slot_because_really_have_reservation_at_same_time' }}</p>
<ul>
<li ng-repeat="r in sameTimeReservations">
<span>{{::r.reservable.name}}{{::r.reservable.title}}</span>
<div class="font-sbold text-u-c">{{ 'app.shared.cart.datetime_to_time' | translate:{START_DATETIME:(r.start_at | amDateFormat:'LLLL'), END_TIME:(r.end_at | amDateFormat:'LT') } }}</div>
</li>
</div>
</div>
<div ng-if="bookSlotAtSameTime" class="modal-footer">
<button class="btn btn-info" ng-click="ok()" translate>{{ 'app.shared.buttons.confirm' }}</button>
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'app.shared.buttons.cancel' }}</button>
</div>

View File

@ -15,9 +15,26 @@ json.training_reservations @member.reservations.where(reservable_type: 'Training
json.start_at r.slots.first.start_at
json.end_at r.slots.first.end_at
json.reservable r.reservable
json.reservable_type 'Training'
json.is_valid @member.statistic_profile.training_ids.include?(r.reservable_id)
json.canceled_at r.slots.first.canceled_at
end
json.machine_reservations @member.reservations.where(reservable_type: 'Machine') do |r|
json.id r.id
json.start_at r.slots.first.start_at
json.end_at r.slots.first.end_at
json.reservable r.reservable
json.reservable_type 'Machine'
json.canceled_at r.slots.first.canceled_at
end
json.space_reservations @member.reservations.where(reservable_type: 'Space') do |r|
json.id r.id
json.start_at r.slots.first.start_at
json.end_at r.slots.first.end_at
json.reservable r.reservable
json.reservable_type 'Space'
json.canceled_at r.slots.first.canceled_at
end
json.all_projects @member.all_projects do |project|
if requested_current || project.state == 'published'
@ -65,6 +82,7 @@ json.events_reservations @member.reservations.where(reservable_type: 'Event').jo
end
end
json.reservable r.reservable
json.reservable_type 'Event'
end
json.invoices @member.invoices.order('reference DESC') do |i|
json.id i.id

View File

@ -28,6 +28,7 @@
Fablab.withoutOnlinePayment = ('<%= Rails.application.secrets.fablab_without_online_payments %>' === 'true');
Fablab.withoutInvoices = ('<%= Rails.application.secrets.fablab_without_invoices %>' === 'true');
Fablab.phoneRequired = ('<%= Rails.application.secrets.phone_required %>' === 'true');
Fablab.bookSlotAtSameTime = ('<%= Rails.application.secrets.book_slot_at_same_time %>' === 'true');
Fablab.eventsInCalendar = ('<%= Rails.application.secrets.events_in_calendar %>' === 'true');
Fablab.slotDuration = parseInt("<%= ApplicationHelper::SLOT_DURATION %>", 10);
Fablab.disqusShortname = "<%= Rails.application.secrets.disqus_shortname %>";

View File

@ -21,6 +21,7 @@ FABLAB_WITHOUT_SPACES: 'true'
FABLAB_WITHOUT_ONLINE_PAYMENT: 'false'
FABLAB_WITHOUT_INVOICES: 'false'
PHONE_REQUIRED: 'true'
BOOK_SLOT_AT_SAME_TIME: 'true'
USER_CONFIRMATION_NEEDED_TO_SIGN_IN: 'false'

View File

@ -439,3 +439,6 @@ en:
a_problem_occurred_during_the_payment_process_please_try_again_later: "A problem occurred during the payment process. Please try again later."
none: "None"
online_payment_disabled: "Online payment is not available. Please contact the Fablab reception directly."
slot_at_same_time: "Booking slot with others reservations at the same time"
do_you_really_want_to_book_slot_at_same_time: "Do you really want to book this slot with others slots at the same time ?"
unable_to_book_slot_because_really_have_reservation_at_same_time: "Unable to book this slot because you really have following reservation at the same time."

View File

@ -416,3 +416,6 @@ es:
a_problem_occurred_during_the_payment_process_please_try_again_later: "A problem occurred during the payment process. Please try again later."
none: "Ninguno"
online_payment_disabled: "El pago en línea no está disponible. Póngase en contacto directamente con la recepción de Fablab."
slot_at_same_time: "Booking slot with others reservations at the same time"
do_you_really_want_to_book_slot_at_same_time: "Do you really want to book this slot with others slots in same time ?"
unable_to_book_slot_because_really_have_reservation_at_same_time: "Unable to book this slot because you really have following reservation at the same time."

View File

@ -439,3 +439,6 @@ fr:
a_problem_occurred_during_the_payment_process_please_try_again_later: "Il y a eu un problème lors de la procédure de paiement. Veuillez réessayer plus tard."
none: "Aucune"
online_payment_disabled: "Le payment par carte bancaire n'est pas disponible. Merci de contacter directement l'accueil du Fablab."
slot_at_same_time: "Réserver un créneau avec autres réservations en même temps"
do_you_really_want_to_book_slot_at_same_time: "Êtes-vous sûr de réserver ce créneau avec autres réservations en même temps ?"
unable_to_book_slot_because_really_have_reservation_at_same_time: "Impossible de réserver ce créneau parce qu'il y a déjà des réservations ci-desous en même temps."

View File

@ -439,3 +439,6 @@ pt:
a_problem_occurred_during_the_payment_process_please_try_again_later: "Um problema ocorreu durante o processo de pagamento. Por favor tente novamente mais tarde."
none: "Vazio"
online_payment_disabled: "O pagamento online não está disponível. Entre em contato diretamente com a recepção do Fablab."
slot_at_same_time: "Booking slot with others reservations at the same time"
do_you_really_want_to_book_slot_at_same_time: "Do you really want to book this slot with others slots in same time ?"
unable_to_book_slot_because_really_have_reservation_at_same_time: "Unable to book this slot because you really have following reservation at the same time."

View File

@ -21,6 +21,7 @@ development:
fablab_without_online_payments: <%= ENV["FABLAB_WITHOUT_ONLINE_PAYMENT"] %>
fablab_without_invoices: <%= ENV["FABLAB_WITHOUT_INVOICES"] %>
phone_required: <%= ENV["PHONE_REQUIRED"] %>
book_slot_at_same_time: <%= ENV["BOOK_SLOT_AT_SAME_TIME"] %>
user_confirmation_needed_to_sign_in: <%= ENV["USER_CONFIRMATION_NEEDED_TO_SIGN_IN"] %>
events_in_calendar: <%= ENV["EVENTS_IN_CALENDAR"] %>
slot_duration: <%= ENV["SLOT_DURATION"] %>
@ -67,6 +68,7 @@ test:
fablab_without_online_payments: false
fablab_without_invoices: false
phone_required: true
book_slot_at_same_time: true
user_confirmation_needed_to_sign_in: <%= ENV["USER_CONFIRMATION_NEEDED_TO_SIGN_IN"] %>
events_in_calendar: false
slot_duration: 60
@ -113,6 +115,7 @@ staging:
fablab_without_online_payments: <%= ENV["FABLAB_WITHOUT_ONLINE_PAYMENT"] %>
fablab_without_invoices: <%= ENV["FABLAB_WITHOUT_INVOICES"] %>
phone_required: <%= ENV["PHONE_REQUIRED"] %>
book_slot_at_same_time: <%= ENV["BOOK_SLOT_AT_SAME_TIME"] %>
user_confirmation_needed_to_sign_in: <%= ENV["USER_CONFIRMATION_NEEDED_TO_SIGN_IN"] %>
events_in_calendar: <%= ENV["EVENTS_IN_CALENDAR"] %>
slot_duration: <%= ENV["SLOT_DURATION"] %>
@ -171,6 +174,7 @@ production:
fablab_without_online_payments: <%= ENV["FABLAB_WITHOUT_ONLINE_PAYMENT"] %>
fablab_without_invoices: <%= ENV["FABLAB_WITHOUT_INVOICES"] %>
phone_required: <%= ENV["PHONE_REQUIRED"] %>
book_slot_at_same_time: <%= ENV["BOOK_SLOT_AT_SAME_TIME"] %>
user_confirmation_needed_to_sign_in: <%= ENV["USER_CONFIRMATION_NEEDED_TO_SIGN_IN"] %>
events_in_calendar: <%= ENV["EVENTS_IN_CALENDAR"] %>
slot_duration: <%= ENV["SLOT_DURATION"] %>

View File

@ -107,6 +107,11 @@ This is useful if you have your own invoicing system and you want to prevent Fab
PHONE_REQUIRED
If set to 'false' the phone number won't be required to register a new user on the software.
<a name="BOOK_SLOT_AT_SAME_TIME"></a>
BOOK_SLOT_AT_SAME_TIME
If set to 'false' user won't book a machine/formation/event slot if he already have a reservation the same day at the same time.
<a name="USER_CONFIRMATION_NEEDED_TO_SIGN_IN"></a>
USER_CONFIRMATION_NEEDED_TO_SIGN_IN

View File

@ -14,6 +14,7 @@ FABLAB_WITHOUT_SPACES=true
FABLAB_WITHOUT_ONLINE_PAYMENT=true
FABLAB_WITHOUT_INVOICES=false
PHONE_REQUIRED=false
BOOK_SLOT_AT_SAME_TIME=true
EVENTS_IN_CALENDAR=false
SLOT_DURATION=60

View File

@ -211,7 +211,7 @@ configure_env_file()
local doc variables secret
doc=$(\curl -sSL https://raw.githubusercontent.com/sleede/fab-manager/master/doc/environment.md)
variables=(STRIPE_API_KEY STRIPE_PUBLISHABLE_KEY STRIPE_CURRENCY INVOICE_PREFIX FABLAB_WITHOUT_PLANS FABLAB_WITHOUT_SPACES FABLAB_WITHOUT_ONLINE_PAYMENT FABLAB_WITHOUT_INVOICES \
PHONE_REQUIRED USER_CONFIRMATION_NEEDED_TO_SIGN_IN EVENTS_IN_CALENDAR SLOT_DURATION DEFAULT_MAIL_FROM DELIVERY_METHOD DEFAULT_HOST DEFAULT_PROTOCOL SMTP_ADDRESS SMTP_PORT SMTP_USER_NAME SMTP_PASSWORD SMTP_AUTHENTICATION \
PHONE_REQUIRED BOOK_SLOT_AT_SAME_TIME USER_CONFIRMATION_NEEDED_TO_SIGN_IN EVENTS_IN_CALENDAR SLOT_DURATION DEFAULT_MAIL_FROM DELIVERY_METHOD DEFAULT_HOST DEFAULT_PROTOCOL SMTP_ADDRESS SMTP_PORT SMTP_USER_NAME SMTP_PASSWORD SMTP_AUTHENTICATION \
SMTP_ENABLE_STARTTLS_AUTO SMTP_OPENSSL_VERIFY_MODE SMTP_TLS GA_ID RECAPTCHA_SITE_KEY RECAPTCHA_SECRET_KEY DISQUS_SHORTNAME TWITTER_NAME \
FACEBOOK_APP_ID LOG_LEVEL ALLOWED_EXTENSIONS ALLOWED_MIME_TYPES MAX_IMAGE_SIZE MAX_CAO_SIZE MAX_IMPORT_SIZE DISK_SPACE_MB_ALERT \
SUPERADMIN_EMAIL APP_LOCALE RAILS_LOCALE MOMENT_LOCALE SUMMERNOTE_LOCALE ANGULAR_LOCALE MESSAGEFORMAT_LOCALE FULLCALENDAR_LOCALE ELASTICSEARCH_LANGUAGE_ANALYZER TIME_ZONE \