diff --git a/.fabmanager-version b/.fabmanager-version index e393c3c55..437459cd9 100644 --- a/.fabmanager-version +++ b/.fabmanager-version @@ -1 +1 @@ -2.4.11 \ No newline at end of file +2.5.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 9866cc9e2..8919751d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,29 @@ # Changelog Fab Manager +## v2.5.0 2017 March 28 + +- Ability to remove an unused custom price for an event (#61) +- Prevent polling notifications when the application is in background +- Ability to export the availabilities and their reservation rate from the admin calendar +- Ability to create, manage and reserve spaces +- Improved admin's interface to create availabilities +- Complete rewrote of the reservation cart functionality with improved stability, performance and sustainability +- Replaced letter_opener by MailCatcher to preview e-mails in development environments +- Ability to create plans with durations based on weeks +- Ease installations with docker-compose, in any directory (#63) +- Fix a bug: trainings reservations are not shown in the admin's calendar +- Fix a bug: unable to delete an administrator from the system +- Fix a bug: unable to delete an event with a linked custom price (#61) +- Fix a bug: navigation in client calendar is bogus when browsing months (#59) +- Fix a bug: subscription name is not shown in invoices +- Fix a bug: new plans statistics are not shown +- [TODO DEPLOY] `rake db:migrate`, then `rake db:seed` +- [TODO DEPLOY] add the `FABLAB_WITHOUT_SPACES` environment variable +- [TODO DEPLOY] `rake fablab:es_add_spaces` +- [TODO DEPLOY] `rake fablab:fix:new_plans_statistics` if you have created plans from v2.4.10 + ## v2.4.11 2017 March 15 + - Fix a bug: editing and saving a plan, result in removing the rolling attribute - [TODO DEPLOY] `rake fablab:fix:rolling_plans` diff --git a/Gemfile b/Gemfile index a2394f4fc..0796722e2 100644 --- a/Gemfile +++ b/Gemfile @@ -40,7 +40,7 @@ end group :development do # Preview mail in the browser - gem 'letter_opener' + gem 'mailcatcher' gem 'awesome_print' gem "puma" diff --git a/Gemfile.lock b/Gemfile.lock index 0eab18efc..01219da99 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -128,6 +128,7 @@ GEM tins (>= 1.6.0, < 2) crack (0.4.3) safe_yaml (~> 1.0.0) + daemons (1.2.4) database_cleaner (1.4.1) debug_inspector (0.0.2) descendants_tracker (0.0.4) @@ -166,6 +167,7 @@ GEM multi_json equalizer (0.0.11) erubis (2.7.0) + eventmachine (1.0.9.1) execjs (2.7.0) faker (1.4.3) i18n (~> 0.5) @@ -217,15 +219,19 @@ GEM actionpack (>= 3.0.0) activesupport (>= 3.0.0) kgio (2.9.3) - launchy (2.4.3) - addressable (~> 2.3) - letter_opener (1.3.0) - launchy (~> 2.2) libv8 (3.16.14.11) loofah (2.0.3) nokogiri (>= 1.5.9) mail (2.6.3) mime-types (>= 1.16, < 3) + mailcatcher (0.6.5) + eventmachine (= 1.0.9.1) + mail (~> 2.3) + rack (~> 1.5) + sinatra (~> 1.2) + skinny (~> 0.2.3) + sqlite3 (~> 1.3) + thin (~> 1.5.0) memoizable (0.4.2) thread_safe (~> 0.3, >= 0.3.1) message_format (0.0.3) @@ -391,6 +397,9 @@ GEM rack (~> 1.4) rack-protection (~> 1.4) tilt (>= 1.3, < 3) + skinny (0.2.4) + eventmachine (~> 1.0.0) + thin (>= 1.5, < 1.7) spring (1.3.5) sprockets (2.12.4) hike (~> 1.2) @@ -401,6 +410,7 @@ GEM actionpack (>= 3.0) activesupport (>= 3.0) sprockets (>= 2.8, < 4.0) + sqlite3 (1.3.13) stripe (1.30.2) json (~> 1.8.1) rest-client (~> 1.4) @@ -411,6 +421,10 @@ GEM therubyracer (0.12.0) libv8 (~> 3.16.14.0) ref + thin (1.5.1) + daemons (>= 1.0.9) + eventmachine (>= 0.12.6) + rack (>= 1.0.0) thor (0.19.1) thread_safe (0.3.5) tilt (1.4.1) @@ -505,7 +519,7 @@ DEPENDENCIES jbuilder_cache_multi jquery-rails kaminari - letter_opener + mailcatcher message_format mini_magick minitest-reporters diff --git a/Procfile b/Procfile index d6f589654..f8a9f7b3c 100644 --- a/Procfile +++ b/Procfile @@ -1,2 +1,3 @@ web: bundle exec rails server puma -p $PORT -b0.0.0.0 worker: bundle exec sidekiq -C ./config/sidekiq.yml +mail: bundle exec mailcatcher --foreground diff --git a/README.md b/README.md index 2a08d7572..a8fd03ba9 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ FabManager is the FabLab management solution. It is web-based, open-source and totally free. -##### Table of Contents +##### Table of Contents 1. [Software stack](#software-stack) 2. [Contributing](#contributing) 3. [Setup a production environment](#setup-a-production-environment) @@ -122,9 +122,12 @@ In you only intend to run fab-manager on your local machine for testing purposes ``` 8. Build the database. You may have to follow the steps described in [the PostgreSQL configuration chapter](#setup-fabmanager-in-postgresql) before, if you don't already had done it. + **Warning**: **NO NOT** run `rake db:setup` instead of these commands, as this will not run some required raw SQL instructions. ```bash - rake db:setup + rake db:create + rake db:migrate + rake db:seed ``` 9. Create the pids folder used by Sidekiq. If you want to use a different location, you can configure it in `config/sidekiq.yml` @@ -145,6 +148,9 @@ In you only intend to run fab-manager on your local machine for testing purposes - user: admin@fab-manager.com - password: adminadmin +13. Email notifications will be caught by MailCatcher. + To see the emails sent by the platform, open your web browser at `http://localhost:1080` to access the MailCatcher interface. + ### Environment Configuration @@ -198,6 +204,12 @@ The PDF file name will be of the form "(INVOICE_PREFIX) - (invoice ID) _ (invoic FABLAB_WITHOUT_PLANS If set to 'true', the subscription plans will be fully disabled and invisible in the application. +It is not recommended to disable plans if at least one subscription was took on the platform. + + FABLAB_WITHOUT_SPACES + +If set to 'false', enable the spaces management and reservation in the application. +It is not recommended to disable spaces if at least one space reservation was made on the system. DEFAULT_MAIL_FROM @@ -261,7 +273,7 @@ Please consider that allowing file archives (eg. application/zip) or binary exec MAX_IMAGE_SIZE -Maximum size (in bytes) allowed for image uploaded on the platform. +Maximum size (in bytes) allowed for image uploaded on the platform. This parameter concerns events, plans, user's avatars, projects and steps of projects. If this parameter is not specified the maximum size allowed will be 2MB. @@ -325,7 +337,7 @@ Otherwise, please follow the official instructions on the project's website. ### Setup the FabManager database in PostgreSQL -Before running `rake db:setup`, you have to make sure that the user configured in [config/database.yml](config/database.yml.default) for the `development` environment exists. +Before running `rake db:create`, you have to make sure that the user configured in [config/database.yml](config/database.yml.default) for the `development` environment exists. To create it, please follow these instructions: 1. Run the PostgreSQL administration command line interface, logged as the postgres user @@ -355,19 +367,13 @@ To create it, please follow these instructions: ALTER ROLE sleede WITH CREATEDB; ``` -4. Then, create the fabmanager_development and fabmanager_test databases - - ```sql - CREATE DATABASE fabmanager_development OWNER sleede; - CREATE DATABASE fabmanager_test OWNER sleede; - ``` - -5. To finish, attribute a password to this user +4. Then, attribute a password to this user ```sql ALTER USER sleede WITH ENCRYPTED PASSWORD 'sleede'; ``` -6. Finally, have a look at the [PostgreSQL Limitations](#postgresql-limitations) section or some errors will occurs preventing you from finishing the installation procedure. + +5. Finally, have a look at the [PostgreSQL Limitations](#postgresql-limitations) section or some errors will occurs preventing you from finishing the installation procedure. ### PostgreSQL Limitations @@ -683,12 +689,12 @@ Developers may find information on how to implement their own authentication pro - When running the tests suite with `rake test`, all tests may fail with errors similar to the following: Error: - ... - ActiveRecord::InvalidForeignKey: PG::ForeignKeyViolation: ERROR: insert or update on table "..." violates foreign key constraint "fk_rails_..." - DETAIL: Key (group_id)=(1) is not present in table "groups". - : ... - test_after_commit (1.0.0) lib/test_after_commit/database_statements.rb:11:in `block in transaction' - test_after_commit (1.0.0) lib/test_after_commit/database_statements.rb:5:in `transaction' + ... + ActiveRecord::InvalidForeignKey: PG::ForeignKeyViolation: ERROR: insert or update on table "..." violates foreign key constraint "fk_rails_..." + DETAIL: Key (group_id)=(1) is not present in table "groups". + : ... + test_after_commit (1.0.0) lib/test_after_commit/database_statements.rb:11:in `block in transaction' + test_after_commit (1.0.0) lib/test_after_commit/database_statements.rb:5:in `transaction' This is due to an ActiveRecord behavior witch disable referential integrity in PostgreSQL to load the fixtures. PostgreSQL will prevent any users to disable referential integrity on the fly if they doesn't have the `SUPERUSER` role. diff --git a/app/assets/javascripts/app.js.erb b/app/assets/javascripts/app.js.erb index d363714d0..7049d79b8 100644 --- a/app/assets/javascripts/app.js.erb +++ b/app/assets/javascripts/app.js.erb @@ -80,6 +80,8 @@ config(['$httpProvider', 'AuthProvider', "growlProvider", "unsavedWarningsConfig // Global config: if true, the whole 'Plans & Subscriptions' feature will be disabled in the application $rootScope.fablabWithoutPlans = Fablab.withoutPlans; + // Global config: it true, the whole 'Spaces' features will be disabled in the application + $rootScope.fablabWithoutSpaces = Fablab.withoutSpaces; // Global function to allow the user to navigate to the previous screen (ie. $state). // If no previous $state were recorded, navigate to the home page diff --git a/app/assets/javascripts/controllers/admin/calendar.coffee.erb b/app/assets/javascripts/controllers/admin/calendar.coffee.erb index 7bd5e9662..33f9394ad 100644 --- a/app/assets/javascripts/controllers/admin/calendar.coffee.erb +++ b/app/assets/javascripts/controllers/admin/calendar.coffee.erb @@ -4,8 +4,8 @@ # Controller used in the calendar management page ## -Application.Controllers.controller "AdminCalendarController", ["$scope", "$state", "$uibModal", "moment", "Availability", 'Slot', 'Setting', 'growl', 'dialogs', 'bookingWindowStart', 'bookingWindowEnd', 'machinesPromise', '_t', 'uiCalendarConfig', 'CalendarConfig' -($scope, $state, $uibModal, moment, Availability, Slot, Setting, growl, dialogs, bookingWindowStart, bookingWindowEnd, machinesPromise, _t, uiCalendarConfig, CalendarConfig) -> +Application.Controllers.controller "AdminCalendarController", ["$scope", "$state", "$uibModal", "moment", "Availability", 'Slot', 'Setting', 'Export', 'growl', 'dialogs', 'bookingWindowStart', 'bookingWindowEnd', 'machinesPromise', '_t', 'uiCalendarConfig', 'CalendarConfig' +($scope, $state, $uibModal, moment, Availability, Slot, Setting, Export, growl, dialogs, bookingWindowStart, bookingWindowEnd, machinesPromise, _t, uiCalendarConfig, CalendarConfig) -> @@ -64,8 +64,8 @@ Application.Controllers.controller "AdminCalendarController", ["$scope", "$state dialogs.confirm resolve: object: -> - title: _t('confirmation_required') - msg: _t("do_you_really_want_to_cancel_the_USER_s_reservation_the_DATE_at_TIME_concerning_RESERVATION" + title: _t('admin_calendar.confirmation_required') + msg: _t("admin_calendar.do_you_really_want_to_cancel_the_USER_s_reservation_the_DATE_at_TIME_concerning_RESERVATION" , { GENDER:getGender($scope.currentUser), USER:slot.user.name, DATE:moment(slot.start_at).format('L'), TIME:moment(slot.start_at).format('LT'), RESERVATION:slot.reservable.name } , 'messageformat') , -> @@ -78,9 +78,9 @@ Application.Controllers.controller "AdminCalendarController", ["$scope", "$state resa.canceled_at = data.canceled_at break # notify the admin - growl.success(_t('reservation_was_successfully_cancelled')) + growl.success(_t('admin_calendar.reservation_was_successfully_cancelled')) , (data, status) -> # failed - growl.error(_t('reservation_cancellation_failed')) + growl.error(_t('admin_calendar.reservation_cancellation_failed')) @@ -91,16 +91,16 @@ Application.Controllers.controller "AdminCalendarController", ["$scope", "$state ## $scope.removeMachine = (machine) -> if $scope.availability.machine_ids.length == 1 - growl.error(_t('unable_to_remove_the_last_machine_of_the_slot_delete_the_slot_rather')) + growl.error(_t('admin_calendar.unable_to_remove_the_last_machine_of_the_slot_delete_the_slot_rather')) else # open a confirmation dialog dialogs.confirm resolve: object: -> - title: _t('confirmation_required') - msg: _t('do_you_really_want_to_remove_MACHINE_from_this_slot', {GENDER:getGender($scope.currentUser), MACHINE:machine.name}, "messageformat") + ' ' + - _t('this_will_prevent_any_new_reservation_on_this_slot_but_wont_cancel_those_existing') + ' ' + - _t('beware_this_cannot_be_reverted') + title: _t('admin_calendar.confirmation_required') + msg: _t('admin_calendar.do_you_really_want_to_remove_MACHINE_from_this_slot', {GENDER:getGender($scope.currentUser), MACHINE:machine.name}, "messageformat") + ' ' + + _t('admin_calendar.this_will_prevent_any_new_reservation_on_this_slot_but_wont_cancel_those_existing') + ' ' + + _t('admin_calendar.beware_this_cannot_be_reverted') , -> # the admin has confirmed, remove the machine machines = $scope.availability.machine_ids @@ -115,9 +115,20 @@ Application.Controllers.controller "AdminCalendarController", ["$scope", "$state $scope.availability.title = data.title uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents' # notify the admin - growl.success(_t('the_machine_was_successfully_removed_from_the_slot')) + growl.success(_t('admin_calendar.the_machine_was_successfully_removed_from_the_slot')) , (data, status) -> # failed - growl.error(_t('deletion_failed')) + growl.error(_t('admin_calendar.deletion_failed')) + + + + ## + # Callback to alert the admin that the export request was acknowledged and is + # processing right now. + ## + $scope.alertExport = (type) -> + Export.status({category: 'availabilities', type: type}).then (res) -> + unless (res.data.exists) + growl.success _t('admin_calendar.export_is_running_you_ll_be_notified_when_its_ready') @@ -150,6 +161,15 @@ Application.Controllers.controller "AdminCalendarController", ["$scope", "$state resolve: start: -> start end: -> end + machinesPromise: ['Machine', (Machine)-> + Machine.query().$promise + ] + trainingsPromise: ['Training', (Training)-> + Training.query().$promise + ] + spacesPromise: ['Space', (Space)-> + Space.query().$promise + ] # when the modal is closed, we send the slot to the server for saving modalInstance.result.then (availability) -> uiCalendarConfig.calendars.calendar.fullCalendar 'renderEvent', @@ -184,9 +204,9 @@ Application.Controllers.controller "AdminCalendarController", ["$scope", "$state Availability.delete id: event.id, -> uiCalendarConfig.calendars.calendar.fullCalendar 'removeEvents', event.id - growl.success(_t('the_slot_START-END_has_been_successfully_deleted', {START:moment(event.start).format('LL LT'), END:moment(event.end).format('LT')})) + growl.success(_t('admin_calendar.the_slot_START-END_has_been_successfully_deleted', {START:moment(event.start).format('LL LT'), END:moment(event.end).format('LT')})) ,-> - growl.error(_t('unable_to_delete_the_slot_START-END_because_it_s_already_reserved_by_a_member', {START:+moment(event.start).format('LL LT'), END:moment(event.end).format('LT')})) + growl.error(_t('admin_calendar.unable_to_delete_the_slot_START-END_because_it_s_already_reserved_by_a_member', {START:moment(event.start).format('LL LT'), END:moment(event.end).format('LT')})) # if the user has only clicked on the event, display its reservations else Availability.reservations {id: event.id}, (reservations) -> @@ -227,7 +247,8 @@ Application.Controllers.controller "AdminCalendarController", ["$scope", "$state ## # Controller used in the slot creation modal window ## -Application.Controllers.controller 'CreateEventModalController', ["$scope", "$uibModalInstance", "moment", "start", "end", "Machine", "Availability", "Training", 'Tag', 'growl', '_t', ($scope, $uibModalInstance, moment, start, end, Machine, Availability, Training, Tag, growl, _t) -> +Application.Controllers.controller 'CreateEventModalController', ["$scope", "$uibModalInstance", "moment", "start", "end", "machinesPromise", "Availability", "trainingsPromise", "spacesPromise", 'Tag', 'growl', '_t' +, ($scope, $uibModalInstance, moment, start, end, machinesPromise, Availability, trainingsPromise, spacesPromise, Tag, growl, _t) -> ## $uibModal parameter $scope.start = start @@ -236,14 +257,26 @@ Application.Controllers.controller 'CreateEventModalController', ["$scope", "$ui $scope.end = end ## machines list - $scope.machines = [] + $scope.machines = machinesPromise ## trainings list - $scope.trainings = [] + $scope.trainings = trainingsPromise + + ## spaces list + $scope.spaces = spacesPromise ## machines associated with the created slot $scope.selectedMachines = [] + ## training associated with the created slot + $scope.selectedTraining = null + + ## space associated with the created slot + $scope.selectedSpace = null + + ## UI step + $scope.step = 1 + ## the user is not able to edit the ending time of the availability, unless he set the type to 'training' $scope.endDateReadOnly = true @@ -281,14 +314,16 @@ Application.Controllers.controller 'CreateEventModalController', ["$scope", "$ui # Callback for the modal window validation: save the slot and closes the modal ## $scope.ok = -> - if $scope.availability.available_type == "machines" + if $scope.availability.available_type == 'machines' if $scope.selectedMachines.length > 0 $scope.availability.machine_ids = $scope.selectedMachines.map (m) -> m.id else - growl.error(_t('you_should_link_a_training_or_a_machine_to_this_slot')) + growl.error(_t('admin_calendar.you_should_select_at_least_a_machine')) return - else + else if $scope.availability.available_type == 'training' $scope.availability.training_ids = [$scope.selectedTraining.id] + else if $scope.availability.available_type == 'space' + $scope.availability.space_ids = [$scope.selectedSpace.id] Availability.save availability: $scope.availability , (availability) -> @@ -296,6 +331,23 @@ Application.Controllers.controller 'CreateEventModalController', ["$scope", "$ui + ## + # Move the modal UI to the next step + ## + $scope.next = -> + $scope.setNbTotalPlaces() if $scope.step == 1 + $scope.step++ + + + + ## + # Move the modal UI to the next step + ## + $scope.previous = -> + $scope.step-- + + + ## # Callback to cancel the slot creation ## @@ -304,22 +356,14 @@ Application.Controllers.controller 'CreateEventModalController', ["$scope", "$ui - ## - # Switches the slot type : machine availability or training availability - ## - $scope.changeAvailableType = -> - if $scope.availability.available_type == "machines" - $scope.availability.available_type = "training" - else - $scope.availability.available_type = "machines" - - - ## # For training avaiabilities, set the maximum number of people allowed to register on this slot ## $scope.setNbTotalPlaces = -> - $scope.availability.nb_total_places = $scope.selectedTraining.nb_total_places + if $scope.availability.available_type == 'training' + $scope.availability.nb_total_places = $scope.selectedTraining.nb_total_places + else if $scope.availability.available_type == 'space' + $scope.availability.nb_total_places = $scope.selectedSpace.default_places ### PRIVATE SCOPE ### @@ -328,18 +372,11 @@ Application.Controllers.controller 'CreateEventModalController', ["$scope", "$ui # Kind of constructor: these actions will be realized first when the controller is loaded ## initialize = -> - Machine.query().$promise.then (data)-> - $scope.machines = data.map (d) -> - id: d.id - name: d.name - Training.query().$promise.then (data)-> - $scope.trainings = data.map (d) -> - id: d.id - name: d.name - nb_total_places: d.nb_total_places - if $scope.trainings.length > 0 - $scope.selectedTraining = $scope.trainings[0] - $scope.setNbTotalPlaces() + if $scope.trainings.length > 0 + $scope.selectedTraining = $scope.trainings[0] + if $scope.spaces.length > 0 + $scope.selectedSpace = $scope.spaces[0] + Tag.query().$promise.then (data) -> $scope.tags = data @@ -347,7 +384,7 @@ Application.Controllers.controller 'CreateEventModalController', ["$scope", "$ui ## time must be dividable by 60 minutes (base slot duration). For training availabilities, the user ## can configure any duration as it does not matters. $scope.$watch 'availability.available_type', (newValue, oldValue, scope) -> - if newValue == 'machines' + if newValue == 'machines' or newValue == 'space' $scope.endDateReadOnly = true diff = moment($scope.end).diff($scope.start, 'hours') # the result is rounded down by moment.js $scope.end = moment($scope.start).add(diff, 'hours').toDate() @@ -358,8 +395,8 @@ Application.Controllers.controller 'CreateEventModalController', ["$scope", "$ui ## When the start date is changed, if we are configuring a machine availability, ## maintain the relative length of the slot (ie. change the end time accordingly) $scope.$watch 'start', (newValue, oldValue, scope) -> - # for machine availabilities, adjust the end time - if $scope.availability.available_type == 'machines' + # for machine or space availabilities, adjust the end time + if $scope.availability.available_type == 'machines' or $scope.availability.available_type == 'space' end = moment($scope.end) end.add(moment(newValue).diff(oldValue), 'milliseconds') $scope.end = end.toDate() diff --git a/app/assets/javascripts/controllers/admin/events.coffee.erb b/app/assets/javascripts/controllers/admin/events.coffee.erb index 9ef91e1c5..728a58cbb 100644 --- a/app/assets/javascripts/controllers/admin/events.coffee.erb +++ b/app/assets/javascripts/controllers/admin/events.coffee.erb @@ -16,6 +16,8 @@ # - $scope.toggleStartDatePicker($event) # - $scope.toggleEndDatePicker($event) # - $scope.toggleRecurrenceEnd(e) +# - $scope.addPrice() +# - $scope.removePrice(price, $event) # # Requires : # - $scope.event.event_files_attributes = [] @@ -137,6 +139,21 @@ class EventsController + ## + # Remove the price or mark it as 'to delete' + ## + $scope.removePrice = (price, event) -> + event.preventDefault() + event.stopPropagation() + if price.id + price._destroy = true + else + index = $scope.event.prices.indexOf(price) + $scope.event.prices.splice(index, 1) + + + + ## # Controller used in the events listing page (admin view) ## diff --git a/app/assets/javascripts/controllers/admin/plans.coffee.erb b/app/assets/javascripts/controllers/admin/plans.coffee.erb index 31b722d60..588718641 100644 --- a/app/assets/javascripts/controllers/admin/plans.coffee.erb +++ b/app/assets/javascripts/controllers/admin/plans.coffee.erb @@ -6,7 +6,7 @@ class PlanController - constructor: ($scope, groups, plans, machines, prices, partners, CSRF) -> + constructor: ($scope, groups, prices, partners, CSRF) -> # protection against request forgery CSRF.setMetaTags() @@ -15,12 +15,6 @@ class PlanController ## groups list $scope.groups = groups - ## plans list - $scope.plans = plans - - ## machines list - $scope.machines = machines - ## users with role 'partner', notifiables for a partner plan $scope.partners = partners.users @@ -48,38 +42,11 @@ class PlanController - ## - # Retrieve a plan from its numeric identifier - # @param id {number} plan ID - # @returns {Object} Plan, inherits from $resource - ## - $scope.getPlanFromId = (id) -> - for plan in $scope.plans - if plan.id == id - return plan - - - - ## - # Retrieve the name of a machine from its ID - # @param machine_id {number} machine identifier - # @returns {string} Machine's name - ## - $scope.getMachineName = (machine_id) -> - for machine in $scope.machines - if machine.id == machine_id - return machine.name - - - - - - ## # Controller used in the plan creation form ## -Application.Controllers.controller 'NewPlanController', ['$scope', '$uibModal', 'groups', 'plans', 'machines', 'prices', 'partners', 'CSRF', '$state', 'growl', '_t' -, ($scope, $uibModal, groups, plans, machines, prices, partners, CSRF, $state, growl, _t) -> +Application.Controllers.controller 'NewPlanController', ['$scope', '$uibModal', 'groups', 'prices', 'partners', 'CSRF', '$state', 'growl', '_t' +, ($scope, $uibModal, groups, prices, partners, CSRF, $state, growl, _t) -> @@ -146,7 +113,7 @@ Application.Controllers.controller 'NewPlanController', ['$scope', '$uibModal', $scope.partner.name = "#{user.first_name} #{user.last_name}" $uibModalInstance.close($scope.partner) , (error)-> - growl.error(_t('unable_to_save_this_user_check_that_there_isnt_an_already_a_user_with_the_same_name')) + growl.error(_t('new_plan.unable_to_save_this_user_check_that_there_isnt_an_already_a_user_with_the_same_name')) $scope.cancel = -> $uibModalInstance.dismiss('cancel') ] @@ -164,9 +131,9 @@ Application.Controllers.controller 'NewPlanController', ['$scope', '$uibModal', ## $scope.afterSubmit = (content) -> if !content.id? and !content.plan_ids? - growl.error(_t('unable_to_create_the_subscription_please_try_again')) + growl.error(_t('new_plan.unable_to_create_the_subscription_please_try_again')) else - growl.success(_t('successfully_created_subscription(s)_dont_forget_to_redefine_prices')) + growl.success(_t('new_plan.successfully_created_subscription(s)_dont_forget_to_redefine_prices')) if content.plan_ids? $state.go('app.admin.pricing') else @@ -175,7 +142,7 @@ Application.Controllers.controller 'NewPlanController', ['$scope', '$uibModal', - new PlanController($scope, groups, plans, machines, prices, partners, CSRF) + new PlanController($scope, groups, prices, partners, CSRF) ] @@ -183,13 +150,25 @@ Application.Controllers.controller 'NewPlanController', ['$scope', '$uibModal', ## # Controller used in the plan edition form ## -Application.Controllers.controller 'EditPlanController', ['$scope', 'groups', 'plans', 'planPromise', 'machines', 'prices', 'partners', 'CSRF', '$state', '$stateParams', 'growl', '$filter', '_t', 'Plan' -, ($scope, groups, plans, planPromise, machines, prices, partners, CSRF, $state, $stateParams, growl, $filter, _t, Plan) -> +Application.Controllers.controller 'EditPlanController', ['$scope', 'groups', 'plans', 'planPromise', 'machines', 'spaces', 'prices', 'partners', 'CSRF', '$state', '$stateParams', 'growl', '$filter', '_t', 'Plan' +, ($scope, groups, plans, planPromise, machines, spaces, prices, partners, CSRF, $state, $stateParams, growl, $filter, _t, Plan) -> ### PUBLIC SCOPE ### + + ## List of spaces + $scope.spaces = spaces + + ## List of plans + $scope.plans = plans + + ## List of machines + $scope.machines = machines + + ## List of groups $scope.groups = groups + ## current form is used for edition mode $scope.mode = 'edition' @@ -231,9 +210,9 @@ Application.Controllers.controller 'EditPlanController', ['$scope', 'groups', 'p ## $scope.afterSubmit = (content) -> if !content.id? and !content.plan_ids? - growl.error(_t('unable_to_save_subscription_changes_please_try_again')) + growl.error(_t('edit_plan.unable_to_save_subscription_changes_please_try_again')) else - growl.success(_t('subscription_successfully_changed')) + growl.success(_t('edit_plan.subscription_successfully_changed')) $state.go('app.admin.pricing') @@ -251,6 +230,30 @@ Application.Controllers.controller 'EditPlanController', ['$scope', 'groups', 'p + ## + # Retrieve the name of a machine from its ID + # @param machine_id {number} machine identifier + # @returns {string} Machine's name + ## + $scope.getMachineName = (machine_id) -> + for machine in $scope.machines + if machine.id == machine_id + return machine.name + + + + ## + # Retrieve the name of a space from its ID + # @param space_id {number} space identifier + # @returns {string} Space's name + ## + $scope.getSpaceName = (space_id) -> + for space in $scope.spaces + if space.id == space_id + return space.name + + + ### PRIVATE SCOPE ### ## @@ -258,7 +261,7 @@ Application.Controllers.controller 'EditPlanController', ['$scope', 'groups', 'p ## initialize = -> # Using the PlansController - new PlanController($scope, groups, plans, machines, prices, partners, CSRF) + new PlanController($scope, groups, prices, partners, CSRF) ## !!! MUST BE CALLED AT THE END of the controller initialize() diff --git a/app/assets/javascripts/controllers/admin/pricing.coffee.erb b/app/assets/javascripts/controllers/admin/pricing.coffee.erb index 76bcf7cf6..c77e181e4 100644 --- a/app/assets/javascripts/controllers/admin/pricing.coffee.erb +++ b/app/assets/javascripts/controllers/admin/pricing.coffee.erb @@ -3,8 +3,8 @@ ## # Controller used in the prices edition page ## -Application.Controllers.controller "EditPricingController", ["$scope", "$state", '$uibModal', '$filter', 'TrainingsPricing', 'Credit', 'Pricing', 'Plan', 'Coupon', 'plans', 'groups', 'growl', 'machinesPricesPromise', 'Price', 'dialogs', 'trainingsPricingsPromise', 'trainingsPromise', 'machineCreditsPromise', 'machinesPromise', 'trainingCreditsPromise', 'couponsPromise', '_t' -, ($scope, $state, $uibModal, $filter, TrainingsPricing, Credit, Pricing, Plan, Coupon, plans, groups, growl, machinesPricesPromise, Price, dialogs, trainingsPricingsPromise, trainingsPromise, machineCreditsPromise, machinesPromise, trainingCreditsPromise, couponsPromise, _t) -> +Application.Controllers.controller "EditPricingController", ["$scope", "$state", '$uibModal', '$filter', 'TrainingsPricing', 'Credit', 'Pricing', 'Plan', 'Coupon', 'plans', 'groups', 'growl', 'machinesPricesPromise', 'Price', 'dialogs', 'trainingsPricingsPromise', 'trainingsPromise', 'machineCreditsPromise', 'machinesPromise', 'trainingCreditsPromise', 'couponsPromise', 'spacesPromise', 'spacesPricesPromise', 'spacesCreditsPromise', '_t' +, ($scope, $state, $uibModal, $filter, TrainingsPricing, Credit, Pricing, Plan, Coupon, plans, groups, growl, machinesPricesPromise, Price, dialogs, trainingsPricingsPromise, trainingsPromise, machineCreditsPromise, machinesPromise, trainingCreditsPromise, couponsPromise, spacesPromise, spacesPricesPromise, spacesCreditsPromise, _t) -> ### PUBLIC SCOPE ### ## List of machines prices (not considering any plan) @@ -37,6 +37,15 @@ Application.Controllers.controller "EditPricingController", ["$scope", "$state", ## List of coupons $scope.coupons = couponsPromise + ## List of spaces + $scope.spaces = spacesPromise + + ## Associate free space hours with subscriptions + $scope.spaceCredits = spacesCreditsPromise + + ## List of spaces prices (not considering any plan) + $scope.spacesPrices = spacesPricesPromise + ## The plans list ordering. Default: by group $scope.orderPlans = 'group_id' @@ -56,7 +65,7 @@ Application.Controllers.controller "EditPricingController", ["$scope", "$state", if data? TrainingsPricing.update({ id: trainingsPricing.id }, { trainings_pricing: { amount: data } }).$promise else - _t('please_specify_a_number') + _t('pricing.please_specify_a_number') ## # Retrieve a plan from its given identifier and returns it @@ -89,13 +98,13 @@ Application.Controllers.controller "EditPricingController", ["$scope", "$state", ## $scope.showTrainings = (trainings) -> unless angular.isArray(trainings) and trainings.length > 0 - return _t('none') + return _t('pricing.none') selected = [] angular.forEach $scope.trainings, (t) -> if trainings.indexOf(t.id) >= 0 selected.push t.name - return if selected.length then selected.join(' | ') else _t('none') + return if selected.length then selected.join(' | ') else _t('pricing.none') @@ -110,7 +119,7 @@ Application.Controllers.controller "EditPricingController", ["$scope", "$state", training_credit_nb: newdata.training_credits , angular.noop() # do nothing in case of success , (error) -> - growl.error(_t('an_error_occurred_while_saving_the_number_of_credits')) + growl.error(_t('pricing.an_error_occurred_while_saving_the_number_of_credits')) # save the associated trainings angular.forEach $scope.trainingCreditsGroups, (original, key) -> @@ -126,9 +135,9 @@ Application.Controllers.controller "EditPricingController", ["$scope", "$state", $scope.trainingCredits.splice($scope.trainingCredits.indexOf(tc), 1) $scope.trainingCreditsGroups[planId].splice($scope.trainingCreditsGroups[planId].indexOf(tc.id), 1) , (error) -> - growl.error(_t('an_error_occurred_while_deleting_credit_with_the_TRAINING', {TRAINING:tc.creditable.name})) + growl.error(_t('pricing.an_error_occurred_while_deleting_credit_with_the_TRAINING', {TRAINING:tc.creditable.name})) else - growl.error(_t('an_error_occurred_unable_to_find_the_credit_to_revoke')) + growl.error(_t('pricing.an_error_occurred_unable_to_find_the_credit_to_revoke')) # iterate through the new credits to add angular.forEach newdata.training_ids, (newTrainingId) -> @@ -143,7 +152,7 @@ Application.Controllers.controller "EditPricingController", ["$scope", "$state", $scope.trainingCreditsGroups[newTc.plan_id].push(newTc.creditable_id) , (error) -> # failed training = getTrainingFromId(newTrainingId) - growl.error(_t('an_error_occurred_while_creating_credit_with_the_TRAINING', {TRAINING: training.name})) + growl.error(_t('pricing.an_error_occurred_while_creating_credit_with_the_TRAINING', {TRAINING: training.name})) console.error(error) @@ -177,11 +186,16 @@ Application.Controllers.controller "EditPricingController", ["$scope", "$state", # @param credit {Object} credit object, inherited from $resource ## $scope.showCreditableName = (credit) -> - selected = _t('not_set') + selected = _t('pricing.not_set') if credit and credit.creditable_id - angular.forEach $scope.machines, (m)-> - if m.id == credit.creditable_id - selected = m.name+' ( id. '+m.id+' )' + if credit.creditable_type == 'Machine' + angular.forEach $scope.machines, (m)-> + if m.id == credit.creditable_id + selected = m.name + ' ( id. ' + m.id + ' )' + else if credit.creditable_type == 'Space' + angular.forEach $scope.spaces, (s)-> + if s.id == credit.creditable_id + selected = s.name return selected @@ -195,27 +209,27 @@ Application.Controllers.controller "EditPricingController", ["$scope", "$state", $scope.saveMachineCredit = (data, id) -> for mc in $scope.machineCredits if mc.plan_id == data.plan_id and mc.creditable_id == data.creditable_id and (id == null or mc.id != id) - growl.error(_t('error_a_credit_linking_this_machine_with_that_subscription_already_exists')) + growl.error(_t('pricing.error_a_credit_linking_this_machine_with_that_subscription_already_exists')) unless id $scope.machineCredits.pop() return false if id? Credit.update {id: id}, credit: data, -> - growl.success(_t('changes_have_been_successfully_saved')) + growl.success(_t('pricing.changes_have_been_successfully_saved')) else data.creditable_type = 'Machine' Credit.save credit: data , (resp) -> $scope.machineCredits[$scope.machineCredits.length-1].id = resp.id - growl.success(_t('credit_was_successfully_saved')) + growl.success(_t('pricing.credit_was_successfully_saved')) ## # Removes the newly inserted but not saved machine credit / Cancel the current machine credit modification # @param rowform {Object} see http://vitalets.github.io/angular-xeditable/ - # @param index {number} theme index in the $scope.machineCredits array + # @param index {number} credit index in the $scope.machineCredits array ## $scope.cancelMachineCredit = (rowform, index) -> if $scope.machineCredits[index].id? @@ -235,6 +249,70 @@ Application.Controllers.controller "EditPricingController", ["$scope", "$state", + ## + # Create a new empty entry in the $scope.spaceCredits array + # @param e {Object} see https://docs.angularjs.org/guide/expression#-event- + ## + $scope.addSpaceCredit = (e)-> + e.preventDefault() + e.stopPropagation() + $scope.inserted = + creditable_type: 'Space' + $scope.spaceCredits.push($scope.inserted) + $scope.status.isopen = !$scope.status.isopen + + + + ## + # Validation callback when editing space's credits. Save the changes. + # This will prevent the creation of two credits associated with the same space and plan. + # @param data {Object} space, associated plan and number of credit hours. + # @param [id] {number} credit id for edition, create a new credit object if not provided + ## + $scope.saveSpaceCredit = (data, id) -> + for sc in $scope.spaceCredits + if sc.plan_id == data.plan_id and sc.creditable_id == data.creditable_id and (id == null or sc.id != id) + growl.error(_t('pricing.error_a_credit_linking_this_space_with_that_subscription_already_exists')) + unless id + $scope.spaceCredits.pop() + return false + + if id? + Credit.update {id: id}, credit: data, -> + growl.success(_t('pricing.changes_have_been_successfully_saved')) + else + data.creditable_type = 'Space' + Credit.save + credit: data + , (resp) -> + $scope.spaceCredits[$scope.spaceCredits.length - 1].id = resp.id + growl.success(_t('pricing.credit_was_successfully_saved')) + + + + ## + # Removes the newly inserted but not saved space credit / Cancel the current space credit modification + # @param rowform {Object} see http://vitalets.github.io/angular-xeditable/ + # @param index {number} credit index in the $scope.spaceCredits array + ## + $scope.cancelSpaceCredit = (rowform, index) -> + if $scope.spaceCredits[index].id? + rowform.$cancel() + else + $scope.spaceCredits.splice(index, 1) + + + + ## + # Deletes the space credit at the specified index + # @param index {number} space credit index in the $scope.spaceCredits array + ## + $scope.removeSpaceCredit = (index) -> + Credit.delete $scope.spaceCredits[index] + $scope.spaceCredits.splice(index, 1) + + + ## # If the plan does not have a type, return a default value for display purposes # @param type {string|undefined|null} plan's type (eg. 'partner') @@ -242,8 +320,8 @@ Application.Controllers.controller "EditPricingController", ["$scope", "$state", ## $scope.getPlanType = (type) -> if type == 'PartnerPlan' - return _t('partner') - else return _t('standard') + return _t('pricing.partner') + else return _t('pricing.standard') ## # Change the plans ordering criterion to the one provided @@ -270,7 +348,7 @@ Application.Controllers.controller "EditPricingController", ["$scope", "$state", if data? Price.update({ id: price.id }, { price: { amount: data } }).$promise else - _t('please_specify_a_number') + _t('pricing.please_specify_a_number') ## # Delete the specified subcription plan @@ -284,17 +362,17 @@ Application.Controllers.controller "EditPricingController", ["$scope", "$state", dialogs.confirm resolve: object: -> - title: _t('confirmation_required') - msg: _t('do_you_really_want_to_delete_this_subscription_plan') + title: _t('pricing.confirmation_required') + msg: _t('pricing.do_you_really_want_to_delete_this_subscription_plan') , -> # the admin has confirmed, delete the plan Plan.delete {id: id}, (res) -> - growl.success(_t('subscription_plan_was_successfully_deleted')) + growl.success(_t('pricing.subscription_plan_was_successfully_deleted')) $scope.plans.splice(findItemIdxById(plans, id), 1) , (error) -> console.error('[EditPricingController::deletePlan] Error: '+error.statusText) if error.statusText - growl.error(_t('unable_to_delete_the_specified_subscription_an_error_occurred')) + growl.error(_t('pricing.unable_to_delete_the_specified_subscription_an_error_occurred')) @@ -324,8 +402,8 @@ Application.Controllers.controller "EditPricingController", ["$scope", "$state", dialogs.confirm resolve: object: -> - title: _t('confirmation_required') - msg: _t('do_you_really_want_to_delete_this_coupon') + title: _t('pricing.confirmation_required') + msg: _t('pricing.do_you_really_want_to_delete_this_coupon') , -> # the admin has confirmed, delete the coupon Coupon.delete {id: id}, (res) -> @@ -335,9 +413,9 @@ Application.Controllers.controller "EditPricingController", ["$scope", "$state", , (error) -> console.error('[EditPricingController::deleteCoupon] Error: '+error.statusText) if error.statusText if error.status == 422 - growl.error(_t('unable_to_delete_the_specified_coupon_already_in_use')) + growl.error(_t('pricing.unable_to_delete_the_specified_coupon_already_in_use')) else - growl.error(_t('unable_to_delete_the_specified_coupon_an_unexpected_error_occurred')) + growl.error(_t('pricing.unable_to_delete_the_specified_coupon_an_unexpected_error_occurred')) @@ -363,10 +441,10 @@ Application.Controllers.controller "EditPricingController", ["$scope", "$state", ## Callback to validate sending of the coupon $scope.ok = -> Coupon.send {coupon_code: coupon.code, user_id: $scope.ctrl.member.id}, (res) -> - growl.success(_t('coupon_successfully_sent_to_USER', {USER: $scope.ctrl.member.name})) + growl.success(_t('pricing.coupon_successfully_sent_to_USER', {USER: $scope.ctrl.member.name})) $uibModalInstance.close({user_id: $scope.ctrl.member.id}) , (err) -> - growl.error(_t('an_error_occurred_unable_to_send_the_coupon')) + growl.error(_t('pricing.an_error_occurred_unable_to_send_the_coupon')) ## Callback to close the modal and cancel the sending process $scope.cancel = -> diff --git a/app/assets/javascripts/controllers/admin/settings.coffee b/app/assets/javascripts/controllers/admin/settings.coffee index a1e3b94a8..1bb3ff416 100644 --- a/app/assets/javascripts/controllers/admin/settings.coffee +++ b/app/assets/javascripts/controllers/admin/settings.coffee @@ -46,6 +46,7 @@ Application.Controllers.controller "SettingsController", ["$scope", 'Setting', ' $scope.trainingInformationMessage = { name: 'training_information_message', value: settingsPromise.training_information_message} $scope.subscriptionExplicationsAlert = { name: 'subscription_explications_alert', value: settingsPromise.subscription_explications_alert } $scope.eventExplicationsAlert = {name: 'event_explications_alert', value: settingsPromise.event_explications_alert } + $scope.spaceExplicationsAlert = { name: 'space_explications_alert', value: settingsPromise.space_explications_alert } $scope.windowStart = { name: 'booking_window_start', value: settingsPromise.booking_window_start } $scope.windowEnd = { name: 'booking_window_end', value: settingsPromise.booking_window_end } $scope.mainColorSetting = { name: 'main_color', value: settingsPromise.main_color } @@ -116,7 +117,7 @@ Application.Controllers.controller "SettingsController", ["$scope", 'Setting', ' value = setting.value Setting.update { name: setting.name }, { value: value }, (data)-> - growl.success(_t('customization_of_SETTING_successfully_saved', {SETTING:_t(setting.name)})) + growl.success(_t('settings.customization_of_SETTING_successfully_saved', { SETTING:_t('settings.' + setting.name) })) , (error)-> console.log(error) @@ -135,7 +136,7 @@ Application.Controllers.controller "SettingsController", ["$scope", 'Setting', ' angular.forEach v, (err)-> growl.error(err) else - growl.success(_t('file_successfully_updated')) + growl.success(_t('settings.file_successfully_updated')) if content.custom_asset.name is 'cgu-file' $scope.cguFile = content.custom_asset $scope.methods.cgu = 'put' diff --git a/app/assets/javascripts/controllers/admin/statistics.coffee.erb b/app/assets/javascripts/controllers/admin/statistics.coffee.erb index b99297b21..834cae4db 100644 --- a/app/assets/javascripts/controllers/admin/statistics.coffee.erb +++ b/app/assets/javascripts/controllers/admin/statistics.coffee.erb @@ -144,7 +144,7 @@ Application.Controllers.controller "StatisticsController", ["$scope", "$state", ## # Callback called when the active tab is changed. # recover the current tab and store its value in $scope.selectedIndex - # @param tab {Object} elasticsearch statistic structure + # @param tab {Object} elasticsearch statistic structure (from statistic_indices table) ## $scope.setActiveTab = (tab) -> $scope.selectedIndex = tab @@ -160,6 +160,23 @@ Application.Controllers.controller "StatisticsController", ["$scope", "$state", + ## + # Returns true if the provided tab must be hidden due to some global or local configuration + # @param tab {Object} elasticsearch statistic structure (from statistic_indices table) + ## + $scope.hiddenTab = (tab) -> + if tab.table + if tab.es_type_key == 'subscription' && $rootScope.fablabWithoutPlans + true + else if tab.es_type_key == 'space' && $rootScope.fablabWithoutSpaces + true + else + false + else + true + + + ## # Callback to validate the filters and send a new request to elastic ## diff --git a/app/assets/javascripts/controllers/application.coffee.erb b/app/assets/javascripts/controllers/application.coffee.erb index 683c2e3ff..f08676e60 100644 --- a/app/assets/javascripts/controllers/application.coffee.erb +++ b/app/assets/javascripts/controllers/application.coffee.erb @@ -242,6 +242,10 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco # user is not logged in openLoginModal(toState, toParams) + # we stop polling notifications when the page is not in foreground + onPageVisible (state) -> + $rootScope.toCheckNotifications = (state is 'visible') + Setting.get { name: 'fablab_name' }, (data)-> $scope.fablabName = data.setting.value Setting.get { name: 'name_genre' }, (data)-> @@ -250,8 +254,9 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco # shorthands - $scope.isAuthenticated = Auth.isAuthenticated; - $scope.isAuthorized = AuthService.isAuthorized; + $scope.isAuthenticated = Auth.isAuthenticated + $scope.isAuthorized = AuthService.isAuthorized + $rootScope.login = $scope.login @@ -370,6 +375,52 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco + ## + # Detect if the current page (tab/window) is active of put as background. + # When the status changes, the callback is triggered with the new status as parameter + # Inspired by http://stackoverflow.com/questions/1060008/is-there-a-way-to-detect-if-a-browser-window-is-not-currently-active#answer-1060034 + ## + onPageVisible = (callback) -> + hidden = 'hidden' + + onchange = (evt) -> + v = 'visible' + h = 'hidden' + evtMap = + focus: v + focusin: v + pageshow: v + blur: h + focusout: h + pagehide: h + evt = evt or window.event + if evt.type of evtMap + if typeof callback == 'function' then callback(evtMap[evt.type]) + else + if typeof callback == 'function' then callback(if @[hidden] then 'hidden' else 'visible') + return + + # Standards: + if hidden of document + document.addEventListener 'visibilitychange', onchange + else if (hidden = 'mozHidden') of document + document.addEventListener 'mozvisibilitychange', onchange + else if (hidden = 'webkitHidden') of document + document.addEventListener 'webkitvisibilitychange', onchange + else if (hidden = 'msHidden') of document + document.addEventListener 'msvisibilitychange', onchange + # IE 9 and lower + else if 'onfocusin' of document + document.onfocusin = document.onfocusout = onchange + # All others + else + window.onpageshow = window.onpagehide = window.onfocus = window.onblur = onchange + # set the initial state (but only if browser supports the Page Visibility API) + if document[hidden] != undefined + onchange type: if document[hidden] then 'blur' else 'focus' + + + ## !!! MUST BE CALLED AT THE END of the controller initialize() ] diff --git a/app/assets/javascripts/controllers/calendar.coffee b/app/assets/javascripts/controllers/calendar.coffee index bb020c1ff..6b2e9e8b5 100644 --- a/app/assets/javascripts/controllers/calendar.coffee +++ b/app/assets/javascripts/controllers/calendar.coffee @@ -4,14 +4,15 @@ # Controller used in the public calendar global ## -Application.Controllers.controller "CalendarController", ["$scope", "$state", "$aside", "moment", "Availability", 'Slot', 'Setting', 'growl', 'dialogs', 'bookingWindowStart', 'bookingWindowEnd', '_t', 'uiCalendarConfig', 'CalendarConfig', 'trainingsPromise', 'machinesPromise', -($scope, $state, $aside, moment, Availability, Slot, Setting, growl, dialogs, bookingWindowStart, bookingWindowEnd, _t, uiCalendarConfig, CalendarConfig, trainingsPromise, machinesPromise) -> +Application.Controllers.controller "CalendarController", ["$scope", "$state", "$aside", "moment", "Availability", 'Slot', 'Setting', 'growl', 'dialogs', 'bookingWindowStart', 'bookingWindowEnd', '_t', 'uiCalendarConfig', 'CalendarConfig', 'trainingsPromise', 'machinesPromise', 'spacesPromise', +($scope, $state, $aside, moment, Availability, Slot, Setting, growl, dialogs, bookingWindowStart, bookingWindowEnd, _t, uiCalendarConfig, CalendarConfig, trainingsPromise, machinesPromise, spacesPromise) -> ### PRIVATE STATIC CONSTANTS ### currentMachineEvent = null machinesPromise.forEach((m) -> m.checked = true) trainingsPromise.forEach((t) -> t.checked = true) + spacesPromise.forEach((s) -> s.checked = true) ## check all formation/machine is select in filter isSelectAll = (type, scope) -> @@ -25,6 +26,9 @@ Application.Controllers.controller "CalendarController", ["$scope", "$state", "$ ## List of machines $scope.machines = machinesPromise + ## List of spaces + $scope.spaces = spacesPromise + ## add availabilities source to event sources $scope.eventSources = [] @@ -34,6 +38,7 @@ Application.Controllers.controller "CalendarController", ["$scope", "$state", "$ scope.filter = $scope.filter = trainings: isSelectAll('trainings', scope) machines: isSelectAll('machines', scope) + spaces: isSelectAll('spaces', scope) evt: filter.evt dispo: filter.dispo $scope.calendarConfig.events = availabilitySourceUrl() @@ -43,6 +48,7 @@ Application.Controllers.controller "CalendarController", ["$scope", "$state", "$ $scope.filter = trainings: isSelectAll('trainings', $scope) machines: isSelectAll('machines', $scope) + spaces: isSelectAll('spaces', $scope) evt: true dispo: true @@ -62,15 +68,18 @@ Application.Controllers.controller "CalendarController", ["$scope", "$state", "$ $scope.trainings machines: -> $scope.machines + spaces: -> + $scope.spaces filter: -> $scope.filter toggleFilter: -> $scope.toggleFilter filterAvailabilities: -> $scope.filterAvailabilities - controller: ['$scope', '$uibModalInstance', 'trainings', 'machines', 'filter', 'toggleFilter', 'filterAvailabilities', ($scope, $uibModalInstance, trainings, machines, filter, toggleFilter, filterAvailabilities) -> + controller: ['$scope', '$uibModalInstance', 'trainings', 'machines', 'spaces', 'filter', 'toggleFilter', 'filterAvailabilities', ($scope, $uibModalInstance, trainings, machines, spaces, filter, toggleFilter, filterAvailabilities) -> $scope.trainings = trainings $scope.machines = machines + $scope.spaces = spaces $scope.filter = filter $scope.toggleFilter = (type, filter) -> @@ -94,13 +103,19 @@ Application.Controllers.controller "CalendarController", ["$scope", "$state", "$ currentMachineEvent = event calendar.fullCalendar('changeView', 'agendaDay') calendar.fullCalendar('gotoDate', event.start) + else if event.available_type == 'space' + calendar.fullCalendar('changeView', 'agendaDay') + calendar.fullCalendar('gotoDate', event.start) + else if event.available_type == 'event' + $state.go('app.public.events_show', {id: event.event_id}) + else if event.available_type == 'training' + $state.go('app.public.training_show', {id: event.training_id}) else - if event.available_type == 'event' - $state.go('app.public.events_show', {id: event.event_id}) - else if event.available_type == 'training' - $state.go('app.public.training_show', {id: event.training_id}) - else + if event.machine_id $state.go('app.public.machines_show', {id: event.machine_id}) + else if event.space_id + $state.go('app.public.space_show', {id: event.space_id}) + ## agendaDay view: disable slotEventOverlap ## agendaWeek view: enable slotEventOverlap @@ -109,10 +124,10 @@ Application.Controllers.controller "CalendarController", ["$scope", "$state", "$ # ui-calendar will trigger rerender calendar $scope.calendarConfig.defaultView = view.type today = if currentMachineEvent then currentMachineEvent.start else moment().utc().startOf('day') - if today > view.start and today < view.end and today != view.start + if today > view.intervalStart and today < view.intervalEnd and today != view.intervalStart $scope.calendarConfig.defaultDate = today else - $scope.calendarConfig.defaultDate = view.start + $scope.calendarConfig.defaultDate = view.intervalStart if view.type == 'agendaDay' $scope.calendarConfig.slotEventOverlap = false else @@ -136,7 +151,8 @@ Application.Controllers.controller "CalendarController", ["$scope", "$state", "$ getFilter = -> t = $scope.trainings.filter((t) -> t.checked).map((t) -> t.id) m = $scope.machines.filter((m) -> m.checked).map((m) -> m.id) - {t: t, m: m, evt: $scope.filter.evt, dispo: $scope.filter.dispo} + s = $scope.spaces.filter((s) -> s.checked).map((s) -> s.id) + {t: t, m: m, s: s, evt: $scope.filter.evt, dispo: $scope.filter.dispo} availabilitySourceUrl = -> "/api/availabilities/public?#{$.param(getFilter())}" diff --git a/app/assets/javascripts/controllers/machines.coffee.erb b/app/assets/javascripts/controllers/machines.coffee.erb index 6234165e5..fd3d0ed5e 100644 --- a/app/assets/javascripts/controllers/machines.coffee.erb +++ b/app/assets/javascripts/controllers/machines.coffee.erb @@ -93,7 +93,7 @@ _reserveMachine = (machine, e) -> # if the currently logged'in user has completed the training for this machine, or this machine does not require # a prior training, just redirect him to the machine's booking page if machine.current_user_is_training or machine.trainings.length == 0 - _this.$state.go('app.logged.machines_reserve', {id: machine.id}) + _this.$state.go('app.logged.machines_reserve', {id: machine.slug}) else # otherwise, if a user is authenticated ... if _this.$scope.isAuthenticated() @@ -234,7 +234,7 @@ Application.Controllers.controller "EditMachineController", ["$scope", '$state', Application.Controllers.controller "ShowMachineController", ['$scope', '$state', '$uibModal', '$stateParams', '_t', 'Machine', 'growl', 'machinePromise', 'dialogs' , ($scope, $state, $uibModal, $stateParams, _t, Machine, growl, machinePromise, dialogs) -> -## Retrieve the details for the machine id in the URL, if an error occurs redirect the user to the machines list + ## Retrieve the details for the machine id in the URL, if an error occurs redirect the user to the machines list $scope.machine = machinePromise ## @@ -274,20 +274,20 @@ Application.Controllers.controller "ShowMachineController", ['$scope', '$state', # This controller workflow is pretty similar to the trainings reservation controller. ## -Application.Controllers.controller "ReserveMachineController", ["$scope", "$state", '$stateParams', "$uibModal", '_t', "moment", 'Machine', 'Auth', 'dialogs', '$timeout', 'Price', 'Member', 'Availability', 'Slot', 'Setting', 'CustomAsset', 'plansPromise', 'groupsPromise', 'growl', 'machinePromise', 'settingsPromise', 'Wallet', 'helpers', 'uiCalendarConfig', 'CalendarConfig', -($scope, $state, $stateParams, $uibModal, _t, moment, Machine, Auth, dialogs, $timeout, Price, Member, Availability, Slot, Setting, CustomAsset, plansPromise, groupsPromise, growl, machinePromise, settingsPromise, Wallet, helpers, uiCalendarConfig, CalendarConfig) -> +Application.Controllers.controller "ReserveMachineController", ["$scope", '$stateParams', '_t', "moment", 'Auth', '$timeout', 'Member', 'Availability', 'plansPromise', 'groupsPromise', 'machinePromise', 'settingsPromise', 'uiCalendarConfig', 'CalendarConfig', +($scope, $stateParams, _t, moment, Auth, $timeout, Member, Availability, plansPromise, groupsPromise, machinePromise, settingsPromise, uiCalendarConfig, CalendarConfig) -> ### PRIVATE STATIC CONSTANTS ### - # Slot already booked by the current user + # Slot free to be booked FREE_SLOT_BORDER_COLOR = '<%= AvailabilityHelper::MACHINE_COLOR %>' # Slot already booked by another user UNAVAILABLE_SLOT_BORDER_COLOR = '<%= AvailabilityHelper::MACHINE_IS_RESERVED_BY_USER %>' - # Slot free to be booked + # Slot already booked by the current user BOOKED_SLOT_BORDER_COLOR = '<%= AvailabilityHelper::IS_RESERVED_BY_CURRENT_USER %>' @@ -297,33 +297,31 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat ## bind the machine availabilities with full-Calendar events $scope.eventSources = [] - ## fullCalendar event. The last selected slot that the user want to book - $scope.slotToPlace = null - - ## fullCalendar event. An already booked slot that the user want to modify - $scope.slotToModify = null - ## indicates the state of the current view : calendar or plans information $scope.plansAreShown = false ## will store the user's plan if he choosed to buy one $scope.selectedPlan = null - ## array of fullCalendar events. Slots where the user want to book - $scope.eventsReserved = [] + ## the moment when the plan selection changed for the last time, used to trigger changes in the cart + $scope.planSelectionTime = null - ## total amount of the bill to pay - $scope.amountTotal = 0 + ## mapping of fullCalendar events. + $scope.events = + reserved: [] # Slots that the user wants to book + modifiable: null # Slot that the user wants to change + placable: null # Destination slot for the change + paid: [] # Slots that were just booked by the user (transaction ok) + moved: null # Slots that were just moved by the user (change done) -> {newSlot:* oldSlot: *} - ## total amount of the elements in the cart, without considering any coupon - $scope.totalNoCoupon = 0 + ## the moment when the slot selection changed for the last time, used to trigger changes in the cart + $scope.selectionTime = null - ## Discount coupon to apply to the basket, if any - $scope.coupon = - applied: null + ## the last clicked event in the calender + $scope.selectedEvent = null - ## is the user allowed to change the date of his booking - $scope.enableBookingMove = true + ## the application global settings + $scope.settings = settingsPromise ## list of plans, classified by group $scope.plansClassifiedByGroup = [] @@ -352,86 +350,87 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat ## Global config: message to the end user concerning the subscriptions rules $scope.subscriptionExplicationsAlert = settingsPromise.subscription_explications_alert - ## Gloabl config: message to the end user concerning the machine bookings + ## Global config: message to the end user concerning the machine bookings $scope.machineExplicationsAlert = settingsPromise.machine_explications_alert - ## Global config: is the user authorized to change his bookings slots? - $scope.enableBookingMove = (settingsPromise.booking_move_enable == "true") - ## Global config: delay in hours before a booking while changing the booking slot is forbidden - $scope.moveBookingDelay = parseInt(settingsPromise.booking_move_delay) - ## Global config: is the user authorized to cancel his bookings? - $scope.enableBookingCancel = (settingsPromise.booking_cancel_enable == "true") - - ## Global config: delay in hours before a booking while the cancellation is forbidden - $scope.cancelBookingDelay = parseInt(settingsPromise.booking_cancel_delay) + ## + # Change the last selected slot's appearence to looks like 'added to cart' + ## + $scope.markSlotAsAdded = -> + $scope.selectedEvent.backgroundColor = FREE_SLOT_BORDER_COLOR + $scope.selectedEvent.title = _t('i_reserve') + updateCalendar() ## - # Cancel the current booking modification, removing the previously booked slot from the selection - # @param e {Object} see https://docs.angularjs.org/guide/expression#-event- + # Change the last selected slot's appearence to looks like 'never added to cart' ## - $scope.removeSlotToModify = (e) -> - e.preventDefault() - if $scope.slotToPlace - $scope.slotToPlace.backgroundColor = 'white' - $scope.slotToPlace.title = '' - $scope.slotToPlace = null - $scope.slotToModify.title = if $scope.currentUser.role isnt 'admin' then _t('i_ve_reserved') else _t('not_available') - $scope.slotToModify.backgroundColor = 'white' - $scope.slotToModify = null - uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents' + $scope.markSlotAsRemoved = (slot) -> + slot.backgroundColor = 'white' + slot.borderColor = FREE_SLOT_BORDER_COLOR + slot.title = '' + slot.isValid = false + slot.id = null + slot.is_reserved = false + slot.can_modify = false + slot.offered = false + updateCalendar() ## - # When modifying an already booked reservation, cancel the choice of the new slot - # @param e {Object} see https://docs.angularjs.org/guide/expression#-event- + # Callback when a slot was successfully cancelled. Reset the slot style as 'ready to book' ## - $scope.removeSlotToPlace = (e)-> - e.preventDefault() - $scope.slotToPlace.backgroundColor = 'white' - $scope.slotToPlace.title = '' - $scope.slotToPlace = null - uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents' + $scope.slotCancelled = -> + $scope.markSlotAsRemoved($scope.selectedEvent) ## - # When modifying an already booked reservation, confirm the modification. + # Change the last selected slot's appearence to looks like 'currently looking for a new destination to exchange' + ## + $scope.markSlotAsModifying = -> + $scope.selectedEvent.backgroundColor = '#eee' + $scope.selectedEvent.title = _t('i_change') + updateCalendar() + + + + ## + # Change the last selected slot's appearence to looks like 'the slot being exchanged will take this place' + ## + $scope.changeModifyMachineSlot = -> + if $scope.events.placable + $scope.events.placable.backgroundColor = 'white' + $scope.events.placable.title = '' + if !$scope.events.placable or $scope.events.placable._id != $scope.selectedEvent._id + $scope.selectedEvent.backgroundColor = '#bbb' + $scope.selectedEvent.title = _t('i_shift') + updateCalendar() + + + ## + # When modifying an already booked reservation, callback when the modification was successfully done. ## $scope.modifyMachineSlot = -> - Slot.update {id: $scope.slotToModify.id}, - slot: - start_at: $scope.slotToPlace.start - end_at: $scope.slotToPlace.end - availability_id: $scope.slotToPlace.availability_id - , -> # success - $scope.modifiedSlots = - newReservedSlot: $scope.slotToPlace - oldReservedSlot: $scope.slotToModify - $scope.slotToPlace.title = if $scope.currentUser.role isnt 'admin' then _t('i_ve_reserved') else _t('not_available') - $scope.slotToPlace.backgroundColor = 'white' - $scope.slotToPlace.borderColor = $scope.slotToModify.borderColor - $scope.slotToPlace.id = $scope.slotToModify.id - $scope.slotToPlace.is_reserved = true - $scope.slotToPlace.can_modify = true - $scope.slotToPlace = null + $scope.events.placable.title = if $scope.currentUser.role isnt 'admin' then _t('i_ve_reserved') else _t('not_available') + $scope.events.placable.backgroundColor = 'white' + $scope.events.placable.borderColor = $scope.events.modifiable.borderColor + $scope.events.placable.id = $scope.events.modifiable.id + $scope.events.placable.is_reserved = true + $scope.events.placable.can_modify = true - $scope.slotToModify.backgroundColor = 'white' - $scope.slotToModify.title = '' - $scope.slotToModify.borderColor = FREE_SLOT_BORDER_COLOR - $scope.slotToModify.id = null - $scope.slotToModify.is_reserved = false - $scope.slotToModify.can_modify = false - $scope.slotToModify = null + $scope.events.modifiable.backgroundColor = 'white' + $scope.events.modifiable.title = '' + $scope.events.modifiable.borderColor = FREE_SLOT_BORDER_COLOR + $scope.events.modifiable.id = null + $scope.events.modifiable.is_reserved = false + $scope.events.modifiable.can_modify = false - uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents' - , (err) -> # failure - growl.error(_t('unable_to_change_the_reservation')) - console.error(err) + updateCalendar() @@ -439,14 +438,13 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat # Cancel the current booking modification, reseting the whole process ## $scope.cancelModifyMachineSlot = -> - $scope.slotToPlace.backgroundColor = 'white' - $scope.slotToPlace.title = '' - $scope.slotToPlace = null - $scope.slotToModify.title = if $scope.currentUser.role isnt 'admin' then _t('i_ve_reserved') else _t('not_available') - $scope.slotToModify.backgroundColor = 'white' - $scope.slotToModify = null + if $scope.events.placable + $scope.events.placable.backgroundColor = 'white' + $scope.events.placable.title = '' + $scope.events.modifiable.title = if $scope.currentUser.role isnt 'admin' then _t('i_ve_reserved') else _t('not_available') + $scope.events.modifiable.backgroundColor = 'white' - uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents' + updateCalendar() @@ -455,67 +453,10 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat # reservations. (admins only) ## $scope.updateMember = -> - $scope.paidMachineSlots = null $scope.plansAreShown = false $scope.selectedPlan = null Member.get {id: $scope.ctrl.member.id}, (member) -> $scope.ctrl.member = member - updateCartPrice() - - - - ## - # Add the provided slot to the shopping cart (state transition from free to 'about to be reserved') - # and increment the total amount of the cart if needed. - # @param machineSlot {Object} fullCalendar event object - ## - $scope.validMachineSlot = (machineSlot)-> - machineSlot.isValid = true - updateCartPrice() - - - - ## - # Remove the provided slot from the shopping cart (state transition from 'about to be reserved' to free) - # and decrement the total amount of the cart if needed. - # @param machineSlot {Object} fullCalendar event object - # @param e {Object} see https://docs.angularjs.org/guide/expression#-event- - ## - $scope.removeMachineSlot = (machineSlot, e)-> - e.preventDefault() if e - machineSlot.backgroundColor = 'white' - machineSlot.borderColor = FREE_SLOT_BORDER_COLOR - machineSlot.title = '' - machineSlot.isValid = false - - if machineSlot.machine.is_reduced_amount - angular.forEach $scope.ctrl.member.machine_credits, (credit)-> - if credit.machine_id = machineSlot.machine.id - credit.hours_used-- - machineSlot.machine.is_reduced_amount = false - - index = $scope.eventsReserved.indexOf(machineSlot) - $scope.eventsReserved.splice(index, 1) - if $scope.eventsReserved.length == 0 - if $scope.plansAreShown - $scope.selectedPlan = null - $scope.plansAreShown = false - updateCartPrice() - $timeout -> - uiCalendarConfig.calendars.calendar.fullCalendar 'refetchEvents' - uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents' - - - - ## - # Checks that every selected slots were added to the shopping cart. Ie. will return false if - # any checked slot was not validated by the user. - ## - $scope.machineSlotsValid = -> - isValid = true - angular.forEach $scope.eventsReserved, (m)-> - isValid = false if !m.isValid - isValid @@ -527,30 +468,10 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat e.preventDefault() $scope.plansAreShown = false $scope.selectPlan($scope.selectedPlan) + $scope.planSelectionTime = new Date() - ## - # Validates the shopping chart and redirect the user to the payment step - ## - $scope.payMachine = -> - - # first, we check that a user was selected - if Object.keys($scope.ctrl.member).length > 0 - reservation = mkReservation($scope.ctrl.member, $scope.eventsReserved, $scope.selectedPlan) - - Wallet.getWalletByUser {user_id: $scope.ctrl.member.id}, (wallet) -> - amountToPay = helpers.getAmountToPay($scope.amountTotal, wallet.amount) - if $scope.currentUser.role isnt 'admin' and amountToPay > 0 - payByStripe(reservation) - else - if $scope.currentUser.role is 'admin' or amountToPay is 0 - payOnSite(reservation) - else - # otherwise we alert, this error musn't occur when the current user is not admin - growl.error(_t('please_select_a_member_first')) - - ## # Switch the user's view from the reservation agenda to the plan subscription ## @@ -564,34 +485,41 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat # @param plan {Object} the plan to subscribe ## $scope.selectPlan = (plan) -> - if $scope.isAuthenticated() - angular.forEach $scope.eventsReserved, (machineSlot)-> - angular.forEach $scope.ctrl.member.machine_credits, (credit)-> - if credit.machine_id = machineSlot.machine.id - credit.hours_used = 0 - machineSlot.machine.is_reduced_amount = false - - if $scope.selectedPlan != plan - $scope.selectedPlan = plan - else - $scope.selectedPlan = null - updateCartPrice() + # toggle selected plan + if $scope.selectedPlan != plan + $scope.selectedPlan = plan else - $scope.login null, -> - $scope.selectedPlan = plan - updateCartPrice() + $scope.selectedPlan = null + $scope.planSelectionTime = new Date() ## - # Checks if $scope.slotToModify and $scope.slotToPlace have tag incompatibilities - # @returns {boolean} true in case of incompatibility + # Once the reservation is booked (payment process successfully completed), change the event style + # in fullCalendar, update the user's subscription and free-credits if needed + # @param reservation {Object} ## - $scope.tagMissmatch = -> - for tag in $scope.slotToModify.tags - if tag.id not in $scope.slotToPlace.tag_ids - return true - false + $scope.afterPayment = (reservation)-> + angular.forEach $scope.events.reserved, (machineSlot, key) -> + machineSlot.is_reserved = true + machineSlot.can_modify = true + if $scope.currentUser.role isnt 'admin' + machineSlot.title = _t('i_ve_reserved') + machineSlot.borderColor = BOOKED_SLOT_BORDER_COLOR + updateMachineSlot(machineSlot, reservation, $scope.currentUser) + else + machineSlot.title = _t('not_available') + machineSlot.borderColor = UNAVAILABLE_SLOT_BORDER_COLOR + updateMachineSlot(machineSlot, reservation, $scope.ctrl.member) + machineSlot.backgroundColor = 'white' + + if $scope.selectedPlan + $scope.ctrl.member.subscribed_plan = angular.copy($scope.selectedPlan) + Auth._currentUser.subscribed_plan = angular.copy($scope.selectedPlan) + $scope.plansAreShown = false + $scope.selectedPlan = null + + refetchCalendar() @@ -609,75 +537,6 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat if $scope.currentUser.role isnt 'admin' $scope.ctrl.member = $scope.currentUser - # watch when a coupon is applied to re-compute the total price - $scope.$watch 'coupon.applied', (newValue, oldValue) -> - unless newValue == null and oldValue == null - updateCartPrice() - - - - ## - # Create an hash map implementing the Reservation specs - # @param member {Object} User as retreived from the API: current user / selected user if current is admin - # @param slots {Array} Array of fullCalendar events: slots selected on the calendar - # @param [plan] {Object} Plan as retrived from the API: plan to buy with the current reservation - # @return {{user_id:Number, reservable_id:Number, reservable_type:String, slots_attributes:Array, plan_id:Number|null}} - ## - mkReservation = (member, slots, plan = null) -> - reservation = - user_id: member.id - reservable_id: (slots[0].machine.id if slots.length > 0) - reservable_type: 'Machine' - slots_attributes: [] - plan_id: (plan.id if plan) - angular.forEach slots, (slot, key) -> - reservation.slots_attributes.push - start_at: slot.start - end_at: slot.end - availability_id: slot.availability_id - offered: slot.offered || false - - reservation - - - - ## - # Format the parameters expected by /api/prices/compute or /api/reservations and return the resulting object - # @param reservation {Object} as returned by mkReservation() - # @param coupon {Object} Coupon as returned from the API - # @return {{reservation:Object, coupon_code:string}} - ## - mkRequestParams = (reservation, coupon) -> - params = - reservation: reservation - coupon_code: (coupon.code if coupon) - - params - - - - ## - # Update the total price of the current selection/reservation - ## - updateCartPrice = -> - if Object.keys($scope.ctrl.member).length > 0 - r = mkReservation($scope.ctrl.member, $scope.eventsReserved, $scope.selectedPlan) - Price.compute mkRequestParams(r, $scope.coupon.applied), (res) -> - $scope.amountTotal = res.price - $scope.totalNoCoupon = res.price_without_coupon - setSlotsDetails(res.details) - else - # otherwise we alert, this error musn't occur when the current user is not admin - growl.warning(_t('please_select_a_member_first')) - $scope.amountTotal = null - - - setSlotsDetails = (details) -> - angular.forEach $scope.eventsReserved, (slot) -> - angular.forEach details.slots, (s) -> - if moment(s.start_at).isSame(slot.start) - slot.promo = s.promo - slot.price = s.price ## @@ -687,67 +546,8 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat # if it's too late). ## calendarEventClickCb = (event, jsEvent, view) -> - - if !event.is_reserved && !$scope.slotToModify - index = $scope.eventsReserved.indexOf(event) - if index == -1 - event.backgroundColor = FREE_SLOT_BORDER_COLOR - event.title = _t('i_reserve') - $scope.eventsReserved.push event - else - $scope.removeMachineSlot(event) - $scope.paidMachineSlots = null - $scope.selectedPlan = null - $scope.modifiedSlots = null - else if !event.is_reserved && $scope.slotToModify - if $scope.slotToPlace - $scope.slotToPlace.backgroundColor = 'white' - $scope.slotToPlace.title = '' - $scope.slotToPlace = event - event.backgroundColor = '#bbb' - event.title = _t('i_shift') - else if event.is_reserved and (slotCanBeModified(event) or slotCanBeCanceled(event)) and !$scope.slotToModify and $scope.eventsReserved.length == 0 - event.movable = slotCanBeModified(event) - event.cancelable = slotCanBeCanceled(event) - dialogs.confirm - templateUrl: '<%= asset_path "shared/confirm_modify_slot_modal.html" %>' - resolve: - object: -> event - , (type) -> - if type == 'move' - $scope.modifiedSlots = null - $scope.slotToModify = event - event.backgroundColor = '#eee' - event.title = _t('i_change') - uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents' - else if type == 'cancel' - dialogs.confirm - resolve: - object: -> - title: _t('confirmation_required') - msg: _t('do_you_really_want_to_cancel_this_reservation') - , -> # cancel confirmed - Slot.cancel {id: event.id}, -> # successfully canceled - growl.success _t('reservation_was_cancelled_successfully') - $scope.canceledSlot = event - $scope.canceledSlot.backgroundColor = 'white' - $scope.canceledSlot.title = '' - $scope.canceledSlot.borderColor = FREE_SLOT_BORDER_COLOR - $scope.canceledSlot.id = null - $scope.canceledSlot.is_reserved = false - $scope.canceledSlot.can_modify = false - $scope.canceledSlot = null - uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents' - , -> # error while canceling - growl.error _t('cancellation_failed') - , -> - $scope.paidMachineSlots = null - $scope.selectedPlan = null - $scope.modifiedSlots = null - - uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents' - - updateCartPrice() + $scope.selectedEvent = event + $scope.selectionTime = new Date() @@ -767,201 +567,6 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat - ## - # Open a modal window that allows the user to process a credit card payment for his current shopping cart. - ## - payByStripe = (reservation) -> - - $uibModal.open - templateUrl: '<%= asset_path "stripe/payment_modal.html" %>' - size: 'md' - resolve: - reservation: -> - reservation - price: -> - Price.compute(mkRequestParams(reservation, $scope.coupon.applied)).$promise - wallet: -> - Wallet.getWalletByUser({user_id: reservation.user_id}).$promise - cgv: -> - CustomAsset.get({name: 'cgv-file'}).$promise - coupon: -> - $scope.coupon.applied - controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'cgv', 'Auth', 'Reservation', 'wallet', 'helpers', '$filter', 'coupon', - ($scope, $uibModalInstance, $state, reservation, price, cgv, Auth, Reservation, wallet, helpers, $filter, coupon) -> - # user wallet amount - $scope.walletAmount = wallet.amount - - # Price - $scope.amount = helpers.getAmountToPay(price.price, wallet.amount) - - # CGV - $scope.cgv = cgv.custom_asset - - # Reservation - $scope.reservation = reservation - - # Used in wallet info template to interpolate some translations - $scope.numberFilter = $filter('number') - - ## - # Callback to process the payment with Stripe, triggered on button click - ## - $scope.payment = (status, response) -> - if response.error - growl.error(response.error.message) - else - $scope.attempting = true - $scope.reservation.card_token = response.id - Reservation.save mkRequestParams($scope.reservation, coupon), (reservation) -> - $uibModalInstance.close(reservation) - , (response)-> - $scope.alerts = [] - if response.status == 500 - $scope.alerts.push - msg: response.statusText - type: 'danger' - else - if response.data.card and response.data.card.join('').length > 0 - $scope.alerts.push - msg: response.data.card.join('. ') - type: 'danger' - else if response.data.payment and response.data.payment.join('').length > 0 - $scope.alerts.push - msg: response.data.payment.join('. ') - type: 'danger' - $scope.attempting = false - ] - .result['finally'](null).then (reservation)-> - afterPayment(reservation) - - - - ## - # Open a modal window that allows the user to process a local payment for his current shopping cart (admin only). - ## - payOnSite = (reservation) -> - - $uibModal.open - templateUrl: '<%= asset_path "shared/valid_reservation_modal.html" %>' - size: 'sm' - resolve: - reservation: -> - reservation - price: -> - Price.compute(mkRequestParams(reservation, $scope.coupon.applied)).$promise - wallet: -> - Wallet.getWalletByUser({user_id: reservation.user_id}).$promise - coupon: -> - $scope.coupon.applied - controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'Auth', 'Reservation', 'wallet', 'helpers', '$filter', 'coupon', - ($scope, $uibModalInstance, $state, reservation, price, Auth, Reservation, wallet, helpers, $filter, coupon) -> - - # user wallet amount - $scope.walletAmount = wallet.amount - - # Global price (total of all items) - $scope.price = price.price - - # Price to pay (wallet deducted) - $scope.amount = helpers.getAmountToPay(price.price, wallet.amount) - - # Reservation - $scope.reservation = reservation - - # Used in wallet info template to interpolate some translations - $scope.numberFilter = $filter('number') - - # Button label - if $scope.amount > 0 - $scope.validButtonName = _t('confirm_payment_of_html', {ROLE:$scope.currentUser.role, AMOUNT:$filter('currency')($scope.amount)}, "messageformat") - else - if price.price > 0 and $scope.walletAmount == 0 - $scope.validButtonName = _t('confirm_payment_of_html', {ROLE:$scope.currentUser.role, AMOUNT:$filter('currency')(price.price)}, "messageformat") - else - $scope.validButtonName = _t('confirm') - - ## - # Callback to process the local payment, triggered on button click - ## - $scope.ok = -> - $scope.attempting = true - Reservation.save mkRequestParams($scope.reservation, coupon), (reservation) -> - $uibModalInstance.close(reservation) - $scope.attempting = true - , (response)-> - $scope.alerts = [] - $scope.alerts.push({msg: _t('a_problem_occured_during_the_payment_process_please_try_again_later'), type: 'danger' }) - $scope.attempting = false - $scope.cancel = -> - $uibModalInstance.dismiss('cancel') - ] - .result['finally'](null).then (reservation)-> - afterPayment(reservation) - - - - ## - # Determines if the provided booked slot is able to be modified by the user. - # @param slot {Object} fullCalendar event object - ## - slotCanBeModified = (slot)-> - return true if $scope.currentUser.role is 'admin' - slotStart = moment(slot.start) - now = moment() - if slot.can_modify and $scope.enableBookingMove and slotStart.diff(now, "hours") >= $scope.moveBookingDelay - return true - else - return false - - - - ## - # Determines if the provided booked slot is able to be canceled by the user. - # @param slot {Object} fullCalendar event object - ## - slotCanBeCanceled = (slot) -> - return true if $scope.currentUser.role is 'admin' - slotStart = moment(slot.start) - now = moment() - if slot.can_modify and $scope.enableBookingCancel and slotStart.diff(now, "hours") >= $scope.cancelBookingDelay - return true - else - return false - - - ## - # Once the reservation is booked (payment process successfully completed), change the event style - # in fullCalendar, update the user's subscription and free-credits if needed - # @param reservation {Object} - ## - afterPayment = (reservation)-> - angular.forEach $scope.eventsReserved, (machineSlot, key) -> - machineSlot.is_reserved = true - machineSlot.can_modify = true - if $scope.currentUser.role isnt 'admin' - machineSlot.title = _t('i_ve_reserved') - machineSlot.borderColor = BOOKED_SLOT_BORDER_COLOR - updateMachineSlot(machineSlot, reservation, $scope.currentUser) - else - machineSlot.title = _t('not_available') - machineSlot.borderColor = UNAVAILABLE_SLOT_BORDER_COLOR - updateMachineSlot(machineSlot, reservation, $scope.ctrl.member) - machineSlot.backgroundColor = 'white' - $scope.paidMachineSlots = $scope.eventsReserved - - $scope.eventsReserved = [] - $scope.coupon.applied = null - - if $scope.selectedPlan - $scope.ctrl.member.subscribed_plan = angular.copy($scope.selectedPlan) - Auth._currentUser.subscribed_plan = angular.copy($scope.selectedPlan) - $scope.plansAreShown = false - - uiCalendarConfig.calendars.calendar.fullCalendar 'refetchEvents' - uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents' - - - ## # After payment, update the id of the newly reserved slot with the id returned by the server. # This will allow the user to modify the reservation he just booked. The associated user will also be registered @@ -979,15 +584,20 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat ## - # Search for the requested plan in the provided array and return its price. - # @param plansArray {Array} full list of plans - # @param planId {Number} plan identifier - # @returns {Number|null} price of the given plan or null if not found + # Update the calendar's display to render the new attributes of the events ## - findAmountByPlanId = (plansArray, planId)-> - for plan in plansArray - return plan.amount if plan.plan_id == planId - return null + updateCalendar = -> + uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents' + + + + ## + # Asynchronously fetch the events from the API and refresh the calendar's view with these new events + ## + refetchCalendar = -> + $timeout -> + uiCalendarConfig.calendars.calendar.fullCalendar 'refetchEvents' + uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents' ## !!! MUST BE CALLED AT THE END of the controller diff --git a/app/assets/javascripts/controllers/main_nav.coffee.erb b/app/assets/javascripts/controllers/main_nav.coffee.erb index 0b83d9ce3..057a6a282 100644 --- a/app/assets/javascripts/controllers/main_nav.coffee.erb +++ b/app/assets/javascripts/controllers/main_nav.coffee.erb @@ -48,6 +48,13 @@ Application.Controllers.controller "MainNavController", ["$scope", "$location", linkIcon: 'credit-card' }) + unless Fablab.withoutSpaces + $scope.navLinks.splice(3, 0, { + state: 'app.public.spaces_list' + linkText: 'reserve_a_space' + linkIcon: 'rocket' + }) + Fablab.adminNavLinks = Fablab.adminNavLinks || [] adminNavLinks = [ @@ -109,4 +116,11 @@ Application.Controllers.controller "MainNavController", ["$scope", "$location", ].concat(Fablab.adminNavLinks) $scope.adminNavLinks = adminNavLinks + + unless Fablab.withoutSpaces + $scope.adminNavLinks.splice(7, 0, { + state: 'app.public.spaces_list' + linkText: 'manage_the_spaces' + linkIcon: 'rocket' + }) ] diff --git a/app/assets/javascripts/controllers/spaces.coffee.erb b/app/assets/javascripts/controllers/spaces.coffee.erb new file mode 100644 index 000000000..bafa77027 --- /dev/null +++ b/app/assets/javascripts/controllers/spaces.coffee.erb @@ -0,0 +1,518 @@ + +### COMMON CODE ### + +## +# Provides a set of common callback methods to the $scope parameter. These methods are used +# in the various spaces' admin controllers. +# +# Provides : +# - $scope.submited(content) +# - $scope.cancel() +# - $scope.fileinputClass(v) +# - $scope.addFile() +# - $scope.deleteFile(file) +# +# Requires : +# - $scope.space.space_files_attributes = [] +# - $state (Ui-Router) [ 'app.public.spaces_list' ] +## +class SpacesController + constructor: ($scope, $state) -> + ## + # For use with ngUpload (https://github.com/twilson63/ngUpload). + # Intended to be the callback when the upload is done: any raised error will be stacked in the + # $scope.alerts array. If everything goes fine, the user is redirected to the spaces list. + # @param content {Object} JSON - The upload's result + ## + $scope.submited = (content) -> + if !content.id? + $scope.alerts = [] + angular.forEach content, (v, k)-> + angular.forEach v, (err)-> + $scope.alerts.push + msg: k+': '+err + type: 'danger' + else + $state.go('app.public.spaces_list') + + ## + # Changes the current user's view, redirecting him to the spaces list + ## + $scope.cancel = -> + $state.go('app.public.spaces_list') + + ## + # For use with 'ng-class', returns the CSS class name for the uploads previews. + # The preview may show a placeholder or the content of the file depending on the upload state. + # @param v {*} any attribute, will be tested for truthiness (see JS evaluation rules) + ## + $scope.fileinputClass = (v)-> + if v + 'fileinput-exists' + else + 'fileinput-new' + + ## + # This will create a single new empty entry into the space attachements list. + ## + $scope.addFile = -> + $scope.space.space_files_attributes.push {} + + ## + # This will remove the given file from the space attachements list. If the file was previously uploaded + # to the server, it will be marked for deletion on the server. Otherwise, it will be simply truncated from + # the attachements array. + # @param file {Object} the file to delete + ## + $scope.deleteFile = (file) -> + index = $scope.space.space_files_attributes.indexOf(file) + if file.id? + file._destroy = true + else + $scope.space.space_files_attributes.splice(index, 1) + + + +## +# Controller used in the public listing page, allowing everyone to see the list of spaces +## +Application.Controllers.controller 'SpacesController', ['$scope', '$state', 'spacesPromise', ($scope, $state, spacesPromise) -> + + ## Retrieve the list of spaces + $scope.spaces = spacesPromise + + ## + # Redirect the user to the space details page + ## + $scope.showSpace = (space) -> + $state.go('app.public.space_show', { id: space.slug }) + + ## + # Callback to book a reservation for the current space + ## + $scope.reserveSpace = (space) -> + $state.go('app.logged.space_reserve', { id: space.slug }) +] + + + +## +# Controller used in the space creation page (admin) +## +Application.Controllers.controller 'NewSpaceController', ['$scope', '$state', 'CSRF',($scope, $state, CSRF) -> + CSRF.setMetaTags() + + ## API URL where the form will be posted + $scope.actionUrl = "/api/spaces/" + + ## Form action on the above URL + $scope.method = "post" + + ## default space parameters + $scope.space = + space_files_attributes: [] + + ## Using the SpacesController + new SpacesController($scope, $state) +] + + +## +# Controller used in the space edition page (admin) +## +Application.Controllers.controller 'EditSpaceController', ['$scope', '$state', '$stateParams', 'spacePromise', 'CSRF',($scope, $state, $stateParams, spacePromise, CSRF) -> + CSRF.setMetaTags() + + ## API URL where the form will be posted + $scope.actionUrl = "/api/spaces/" + $stateParams.id + + ## Form action on the above URL + $scope.method = "put" + + ## space to modify + $scope.space = spacePromise + + ## Using the SpacesController + new SpacesController($scope, $state) +] + +Application.Controllers.controller 'ShowSpaceController', ['$scope', '$state', 'spacePromise', '_t', 'dialogs', 'growl', ($scope, $state, spacePromise, _t, dialogs, growl) -> + + ## Details of the space witch id/slug is provided in the URL + $scope.space = spacePromise + + ## + # Callback to book a reservation for the current space + # @param event {Object} see https://docs.angularjs.org/guide/expression#-event- + ## + $scope.reserveSpace = (event) -> + event.preventDefault() + $state.go('app.logged.space_reserve', { id: $scope.space.slug }) + + ## + # Callback to book a reservation for the current space + # @param event {Object} see https://docs.angularjs.org/guide/expression#-event- + ## + $scope.deleteSpace = (event) -> + event.preventDefault() + # check the permissions + if $scope.currentUser.role isnt 'admin' + console.error _t('space_show.unauthorized_operation') + else + dialogs.confirm + resolve: + object: -> + title: _t('space_show.confirmation_required') + msg: _t('space_show.do_you_really_want_to_delete_this_space') + , -> # deletion confirmed + # delete the machine then redirect to the machines listing + $scope.space.$delete -> + $state.go('app.public.spaces_list') + , (error)-> + growl.warning(_t('space_show.the_space_cant_be_deleted_because_it_is_already_reserved_by_some_users')) +] + + + +## +# Controller used in the spaces reservation agenda page. +# This controller is very similar to the machine reservation controller with one major difference: here, there is many places +# per slots. +## + +Application.Controllers.controller "ReserveSpaceController", ["$scope", '$stateParams', 'Auth', '$timeout', 'Availability', 'Member', 'availabilitySpacesPromise', 'plansPromise', 'groupsPromise', 'settingsPromise', 'spacePromise', '_t', 'uiCalendarConfig', 'CalendarConfig' +($scope, $stateParams, Auth, $timeout, Availability, Member, availabilitySpacesPromise, plansPromise, groupsPromise, settingsPromise, spacePromise, _t, uiCalendarConfig, CalendarConfig) -> + + + + ### PRIVATE STATIC CONSTANTS ### + + # Color of the selected event backgound + SELECTED_EVENT_BG_COLOR = '#ffdd00' + + # Slot free to be booked + FREE_SLOT_BORDER_COLOR = '<%= AvailabilityHelper::SPACE_COLOR %>' + + # Slot with reservation from current user + RESERVED_SLOT_BORDER_COLOR = '<%= AvailabilityHelper::IS_RESERVED_BY_CURRENT_USER %>' + + + + ### PUBLIC SCOPE ### + + ## bind the spaces availabilities with full-Calendar events + $scope.eventSources = [ { events: availabilitySpacesPromise, textColor: 'black' } ] + + ## the user to deal with, ie. the current user for non-admins + $scope.ctrl = + member: {} + + ## list of plans, classified by group + $scope.plansClassifiedByGroup = [] + for group in groupsPromise + groupObj = { id: group.id, name: group.name, plans: [] } + for plan in plansPromise + groupObj.plans.push(plan) if plan.group_id == group.id + $scope.plansClassifiedByGroup.push(groupObj) + + ## mapping of fullCalendar events. + $scope.events = + reserved: [] # Slots that the user wants to book + modifiable: null # Slot that the user wants to change + placable: null # Destination slot for the change + paid: [] # Slots that were just booked by the user (transaction ok) + moved: null # Slots that were just moved by the user (change done) -> {newSlot:* oldSlot: *} + + ## the moment when the slot selection changed for the last time, used to trigger changes in the cart + $scope.selectionTime = null + + ## the last clicked event in the calender + $scope.selectedEvent = null + + ## indicates the state of the current view : calendar or plans information + $scope.plansAreShown = false + + ## will store the user's plan if he choosed to buy one + $scope.selectedPlan = null + + ## the moment when the plan selection changed for the last time, used to trigger changes in the cart + $scope.planSelectionTime = null + + ## Selected space + $scope.space = spacePromise + + ## fullCalendar (v2) configuration + $scope.calendarConfig = CalendarConfig + minTime: moment.duration(moment(settingsPromise.booking_window_start).format('HH:mm:ss')) + maxTime: moment.duration(moment(settingsPromise.booking_window_end).format('HH:mm:ss')) + eventClick: (event, jsEvent, view) -> + calendarEventClickCb(event, jsEvent, view) + eventRender: (event, element, view) -> + eventRenderCb(event, element, view) + + ## Application global settings + $scope.settings = settingsPromise + + ## Global config: message to the end user concerning the subscriptions rules + $scope.subscriptionExplicationsAlert = settingsPromise.subscription_explications_alert + + ## Global config: message to the end user concerning the space reservation + $scope.spaceExplicationsAlert = settingsPromise.space_explications_alert + + + ## + # Change the last selected slot's appearence to looks like 'added to cart' + ## + $scope.markSlotAsAdded = -> + $scope.selectedEvent.backgroundColor = SELECTED_EVENT_BG_COLOR + updateCalendar() + + + + ## + # Change the last selected slot's appearence to looks like 'never added to cart' + ## + $scope.markSlotAsRemoved = (slot) -> + slot.backgroundColor = 'white' + slot.title = '' + slot.borderColor = FREE_SLOT_BORDER_COLOR + slot.id = null + slot.isValid = false + slot.is_reserved = false + slot.can_modify = false + slot.offered = false + slot.is_completed = false if slot.is_completed + updateCalendar() + + + + ## + # Callback when a slot was successfully cancelled. Reset the slot style as 'ready to book' + ## + $scope.slotCancelled = -> + $scope.markSlotAsRemoved($scope.selectedEvent) + + + + ## + # Change the last selected slot's appearence to looks like 'currently looking for a new destination to exchange' + ## + $scope.markSlotAsModifying = -> + $scope.selectedEvent.backgroundColor = '#eee' + $scope.selectedEvent.title = _t('space_reserve.i_change') + updateCalendar() + + + + ## + # Change the last selected slot's appearence to looks like 'the slot being exchanged will take this place' + ## + $scope.changeModifyTrainingSlot = -> + if $scope.events.placable + $scope.events.placable.backgroundColor = 'white' + $scope.events.placable.title = '' + if !$scope.events.placable or $scope.events.placable._id != $scope.selectedEvent._id + $scope.selectedEvent.backgroundColor = '#bbb' + $scope.selectedEvent.title = _t('space_reserve.i_shift') + updateCalendar() + + + ## + # When modifying an already booked reservation, callback when the modification was successfully done. + ## + $scope.modifyTrainingSlot = -> + $scope.events.placable.title = _t('space_reserve.i_ve_reserved') + $scope.events.placable.backgroundColor = 'white' + $scope.events.placable.borderColor = $scope.events.modifiable.borderColor + $scope.events.placable.id = $scope.events.modifiable.id + $scope.events.placable.is_reserved = true + $scope.events.placable.can_modify = true + + $scope.events.modifiable.backgroundColor = 'white' + $scope.events.modifiable.title = '' + $scope.events.modifiable.borderColor = FREE_SLOT_BORDER_COLOR + $scope.events.modifiable.id = null + $scope.events.modifiable.is_reserved = false + $scope.events.modifiable.can_modify = false + $scope.events.modifiable.is_completed = false if $scope.events.modifiable.is_completed + + updateCalendar() + + + + ## + # Cancel the current booking modification, reseting the whole process + ## + $scope.cancelModifyTrainingSlot = -> + if $scope.events.placable + $scope.events.placable.backgroundColor = 'white' + $scope.events.placable.title = '' + $scope.events.modifiable.title = _t('space_reserve.i_ve_reserved') + $scope.events.modifiable.backgroundColor = 'white' + + updateCalendar() + + + + ## + # Callback to deal with the reservations of the user selected in the dropdown list instead of the current user's + # reservations. (admins only) + ## + $scope.updateMember = -> + if $scope.ctrl.member + Member.get {id: $scope.ctrl.member.id}, (member) -> + $scope.ctrl.member = member + Availability.spaces {spaceId: $scope.space.id, member_id: $scope.ctrl.member.id}, (spaces) -> + uiCalendarConfig.calendars.calendar.fullCalendar 'removeEvents' + $scope.eventSources.splice(0, 1, + events: spaces + textColor: 'black' + ) + # as the events are re-fetched for the new user, we must re-init the cart + $scope.events.reserved = [] + $scope.selectedPlan = null + $scope.plansAreShown = false + + + + ## + # Add the provided plan to the current shopping cart + # @param plan {Object} the plan to subscribe + ## + $scope.selectPlan = (plan) -> + # toggle selected plan + if $scope.selectedPlan != plan + $scope.selectedPlan = plan + else + $scope.selectedPlan = null + $scope.planSelectionTime = new Date() + + + + ## + # Changes the user current view from the plan subsription screen to the machine reservation agenda + # @param e {Object} see https://docs.angularjs.org/guide/expression#-event- + ## + $scope.doNotSubscribePlan = (e)-> + e.preventDefault() + $scope.plansAreShown = false + $scope.selectedPlan = null + $scope.planSelectionTime = new Date() + + + + ## + # Switch the user's view from the reservation agenda to the plan subscription + ## + $scope.showPlans = -> + $scope.plansAreShown = true + + + + ## + # Once the reservation is booked (payment process successfully completed), change the event style + # in fullCalendar, update the user's subscription and free-credits if needed + # @param reservation {Object} + ## + $scope.afterPayment = (reservation)-> + angular.forEach $scope.events.paid, (spaceSlot, key) -> + spaceSlot.is_reserved = true + spaceSlot.can_modify = true + spaceSlot.title = _t('space_reserve.i_ve_reserved') + spaceSlot.backgroundColor = 'white' + spaceSlot.borderColor = RESERVED_SLOT_BORDER_COLOR + updateSpaceSlotId(spaceSlot, reservation) + + + if $scope.selectedPlan + $scope.ctrl.member.subscribed_plan = angular.copy($scope.selectedPlan) + Auth._currentUser.subscribed_plan = angular.copy($scope.selectedPlan) + $scope.plansAreShown = false + $scope.selectedPlan = null + $scope.ctrl.member.training_credits = angular.copy(reservation.user.training_credits) + $scope.ctrl.member.machine_credits = angular.copy(reservation.user.machine_credits) + Auth._currentUser.training_credits = angular.copy(reservation.user.training_credits) + Auth._currentUser.machine_credits = angular.copy(reservation.user.machine_credits) + + refetchCalendar() + + + + ### PRIVATE SCOPE ### + + ## + # Kind of constructor: these actions will be realized first when the controller is loaded + ## + initialize = -> + if $scope.currentUser.role isnt 'admin' + Member.get id: $scope.currentUser.id, (member) -> + $scope.ctrl.member = member + + + + ## + # Triggered when the user clicks on a reservation slot in the agenda. + # Defines the behavior to adopt depending on the slot status (already booked, free, ready to be reserved ...), + # the user's subscription (current or about to be took) and the time (the user cannot modify a booked reservation + # if it's too late). + # @see http://fullcalendar.io/docs/mouse/eventClick/ + ## + calendarEventClickCb = (event, jsEvent, view) -> + $scope.selectedEvent = event + if $stateParams.id is 'all' + $scope.training = event.training + $scope.selectionTime = new Date() + + + + + ## + # Triggered when fullCalendar tries to graphicaly render an event block. + # Append the event tag into the block, just after the event title. + # @see http://fullcalendar.io/docs/event_rendering/eventRender/ + ## + eventRenderCb = (event, element, view)-> + if $scope.currentUser.role is 'admin' and event.tags.length > 0 + html = '' + for tag in event.tags + html += "#{tag.name}" + element.find('.fc-time').append(html) + return + + + + ## + # After payment, update the id of the newly reserved slot with the id returned by the server. + # This will allow the user to modify the reservation he just booked. + # @param slot {Object} + # @param reservation {Object} + ## + updateSpaceSlotId = (slot, reservation)-> + angular.forEach reservation.slots, (s)-> + if slot.start_at == slot.start_at + slot.id = s.id + + + + ## + # Update the calendar's display to render the new attributes of the events + ## + updateCalendar = -> + uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents' + + + + ## + # Asynchronously fetch the events from the API and refresh the calendar's view with these new events + ## + refetchCalendar = -> + $timeout -> + uiCalendarConfig.calendars.calendar.fullCalendar 'refetchEvents' + uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents' + + + + ## !!! MUST BE CALLED AT THE END of the controller + initialize() + +] \ No newline at end of file diff --git a/app/assets/javascripts/controllers/trainings.coffee.erb b/app/assets/javascripts/controllers/trainings.coffee.erb index cdd4563b4..12b6b6a04 100644 --- a/app/assets/javascripts/controllers/trainings.coffee.erb +++ b/app/assets/javascripts/controllers/trainings.coffee.erb @@ -77,8 +77,8 @@ Application.Controllers.controller "ShowTrainingController", ['$scope', '$state' # training can be reserved during the reservation process (the shopping cart may contains only one training and a subscription). ## -Application.Controllers.controller "ReserveTrainingController", ["$scope", "$state", '$stateParams', '$filter', '$compile', "$uibModal", 'Auth', 'dialogs', '$timeout', 'Price', 'Availability', 'Slot', 'Member', 'Setting', 'CustomAsset', 'availabilityTrainingsPromise', 'plansPromise', 'groupsPromise', 'growl', 'settingsPromise', 'trainingPromise', '_t', 'Wallet', 'helpers', 'uiCalendarConfig', 'CalendarConfig' -($scope, $state, $stateParams, $filter, $compile, $uibModal, Auth, dialogs, $timeout, Price, Availability, Slot, Member, Setting, CustomAsset, availabilityTrainingsPromise, plansPromise, groupsPromise, growl, settingsPromise, trainingPromise, _t, Wallet, helpers, uiCalendarConfig, CalendarConfig) -> +Application.Controllers.controller "ReserveTrainingController", ["$scope", '$stateParams', 'Auth', '$timeout', 'Availability', 'Member', 'availabilityTrainingsPromise', 'plansPromise', 'groupsPromise', 'settingsPromise', 'trainingPromise', '_t', 'uiCalendarConfig', 'CalendarConfig' +($scope, $stateParams, Auth, $timeout, Availability, Member, availabilityTrainingsPromise, plansPromise, groupsPromise, settingsPromise, trainingPromise, _t, uiCalendarConfig, CalendarConfig) -> @@ -87,7 +87,7 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta # Color of the selected event backgound SELECTED_EVENT_BG_COLOR = '#ffdd00' - # Slot already booked by the current user + # Slot free to be booked FREE_SLOT_BORDER_COLOR = '<%= AvailabilityHelper::TRAINING_COLOR %>' @@ -109,39 +109,34 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta groupObj.plans.push(plan) if plan.group_id == group.id $scope.plansClassifiedByGroup.push(groupObj) + ## mapping of fullCalendar events. + $scope.events = + reserved: [] # Slots that the user wants to book + modifiable: null # Slot that the user wants to change + placable: null # Destination slot for the change + paid: [] # Slots that were just booked by the user (transaction ok) + moved: null # Slots that were just moved by the user (change done) -> {newSlot:* oldSlot: *} + + ## the moment when the slot selection changed for the last time, used to trigger changes in the cart + $scope.selectionTime = null + + ## the last clicked event in the calender + $scope.selectedEvent = null + ## indicates the state of the current view : calendar or plans information $scope.plansAreShown = false - ## indicates if the selected training was validated (ie. added to the shopping cart) - $scope.trainingIsValid = false - - ## contains the selected training once it was payed, allows to display a firendly end-of-shopping message - $scope.paidTraining = null - ## will store the user's plan if he choosed to buy one $scope.selectedPlan = null - ## fullCalendar event. Training slot that the user want to book - $scope.selectedTraining = null + ## the moment when the plan selection changed for the last time, used to trigger changes in the cart + $scope.planSelectionTime = null - ## fullCalendar event. An already booked slot that the user want to modify - $scope.slotToModify = null - - ## Once a training reservation was modified, will contains {newReservedSlot:{}, oldReservedSlot:{}} - $scope.modifiedSlots = null - - ## Selected training unless 'all' trainings are displayed + ## Selected training $scope.training = trainingPromise - ## Discount coupon to apply to the basket, if any - $scope.coupon = - applied: null - - ## Total price of the cart, that the user will pay - $scope.amountTotal = 0 - - ## Total amount of the elements in the cart, without considering any coupon - $scope.totalNoCoupon = 0 + ## 'all' OR training's slug + $scope.mode = $stateParams.id ## fullCalendar (v2) configuration $scope.calendarConfig = CalendarConfig @@ -149,19 +144,113 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta maxTime: moment.duration(moment(settingsPromise.booking_window_end).format('HH:mm:ss')) eventClick: (event, jsEvent, view) -> calendarEventClickCb(event, jsEvent, view) - eventAfterAllRender: (view)-> - $scope.events = uiCalendarConfig.calendars.calendar.fullCalendar 'clientEvents' eventRender: (event, element, view) -> eventRenderCb(event, element, view) - ## Custom settings + ## Application global settings + $scope.settings = settingsPromise + + ## Global config: message to the end user concerning the subscriptions rules $scope.subscriptionExplicationsAlert = settingsPromise.subscription_explications_alert + + ## Global config: message to the end user concerning the training reservation $scope.trainingExplicationsAlert = settingsPromise.training_explications_alert + + ## Global config: message to the end user giving advice about the training reservation $scope.trainingInformationMessage = settingsPromise.training_information_message - $scope.enableBookingMove = (settingsPromise.booking_move_enable == "true") - $scope.moveBookingDelay = parseInt(settingsPromise.booking_move_delay) - $scope.enableBookingCancel = (settingsPromise.booking_cancel_enable == "true") - $scope.cancelBookingDelay = parseInt(settingsPromise.booking_cancel_delay) + + + ## + # Change the last selected slot's appearence to looks like 'added to cart' + ## + $scope.markSlotAsAdded = -> + $scope.selectedEvent.backgroundColor = SELECTED_EVENT_BG_COLOR + updateCalendar() + + + + ## + # Change the last selected slot's appearence to looks like 'never added to cart' + ## + $scope.markSlotAsRemoved = (slot) -> + slot.backgroundColor = 'white' + slot.title = slot.training.name + slot.borderColor = FREE_SLOT_BORDER_COLOR + slot.id = null + slot.isValid = false + slot.is_reserved = false + slot.can_modify = false + slot.offered = false + slot.is_completed = false if slot.is_completed + updateCalendar() + + + + ## + # Callback when a slot was successfully cancelled. Reset the slot style as 'ready to book' + ## + $scope.slotCancelled = -> + $scope.markSlotAsRemoved($scope.selectedEvent) + + + + ## + # Change the last selected slot's appearence to looks like 'currently looking for a new destination to exchange' + ## + $scope.markSlotAsModifying = -> + $scope.selectedEvent.backgroundColor = '#eee' + $scope.selectedEvent.title = $scope.selectedEvent.training.name + ' - ' + _t('i_change') + updateCalendar() + + + + ## + # Change the last selected slot's appearence to looks like 'the slot being exchanged will take this place' + ## + $scope.changeModifyTrainingSlot = -> + if $scope.events.placable + $scope.events.placable.backgroundColor = 'white' + $scope.events.placable.title = $scope.events.placable.training.name + if !$scope.events.placable or $scope.events.placable._id != $scope.selectedEvent._id + $scope.selectedEvent.backgroundColor = '#bbb' + $scope.selectedEvent.title = $scope.selectedEvent.training.name + ' - ' + _t('i_shift') + updateCalendar() + + + ## + # When modifying an already booked reservation, callback when the modification was successfully done. + ## + $scope.modifyTrainingSlot = -> + $scope.events.placable.title = if $scope.currentUser.role isnt 'admin' then $scope.events.placable.training.name + " - " + _t('i_ve_reserved') else $scope.events.placable.training.name + $scope.events.placable.backgroundColor = 'white' + $scope.events.placable.borderColor = $scope.events.modifiable.borderColor + $scope.events.placable.id = $scope.events.modifiable.id + $scope.events.placable.is_reserved = true + $scope.events.placable.can_modify = true + + $scope.events.modifiable.backgroundColor = 'white' + $scope.events.modifiable.title = $scope.events.modifiable.training.name + $scope.events.modifiable.borderColor = FREE_SLOT_BORDER_COLOR + $scope.events.modifiable.id = null + $scope.events.modifiable.is_reserved = false + $scope.events.modifiable.can_modify = false + $scope.events.modifiable.is_completed = false if $scope.events.modifiable.is_completed + + updateCalendar() + + + + ## + # Cancel the current booking modification, reseting the whole process + ## + $scope.cancelModifyTrainingSlot = -> + if $scope.events.placable + $scope.events.placable.backgroundColor = 'white' + $scope.events.placable.title = $scope.events.placable.training.name + $scope.events.modifiable.title = if $scope.currentUser.role isnt 'admin' then $scope.events.modifiable.training.name + " - " + _t('i_ve_reserved') else $scope.events.modifiable.training.name + $scope.events.modifiable.backgroundColor = 'white' + + updateCalendar() @@ -173,67 +262,17 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta if $scope.ctrl.member Member.get {id: $scope.ctrl.member.id}, (member) -> $scope.ctrl.member = member - Availability.trainings {trainingId: $stateParams.id, member_id: $scope.ctrl.member.id}, (trainings) -> + id = if $stateParams.id is 'all' then $stateParams.id else $scope.training.id + Availability.trainings {trainingId: id, member_id: $scope.ctrl.member.id}, (trainings) -> uiCalendarConfig.calendars.calendar.fullCalendar 'removeEvents' - $scope.eventSources.push + $scope.eventSources.splice(0, 1, events: trainings textColor: 'black' - $scope.trainingIsValid = false - $scope.paidTraining = null - $scope.plansAreShown = false + ) + # as the events are re-fetched for the new user, we must re-init the cart + $scope.events.reserved = [] $scope.selectedPlan = null - $scope.selectedTraining = null - $scope.slotToModify = null - $scope.modifiedSlots = null - - - - ## - # Callback to mark the selected training as validated (add it to the shopping cart). - ## - $scope.validTraining = -> - $scope.trainingIsValid = true - $scope.updatePrices() - - - - ## - # Remove the training from the shopping cart - # @param e {Object} see https://docs.angularjs.org/guide/expression#-event- - ## - $scope.removeTraining = (e) -> - e.preventDefault() - - $scope.selectedTraining.backgroundColor = 'white' - $scope.selectedTraining = null $scope.plansAreShown = false - $scope.selectedPlan = null - $scope.trainingIsValid = false - $timeout -> - uiCalendarConfig.calendars.fullCalendar 'refetchEvents' - uiCalendarConfig.calendars.fullCalendar 'rerenderEvents' - - - - ## - # Validates the shopping chart and redirect the user to the payment step - ## - $scope.payTraining = -> - - # first, we check that a user was selected - if Object.keys($scope.ctrl.member).length > 0 - reservation = mkReservation($scope.ctrl.member, $scope.selectedTraining, $scope.selectedPlan) - - Wallet.getWalletByUser {user_id: $scope.ctrl.member.id}, (wallet) -> - amountToPay = helpers.getAmountToPay($scope.amountTotal, wallet.amount) - if $scope.currentUser.role isnt 'admin' and amountToPay > 0 - payByStripe(reservation) - else - if $scope.currentUser.role is 'admin' or amountToPay is 0 - payOnSite(reservation) - else - # otherwise we alert, this error musn't occur when the current user is not admin - growl.error(_t('please_select_a_member_first')) @@ -242,17 +281,12 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta # @param plan {Object} the plan to subscribe ## $scope.selectPlan = (plan) -> - if $scope.isAuthenticated() - if $scope.selectedPlan != plan - $scope.selectedPlan = plan - $scope.updatePrices() - else - $scope.selectedPlan = null - $scope.updatePrices() + # toggle selected plan + if $scope.selectedPlan != plan + $scope.selectedPlan = plan else - $scope.login null, -> - $scope.selectedPlan = plan - $scope.updatePrices() + $scope.selectedPlan = null + $scope.planSelectionTime = new Date() @@ -264,7 +298,9 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta e.preventDefault() $scope.plansAreShown = false $scope.selectedPlan = null - $scope.updatePrices() + $scope.planSelectionTime = new Date() + + ## # Switch the user's view from the reservation agenda to the plan subscription @@ -272,96 +308,33 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta $scope.showPlans = -> $scope.plansAreShown = true - ## - # Cancel the current booking modification, removing the previously booked slot from the selection - # @param e {Object} see https://docs.angularjs.org/guide/expression#-event- - ## - $scope.removeSlotToModify = (e) -> - e.preventDefault() - if $scope.slotToPlace - $scope.slotToPlace.backgroundColor = 'white' - $scope.slotToPlace.title = $scope.slotToPlace.training.name - $scope.slotToPlace = null - $scope.slotToModify.title = if $scope.currentUser.role isnt 'admin' then $scope.slotToModify.training.name + " - " + _t('i_ve_reserved') else $scope.slotToModify.training.name - $scope.slotToModify.backgroundColor = 'white' - $scope.slotToModify = null - uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents' - ## - # When modifying an already booked reservation, cancel the choice of the new slot - # @param e {Object} see https://docs.angularjs.org/guide/expression#-event- + # Once the reservation is booked (payment process successfully completed), change the event style + # in fullCalendar, update the user's subscription and free-credits if needed + # @param reservation {Object} ## - $scope.removeSlotToPlace = (e)-> - e.preventDefault() - $scope.slotToPlace.backgroundColor = 'white' - $scope.slotToPlace.title = $scope.slotToPlace.training.name - $scope.slotToPlace = null - uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents' + $scope.afterPayment = (reservation)-> + $scope.events.paid[0].backgroundColor = 'white' + $scope.events.paid[0].is_reserved = true + $scope.events.paid[0].can_modify = true + updateTrainingSlotId($scope.events.paid[0], reservation) + $scope.events.paid[0].borderColor = '#b2e774' + $scope.events.paid[0].title = $scope.events.paid[0].training.name + " - " + _t('i_ve_reserved') + if $scope.selectedPlan + $scope.ctrl.member.subscribed_plan = angular.copy($scope.selectedPlan) + Auth._currentUser.subscribed_plan = angular.copy($scope.selectedPlan) + $scope.plansAreShown = false + $scope.selectedPlan = null + $scope.ctrl.member.training_credits = angular.copy(reservation.user.training_credits) + $scope.ctrl.member.machine_credits = angular.copy(reservation.user.machine_credits) + Auth._currentUser.training_credits = angular.copy(reservation.user.training_credits) + Auth._currentUser.machine_credits = angular.copy(reservation.user.machine_credits) - ## - # When modifying an already booked reservation, confirm the modification. - ## - $scope.modifyTrainingSlot = -> - Slot.update {id: $scope.slotToModify.slot_id}, - slot: - start_at: $scope.slotToPlace.start - end_at: $scope.slotToPlace.end - availability_id: $scope.slotToPlace.id - , -> # success - $scope.modifiedSlots = - newReservedSlot: $scope.slotToPlace - oldReservedSlot: $scope.slotToModify - $scope.slotToPlace.title = if $scope.currentUser.role isnt 'admin' then $scope.slotToPlace.training.name + " - " + _t('i_ve_reserved') else $scope.slotToPlace.training.name - $scope.slotToPlace.backgroundColor = 'white' - $scope.slotToPlace.borderColor = $scope.slotToModify.borderColor - $scope.slotToPlace.slot_id = $scope.slotToModify.slot_id - $scope.slotToPlace.is_reserved = true - $scope.slotToPlace.can_modify = true - $scope.slotToPlace = null - - $scope.slotToModify.backgroundColor = 'white' - $scope.slotToModify.title = $scope.slotToModify.training.name - $scope.slotToModify.borderColor = FREE_SLOT_BORDER_COLOR - $scope.slotToModify.slot_id = null - $scope.slotToModify.is_reserved = false - $scope.slotToModify.can_modify = false - $scope.slotToModify.is_completed = false if $scope.slotToModify.is_completed - $scope.slotToModify = null - uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents' - , -> # failure - growl.error('an_error_occured_preventing_the_booked_slot_from_being_modified') - - - - ## - # Cancel the current booking modification, reseting the whole process - ## - $scope.cancelModifyMachineSlot = -> - $scope.slotToPlace.backgroundColor = 'white' - $scope.slotToPlace.title = $scope.slotToPlace.training.name - $scope.slotToPlace = null - $scope.slotToModify.title = if $scope.currentUser.role isnt 'admin' then $scope.slotToModify.training.name + " - " + _t('i_ve_reserved') else $scope.slotToModify.training.name - $scope.slotToModify.backgroundColor = 'white' - $scope.slotToModify = null - uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents' - - - - ## - # Update the prices, based on the current selection - ## - $scope.updatePrices = -> - if Object.keys($scope.ctrl.member).length > 0 - r = mkReservation($scope.ctrl.member, $scope.selectedTraining, $scope.selectedPlan) - Price.compute mkRequestParams(r, $scope.coupon.applied), (res) -> - $scope.amountTotal = res.price - $scope.totalNoCoupon = res.price_without_coupon - else - $scope.amountTotal = null + refetchCalendar() @@ -375,51 +348,6 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta Member.get id: $scope.currentUser.id, (member) -> $scope.ctrl.member = member - # watch when a coupon is applied to re-compute the total price - $scope.$watch 'coupon.applied', (newValue, oldValue) -> - unless newValue == null and oldValue == null - $scope.updatePrices() - - - - ## - # Create an hash map implementing the Reservation specs - # @param member {Object} User as retreived from the API: current user / selected user if current is admin - # @param training {Object} fullCalendar event: training slot selected on the calendar - # @param [plan] {Object} Plan as retrived from the API: plan to buy with the current reservation - # @return {{user_id:Number, reservable_id:Number, reservable_type:String, slots_attributes:Array, plan_id:Number|null}} - ## - mkReservation = (member, training, plan = null) -> - reservation = - user_id: member.id - reservable_id: training.training.id - reservable_type: 'Training' - slots_attributes: [] - plan_id: (plan.id if plan) - - reservation.slots_attributes.push - start_at: training.start - end_at: training.end - availability_id: training.id - offered: training.offered || false - - reservation - - - - ## - # Format the parameters expected by /api/prices/compute or /api/reservations and return the resulting object - # @param reservation {Object} as returned by mkReservation() - # @param coupon {Object} Coupon as returned from the API - # @return {{reservation:Object, coupon_code:string}} - ## - mkRequestParams = (reservation, coupon) -> - params = - reservation: reservation - coupon_code: (coupon.code if coupon) - - params - ## @@ -430,308 +358,26 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta # @see http://fullcalendar.io/docs/mouse/eventClick/ ## calendarEventClickCb = (event, jsEvent, view) -> - if $scope.ctrl.member - # reserve a training if this training will not be reserved and is not about to move and not is completed - if !event.is_reserved && !$scope.slotToModify && !event.is_completed - if event != $scope.selectedTraining - $scope.selectedTraining = event - $scope.selectedTraining.offered = false - event.backgroundColor = SELECTED_EVENT_BG_COLOR - computeTrainingAmount($scope.selectedTraining) - else - $scope.selectedTraining = null - event.backgroundColor = 'white' - $scope.trainingIsValid = false - $scope.paidTraining = null - $scope.selectedPlan = null - $scope.modifiedSlots = null - # clean all others events background - angular.forEach $scope.events, (e)-> - if event.id != e.id - e.backgroundColor = 'white' - uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents' - # two if below for move training reserved - # if training isnt reserved and have a training to modify and same training and not complete - else if !event.is_reserved && $scope.slotToModify && slotCanBePlaced(event) - if $scope.slotToPlace - $scope.slotToPlace.backgroundColor = 'white' - $scope.slotToPlace.title = event.training.name - $scope.slotToPlace = event - event.backgroundColor = '#bbb' - event.title = event.training.name + ' - ' + _t('i_shift') - uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents' - # if training reserved can modify - else if event.is_reserved and (slotCanBeModified(event) or slotCanBeCanceled(event)) and !$scope.slotToModify and !$scope.selectedTraining - event.movable = slotCanBeModified(event) - event.cancelable = slotCanBeCanceled(event) - if $scope.currentUser.role is 'admin' - event.user = - name: $scope.ctrl.member.name - dialogs.confirm - templateUrl: '<%= asset_path "shared/confirm_modify_slot_modal.html" %>' - resolve: - object: -> event - , (type) -> # success - if type == 'move' - $scope.modifiedSlots = null - $scope.slotToModify = event - event.backgroundColor = '#eee' - event.title = event.training.name + ' - ' + _t('i_change') - uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents' - else if type == 'cancel' - dialogs.confirm - resolve: - object: -> - title: _t('confirmation_required') - msg: _t('do_you_really_want_to_cancel_this_reservation') - , -> # cancel confirmed - Slot.cancel {id: event.slot_id}, -> # successfully canceled - growl.success _t('reservation_was_successfully_cancelled') - $scope.canceledSlot = event - $scope.canceledSlot.backgroundColor = 'white' - $scope.canceledSlot.title = event.training.name - $scope.canceledSlot.borderColor = FREE_SLOT_BORDER_COLOR - $scope.canceledSlot.slot_id = null - $scope.canceledSlot.is_reserved = false - $scope.canceledSlot.can_modify = false - $scope.canceledSlot.is_completed = false if event.is_completed - $scope.canceledSlot = null - uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents' - , -> # error while canceling - growl.error _t('cancellation_failed') - , -> # canceled - $scope.paidMachineSlots = null - $scope.selectedPlan = null - $scope.modifiedSlots = null + $scope.selectedEvent = event + if $stateParams.id is 'all' + $scope.training = event.training + $scope.selectionTime = new Date() + ## - # When events are rendered, adds attributes for popover and compile + # Triggered when fullCalendar tries to graphicaly render an event block. + # Append the event tag into the block, just after the event title. # @see http://fullcalendar.io/docs/event_rendering/eventRender/ ## eventRenderCb = (event, element, view)-> - # Comment these codes for show a popup of description, because we add feature page of training - #element.attr( - # 'uib-popover': $filter('humanize')($filter('simpleText')(event.training.description), 70) - # 'popover-trigger': 'mouseenter' - #) - #$compile(element)($scope) - - - - ## - # Open a modal window that allows the user to process a credit card payment for his current shopping cart. - ## - payByStripe = (reservation) -> - - $uibModal.open - templateUrl: '<%= asset_path "stripe/payment_modal.html" %>' - size: 'md' - resolve: - reservation: -> - reservation - price: -> - Price.compute(mkRequestParams(reservation, $scope.coupon.applied)).$promise - wallet: -> - Wallet.getWalletByUser({user_id: reservation.user_id}).$promise - cgv: -> - CustomAsset.get({name: 'cgv-file'}).$promise - coupon: -> - $scope.coupon.applied - controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'wallet', 'cgv', 'Auth', 'Reservation', 'helpers', '$filter', 'coupon' - ($scope, $uibModalInstance, $state, reservation, price, wallet, cgv, Auth, Reservation, helpers, $filter, coupon) -> - # User's wallet amount - $scope.walletAmount = wallet.amount - - # Price - $scope.amount = helpers.getAmountToPay(price.price, wallet.amount) - - # CGV - $scope.cgv = cgv.custom_asset - - # Reservation - $scope.reservation = reservation - - # Used in wallet info template to interpolate some translations - $scope.numberFilter = $filter('number') - - ## - # Callback to process the payment with Stripe, triggered on button click - ## - $scope.payment = (status, response) -> - if response.error - growl.error(response.error.message) - else - $scope.attempting = true - $scope.reservation.card_token = response.id - Reservation.save mkRequestParams($scope.reservation, coupon), (reservation) -> - $uibModalInstance.close(reservation) - , (response)-> - $scope.alerts = [] - if response.data.card - $scope.alerts.push - msg: response.data.card[0] - type: 'danger' - else - $scope.alerts.push({msg: _t('a_problem_occured_during_the_payment_process_please_try_again_later'), type: 'danger' }) - $scope.attempting = false - ] - .result['finally'](null).then (reservation)-> - afterPayment(reservation) - - - - ## - # Open a modal window that allows the user to process a local payment for his current shopping cart (admin only). - ## - payOnSite = (reservation) -> - - $uibModal.open - templateUrl: '<%= asset_path "shared/valid_reservation_modal.html" %>' - size: 'sm' - resolve: - reservation: -> - reservation - price: -> - Price.compute(mkRequestParams(reservation, $scope.coupon.applied)).$promise - wallet: -> - Wallet.getWalletByUser({user_id: reservation.user_id}).$promise - coupon: -> - $scope.coupon.applied - controller: ['$scope', '$uibModalInstance', '$state', '$filter', 'reservation', 'price', 'wallet', 'Auth', 'Reservation', 'helpers', 'coupon' - ($scope, $uibModalInstance, $state, $filter, reservation, price, wallet, Auth, Reservation, helpers, coupon) -> - # User's wallet amount - $scope.walletAmount = wallet.amount - - # Price - $scope.price = price.price - - # price to pay - $scope.amount = helpers.getAmountToPay(price.price, wallet.amount) - - # Reservation - $scope.reservation = reservation - - # Used in wallet info template to interpolate some translations - $scope.numberFilter = $filter('number') - - # Button label - if $scope.amount > 0 - $scope.validButtonName = _t('confirm_payment_of_html', {ROLE:$scope.currentUser.role, AMOUNT:$filter('currency')($scope.amount)}, "messageformat") - else - if price.price > 0 and $scope.walletAmount == 0 - $scope.validButtonName = _t('confirm_payment_of_html', {ROLE:$scope.currentUser.role, AMOUNT:$filter('currency')(price.price)}, "messageformat") - else - $scope.validButtonName = _t('confirm') - - ## - # Callback to process the local payment, triggered on button click - ## - $scope.ok = -> - $scope.attempting = true - Reservation.save mkRequestParams($scope.reservation, coupon), (reservation) -> - $uibModalInstance.close(reservation) - $scope.attempting = true - , (response)-> - $scope.alerts = [] - $scope.alerts.push({msg: _t('a_problem_occured_during_the_payment_process_please_try_again_later'), type: 'danger' }) - $scope.attempting = false - $scope.cancel = -> - $uibModalInstance.dismiss('cancel') - ] - .result['finally'](null).then (reservation)-> - afterPayment(reservation) - - - - ## - # Computes the training amount depending of the member's credit - # @param training {Object} training slot - ## - computeTrainingAmount = (training)-> - # first we check that a user was selected - if Object.keys($scope.ctrl.member).length > 0 - r = mkReservation($scope.ctrl.member, training) # reservation without any Plan -> we get the training price - Price.compute mkRequestParams(r), (res) -> - $scope.selectedTrainingAmount = res.price - else - $scope.selectedTrainingAmount = null - - - - ## - # Once the reservation is booked (payment process successfully completed), change the event style - # in fullCalendar, update the user's subscription and free-credits if needed - # @param reservation {Object} - ## - afterPayment = (reservation)-> - $scope.paidTraining = $scope.selectedTraining - $scope.paidTraining.backgroundColor = 'white' - $scope.paidTraining.is_reserved = true - $scope.paidTraining.can_modify = true - updateTrainingSlotId($scope.paidTraining, reservation) - $scope.paidTraining.borderColor = '#b2e774' - $scope.paidTraining.title = $scope.paidTraining.training.name + " - " + _t('i_ve_reserved') - - $scope.selectedTraining = null - $scope.trainingIsValid = false - $scope.coupon.applied = null - - if $scope.selectedPlan - $scope.ctrl.member.subscribed_plan = angular.copy($scope.selectedPlan) - Auth._currentUser.subscribed_plan = angular.copy($scope.selectedPlan) - $scope.plansAreShown = false - $scope.selectedPlan = null - $scope.ctrl.member.training_credits = angular.copy(reservation.user.training_credits) - $scope.ctrl.member.machine_credits = angular.copy(reservation.user.machine_credits) - Auth._currentUser.training_credits = angular.copy(reservation.user.training_credits) - Auth._currentUser.machine_credits = angular.copy(reservation.user.machine_credits) - - uiCalendarConfig.calendars.calendar.fullCalendar 'refetchEvents' - uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents' - - - - ## - # Determines if the provided booked slot is able to be modified by the user. - # @param slot {Object} fullCalendar event object - ## - slotCanBeModified = (slot)-> - return true if $scope.currentUser.role is 'admin' - slotStart = moment(slot.start) - now = moment(new Date()) - if slot.can_modify and $scope.enableBookingMove and slotStart.diff(now, "hours") >= $scope.moveBookingDelay - return true - else - return false - - - - ## - # Determines if the provided booked slot is able to be canceled by the user. - # @param slot {Object} fullCalendar event object - ## - slotCanBeCanceled = (slot) -> - return true if $scope.currentUser.role is 'admin' - slotStart = moment(slot.start) - now = moment() - if slot.can_modify and $scope.enableBookingCancel and slotStart.diff(now, "hours") >= $scope.cancelBookingDelay - return true - else - return false - - - - ## - # For booking modifications, checks that the newly selected slot is valid - # @param slot {Object} fullCalendar event object - ## - slotCanBePlaced = (slot)-> - if slot.training.id == $scope.slotToModify.training.id and !slot.is_completed - return true - else - return false + if $scope.currentUser.role is 'admin' and event.tags.length > 0 + html = '' + for tag in event.tags + html += "#{tag.name}" + element.find('.fc-time').append(html) + return @@ -744,11 +390,29 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta updateTrainingSlotId = (slot, reservation)-> angular.forEach reservation.slots, (s)-> if slot.start_at == slot.start_at - slot.slot_id = s.id + slot.id = s.id - ## !!! MUST BE CALLED AT THE END of the controller + ## + # Update the calendar's display to render the new attributes of the events + ## + updateCalendar = -> + uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents' + + + + ## + # Asynchronously fetch the events from the API and refresh the calendar's view with these new events + ## + refetchCalendar = -> + $timeout -> + uiCalendarConfig.calendars.calendar.fullCalendar 'refetchEvents' + uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents' + + + + ## !!! MUST BE CALLED AT THE END of the controller initialize() ] diff --git a/app/assets/javascripts/directives/cart.coffee.erb b/app/assets/javascripts/directives/cart.coffee.erb new file mode 100644 index 000000000..42c10d08b --- /dev/null +++ b/app/assets/javascripts/directives/cart.coffee.erb @@ -0,0 +1,569 @@ +Application.Directives.directive 'cart', [ '$rootScope', '$uibModal', 'dialogs', 'growl', 'Auth', 'Price', 'Wallet', 'CustomAsset', 'Slot', 'helpers', '_t' +, ($rootScope, $uibModal, dialogs, growl, Auth, Price, Wallet, CustomAsset, Slot, helpers, _t) -> + { + restrict: 'E' + scope: + slot: '=' + slotSelectionTime: '=' + events: '=' + user: '=' + modePlans: '=' + plan: '=' + planSelectionTime: '=' + settings: '=' + onSlotAddedToCart: '=' + onSlotRemovedFromCart: '=' + onSlotStartToModify: '=' + onSlotModifyDestination: '=' + onSlotModifySuccess: '=' + onSlotModifyCancel: '=' + onSlotModifyUnselect: '=' + onSlotCancelSuccess: '=' + afterPayment: '=' + reservableId: '@' + reservableType: '@' + reservableName: '@' + limitToOneSlot: '@' + templateUrl: '<%= asset_path "shared/_cart.html" %>' + link: ($scope, element, attributes) -> + ## will store the user's plan if he choosed to buy one + $scope.selectedPlan = null + + ## total amount of the bill to pay + $scope.amountTotal = 0 + + ## total amount of the elements in the cart, without considering any coupon + $scope.totalNoCoupon = 0 + + ## Discount coupon to apply to the basket, if any + $scope.coupon = + applied: null + + ## Global config: is the user authorized to change his bookings slots? + $scope.enableBookingMove = ($scope.settings.booking_move_enable == "true") + + ## Global config: delay in hours before a booking while changing the booking slot is forbidden + $scope.moveBookingDelay = parseInt($scope.settings.booking_move_delay) + + ## Global config: is the user authorized to cancel his bookings? + $scope.enableBookingCancel = ($scope.settings.booking_cancel_enable == "true") + + ## Global config: delay in hours before a booking while the cancellation is forbidden + $scope.cancelBookingDelay = parseInt($scope.settings.booking_cancel_delay) + + + + ## + # Add the provided slot to the shopping cart (state transition from free to 'about to be reserved') + # and increment the total amount of the cart if needed. + # @param slot {Object} fullCalendar event object + ## + $scope.validateSlot = (slot)-> + slot.isValid = true + updateCartPrice() + + + + ## + # Remove the provided slot from the shopping cart (state transition from 'about to be reserved' to free) + # and decrement the total amount of the cart if needed. + # @param slot {Object} fullCalendar event object + # @param index {number} index of the slot in the reservation array + # @param [event] {Object} see https://docs.angularjs.org/guide/expression#-event- + ## + $scope.removeSlot = (slot, index, event)-> + event.preventDefault() if event + $scope.events.reserved.splice(index, 1) + # if is was the last slot, we remove any plan from the cart + if $scope.events.reserved.length == 0 + $scope.selectedPlan = null + $scope.plan = null + $scope.modePlans = false + $scope.onSlotRemovedFromCart(slot) if typeof $scope.onSlotRemovedFromCart == 'function' + updateCartPrice() + + + + ## + # Checks that every selected slots were added to the shopping cart. Ie. will return false if + # any checked slot was not validated by the user. + ## + $scope.isSlotsValid = -> + isValid = true + angular.forEach $scope.events.reserved, (m)-> + isValid = false if !m.isValid + isValid + + + + ## + # Switch the user's view from the reservation agenda to the plan subscription + ## + $scope.showPlans = -> + # first, we ensure that a user was selected (admin) or logged (member) + if Object.keys($scope.user).length > 0 + $scope.modePlans = true + else + # otherwise we alert, this error musn't occur when the current user hasn't the admin role + growl.error(_t('cart.please_select_a_member_first')) + + + ## + # Validates the shopping chart and redirect the user to the payment step + ## + $scope.payCart = -> + # first, we check that a user was selected + if Object.keys($scope.user).length > 0 + reservation = mkReservation($scope.user, $scope.events.reserved, $scope.selectedPlan) + + Wallet.getWalletByUser {user_id: $scope.user.id}, (wallet) -> + amountToPay = helpers.getAmountToPay($scope.amountTotal, wallet.amount) + if not $scope.isAdmin() and amountToPay > 0 + payByStripe(reservation) + else + if $scope.isAdmin() or amountToPay is 0 + payOnSite(reservation) + else + # otherwise we alert, this error musn't occur when the current user is not admin + growl.error(_t('cart.please_select_a_member_first')) + + + ## + # When modifying an already booked reservation, confirm the modification. + ## + $scope.modifySlot = -> + Slot.update {id: $scope.events.modifiable.id}, + slot: + start_at: $scope.events.placable.start + end_at: $scope.events.placable.end + availability_id: $scope.events.placable.availability_id + , -> # success + # -> run the callback + $scope.onSlotModifySuccess() if typeof $scope.onSlotModifySuccess == 'function' + # -> set the events as successfully moved (to display a summary) + $scope.events.moved = + newSlot: $scope.events.placable + oldSlot: $scope.events.modifiable + # -> reset the 'moving' status + $scope.events.placable = null + $scope.events.modifiable = null + , (err) -> # failure + growl.error(_t('cart.unable_to_change_the_reservation')) + console.error(err) + + + + ## + # Cancel the current booking modification, reseting the whole process + # @param event {Object} see https://docs.angularjs.org/guide/expression#-event- + ## + $scope.cancelModifySlot = (event) -> + event.preventDefault() if event + $scope.onSlotModifyCancel() if typeof $scope.onSlotModifyCancel == 'function' + $scope.events.placable = null + $scope.events.modifiable = null + + + + ## + # When modifying an already booked reservation, cancel the choice of the new slot + # @param e {Object} see https://docs.angularjs.org/guide/expression#-event- + ## + $scope.removeSlotToPlace = (e)-> + e.preventDefault() + $scope.onSlotModifyUnselect() if typeof $scope.onSlotModifyUnselect == 'function' + $scope.events.placable = null + + + + ## + # Checks if $scope.events.modifiable and $scope.events.placable have tag incompatibilities + # @returns {boolean} true in case of incompatibility + ## + $scope.tagMissmatch = -> + return false if $scope.events.placable.tag_ids.length == 0 + for tag in $scope.events.modifiable.tags + if tag.id not in $scope.events.placable.tag_ids + return true + false + + + + ## + # Check if the currently logged user has teh 'admin' role? + # @returns {boolean} + ## + $scope.isAdmin = -> + $rootScope.currentUser and $rootScope.currentUser.role is 'admin' + + + + ### PRIVATE SCOPE ### + + ## + # Kind of constructor: these actions will be realized first when the directive is loaded + ## + initialize = -> + # What the binded slot + $scope.$watch 'slotSelectionTime', (newValue, oldValue) -> + if newValue != oldValue + slotSelectionChanged() + $scope.$watch 'user', (newValue, oldValue) -> + if newValue != oldValue + resetCartState() + updateCartPrice() + $scope.$watch 'planSelectionTime', (newValue, oldValue) -> + if newValue != oldValue + planSelectionChanged() + # watch when a coupon is applied to re-compute the total price + $scope.$watch 'coupon.applied', (newValue, oldValue) -> + unless newValue == null and oldValue == null + updateCartPrice() + + + + ## + # Callback triggered when the selected slot changed + ## + slotSelectionChanged = -> + if $scope.slot + if not $scope.slot.is_reserved and not $scope.events.modifiable and not $scope.slot.is_completed + # slot is not reserved and we are not currently modifying a slot + # -> can be added to cart or removed if already present + index = $scope.events.reserved.indexOf($scope.slot) + if index == -1 + if $scope.limitToOneSlot is 'true' and $scope.events.reserved[0] + # if we limit the number of slots in the cart to 1, and there is already + # a slot in the cart, we remove it before adding the new one + $scope.removeSlot($scope.events.reserved[0], 0) + # slot is not in the cart, so we add it + $scope.events.reserved.push $scope.slot + $scope.onSlotAddedToCart() if typeof $scope.onSlotAddedToCart == 'function' + else + # slot is in the cart, remove it + $scope.removeSlot($scope.slot, index) + # in every cases, because a new reservation has started, we reset the cart content + resetCartState() + # finally, we update the prices + updateCartPrice() + else if !$scope.slot.is_reserved and !$scope.slot.is_completed and $scope.events.modifiable + # slot is not reserved but we are currently modifying a slot + # -> we request the calender to change the rendering + $scope.onSlotModifyUnselect() if typeof $scope.onSlotModifyUnselect == 'function' + # -> then, we re-affect the destination slot + if !$scope.events.placable or $scope.events.placable._id != $scope.slot._id + $scope.events.placable = $scope.slot + else + $scope.events.placable = null + else if $scope.slot.is_reserved and $scope.events.modifiable and $scope.slot.is_reserved._id == $scope.events.modifiable._id + # slot is reserved and currently modified + # -> we cancel the modification + $scope.cancelModifySlot() + else if $scope.slot.is_reserved and (slotCanBeModified($scope.slot) or slotCanBeCanceled($scope.slot)) and !$scope.events.modifiable and $scope.events.reserved.length == 0 + # slot is reserved and is ok to be modified or cancelled + # but we are not currently running a modification or having any slots in the cart + # -> first the affect the modification/cancellation rights attributes to the current slot + resetCartState() + $scope.slot.movable = slotCanBeModified($scope.slot) + $scope.slot.cancelable = slotCanBeCanceled($scope.slot) + # -> then, we open a dialog to ask to the user to choose an action + dialogs.confirm + templateUrl: '<%= asset_path "shared/confirm_modify_slot_modal.html" %>' + resolve: + object: -> $scope.slot + , (type) -> + # the user has choosen an action, so we proceed + if type == 'move' + $scope.onSlotStartToModify() if typeof $scope.onSlotStartToModify == 'function' + $scope.events.modifiable = $scope.slot + else if type == 'cancel' + dialogs.confirm + resolve: + object: -> + title: _t('cart.confirmation_required') + msg: _t('cart.do_you_really_want_to_cancel_this_reservation') + , -> # cancel confirmed + Slot.cancel {id: $scope.slot.id}, -> # successfully canceled + growl.success _t('cart.reservation_was_cancelled_successfully') + $scope.onSlotCancelSuccess() if typeof $scope.onSlotCancelSuccess == 'function' + , -> # error while canceling + growl.error _t('cart.cancellation_failed') + + + + ## + # Reset the parameters that may lead to a wrong price but leave the content (events added to cart) + ## + resetCartState = -> + $scope.selectedPlan = null + $scope.coupon.applied = null + $scope.events.moved = null + $scope.events.paid = [] + $scope.events.modifiable = null + $scope.events.placable = null + + + + ## + # Determines if the provided booked slot is able to be modified by the user. + # @param slot {Object} fullCalendar event object + ## + slotCanBeModified = (slot)-> + return true if $scope.isAdmin() + slotStart = moment(slot.start) + now = moment() + if slot.can_modify and $scope.enableBookingMove and slotStart.diff(now, "hours") >= $scope.moveBookingDelay + return true + else + return false + + + + ## + # Determines if the provided booked slot is able to be canceled by the user. + # @param slot {Object} fullCalendar event object + ## + slotCanBeCanceled = (slot) -> + return true if $scope.isAdmin() + slotStart = moment(slot.start) + now = moment() + if slot.can_modify and $scope.enableBookingCancel and slotStart.diff(now, "hours") >= $scope.cancelBookingDelay + return true + else + return false + + + + ## + # Callback triggered when the selected slot changed + ## + planSelectionChanged = -> + if Auth.isAuthenticated() + if $scope.selectedPlan != $scope.plan + $scope.selectedPlan = $scope.plan + else + $scope.selectedPlan = null + updateCartPrice() + else + $rootScope.login null, -> + $scope.selectedPlan = $scope.plan + updateCartPrice() + + + ## + # Update the total price of the current selection/reservation + ## + updateCartPrice = -> + if Object.keys($scope.user).length > 0 + r = mkReservation($scope.user, $scope.events.reserved, $scope.selectedPlan) + Price.compute mkRequestParams(r, $scope.coupon.applied), (res) -> + $scope.amountTotal = res.price + $scope.totalNoCoupon = res.price_without_coupon + setSlotsDetails(res.details) + else + # otherwise we alert, this error musn't occur when the current user is not admin + growl.warning(_t('cart.please_select_a_member_first')) + $scope.amountTotal = null + + + setSlotsDetails = (details) -> + angular.forEach $scope.events.reserved, (slot) -> + angular.forEach details.slots, (s) -> + if moment(s.start_at).isSame(slot.start) + slot.promo = s.promo + slot.price = s.price + + + ## + # Format the parameters expected by /api/prices/compute or /api/reservations and return the resulting object + # @param reservation {Object} as returned by mkReservation() + # @param coupon {Object} Coupon as returned from the API + # @return {{reservation:Object, coupon_code:string}} + ## + mkRequestParams = (reservation, coupon) -> + params = + reservation: reservation + coupon_code: (coupon.code if coupon) + + params + + + + ## + # Create an hash map implementing the Reservation specs + # @param member {Object} User as retreived from the API: current user / selected user if current is admin + # @param slots {Array} Array of fullCalendar events: slots selected on the calendar + # @param [plan] {Object} Plan as retrived from the API: plan to buy with the current reservation + # @return {{user_id:Number, reservable_id:Number, reservable_type:String, slots_attributes:Array, plan_id:Number|null}} + ## + mkReservation = (member, slots, plan = null) -> + reservation = + user_id: member.id + reservable_id: $scope.reservableId + reservable_type: $scope.reservableType + slots_attributes: [] + plan_id: (plan.id if plan) + angular.forEach slots, (slot, key) -> + reservation.slots_attributes.push + start_at: slot.start + end_at: slot.end + availability_id: slot.availability_id + offered: slot.offered || false + + reservation + + + + ## + # Open a modal window that allows the user to process a credit card payment for his current shopping cart. + ## + payByStripe = (reservation) -> + $uibModal.open + templateUrl: '<%= asset_path "stripe/payment_modal.html" %>' + size: 'md' + resolve: + reservation: -> + reservation + price: -> + Price.compute(mkRequestParams(reservation, $scope.coupon.applied)).$promise + wallet: -> + Wallet.getWalletByUser({user_id: reservation.user_id}).$promise + cgv: -> + CustomAsset.get({name: 'cgv-file'}).$promise + coupon: -> + $scope.coupon.applied + controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'cgv', 'Auth', 'Reservation', 'wallet', 'helpers', '$filter', 'coupon', + ($scope, $uibModalInstance, $state, reservation, price, cgv, Auth, Reservation, wallet, helpers, $filter, coupon) -> + # user wallet amount + $scope.walletAmount = wallet.amount + + # Price + $scope.amount = helpers.getAmountToPay(price.price, wallet.amount) + + # CGV + $scope.cgv = cgv.custom_asset + + # Reservation + $scope.reservation = reservation + + # Used in wallet info template to interpolate some translations + $scope.numberFilter = $filter('number') + + ## + # Callback to process the payment with Stripe, triggered on button click + ## + $scope.payment = (status, response) -> + if response.error + growl.error(response.error.message) + else + $scope.attempting = true + $scope.reservation.card_token = response.id + Reservation.save mkRequestParams($scope.reservation, coupon), (reservation) -> + $uibModalInstance.close(reservation) + , (response)-> + $scope.alerts = [] + if response.status == 500 + $scope.alerts.push + msg: response.statusText + type: 'danger' + else + if response.data.card and response.data.card.join('').length > 0 + $scope.alerts.push + msg: response.data.card.join('. ') + type: 'danger' + else if response.data.payment and response.data.payment.join('').length > 0 + $scope.alerts.push + msg: response.data.payment.join('. ') + type: 'danger' + $scope.attempting = false + ] + .result['finally'](null).then (reservation)-> + afterPayment(reservation) + + + + ## + # Open a modal window that allows the user to process a local payment for his current shopping cart (admin only). + ## + payOnSite = (reservation) -> + $uibModal.open + templateUrl: '<%= asset_path "shared/valid_reservation_modal.html" %>' + size: 'sm' + resolve: + reservation: -> + reservation + price: -> + Price.compute(mkRequestParams(reservation, $scope.coupon.applied)).$promise + wallet: -> + Wallet.getWalletByUser({user_id: reservation.user_id}).$promise + coupon: -> + $scope.coupon.applied + controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'Auth', 'Reservation', 'wallet', 'helpers', '$filter', 'coupon', + ($scope, $uibModalInstance, $state, reservation, price, Auth, Reservation, wallet, helpers, $filter, coupon) -> + + # user wallet amount + $scope.walletAmount = wallet.amount + + # Global price (total of all items) + $scope.price = price.price + + # Price to pay (wallet deducted) + $scope.amount = helpers.getAmountToPay(price.price, wallet.amount) + + # Reservation + $scope.reservation = reservation + + # Used in wallet info template to interpolate some translations + $scope.numberFilter = $filter('number') + + # Button label + if $scope.amount > 0 + $scope.validButtonName = _t('cart.confirm_payment_of_html', {ROLE:$rootScope.currentUser.role, AMOUNT:$filter('currency')($scope.amount)}, "messageformat") + else + if price.price > 0 and $scope.walletAmount == 0 + $scope.validButtonName = _t('cart.confirm_payment_of_html', {ROLE:$rootScope.currentUser.role, AMOUNT:$filter('currency')(price.price)}, "messageformat") + else + $scope.validButtonName = _t('confirm') + + ## + # Callback to process the local payment, triggered on button click + ## + $scope.ok = -> + $scope.attempting = true + Reservation.save mkRequestParams($scope.reservation, coupon), (reservation) -> + $uibModalInstance.close(reservation) + $scope.attempting = true + , (response)-> + $scope.alerts = [] + $scope.alerts.push({msg: _t('cart.a_problem_occured_during_the_payment_process_please_try_again_later'), type: 'danger' }) + $scope.attempting = false + $scope.cancel = -> + $uibModalInstance.dismiss('cancel') + ] + .result['finally'](null).then (reservation)-> + afterPayment(reservation) + + + + ## + # Actions to run after the payment was successfull + ## + afterPayment = (reservation) -> + # we set the cart content as 'paid' to display a summary of the transaction + $scope.events.paid = $scope.events.reserved + # we call the external callback if present + $scope.afterPayment(reservation) if typeof $scope.afterPayment == 'function' + # we reset the coupon and the cart content and we unselect the slot + $scope.events.reserved = [] + $scope.coupon.applied = null + $scope.slot = null + $scope.selectedPlan = null + + + + ## !!! MUST BE CALLED AT THE END of the directive + initialize() + } +] + + diff --git a/app/assets/javascripts/router.coffee.erb b/app/assets/javascripts/router.coffee.erb index b62151748..7ffadb9ea 100644 --- a/app/assets/javascripts/router.coffee.erb +++ b/app/assets/javascripts/router.coffee.erb @@ -373,7 +373,7 @@ angular.module('application.router', ['ui.router']). translations: [ 'Translations', (Translations) -> Translations.query(['app.logged.machines_reserve', 'app.shared.plan_subscribe', 'app.shared.member_select', 'app.shared.stripe', 'app.shared.valid_reservation_modal', 'app.shared.confirm_modify_slot_modal', - 'app.shared.wallet', 'app.shared.coupon_input']).$promise + 'app.shared.wallet', 'app.shared.coupon_input', 'app.shared.cart']).$promise ] .state 'app.admin.machines_edit', url: '/machines/:id/edit' @@ -388,6 +388,97 @@ angular.module('application.router', ['ui.router']). translations: [ 'Translations', (Translations) -> Translations.query(['app.admin.machines_edit', 'app.shared.machine']).$promise ] + + # spaces + .state 'app.public.spaces_list', + url: '/spaces' + abstract: Fablab.withoutSpaces + views: + 'main@': + templateUrl: '<%= asset_path "spaces/index.html" %>' + controller: 'SpacesController' + resolve: + spacesPromise: ['Space', (Space)-> + Space.query().$promise + ] + translations: [ 'Translations', (Translations) -> + Translations.query(['app.public.spaces_list']).$promise + ] + .state 'app.admin.space_new', + url: '/spaces/new' + abstract: Fablab.withoutSpaces + views: + 'main@': + templateUrl: '<%= asset_path "spaces/new.html" %>' + controller: 'NewSpaceController' + resolve: + translations: [ 'Translations', (Translations) -> + Translations.query(['app.admin.space_new', 'app.shared.space']).$promise + ] + .state 'app.public.space_show', + url: '/spaces/:id' + abstract: Fablab.withoutSpaces + views: + 'main@': + templateUrl: '<%= asset_path "spaces/show.html" %>' + controller: 'ShowSpaceController' + resolve: + spacePromise: ['Space', '$stateParams', (Space, $stateParams)-> + Space.get(id: $stateParams.id).$promise + ] + translations: [ 'Translations', (Translations) -> + Translations.query(['app.public.space_show']).$promise + ] + .state 'app.admin.space_edit', + url: '/spaces/:id/edit' + abstract: Fablab.withoutSpaces + views: + 'main@': + templateUrl: '<%= asset_path "spaces/edit.html" %>' + controller: 'EditSpaceController' + resolve: + spacePromise: ['Space', '$stateParams', (Space, $stateParams)-> + Space.get(id: $stateParams.id).$promise + ] + translations: [ 'Translations', (Translations) -> + Translations.query(['app.admin.space_edit', 'app.shared.space']).$promise + ] + .state 'app.logged.space_reserve', + url: '/spaces/:id/reserve' + abstract: Fablab.withoutSpaces + views: + 'main@': + templateUrl: '<%= asset_path "spaces/reserve.html" %>' + controller: 'ReserveSpaceController' + resolve: + spacePromise: ['Space', '$stateParams', (Space, $stateParams)-> + Space.get(id: $stateParams.id).$promise + ] + availabilitySpacesPromise: ['Availability', '$stateParams', (Availability, $stateParams)-> + Availability.spaces({spaceId: $stateParams.id}).$promise + ] + plansPromise: ['Plan', (Plan)-> + Plan.query().$promise + ] + groupsPromise: ['Group', (Group)-> + Group.query().$promise + ] + settingsPromise: ['Setting', (Setting)-> + Setting.query(names: "['booking_window_start', + 'booking_window_end', + 'booking_move_enable', + 'booking_move_delay', + 'booking_cancel_enable', + 'booking_cancel_delay', + 'subscription_explications_alert', + 'space_explications_alert']").$promise + ] + translations: [ 'Translations', (Translations) -> + Translations.query(['app.logged.space_reserve', 'app.shared.plan_subscribe', 'app.shared.member_select', + 'app.shared.stripe', 'app.shared.valid_reservation_modal', 'app.shared.confirm_modify_slot_modal', + 'app.shared.wallet', 'app.shared.coupon_input', 'app.shared.cart']).$promise + ] + # trainings .state 'app.public.trainings_list', url: '/trainings' @@ -451,7 +542,7 @@ angular.module('application.router', ['ui.router']). translations: [ 'Translations', (Translations) -> Translations.query(['app.logged.trainings_reserve', 'app.shared.plan_subscribe', 'app.shared.member_select', 'app.shared.stripe', 'app.shared.valid_reservation_modal', 'app.shared.confirm_modify_slot_modal', - 'app.shared.wallet', 'app.shared.coupon_input']).$promise + 'app.shared.wallet', 'app.shared.coupon_input', 'app.shared.cart']).$promise ] # notifications .state 'app.logged.notifications', @@ -549,6 +640,9 @@ angular.module('application.router', ['ui.router']). machinesPromise: ['Machine', (Machine)-> Machine.query().$promise ] + spacesPromise: ['Space', (Space) -> + Space.query().$promise + ] translations: [ 'Translations', (Translations) -> Translations.query(['app.public.calendar']).$promise ] @@ -770,6 +864,15 @@ angular.module('application.router', ['ui.router']). couponsPromise: ['Coupon', (Coupon) -> Coupon.query().$promise ] + spacesPromise: ['Space', (Space) -> + Space.query().$promise + ] + spacesPricesPromise: ['Price', (Price)-> + Price.query(priceable_type: 'Space', plan_id: 'null').$promise + ] + spacesCreditsPromise: ['Credit', (Credit) -> + Credit.query({creditable_type: 'Space'}).$promise + ] # plans .state 'app.admin.plans', @@ -778,15 +881,9 @@ angular.module('application.router', ['ui.router']). prices: ['Pricing', (Pricing) -> Pricing.query().$promise ] - machines: ['Machine', (Machine) -> - Machine.query().$promise - ] groups: ['Group', (Group) -> Group.query().$promise ] - plans: ['Plan', (Plan) -> - Plan.query().$promise - ] partners: ['User', (User) -> User.query({role: 'partner'}).$promise ] @@ -807,6 +904,15 @@ angular.module('application.router', ['ui.router']). templateUrl: '<%= asset_path "admin/plans/edit.html" %>' controller: 'EditPlanController' resolve: + spaces: ['Space', (Space) -> + Space.query().$promise + ] + machines: ['Machine', (Machine) -> + Machine.query().$promise + ] + plans: ['Plan', (Plan) -> + Plan.query().$promise + ] planPromise: ['Plan', '$stateParams', (Plan, $stateParams) -> Plan.get({id: $stateParams.id}).$promise ] @@ -1038,6 +1144,7 @@ angular.module('application.router', ['ui.router']). 'training_information_message', 'subscription_explications_alert', 'event_explications_alert', + 'space_explications_alert', 'booking_window_start', 'booking_window_end', 'booking_move_enable', diff --git a/app/assets/javascripts/services/availability.coffee b/app/assets/javascripts/services/availability.coffee index 8ef8b53a7..4bc448b13 100644 --- a/app/assets/javascripts/services/availability.coffee +++ b/app/assets/javascripts/services/availability.coffee @@ -17,6 +17,11 @@ Application.Services.factory 'Availability', ["$resource", ($resource)-> url: '/api/availabilities/trainings/:trainingId' params: {trainingId: "@trainingId"} isArray: true + spaces: + method: 'GET' + url: '/api/availabilities/spaces/:spaceId' + params: {spaceId: "@spaceId"} + isArray: true update: method: 'PUT' ] diff --git a/app/assets/javascripts/services/space.coffee b/app/assets/javascripts/services/space.coffee new file mode 100644 index 000000000..802da06cd --- /dev/null +++ b/app/assets/javascripts/services/space.coffee @@ -0,0 +1,8 @@ +'use strict' + +Application.Services.factory 'Space', ["$resource", ($resource)-> + $resource "/api/spaces/:id", + {id: "@id"}, + update: + method: 'PUT' +] diff --git a/app/assets/stylesheets/app.colors.scss b/app/assets/stylesheets/app.colors.scss index e0c6d7d63..7637f3c10 100644 --- a/app/assets/stylesheets/app.colors.scss +++ b/app/assets/stylesheets/app.colors.scss @@ -5,6 +5,7 @@ //.bg-yellow { background-color: $yellow !important; } .bg-token { background-color: rgba(230, 208, 137, 0.49); } .bg-machine { background-color: $beige; } +.bg-space { background-color: $cyan } .bg-formation { background-color: $violet; } .bg-event { background-color: $japonica; } .bg-atelier { background-color: $blue; } @@ -39,4 +40,5 @@ .text-purple { color: $violet !important; } .text-japonica { color: $japonica !important; } .text-beige { color: $beige !important; } +.text-cyan { color: $cyan !important; } .text-green, .green { color: #79C84A !important; } diff --git a/app/assets/stylesheets/app.utilities.scss b/app/assets/stylesheets/app.utilities.scss index 245791515..69821a820 100644 --- a/app/assets/stylesheets/app.utilities.scss +++ b/app/assets/stylesheets/app.utilities.scss @@ -176,6 +176,7 @@ p, .widget p { .r-n { border-radius: 0 0 0 0; } .p-xs { padding: 5px;} +.p-s { padding: 10px;} .p-lg { padding: 30px; } .p-l { padding: 16px; } diff --git a/app/assets/stylesheets/bootstrap_and_overrides.scss b/app/assets/stylesheets/bootstrap_and_overrides.scss index e4081ff3a..f39904a68 100644 --- a/app/assets/stylesheets/bootstrap_and_overrides.scss +++ b/app/assets/stylesheets/bootstrap_and_overrides.scss @@ -43,6 +43,7 @@ $blue: $brand-info; $green: $brand-success; $beige: #e4cd78; $violet: #bd7ae9; +$cyan: #3fc7ff; $japonica: #dd7e6b; $border-color: #dddddd; @@ -767,7 +768,7 @@ $panel-footer-padding: $panel-heading-padding !default; $panel-border-radius: $border-radius-large !default; // add sleede -$panel-border: $border-color !default; +$panel-border: $border-color !default; $panel-heading-bg: #fff !default; $panel-footer-bg: #fff !default; diff --git a/app/assets/templates/admin/calendar/calendar.html.erb b/app/assets/templates/admin/calendar/calendar.html.erb index 7cde3e4dd..2e7c16dd3 100644 --- a/app/assets/templates/admin/calendar/calendar.html.erb +++ b/app/assets/templates/admin/calendar/calendar.html.erb @@ -7,14 +7,15 @@
-

{{ 'calendar_management' }}

+

{{ 'admin_calendar.calendar_management' }}

-
- {{ 'trainings' }}
- {{ 'machines' }} +
+ {{ 'admin_calendar.trainings' }}
+ {{ 'admin_calendar.machines' }}
+ {{ 'admin_calendar.spaces' }}
@@ -29,9 +30,21 @@
+
-

{{ 'ongoing_reservations' }}

+

{{ 'admin_calendar.ongoing_reservations' }}

    @@ -42,7 +55,7 @@
-
{{ 'no_reservations' }}
+
{{ 'admin_calendar.no_reservations' }}
@@ -50,7 +63,7 @@
-

{{ 'machines' }}

+

{{ 'admin_calendar.machines' }}

    diff --git a/app/assets/templates/admin/calendar/eventModal.html.erb b/app/assets/templates/admin/calendar/eventModal.html.erb index 6cc3bda96..a0d851ef1 100644 --- a/app/assets/templates/admin/calendar/eventModal.html.erb +++ b/app/assets/templates/admin/calendar/eventModal.html.erb @@ -1,22 +1,41 @@ -