1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-01-29 18:52:22 +01:00

Merge branch 'book_slot_at_same_time' into dev

This commit is contained in:
Sylvain 2020-03-02 15:52:05 +01:00
commit 07b21f5094
16 changed files with 185 additions and 9 deletions

View File

@ -1,5 +1,6 @@
# Changelog Fab-manager
- Ability to configure the policy (allow or prevent) for members booking a machine/formation/event slot, if they 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
@ -59,6 +60,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`

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,48 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
}
};
/**
* Find user's reservations, the same date at the same time, with event
*/
var findReservationsAtSameTime = function () {
let sameTimeReservations = [
'training_reservations',
'machine_reservations',
'space_reservations',
'events_reservations'
].map(function(k) {
return _.filter($scope.ctrl.member[k], function(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.apply(null, 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 +942,3 @@ Application.Controllers.controller('DeleteRecurrentEventController', ['$scope',
}
}
]);

View File

@ -662,7 +662,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(function (k) {
return _.filter($scope.user[k], function(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.apply(null, 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 || currentUser.role === 'admin'" translate>{{ 'app.shared.cart.do_you_really_want_to_book_slot_at_same_time' }}</p>
<p ng-if="!bookSlotAtSameTime && currentUser.role !== 'admin'" 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 class="modal-footer">
<button ng-if="bookSlotAtSameTime || currentUser.role === 'admin'" 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

@ -29,6 +29,7 @@
Fablab.withoutInvoices = ('<%= Rails.application.secrets.fablab_without_invoices %>' === 'true');
Fablab.phoneRequired = ('<%= Rails.application.secrets.phone_required %>' === 'true');
Fablab.fablabWithoutWallet = ('<%= Rails.application.secrets.fablab_without_wallet %>' === '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.featureTourDisplay = "<%= Rails.application.secrets.feature_tour_display %>";

View File

@ -22,6 +22,7 @@ FABLAB_WITHOUT_ONLINE_PAYMENT: 'false'
FABLAB_WITHOUT_INVOICES: 'false'
PHONE_REQUIRED: 'true'
FABLAB_WITHOUT_WALLET: 'false'
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: "Conflict with others reservations"
do_you_really_want_to_book_slot_at_same_time: "Do you really want to book this slot? Other bookings take place at the same time"
unable_to_book_slot_because_really_have_reservation_at_same_time: "Unable to book this slot because the following reservation occurs at the same time."

View File

@ -439,3 +439,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: "Conflict with others reservations"
do_you_really_want_to_book_slot_at_same_time: "Do you really want to book this slot? Other bookings take place at the same time"
unable_to_book_slot_because_really_have_reservation_at_same_time: "Unable to book this slot because the following reservation occurs at the same time."

View File

@ -439,6 +439,9 @@ 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: "Conflit avec d'autres réservations"
do_you_really_want_to_book_slot_at_same_time: "Êtes-vous sûr de réserver ce créneau ? D'autres réservations ont lieu en même temps"
unable_to_book_slot_because_really_have_reservation_at_same_time: "Impossible de réserver ce créneau car les réservations ci-dessous ont lieu en même temps."
tour:
previous: "Précédent"

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: "Conflict with others reservations"
do_you_really_want_to_book_slot_at_same_time: "Do you really want to book this slot? Other bookings take place at the same time"
unable_to_book_slot_because_really_have_reservation_at_same_time: "Unable to book this slot because the following reservation occurs at the same time."

View File

@ -22,6 +22,7 @@ development:
fablab_without_invoices: <%= ENV["FABLAB_WITHOUT_INVOICES"] %>
phone_required: <%= ENV["PHONE_REQUIRED"] %>
fablab_without_wallet: <%= ENV["FABLAB_WITHOUT_WALLET"] %>
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"] %>
@ -70,6 +71,7 @@ test:
fablab_without_invoices: false
phone_required: true
fablab_without_wallet: false
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
@ -118,6 +120,7 @@ staging:
fablab_without_invoices: <%= ENV["FABLAB_WITHOUT_INVOICES"] %>
phone_required: <%= ENV["PHONE_REQUIRED"] %>
fablab_without_wallet: <%= ENV["FABLAB_WITHOUT_WALLET"] %>
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"] %>
@ -178,6 +181,7 @@ production:
fablab_without_invoices: <%= ENV["FABLAB_WITHOUT_INVOICES"] %>
phone_required: <%= ENV["PHONE_REQUIRED"] %>
fablab_without_wallet: <%= ENV["FABLAB_WITHOUT_WALLET"] %>
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

@ -113,6 +113,11 @@ This is useful if you won't use wallet system.
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', users won't be able to book a machine/formation/event slot if they 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

@ -15,6 +15,7 @@ FABLAB_WITHOUT_ONLINE_PAYMENT=true
FABLAB_WITHOUT_INVOICES=false
PHONE_REQUIRED=false
FABLAB_WITHOUT_WALLET=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 FABLAB_WITHOUT_WALLET \
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 FEATURE_TOUR_BEHAVIOR \
SUPERADMIN_EMAIL APP_LOCALE RAILS_LOCALE MOMENT_LOCALE SUMMERNOTE_LOCALE ANGULAR_LOCALE MESSAGEFORMAT_LOCALE FULLCALENDAR_LOCALE ELASTICSEARCH_LANGUAGE_ANALYZER TIME_ZONE \