1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-02-26 20:54:21 +01:00

Merge branch 'spaces' into dev

This commit is contained in:
Sylvain 2017-03-01 16:56:41 +01:00
commit 1092257526
129 changed files with 4762 additions and 2391 deletions

View File

@ -1 +1 @@
2.4.10 2.5.0-dev

View File

@ -1,5 +1,18 @@
# Changelog Fab Manager # Changelog Fab Manager
## next release
- TODO export availabilities (taiga#57)
- TODO bug: calendar (github#59)
- TODO bug: delete event (github#61)
- 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
- Fix a bug: trainings reservations are not shown in the admin's calendar
- Fix a bug: unable to delete an administrator from the system
- [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`
## v2.4.10 2017 January 9 ## v2.4.10 2017 January 9
- Optimized notifications system - Optimized notifications system

View File

@ -3,7 +3,7 @@
FabManager is the FabLab management solution. It is web-based, open-source and totally free. 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) 1. [Software stack](#software-stack)
2. [Contributing](#contributing) 2. [Contributing](#contributing)
3. [Setup a production environment](#setup-a-production-environment) 3. [Setup a production environment](#setup-a-production-environment)
@ -122,7 +122,7 @@ 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. 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. **Warning**: **NO NOT** run `rake db:setup` instead of these commands, as this will not run some required raw SQL instructions.
```bash ```bash
rake db:create rake db:create
@ -201,6 +201,12 @@ The PDF file name will be of the form "(INVOICE_PREFIX) - (invoice ID) _ (invoic
FABLAB_WITHOUT_PLANS FABLAB_WITHOUT_PLANS
If set to 'true', the subscription plans will be fully disabled and invisible in the application. 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 DEFAULT_MAIL_FROM
@ -264,7 +270,7 @@ Please consider that allowing file archives (eg. application/zip) or binary exec
MAX_IMAGE_SIZE 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. 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. If this parameter is not specified the maximum size allowed will be 2MB.
@ -680,12 +686,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: - When running the tests suite with `rake test`, all tests may fail with errors similar to the following:
Error: Error:
... ...
ActiveRecord::InvalidForeignKey: PG::ForeignKeyViolation: ERROR: insert or update on table "..." violates foreign key constraint "fk_rails_..." 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". 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:11:in `block in transaction'
test_after_commit (1.0.0) lib/test_after_commit/database_statements.rb:5: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. 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. PostgreSQL will prevent any users to disable referential integrity on the fly if they doesn't have the `SUPERUSER` role.

View File

@ -80,6 +80,8 @@ config(['$httpProvider', 'AuthProvider', "growlProvider", "unsavedWarningsConfig
// Global config: if true, the whole 'Plans & Subscriptions' feature will be disabled in the application // Global config: if true, the whole 'Plans & Subscriptions' feature will be disabled in the application
$rootScope.fablabWithoutPlans = Fablab.withoutPlans; $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). // 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 // If no previous $state were recorded, navigate to the home page

View File

@ -64,8 +64,8 @@ Application.Controllers.controller "AdminCalendarController", ["$scope", "$state
dialogs.confirm dialogs.confirm
resolve: resolve:
object: -> object: ->
title: _t('confirmation_required') title: _t('admin_calendar.confirmation_required')
msg: _t("do_you_really_want_to_cancel_the_USER_s_reservation_the_DATE_at_TIME_concerning_RESERVATION" 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 } , { 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') , 'messageformat')
, -> , ->
@ -78,9 +78,9 @@ Application.Controllers.controller "AdminCalendarController", ["$scope", "$state
resa.canceled_at = data.canceled_at resa.canceled_at = data.canceled_at
break break
# notify the admin # notify the admin
growl.success(_t('reservation_was_successfully_cancelled')) growl.success(_t('admin_calendar.reservation_was_successfully_cancelled'))
, (data, status) -> # failed , (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) -> $scope.removeMachine = (machine) ->
if $scope.availability.machine_ids.length == 1 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 else
# open a confirmation dialog # open a confirmation dialog
dialogs.confirm dialogs.confirm
resolve: resolve:
object: -> object: ->
title: _t('confirmation_required') title: _t('admin_calendar.confirmation_required')
msg: _t('do_you_really_want_to_remove_MACHINE_from_this_slot', {GENDER:getGender($scope.currentUser), MACHINE:machine.name}, "messageformat") + ' ' + msg: _t('admin_calendar.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('admin_calendar.this_will_prevent_any_new_reservation_on_this_slot_but_wont_cancel_those_existing') + ' ' +
_t('beware_this_cannot_be_reverted') _t('admin_calendar.beware_this_cannot_be_reverted')
, -> , ->
# the admin has confirmed, remove the machine # the admin has confirmed, remove the machine
machines = $scope.availability.machine_ids machines = $scope.availability.machine_ids
@ -115,9 +115,9 @@ Application.Controllers.controller "AdminCalendarController", ["$scope", "$state
$scope.availability.title = data.title $scope.availability.title = data.title
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents' uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
# notify the admin # 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 , (data, status) -> # failed
growl.error(_t('deletion_failed')) growl.error(_t('admin_calendar.deletion_failed'))
@ -150,6 +150,15 @@ Application.Controllers.controller "AdminCalendarController", ["$scope", "$state
resolve: resolve:
start: -> start start: -> start
end: -> end 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 # when the modal is closed, we send the slot to the server for saving
modalInstance.result.then (availability) -> modalInstance.result.then (availability) ->
uiCalendarConfig.calendars.calendar.fullCalendar 'renderEvent', uiCalendarConfig.calendars.calendar.fullCalendar 'renderEvent',
@ -184,9 +193,9 @@ Application.Controllers.controller "AdminCalendarController", ["$scope", "$state
Availability.delete id: event.id, -> Availability.delete id: event.id, ->
uiCalendarConfig.calendars.calendar.fullCalendar 'removeEvents', 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 # if the user has only clicked on the event, display its reservations
else else
Availability.reservations {id: event.id}, (reservations) -> Availability.reservations {id: event.id}, (reservations) ->
@ -227,7 +236,8 @@ Application.Controllers.controller "AdminCalendarController", ["$scope", "$state
## ##
# Controller used in the slot creation modal window # 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 ## $uibModal parameter
$scope.start = start $scope.start = start
@ -236,14 +246,26 @@ Application.Controllers.controller 'CreateEventModalController', ["$scope", "$ui
$scope.end = end $scope.end = end
## machines list ## machines list
$scope.machines = [] $scope.machines = machinesPromise
## trainings list ## trainings list
$scope.trainings = [] $scope.trainings = trainingsPromise
## spaces list
$scope.spaces = spacesPromise
## machines associated with the created slot ## machines associated with the created slot
$scope.selectedMachines = [] $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' ## the user is not able to edit the ending time of the availability, unless he set the type to 'training'
$scope.endDateReadOnly = true $scope.endDateReadOnly = true
@ -281,14 +303,16 @@ Application.Controllers.controller 'CreateEventModalController', ["$scope", "$ui
# Callback for the modal window validation: save the slot and closes the modal # Callback for the modal window validation: save the slot and closes the modal
## ##
$scope.ok = -> $scope.ok = ->
if $scope.availability.available_type == "machines" if $scope.availability.available_type == 'machines'
if $scope.selectedMachines.length > 0 if $scope.selectedMachines.length > 0
$scope.availability.machine_ids = $scope.selectedMachines.map (m) -> m.id $scope.availability.machine_ids = $scope.selectedMachines.map (m) -> m.id
else 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 return
else else if $scope.availability.available_type == 'training'
$scope.availability.training_ids = [$scope.selectedTraining.id] $scope.availability.training_ids = [$scope.selectedTraining.id]
else if $scope.availability.available_type == 'space'
$scope.availability.space_ids = [$scope.selectedSpace.id]
Availability.save Availability.save
availability: $scope.availability availability: $scope.availability
, (availability) -> , (availability) ->
@ -296,6 +320,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 # Callback to cancel the slot creation
## ##
@ -304,22 +345,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 # For training avaiabilities, set the maximum number of people allowed to register on this slot
## ##
$scope.setNbTotalPlaces = -> $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 ### ### PRIVATE SCOPE ###
@ -328,18 +361,11 @@ Application.Controllers.controller 'CreateEventModalController', ["$scope", "$ui
# Kind of constructor: these actions will be realized first when the controller is loaded # Kind of constructor: these actions will be realized first when the controller is loaded
## ##
initialize = -> initialize = ->
Machine.query().$promise.then (data)-> if $scope.trainings.length > 0
$scope.machines = data.map (d) -> $scope.selectedTraining = $scope.trainings[0]
id: d.id if $scope.spaces.length > 0
name: d.name $scope.selectedSpace = $scope.spaces[0]
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()
Tag.query().$promise.then (data) -> Tag.query().$promise.then (data) ->
$scope.tags = data $scope.tags = data
@ -347,7 +373,7 @@ Application.Controllers.controller 'CreateEventModalController', ["$scope", "$ui
## time must be dividable by 60 minutes (base slot duration). For training availabilities, the user ## time must be dividable by 60 minutes (base slot duration). For training availabilities, the user
## can configure any duration as it does not matters. ## can configure any duration as it does not matters.
$scope.$watch 'availability.available_type', (newValue, oldValue, scope) -> $scope.$watch 'availability.available_type', (newValue, oldValue, scope) ->
if newValue == 'machines' if newValue == 'machines' or newValue == 'space'
$scope.endDateReadOnly = true $scope.endDateReadOnly = true
diff = moment($scope.end).diff($scope.start, 'hours') # the result is rounded down by moment.js 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() $scope.end = moment($scope.start).add(diff, 'hours').toDate()
@ -358,8 +384,8 @@ Application.Controllers.controller 'CreateEventModalController', ["$scope", "$ui
## When the start date is changed, if we are configuring a machine availability, ## 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) ## maintain the relative length of the slot (ie. change the end time accordingly)
$scope.$watch 'start', (newValue, oldValue, scope) -> $scope.$watch 'start', (newValue, oldValue, scope) ->
# for machine availabilities, adjust the end time # for machine or space availabilities, adjust the end time
if $scope.availability.available_type == 'machines' if $scope.availability.available_type == 'machines' or $scope.availability.available_type == 'space'
end = moment($scope.end) end = moment($scope.end)
end.add(moment(newValue).diff(oldValue), 'milliseconds') end.add(moment(newValue).diff(oldValue), 'milliseconds')
$scope.end = end.toDate() $scope.end = end.toDate()

View File

@ -6,7 +6,7 @@
class PlanController class PlanController
constructor: ($scope, groups, plans, machines, prices, partners, CSRF) -> constructor: ($scope, groups, prices, partners, CSRF) ->
# protection against request forgery # protection against request forgery
CSRF.setMetaTags() CSRF.setMetaTags()
@ -15,12 +15,6 @@ class PlanController
## groups list ## groups list
$scope.groups = groups $scope.groups = groups
## plans list
$scope.plans = plans
## machines list
$scope.machines = machines
## users with role 'partner', notifiables for a partner plan ## users with role 'partner', notifiables for a partner plan
$scope.partners = partners.users $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 # Controller used in the plan creation form
## ##
Application.Controllers.controller 'NewPlanController', ['$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, plans, machines, 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}" $scope.partner.name = "#{user.first_name} #{user.last_name}"
$uibModalInstance.close($scope.partner) $uibModalInstance.close($scope.partner)
, (error)-> , (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 = -> $scope.cancel = ->
$uibModalInstance.dismiss('cancel') $uibModalInstance.dismiss('cancel')
] ]
@ -164,9 +131,9 @@ Application.Controllers.controller 'NewPlanController', ['$scope', '$uibModal',
## ##
$scope.afterSubmit = (content) -> $scope.afterSubmit = (content) ->
if !content.id? and !content.plan_ids? 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 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? if content.plan_ids?
$state.go('app.admin.pricing') $state.go('app.admin.pricing')
else 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 # 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' Application.Controllers.controller 'EditPlanController', ['$scope', 'groups', 'plans', 'planPromise', 'machines', 'spaces', 'prices', 'partners', 'CSRF', '$state', '$stateParams', 'growl', '$filter', '_t', 'Plan'
, ($scope, groups, plans, planPromise, machines, 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 ### ### 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 $scope.groups = groups
## current form is used for edition mode ## current form is used for edition mode
$scope.mode = 'edition' $scope.mode = 'edition'
@ -231,9 +210,9 @@ Application.Controllers.controller 'EditPlanController', ['$scope', 'groups', 'p
## ##
$scope.afterSubmit = (content) -> $scope.afterSubmit = (content) ->
if !content.id? and !content.plan_ids? 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 else
growl.success(_t('subscription_successfully_changed')) growl.success(_t('edit_plan.subscription_successfully_changed'))
$state.go('app.admin.pricing') $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 ### ### PRIVATE SCOPE ###
## ##
@ -258,7 +261,7 @@ Application.Controllers.controller 'EditPlanController', ['$scope', 'groups', 'p
## ##
initialize = -> initialize = ->
# Using the PlansController # 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 ## !!! MUST BE CALLED AT THE END of the controller
initialize() initialize()

View File

@ -3,8 +3,8 @@
## ##
# Controller used in the prices edition page # 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' 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, _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 ### ### PUBLIC SCOPE ###
## List of machines prices (not considering any plan) ## List of machines prices (not considering any plan)
@ -37,6 +37,15 @@ Application.Controllers.controller "EditPricingController", ["$scope", "$state",
## List of coupons ## List of coupons
$scope.coupons = couponsPromise $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 ## The plans list ordering. Default: by group
$scope.orderPlans = 'group_id' $scope.orderPlans = 'group_id'
@ -56,7 +65,7 @@ Application.Controllers.controller "EditPricingController", ["$scope", "$state",
if data? if data?
TrainingsPricing.update({ id: trainingsPricing.id }, { trainings_pricing: { amount: data } }).$promise TrainingsPricing.update({ id: trainingsPricing.id }, { trainings_pricing: { amount: data } }).$promise
else else
_t('please_specify_a_number') _t('pricing.please_specify_a_number')
## ##
# Retrieve a plan from its given identifier and returns it # Retrieve a plan from its given identifier and returns it
@ -89,13 +98,13 @@ Application.Controllers.controller "EditPricingController", ["$scope", "$state",
## ##
$scope.showTrainings = (trainings) -> $scope.showTrainings = (trainings) ->
unless angular.isArray(trainings) and trainings.length > 0 unless angular.isArray(trainings) and trainings.length > 0
return _t('none') return _t('pricing.none')
selected = [] selected = []
angular.forEach $scope.trainings, (t) -> angular.forEach $scope.trainings, (t) ->
if trainings.indexOf(t.id) >= 0 if trainings.indexOf(t.id) >= 0
selected.push t.name 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 training_credit_nb: newdata.training_credits
, angular.noop() # do nothing in case of success , angular.noop() # do nothing in case of success
, (error) -> , (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 # save the associated trainings
angular.forEach $scope.trainingCreditsGroups, (original, key) -> 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.trainingCredits.splice($scope.trainingCredits.indexOf(tc), 1)
$scope.trainingCreditsGroups[planId].splice($scope.trainingCreditsGroups[planId].indexOf(tc.id), 1) $scope.trainingCreditsGroups[planId].splice($scope.trainingCreditsGroups[planId].indexOf(tc.id), 1)
, (error) -> , (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 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 # iterate through the new credits to add
angular.forEach newdata.training_ids, (newTrainingId) -> 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) $scope.trainingCreditsGroups[newTc.plan_id].push(newTc.creditable_id)
, (error) -> # failed , (error) -> # failed
training = getTrainingFromId(newTrainingId) 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) console.error(error)
@ -177,11 +186,16 @@ Application.Controllers.controller "EditPricingController", ["$scope", "$state",
# @param credit {Object} credit object, inherited from $resource # @param credit {Object} credit object, inherited from $resource
## ##
$scope.showCreditableName = (credit) -> $scope.showCreditableName = (credit) ->
selected = _t('not_set') selected = _t('pricing.not_set')
if credit and credit.creditable_id if credit and credit.creditable_id
angular.forEach $scope.machines, (m)-> if credit.creditable_type == 'Machine'
if m.id == credit.creditable_id angular.forEach $scope.machines, (m)->
selected = m.name+' ( id. '+m.id+' )' 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 return selected
@ -195,27 +209,27 @@ Application.Controllers.controller "EditPricingController", ["$scope", "$state",
$scope.saveMachineCredit = (data, id) -> $scope.saveMachineCredit = (data, id) ->
for mc in $scope.machineCredits 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) 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 unless id
$scope.machineCredits.pop() $scope.machineCredits.pop()
return false return false
if id? if id?
Credit.update {id: id}, credit: data, -> Credit.update {id: id}, credit: data, ->
growl.success(_t('changes_have_been_successfully_saved')) growl.success(_t('pricing.changes_have_been_successfully_saved'))
else else
data.creditable_type = 'Machine' data.creditable_type = 'Machine'
Credit.save Credit.save
credit: data credit: data
, (resp) -> , (resp) ->
$scope.machineCredits[$scope.machineCredits.length-1].id = resp.id $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 # 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 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) -> $scope.cancelMachineCredit = (rowform, index) ->
if $scope.machineCredits[index].id? 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 # 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') # @param type {string|undefined|null} plan's type (eg. 'partner')
@ -242,8 +320,8 @@ Application.Controllers.controller "EditPricingController", ["$scope", "$state",
## ##
$scope.getPlanType = (type) -> $scope.getPlanType = (type) ->
if type == 'PartnerPlan' if type == 'PartnerPlan'
return _t('partner') return _t('pricing.partner')
else return _t('standard') else return _t('pricing.standard')
## ##
# Change the plans ordering criterion to the one provided # Change the plans ordering criterion to the one provided
@ -270,7 +348,7 @@ Application.Controllers.controller "EditPricingController", ["$scope", "$state",
if data? if data?
Price.update({ id: price.id }, { price: { amount: data } }).$promise Price.update({ id: price.id }, { price: { amount: data } }).$promise
else else
_t('please_specify_a_number') _t('pricing.please_specify_a_number')
## ##
# Delete the specified subcription plan # Delete the specified subcription plan
@ -284,17 +362,17 @@ Application.Controllers.controller "EditPricingController", ["$scope", "$state",
dialogs.confirm dialogs.confirm
resolve: resolve:
object: -> object: ->
title: _t('confirmation_required') title: _t('pricing.confirmation_required')
msg: _t('do_you_really_want_to_delete_this_subscription_plan') msg: _t('pricing.do_you_really_want_to_delete_this_subscription_plan')
, -> , ->
# the admin has confirmed, delete the plan # the admin has confirmed, delete the plan
Plan.delete {id: id}, (res) -> 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) $scope.plans.splice(findItemIdxById(plans, id), 1)
, (error) -> , (error) ->
console.error('[EditPricingController::deletePlan] Error: '+error.statusText) if error.statusText 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 dialogs.confirm
resolve: resolve:
object: -> object: ->
title: _t('confirmation_required') title: _t('pricing.confirmation_required')
msg: _t('do_you_really_want_to_delete_this_coupon') msg: _t('pricing.do_you_really_want_to_delete_this_coupon')
, -> , ->
# the admin has confirmed, delete the coupon # the admin has confirmed, delete the coupon
Coupon.delete {id: id}, (res) -> Coupon.delete {id: id}, (res) ->
@ -335,9 +413,9 @@ Application.Controllers.controller "EditPricingController", ["$scope", "$state",
, (error) -> , (error) ->
console.error('[EditPricingController::deleteCoupon] Error: '+error.statusText) if error.statusText console.error('[EditPricingController::deleteCoupon] Error: '+error.statusText) if error.statusText
if error.status == 422 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 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 ## Callback to validate sending of the coupon
$scope.ok = -> $scope.ok = ->
Coupon.send {coupon_code: coupon.code, user_id: $scope.ctrl.member.id}, (res) -> 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}) $uibModalInstance.close({user_id: $scope.ctrl.member.id})
, (err) -> , (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 ## Callback to close the modal and cancel the sending process
$scope.cancel = -> $scope.cancel = ->

View File

@ -46,6 +46,7 @@ Application.Controllers.controller "SettingsController", ["$scope", 'Setting', '
$scope.trainingInformationMessage = { name: 'training_information_message', value: settingsPromise.training_information_message} $scope.trainingInformationMessage = { name: 'training_information_message', value: settingsPromise.training_information_message}
$scope.subscriptionExplicationsAlert = { name: 'subscription_explications_alert', value: settingsPromise.subscription_explications_alert } $scope.subscriptionExplicationsAlert = { name: 'subscription_explications_alert', value: settingsPromise.subscription_explications_alert }
$scope.eventExplicationsAlert = {name: 'event_explications_alert', value: settingsPromise.event_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.windowStart = { name: 'booking_window_start', value: settingsPromise.booking_window_start }
$scope.windowEnd = { name: 'booking_window_end', value: settingsPromise.booking_window_end } $scope.windowEnd = { name: 'booking_window_end', value: settingsPromise.booking_window_end }
$scope.mainColorSetting = { name: 'main_color', value: settingsPromise.main_color } $scope.mainColorSetting = { name: 'main_color', value: settingsPromise.main_color }
@ -116,7 +117,7 @@ Application.Controllers.controller "SettingsController", ["$scope", 'Setting', '
value = setting.value value = setting.value
Setting.update { name: setting.name }, { value: value }, (data)-> 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)-> , (error)->
console.log(error) console.log(error)
@ -135,7 +136,7 @@ Application.Controllers.controller "SettingsController", ["$scope", 'Setting', '
angular.forEach v, (err)-> angular.forEach v, (err)->
growl.error(err) growl.error(err)
else else
growl.success(_t('file_successfully_updated')) growl.success(_t('settings.file_successfully_updated'))
if content.custom_asset.name is 'cgu-file' if content.custom_asset.name is 'cgu-file'
$scope.cguFile = content.custom_asset $scope.cguFile = content.custom_asset
$scope.methods.cgu = 'put' $scope.methods.cgu = 'put'

View File

@ -144,7 +144,7 @@ Application.Controllers.controller "StatisticsController", ["$scope", "$state",
## ##
# Callback called when the active tab is changed. # Callback called when the active tab is changed.
# recover the current tab and store its value in $scope.selectedIndex # 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.setActiveTab = (tab) ->
$scope.selectedIndex = 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 # Callback to validate the filters and send a new request to elastic
## ##

View File

@ -250,8 +250,9 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
# shorthands # shorthands
$scope.isAuthenticated = Auth.isAuthenticated; $scope.isAuthenticated = Auth.isAuthenticated
$scope.isAuthorized = AuthService.isAuthorized; $scope.isAuthorized = AuthService.isAuthorized
$rootScope.login = $scope.login

View File

@ -4,14 +4,15 @@
# Controller used in the public calendar global # 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', 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) -> ($scope, $state, $aside, moment, Availability, Slot, Setting, growl, dialogs, bookingWindowStart, bookingWindowEnd, _t, uiCalendarConfig, CalendarConfig, trainingsPromise, machinesPromise, spacesPromise) ->
### PRIVATE STATIC CONSTANTS ### ### PRIVATE STATIC CONSTANTS ###
currentMachineEvent = null currentMachineEvent = null
machinesPromise.forEach((m) -> m.checked = true) machinesPromise.forEach((m) -> m.checked = true)
trainingsPromise.forEach((t) -> t.checked = true) trainingsPromise.forEach((t) -> t.checked = true)
spacesPromise.forEach((s) -> s.checked = true)
## check all formation/machine is select in filter ## check all formation/machine is select in filter
isSelectAll = (type, scope) -> isSelectAll = (type, scope) ->
@ -25,6 +26,9 @@ Application.Controllers.controller "CalendarController", ["$scope", "$state", "$
## List of machines ## List of machines
$scope.machines = machinesPromise $scope.machines = machinesPromise
## List of spaces
$scope.spaces = spacesPromise
## add availabilities source to event sources ## add availabilities source to event sources
$scope.eventSources = [] $scope.eventSources = []
@ -34,6 +38,7 @@ Application.Controllers.controller "CalendarController", ["$scope", "$state", "$
scope.filter = $scope.filter = scope.filter = $scope.filter =
trainings: isSelectAll('trainings', scope) trainings: isSelectAll('trainings', scope)
machines: isSelectAll('machines', scope) machines: isSelectAll('machines', scope)
spaces: isSelectAll('spaces', scope)
evt: filter.evt evt: filter.evt
dispo: filter.dispo dispo: filter.dispo
$scope.calendarConfig.events = availabilitySourceUrl() $scope.calendarConfig.events = availabilitySourceUrl()
@ -43,6 +48,7 @@ Application.Controllers.controller "CalendarController", ["$scope", "$state", "$
$scope.filter = $scope.filter =
trainings: isSelectAll('trainings', $scope) trainings: isSelectAll('trainings', $scope)
machines: isSelectAll('machines', $scope) machines: isSelectAll('machines', $scope)
spaces: isSelectAll('spaces', $scope)
evt: true evt: true
dispo: true dispo: true
@ -62,15 +68,18 @@ Application.Controllers.controller "CalendarController", ["$scope", "$state", "$
$scope.trainings $scope.trainings
machines: -> machines: ->
$scope.machines $scope.machines
spaces: ->
$scope.spaces
filter: -> filter: ->
$scope.filter $scope.filter
toggleFilter: -> toggleFilter: ->
$scope.toggleFilter $scope.toggleFilter
filterAvailabilities: -> filterAvailabilities: ->
$scope.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.trainings = trainings
$scope.machines = machines $scope.machines = machines
$scope.spaces = spaces
$scope.filter = filter $scope.filter = filter
$scope.toggleFilter = (type, filter) -> $scope.toggleFilter = (type, filter) ->
@ -94,13 +103,19 @@ Application.Controllers.controller "CalendarController", ["$scope", "$state", "$
currentMachineEvent = event currentMachineEvent = event
calendar.fullCalendar('changeView', 'agendaDay') calendar.fullCalendar('changeView', 'agendaDay')
calendar.fullCalendar('gotoDate', event.start) 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 else
if event.available_type == 'event' if event.machine_id
$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
$state.go('app.public.machines_show', {id: 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 ## agendaDay view: disable slotEventOverlap
## agendaWeek view: enable slotEventOverlap ## agendaWeek view: enable slotEventOverlap
@ -136,7 +151,8 @@ Application.Controllers.controller "CalendarController", ["$scope", "$state", "$
getFilter = -> getFilter = ->
t = $scope.trainings.filter((t) -> t.checked).map((t) -> t.id) t = $scope.trainings.filter((t) -> t.checked).map((t) -> t.id)
m = $scope.machines.filter((m) -> m.checked).map((m) -> m.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 = -> availabilitySourceUrl = ->
"/api/availabilities/public?#{$.param(getFilter())}" "/api/availabilities/public?#{$.param(getFilter())}"

View File

@ -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 # 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 # a prior training, just redirect him to the machine's booking page
if machine.current_user_is_training or machine.trainings.length == 0 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 else
# otherwise, if a user is authenticated ... # otherwise, if a user is authenticated ...
if _this.$scope.isAuthenticated() 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' Application.Controllers.controller "ShowMachineController", ['$scope', '$state', '$uibModal', '$stateParams', '_t', 'Machine', 'growl', 'machinePromise', 'dialogs'
, ($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 $scope.machine = machinePromise
## ##
@ -274,20 +274,20 @@ Application.Controllers.controller "ShowMachineController", ['$scope', '$state',
# This controller workflow is pretty similar to the trainings reservation controller. # 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', Application.Controllers.controller "ReserveMachineController", ["$scope", '$stateParams', '_t', "moment", 'Auth', '$timeout', 'Member', 'Availability', 'plansPromise', 'groupsPromise', 'machinePromise', 'settingsPromise', '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) -> ($scope, $stateParams, _t, moment, Auth, $timeout, Member, Availability, plansPromise, groupsPromise, machinePromise, settingsPromise, uiCalendarConfig, CalendarConfig) ->
### PRIVATE STATIC CONSTANTS ### ### PRIVATE STATIC CONSTANTS ###
# Slot already booked by the current user # Slot free to be booked
FREE_SLOT_BORDER_COLOR = '<%= AvailabilityHelper::MACHINE_COLOR %>' FREE_SLOT_BORDER_COLOR = '<%= AvailabilityHelper::MACHINE_COLOR %>'
# Slot already booked by another user # Slot already booked by another user
UNAVAILABLE_SLOT_BORDER_COLOR = '<%= AvailabilityHelper::MACHINE_IS_RESERVED_BY_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 %>' 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 ## bind the machine availabilities with full-Calendar events
$scope.eventSources = [] $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 ## indicates the state of the current view : calendar or plans information
$scope.plansAreShown = false $scope.plansAreShown = false
## will store the user's plan if he choosed to buy one ## will store the user's plan if he choosed to buy one
$scope.selectedPlan = null $scope.selectedPlan = null
## array of fullCalendar events. Slots where the user want to book ## the moment when the plan selection changed for the last time, used to trigger changes in the cart
$scope.eventsReserved = [] $scope.planSelectionTime = null
## total amount of the bill to pay ## mapping of fullCalendar events.
$scope.amountTotal = 0 $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 ## the moment when the slot selection changed for the last time, used to trigger changes in the cart
$scope.totalNoCoupon = 0 $scope.selectionTime = null
## Discount coupon to apply to the basket, if any ## the last clicked event in the calender
$scope.coupon = $scope.selectedEvent = null
applied: null
## is the user allowed to change the date of his booking ## the application global settings
$scope.enableBookingMove = true $scope.settings = settingsPromise
## list of plans, classified by group ## list of plans, classified by group
$scope.plansClassifiedByGroup = [] $scope.plansClassifiedByGroup = []
@ -352,86 +350,87 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat
## Global config: message to the end user concerning the subscriptions rules ## Global config: message to the end user concerning the subscriptions rules
$scope.subscriptionExplicationsAlert = settingsPromise.subscription_explications_alert $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 $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") # Change the last selected slot's appearence to looks like 'added to cart'
##
## Global config: delay in hours before a booking while the cancellation is forbidden $scope.markSlotAsAdded = ->
$scope.cancelBookingDelay = parseInt(settingsPromise.booking_cancel_delay) $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 # Change the last selected slot's appearence to looks like 'never added to cart'
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
## ##
$scope.removeSlotToModify = (e) -> $scope.markSlotAsRemoved = (slot) ->
e.preventDefault() slot.backgroundColor = 'white'
if $scope.slotToPlace slot.borderColor = FREE_SLOT_BORDER_COLOR
$scope.slotToPlace.backgroundColor = 'white' slot.title = ''
$scope.slotToPlace.title = '' slot.isValid = false
$scope.slotToPlace = null slot.id = null
$scope.slotToModify.title = if $scope.currentUser.role isnt 'admin' then _t('i_ve_reserved') else _t('not_available') slot.is_reserved = false
$scope.slotToModify.backgroundColor = 'white' slot.can_modify = false
$scope.slotToModify = null slot.offered = false
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents' updateCalendar()
## ##
# When modifying an already booked reservation, cancel the choice of the new slot # Callback when a slot was successfully cancelled. Reset the slot style as 'ready to book'
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
## ##
$scope.removeSlotToPlace = (e)-> $scope.slotCancelled = ->
e.preventDefault() $scope.markSlotAsRemoved($scope.selectedEvent)
$scope.slotToPlace.backgroundColor = 'white'
$scope.slotToPlace.title = ''
$scope.slotToPlace = null
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
## ##
# 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 = -> $scope.modifyMachineSlot = ->
Slot.update {id: $scope.slotToModify.id}, $scope.events.placable.title = if $scope.currentUser.role isnt 'admin' then _t('i_ve_reserved') else _t('not_available')
slot: $scope.events.placable.backgroundColor = 'white'
start_at: $scope.slotToPlace.start $scope.events.placable.borderColor = $scope.events.modifiable.borderColor
end_at: $scope.slotToPlace.end $scope.events.placable.id = $scope.events.modifiable.id
availability_id: $scope.slotToPlace.availability_id $scope.events.placable.is_reserved = true
, -> # success $scope.events.placable.can_modify = true
$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.slotToModify.backgroundColor = 'white' $scope.events.modifiable.backgroundColor = 'white'
$scope.slotToModify.title = '' $scope.events.modifiable.title = ''
$scope.slotToModify.borderColor = FREE_SLOT_BORDER_COLOR $scope.events.modifiable.borderColor = FREE_SLOT_BORDER_COLOR
$scope.slotToModify.id = null $scope.events.modifiable.id = null
$scope.slotToModify.is_reserved = false $scope.events.modifiable.is_reserved = false
$scope.slotToModify.can_modify = false $scope.events.modifiable.can_modify = false
$scope.slotToModify = null
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents' updateCalendar()
, (err) -> # failure
growl.error(_t('unable_to_change_the_reservation'))
console.error(err)
@ -439,14 +438,13 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat
# Cancel the current booking modification, reseting the whole process # Cancel the current booking modification, reseting the whole process
## ##
$scope.cancelModifyMachineSlot = -> $scope.cancelModifyMachineSlot = ->
$scope.slotToPlace.backgroundColor = 'white' if $scope.events.placable
$scope.slotToPlace.title = '' $scope.events.placable.backgroundColor = 'white'
$scope.slotToPlace = null $scope.events.placable.title = ''
$scope.slotToModify.title = if $scope.currentUser.role isnt 'admin' then _t('i_ve_reserved') else _t('not_available') $scope.events.modifiable.title = if $scope.currentUser.role isnt 'admin' then _t('i_ve_reserved') else _t('not_available')
$scope.slotToModify.backgroundColor = 'white' $scope.events.modifiable.backgroundColor = 'white'
$scope.slotToModify = null
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents' updateCalendar()
@ -455,67 +453,10 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat
# reservations. (admins only) # reservations. (admins only)
## ##
$scope.updateMember = -> $scope.updateMember = ->
$scope.paidMachineSlots = null
$scope.plansAreShown = false $scope.plansAreShown = false
$scope.selectedPlan = null $scope.selectedPlan = null
Member.get {id: $scope.ctrl.member.id}, (member) -> Member.get {id: $scope.ctrl.member.id}, (member) ->
$scope.ctrl.member = 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() e.preventDefault()
$scope.plansAreShown = false $scope.plansAreShown = false
$scope.selectPlan($scope.selectedPlan) $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 # 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 # @param plan {Object} the plan to subscribe
## ##
$scope.selectPlan = (plan) -> $scope.selectPlan = (plan) ->
if $scope.isAuthenticated() # toggle selected plan
angular.forEach $scope.eventsReserved, (machineSlot)-> if $scope.selectedPlan != plan
angular.forEach $scope.ctrl.member.machine_credits, (credit)-> $scope.selectedPlan = plan
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()
else else
$scope.login null, -> $scope.selectedPlan = null
$scope.selectedPlan = plan $scope.planSelectionTime = new Date()
updateCartPrice()
## ##
# Checks if $scope.slotToModify and $scope.slotToPlace have tag incompatibilities # Once the reservation is booked (payment process successfully completed), change the event style
# @returns {boolean} true in case of incompatibility # in fullCalendar, update the user's subscription and free-credits if needed
# @param reservation {Object}
## ##
$scope.tagMissmatch = -> $scope.afterPayment = (reservation)->
for tag in $scope.slotToModify.tags angular.forEach $scope.events.reserved, (machineSlot, key) ->
if tag.id not in $scope.slotToPlace.tag_ids machineSlot.is_reserved = true
return true machineSlot.can_modify = true
false 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' if $scope.currentUser.role isnt 'admin'
$scope.ctrl.member = $scope.currentUser $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<Object>} 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<Object>, 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). # if it's too late).
## ##
calendarEventClickCb = (event, jsEvent, view) -> calendarEventClickCb = (event, jsEvent, view) ->
$scope.selectedEvent = event
if !event.is_reserved && !$scope.slotToModify $scope.selectionTime = new Date()
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()
@ -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. # 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 # 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. # Update the calendar's display to render the new attributes of the events
# @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
## ##
findAmountByPlanId = (plansArray, planId)-> updateCalendar = ->
for plan in plansArray uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
return plan.amount if plan.plan_id == planId
return null
##
# 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 ## !!! MUST BE CALLED AT THE END of the controller

View File

@ -48,6 +48,13 @@ Application.Controllers.controller "MainNavController", ["$scope", "$location",
linkIcon: 'credit-card' 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 || [] Fablab.adminNavLinks = Fablab.adminNavLinks || []
adminNavLinks = [ adminNavLinks = [
@ -109,4 +116,11 @@ Application.Controllers.controller "MainNavController", ["$scope", "$location",
].concat(Fablab.adminNavLinks) ].concat(Fablab.adminNavLinks)
$scope.adminNavLinks = adminNavLinks $scope.adminNavLinks = adminNavLinks
unless Fablab.withoutSpaces
$scope.adminNavLinks.splice(7, 0, {
state: 'app.public.spaces_list'
linkText: 'manage_the_spaces'
linkIcon: 'rocket'
})
] ]

View File

@ -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 += "<span class='label label-success text-white' title='#{tag.name}'>#{tag.name}</span>"
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()
]

View File

@ -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). # 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' Application.Controllers.controller "ReserveTrainingController", ["$scope", '$stateParams', 'Auth', '$timeout', 'Availability', 'Member', 'availabilityTrainingsPromise', 'plansPromise', 'groupsPromise', 'settingsPromise', 'trainingPromise', '_t', '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) -> ($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 # Color of the selected event backgound
SELECTED_EVENT_BG_COLOR = '#ffdd00' SELECTED_EVENT_BG_COLOR = '#ffdd00'
# Slot already booked by the current user # Slot free to be booked
FREE_SLOT_BORDER_COLOR = '<%= AvailabilityHelper::TRAINING_COLOR %>' 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 groupObj.plans.push(plan) if plan.group_id == group.id
$scope.plansClassifiedByGroup.push(groupObj) $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 ## indicates the state of the current view : calendar or plans information
$scope.plansAreShown = false $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 ## will store the user's plan if he choosed to buy one
$scope.selectedPlan = null $scope.selectedPlan = null
## fullCalendar event. Training slot that the user want to book ## the moment when the plan selection changed for the last time, used to trigger changes in the cart
$scope.selectedTraining = null $scope.planSelectionTime = null
## fullCalendar event. An already booked slot that the user want to modify ## Selected training
$scope.slotToModify = null
## Once a training reservation was modified, will contains {newReservedSlot:{}, oldReservedSlot:{}}
$scope.modifiedSlots = null
## Selected training unless 'all' trainings are displayed
$scope.training = trainingPromise $scope.training = trainingPromise
## Discount coupon to apply to the basket, if any ## 'all' OR training's slug
$scope.coupon = $scope.mode = $stateParams.id
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
## fullCalendar (v2) configuration ## fullCalendar (v2) configuration
$scope.calendarConfig = CalendarConfig $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')) maxTime: moment.duration(moment(settingsPromise.booking_window_end).format('HH:mm:ss'))
eventClick: (event, jsEvent, view) -> eventClick: (event, jsEvent, view) ->
calendarEventClickCb(event, jsEvent, view) calendarEventClickCb(event, jsEvent, view)
eventAfterAllRender: (view)->
$scope.events = uiCalendarConfig.calendars.calendar.fullCalendar 'clientEvents'
eventRender: (event, element, view) -> eventRender: (event, element, view) ->
eventRenderCb(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 $scope.subscriptionExplicationsAlert = settingsPromise.subscription_explications_alert
## Global config: message to the end user concerning the training reservation
$scope.trainingExplicationsAlert = settingsPromise.training_explications_alert $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.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 if $scope.ctrl.member
Member.get {id: $scope.ctrl.member.id}, (member) -> Member.get {id: $scope.ctrl.member.id}, (member) ->
$scope.ctrl.member = 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' uiCalendarConfig.calendars.calendar.fullCalendar 'removeEvents'
$scope.eventSources.push $scope.eventSources.splice(0, 1,
events: trainings events: trainings
textColor: 'black' textColor: 'black'
$scope.trainingIsValid = false )
$scope.paidTraining = null # as the events are re-fetched for the new user, we must re-init the cart
$scope.plansAreShown = false $scope.events.reserved = []
$scope.selectedPlan = null $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.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 # @param plan {Object} the plan to subscribe
## ##
$scope.selectPlan = (plan) -> $scope.selectPlan = (plan) ->
if $scope.isAuthenticated() # toggle selected plan
if $scope.selectedPlan != plan if $scope.selectedPlan != plan
$scope.selectedPlan = plan $scope.selectedPlan = plan
$scope.updatePrices()
else
$scope.selectedPlan = null
$scope.updatePrices()
else else
$scope.login null, -> $scope.selectedPlan = null
$scope.selectedPlan = plan $scope.planSelectionTime = new Date()
$scope.updatePrices()
@ -264,7 +298,9 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
e.preventDefault() e.preventDefault()
$scope.plansAreShown = false $scope.plansAreShown = false
$scope.selectedPlan = null $scope.selectedPlan = null
$scope.updatePrices() $scope.planSelectionTime = new Date()
## ##
# Switch the user's view from the reservation agenda to the plan subscription # 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.showPlans = ->
$scope.plansAreShown = true $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 # Once the reservation is booked (payment process successfully completed), change the event style
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event- # in fullCalendar, update the user's subscription and free-credits if needed
# @param reservation {Object}
## ##
$scope.removeSlotToPlace = (e)-> $scope.afterPayment = (reservation)->
e.preventDefault() $scope.events.paid[0].backgroundColor = 'white'
$scope.slotToPlace.backgroundColor = 'white' $scope.events.paid[0].is_reserved = true
$scope.slotToPlace.title = $scope.slotToPlace.training.name $scope.events.paid[0].can_modify = true
$scope.slotToPlace = null updateTrainingSlotId($scope.events.paid[0], reservation)
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents' $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)
## refetchCalendar()
# 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
@ -375,51 +348,6 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
Member.get id: $scope.currentUser.id, (member) -> Member.get id: $scope.currentUser.id, (member) ->
$scope.ctrl.member = 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<Object>, 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/ # @see http://fullcalendar.io/docs/mouse/eventClick/
## ##
calendarEventClickCb = (event, jsEvent, view) -> calendarEventClickCb = (event, jsEvent, view) ->
if $scope.ctrl.member $scope.selectedEvent = event
# reserve a training if this training will not be reserved and is not about to move and not is completed if $stateParams.id is 'all'
if !event.is_reserved && !$scope.slotToModify && !event.is_completed $scope.training = event.training
if event != $scope.selectedTraining $scope.selectionTime = new Date()
$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
## ##
# 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/ # @see http://fullcalendar.io/docs/event_rendering/eventRender/
## ##
eventRenderCb = (event, element, view)-> eventRenderCb = (event, element, view)->
# Comment these codes for show a popup of description, because we add feature page of training if $scope.currentUser.role is 'admin' and event.tags.length > 0
#element.attr( html = ''
# 'uib-popover': $filter('humanize')($filter('simpleText')(event.training.description), 70) for tag in event.tags
# 'popover-trigger': 'mouseenter' html += "<span class='label label-success text-white' title='#{tag.name}'>#{tag.name}</span>"
#) element.find('.fc-time').append(html)
#$compile(element)($scope) return
##
# 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
@ -744,11 +390,29 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
updateTrainingSlotId = (slot, reservation)-> updateTrainingSlotId = (slot, reservation)->
angular.forEach reservation.slots, (s)-> angular.forEach reservation.slots, (s)->
if slot.start_at == slot.start_at 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() initialize()
] ]

View File

@ -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<Object>} 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<Object>, 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()
}
]

View File

@ -373,7 +373,7 @@ angular.module('application.router', ['ui.router']).
translations: [ 'Translations', (Translations) -> translations: [ 'Translations', (Translations) ->
Translations.query(['app.logged.machines_reserve', 'app.shared.plan_subscribe', 'app.shared.member_select', 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.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', .state 'app.admin.machines_edit',
url: '/machines/:id/edit' url: '/machines/:id/edit'
@ -388,6 +388,97 @@ angular.module('application.router', ['ui.router']).
translations: [ 'Translations', (Translations) -> translations: [ 'Translations', (Translations) ->
Translations.query(['app.admin.machines_edit', 'app.shared.machine']).$promise 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 # trainings
.state 'app.public.trainings_list', .state 'app.public.trainings_list',
url: '/trainings' url: '/trainings'
@ -451,7 +542,7 @@ angular.module('application.router', ['ui.router']).
translations: [ 'Translations', (Translations) -> translations: [ 'Translations', (Translations) ->
Translations.query(['app.logged.trainings_reserve', 'app.shared.plan_subscribe', 'app.shared.member_select', 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.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 # notifications
.state 'app.logged.notifications', .state 'app.logged.notifications',
@ -549,6 +640,9 @@ angular.module('application.router', ['ui.router']).
machinesPromise: ['Machine', (Machine)-> machinesPromise: ['Machine', (Machine)->
Machine.query().$promise Machine.query().$promise
] ]
spacesPromise: ['Space', (Space) ->
Space.query().$promise
]
translations: [ 'Translations', (Translations) -> translations: [ 'Translations', (Translations) ->
Translations.query(['app.public.calendar']).$promise Translations.query(['app.public.calendar']).$promise
] ]
@ -770,6 +864,15 @@ angular.module('application.router', ['ui.router']).
couponsPromise: ['Coupon', (Coupon) -> couponsPromise: ['Coupon', (Coupon) ->
Coupon.query().$promise 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 # plans
.state 'app.admin.plans', .state 'app.admin.plans',
@ -778,15 +881,9 @@ angular.module('application.router', ['ui.router']).
prices: ['Pricing', (Pricing) -> prices: ['Pricing', (Pricing) ->
Pricing.query().$promise Pricing.query().$promise
] ]
machines: ['Machine', (Machine) ->
Machine.query().$promise
]
groups: ['Group', (Group) -> groups: ['Group', (Group) ->
Group.query().$promise Group.query().$promise
] ]
plans: ['Plan', (Plan) ->
Plan.query().$promise
]
partners: ['User', (User) -> partners: ['User', (User) ->
User.query({role: 'partner'}).$promise User.query({role: 'partner'}).$promise
] ]
@ -807,6 +904,15 @@ angular.module('application.router', ['ui.router']).
templateUrl: '<%= asset_path "admin/plans/edit.html" %>' templateUrl: '<%= asset_path "admin/plans/edit.html" %>'
controller: 'EditPlanController' controller: 'EditPlanController'
resolve: resolve:
spaces: ['Space', (Space) ->
Space.query().$promise
]
machines: ['Machine', (Machine) ->
Machine.query().$promise
]
plans: ['Plan', (Plan) ->
Plan.query().$promise
]
planPromise: ['Plan', '$stateParams', (Plan, $stateParams) -> planPromise: ['Plan', '$stateParams', (Plan, $stateParams) ->
Plan.get({id: $stateParams.id}).$promise Plan.get({id: $stateParams.id}).$promise
] ]
@ -1038,6 +1144,7 @@ angular.module('application.router', ['ui.router']).
'training_information_message', 'training_information_message',
'subscription_explications_alert', 'subscription_explications_alert',
'event_explications_alert', 'event_explications_alert',
'space_explications_alert',
'booking_window_start', 'booking_window_start',
'booking_window_end', 'booking_window_end',
'booking_move_enable', 'booking_move_enable',

View File

@ -17,6 +17,11 @@ Application.Services.factory 'Availability', ["$resource", ($resource)->
url: '/api/availabilities/trainings/:trainingId' url: '/api/availabilities/trainings/:trainingId'
params: {trainingId: "@trainingId"} params: {trainingId: "@trainingId"}
isArray: true isArray: true
spaces:
method: 'GET'
url: '/api/availabilities/spaces/:spaceId'
params: {spaceId: "@spaceId"}
isArray: true
update: update:
method: 'PUT' method: 'PUT'
] ]

View File

@ -0,0 +1,8 @@
'use strict'
Application.Services.factory 'Space', ["$resource", ($resource)->
$resource "/api/spaces/:id",
{id: "@id"},
update:
method: 'PUT'
]

View File

@ -5,6 +5,7 @@
//.bg-yellow { background-color: $yellow !important; } //.bg-yellow { background-color: $yellow !important; }
.bg-token { background-color: rgba(230, 208, 137, 0.49); } .bg-token { background-color: rgba(230, 208, 137, 0.49); }
.bg-machine { background-color: $beige; } .bg-machine { background-color: $beige; }
.bg-space { background-color: $cyan }
.bg-formation { background-color: $violet; } .bg-formation { background-color: $violet; }
.bg-event { background-color: $japonica; } .bg-event { background-color: $japonica; }
.bg-atelier { background-color: $blue; } .bg-atelier { background-color: $blue; }
@ -39,4 +40,5 @@
.text-purple { color: $violet !important; } .text-purple { color: $violet !important; }
.text-japonica { color: $japonica !important; } .text-japonica { color: $japonica !important; }
.text-beige { color: $beige !important; } .text-beige { color: $beige !important; }
.text-cyan { color: $cyan !important; }
.text-green, .green { color: #79C84A !important; } .text-green, .green { color: #79C84A !important; }

View File

@ -176,6 +176,7 @@ p, .widget p {
.r-n { border-radius: 0 0 0 0; } .r-n { border-radius: 0 0 0 0; }
.p-xs { padding: 5px;} .p-xs { padding: 5px;}
.p-s { padding: 10px;}
.p-lg { padding: 30px; } .p-lg { padding: 30px; }
.p-l { padding: 16px; } .p-l { padding: 16px; }

View File

@ -43,6 +43,7 @@ $blue: $brand-info;
$green: $brand-success; $green: $brand-success;
$beige: #e4cd78; $beige: #e4cd78;
$violet: #bd7ae9; $violet: #bd7ae9;
$cyan: #3fc7ff;
$japonica: #dd7e6b; $japonica: #dd7e6b;
$border-color: #dddddd; $border-color: #dddddd;
@ -767,7 +768,7 @@ $panel-footer-padding: $panel-heading-padding !default;
$panel-border-radius: $border-radius-large !default; $panel-border-radius: $border-radius-large !default;
// add sleede // add sleede
$panel-border: $border-color !default; $panel-border: $border-color !default;
$panel-heading-bg: #fff !default; $panel-heading-bg: #fff !default;
$panel-footer-bg: #fff !default; $panel-footer-bg: #fff !default;

View File

@ -7,14 +7,15 @@
</div> </div>
<div class="col-xs-10 col-sm-10 col-md-8 b-l b-r-md"> <div class="col-xs-10 col-sm-10 col-md-8 b-l b-r-md">
<section class="heading-title"> <section class="heading-title">
<h1 translate>{{ 'calendar_management' }}</h1> <h1 translate>{{ 'admin_calendar.calendar_management' }}</h1>
</section> </section>
</div> </div>
<div class="col-xs-12 col-sm-12 col-md-3 b-t hide-b-md"> <div class="col-xs-12 col-sm-12 col-md-3 b-t hide-b-md">
<section class="heading-actions wrapper"> <section class="heading-actions wrapper" ng-class="{'p-s': !fablabWithoutSpaces}">
<span class="badge text-sm bg-formation m-t-sm" translate>{{ 'trainings' }}</span><br> <span class="badge text-sm bg-formation" ng-class="{'m-t-sm': fablabWithoutSpaces}" translate>{{ 'admin_calendar.trainings' }}</span><br>
<span class="badge text-sm bg-machine" translate>{{ 'machines' }}</span> <span class="badge text-sm bg-machine" translate>{{ 'admin_calendar.machines' }}</span><br>
<span class="badge text-sm bg-space" ng-hide="fablabWithoutSpaces" translate>{{ 'admin_calendar.spaces' }}</span>
</section> </section>
</div> </div>
@ -31,7 +32,7 @@
<div class="col-sm-12 col-md-12 col-lg-3"> <div class="col-sm-12 col-md-12 col-lg-3">
<div class="widget panel b-a m m-t-lg"> <div class="widget panel b-a m m-t-lg">
<div class="panel-heading b-b small"> <div class="panel-heading b-b small">
<h3 translate>{{ 'ongoing_reservations' }}</h3> <h3 translate>{{ 'admin_calendar.ongoing_reservations' }}</h3>
</div> </div>
<div class="widget-content no-bg auto wrapper"> <div class="widget-content no-bg auto wrapper">
<ul class="list-unstyled" ng-if="reservations.length > 0"> <ul class="list-unstyled" ng-if="reservations.length > 0">
@ -42,7 +43,7 @@
<span class="btn btn-warning btn-xs" ng-click="cancelBooking(r)" ng-if="!r.canceled_at"><i class="fa fa-times red"></i></span> <span class="btn btn-warning btn-xs" ng-click="cancelBooking(r)" ng-if="!r.canceled_at"><i class="fa fa-times red"></i></span>
</li> </li>
</ul> </ul>
<div ng-if="reservations.length == 0" translate>{{ 'no_reservations' }}</div> <div ng-if="reservations.length == 0" translate>{{ 'admin_calendar.no_reservations' }}</div>
</div> </div>
</div> </div>
</div> </div>
@ -50,7 +51,7 @@
<div class="col-sm-12 col-md-12 col-lg-3" ng-if="availability.machine_ids.length > 0"> <div class="col-sm-12 col-md-12 col-lg-3" ng-if="availability.machine_ids.length > 0">
<div class="widget panel b-a m m-t-lg"> <div class="widget panel b-a m m-t-lg">
<div class="panel-heading b-b small"> <div class="panel-heading b-b small">
<h3 translate>{{ 'machines' }}</h3> <h3 translate>{{ 'admin_calendar.machines' }}</h3>
</div> </div>
<div class="widget-content no-bg auto wrapper"> <div class="widget-content no-bg auto wrapper">
<ul class="list-unstyled"> <ul class="list-unstyled">

View File

@ -1,22 +1,41 @@
<div class="modal-header"> <div class="modal-header">
<h3 class="text-center red"> <h3 class="text-center red">
{{ 'DATE_slot' | translate:{DATE:(start | amDateFormat: 'LL')} }} {{start | amDateFormat:'LT'}} - {{end | amDateFormat:'LT'}} {{ 'admin_calendar.DATE_slot' | translate:{DATE:(start | amDateFormat: 'LL')} }} {{start | amDateFormat:'LT'}} - {{end | amDateFormat:'LT'}}
</h3> </h3>
</div> </div>
<div class="modal-body"> <div class="modal-body" ng-show="step === 1">
<p class="text-center font-sbold" translate>{{ 'you_can_define_a_training_on_that_slot' }}</p> <label class="m-t-sm" translate>{{ 'admin_calendar.what_kind_of_slot_do_you_want_to_create' }}</label>
<div> <div class="form-group">
<label class="checkbox-inline"> <div class="radio">
<input type="checkbox" ng-model="available_type" ng-change="changeAvailableType()"> {{ 'link_a_training' | translate }} <label>
</label> <input type="radio" id="training" name="available_type" value="training" ng-model="availability.available_type">
<span translate>{{ 'admin_calendar.training' }}</span>
</label>
</div>
<div class="radio">
<label>
<input type="radio" id="machine" name="available_type" value="machines" ng-model="availability.available_type">
<span translate>{{ 'admin_calendar.machine' }}</span>
</label>
</div>
<div class="radio" ng-hide="fablabWithoutSpaces">
<label>
<input type="radio" id="space" name="available_type" value="space" ng-model="availability.available_type" ng-disabled="spaces.length === 0">
<span translate>{{ 'admin_calendar.space' }}</span>
</label>
</div>
</div> </div>
</div>
<div class="modal-body" ng-show="step === 2">
<p class="text-center font-sbold m-t" ng-show="availability.available_type == 'machines'"><span class="underline" translate>{{ 'or_' }}</span> {{ '_select_some_machines' | translate }}</p> <div ng-show="availability.available_type == 'machines'">
<p class="text-center font-sbold m-t-sm">{{ 'admin_calendar.select_some_machines' | translate }}</p>
<div class="checkbox" ng-show="availability.available_type == 'machines'"> <div class="form-group m-l-xl">
<label class="checkbox" ng-repeat="machine in machines"> <label class="checkbox" ng-repeat="machine in machines">
<input type="checkbox" ng-click="toggleSelection(machine)"> {{machine.name}} <span class="text-xs">(id {{machine.id}})</span> <input type="checkbox" ng-click="toggleSelection(machine)"> {{machine.name}} <span class="text-xs">(id {{machine.id}})</span>
</label> </label>
</div>
</div> </div>
<div ng-show="availability.available_type == 'training'"> <div ng-show="availability.available_type == 'training'">
@ -24,27 +43,42 @@
</select> </select>
<div class="row m-t"> <div class="row m-t">
<div class="form-group"> <div class="form-group">
<label class="col-sm-6 control-label" translate>{{ 'number_of_tickets' }}</label> <label class="col-sm-6 control-label" for="nb_places_training" translate>{{ 'admin_calendar.number_of_tickets' }}</label>
<div class="col-sm-6"> <div class="col-sm-6">
<input type="number" class="form-control" ng-model="availability.nb_total_places"> <input type="number" id="nb_places_training" class="form-control" ng-model="availability.nb_total_places">
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div id="timeAdjust" class="m-t-lg">
<p class="text-center font-sbold" translate>{{ 'adjust_the_opening_hours' }}</p> <div ng-show="availability.available_type == 'space'">
<select ng-model="selectedSpace" class="form-control m-t-sm" ng-options="t.name for t in spaces" ng-change="setNbTotalPlaces()">
</select>
<div class="row m-t">
<div class="form-group">
<label class="col-sm-6 control-label" for="nb_places_space" translate>{{ 'admin_calendar.number_of_tickets' }}</label>
<div class="col-sm-6">
<input type="number" id="nb_places_space" class="form-control" ng-model="availability.nb_total_places">
</div>
</div>
</div>
</div>
</div>
<div class="modal-body" ng-show="step === 3">
<div id="timeAdjust" class="m-t-sm">
<p class="text-center font-sbold" translate>{{ 'admin_calendar.adjust_the_opening_hours' }}</p>
<div class="row"> <div class="row">
<div class="col-md-3 col-md-offset-2"> <div class="col-md-3 col-md-offset-2">
<uib-timepicker ng-model="start" hour-step="timepickers.start.hstep" readonly-input="true" minute-step="timepickers.start.mstep" show-meridian="false"></uib-timepicker> <uib-timepicker ng-model="start" hour-step="timepickers.start.hstep" readonly-input="true" minute-step="timepickers.start.mstep" show-meridian="false"></uib-timepicker>
</div> </div>
<span class="col-md-1 m-t-xl m-l" translate>{{ 'to_time' }}</span> <span class="col-md-1 m-t-xl m-l" translate>{{ 'admin_calendar.to_time' }}</span>
<fieldset ng-disabled="endDateReadOnly" class="col-md-5"> <fieldset ng-disabled="endDateReadOnly" class="col-md-5">
<uib-timepicker ng-model="end" hour-step="timepickers.end.hstep" readonly-input="true" minute-step="timepickers.end.mstep" show-meridian="false"></uib-timepicker> <uib-timepicker ng-model="end" hour-step="timepickers.end.hstep" readonly-input="true" minute-step="timepickers.end.mstep" show-meridian="false"></uib-timepicker>
</fieldset> </fieldset>
</div> </div>
</div> </div>
<div id="tagAssociate" class="m-t-lg"> <div id="tagAssociate" class="m-t-lg">
<p class="text-center font-sbold" translate>{{ 'restrict_this_slot_with_labels_(optional)' }}</p> <p class="text-center font-sbold" translate>{{ 'admin_calendar.restrict_this_slot_with_labels_(optional)' }}</p>
<div class="row"> <div class="row">
<div class="col-sm-12"> <div class="col-sm-12">
<ui-select multiple ng-model="availability.tag_ids" class="form-control"> <ui-select multiple ng-model="availability.tag_ids" class="form-control">
@ -59,7 +93,13 @@
</div> </div>
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer" ng-show="step < 3">
<button class="btn btn-info" ng-click="previous()" ng-disabled="step === 1" translate>{{ 'admin_calendar.previous' }}</button>
<button class="btn btn-info" ng-click="next()" translate>{{ 'admin_calendar.next' }}</button>
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'cancel' }}</button>
</div>
<div class="modal-footer" ng-show="step === 3">
<button class="btn btn-info" ng-click="previous()" translate>{{ 'admin_calendar.previous' }}</button>
<button class="btn btn-warning" ng-click="ok()" translate>{{ 'confirm' }}</button> <button class="btn btn-warning" ng-click="ok()" translate>{{ 'confirm' }}</button>
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'cancel' }}</button> <button class="btn btn-default" ng-click="cancel()" translate>{{ 'cancel' }}</button>
</div> </div>

View File

@ -1,19 +1,19 @@
<h2 translate>{{ 'general_information' }}</h2> <h2 translate>{{ 'plan_form.general_information' }}</h2>
<input type="hidden" name="_method" value="{{method}}"> <input type="hidden" name="_method" value="{{method}}">
<div class="form-group" ng-class="{'has-error': planForm['plan[base_name]'].$dirty && planForm['plan[base_name]'].$invalid}"> <div class="form-group" ng-class="{'has-error': planForm['plan[base_name]'].$dirty && planForm['plan[base_name]'].$invalid}">
<label for="plan[base_name]">{{ 'name' | translate }} *</label> <label for="plan[base_name]">{{ 'plan_form.name' | translate }} *</label>
<input type="text" id="plan[base_name]" <input type="text" id="plan[base_name]"
name="plan[base_name]" name="plan[base_name]"
class="form-control" class="form-control"
ng-maxlength="24" ng-maxlength="24"
ng-model="plan.base_name" ng-model="plan.base_name"
required="required"/> required="required"/>
<span class="help-block error" ng-show="planForm['plan[base_name]'].$dirty && planForm['plan[base_name]'].$error.required" translate>{{ 'name_is_required' }}</span> <span class="help-block error" ng-show="planForm['plan[base_name]'].$dirty && planForm['plan[base_name]'].$error.required" translate>{{ 'plan_form.name_is_required' }}</span>
<span class="help-block error" ng-show="planForm['plan[base_name]'].$dirty && planForm['plan[base_name]'].$error.maxlength" translate>{{ 'name_length_must_be_less_than_24_characters' }}</span> <span class="help-block error" ng-show="planForm['plan[base_name]'].$dirty && planForm['plan[base_name]'].$error.maxlength" translate>{{ 'plan_form.name_length_must_be_less_than_24_characters' }}</span>
</div> </div>
<div class="form-group" ng-class="{'has-error': planForm['plan[type]'].$dirty && planForm['plan[type]'].$invalid}"> <div class="form-group" ng-class="{'has-error': planForm['plan[type]'].$dirty && planForm['plan[type]'].$invalid}">
<label for="plan[type]">{{ 'type' | translate }} *</label> <label for="plan[type]">{{ 'plan_form.type' | translate }} *</label>
<select id="plan[type]" <select id="plan[type]"
name="plan[type]" name="plan[type]"
class="form-control" class="form-control"
@ -23,40 +23,40 @@
<option value="Plan" ng-selected="plan.type == 'Plan'" translate>{{ 'standard' }}</option> <option value="Plan" ng-selected="plan.type == 'Plan'" translate>{{ 'standard' }}</option>
<option value="PartnerPlan" ng-selected="plan.type == 'PartnerPlan'" translate>{{ 'partner' }}</option> <option value="PartnerPlan" ng-selected="plan.type == 'PartnerPlan'" translate>{{ 'partner' }}</option>
</select> </select>
<span class="help-block error" ng-show="planForm['plan[type]'].$dirty && planForm['plan[type]'].$error.required" translate>{{ 'type_is_required' }}</span> <span class="help-block error" ng-show="planForm['plan[type]'].$dirty && planForm['plan[type]'].$error.required" translate>{{ 'plan_form.type_is_required' }}</span>
</div> </div>
<div class="form-group" ng-class="{'has-error': planForm['plan[group_id]'].$dirty && planForm['plan[group_id]'].$invalid}"> <div class="form-group" ng-class="{'has-error': planForm['plan[group_id]'].$dirty && planForm['plan[group_id]'].$invalid}">
<label for="plan[group_id]">{{ 'group' | translate }} *</label> <label for="plan[group_id]">{{ 'plan_form.group' | translate }} *</label>
<select id="plan[group_id]" <select id="plan[group_id]"
name="plan[group_id]" name="plan[group_id]"
class="form-control" class="form-control"
ng-model="plan.group_id" ng-model="plan.group_id"
required="required" required="required"
ng-disabled="method == 'PATCH'"> ng-disabled="method == 'PATCH'">
<option value="all" translate>{{ 'transversal_(all_groups)' }}</option> <option value="all" translate>{{ 'plan_form.transversal_(all_groups)' }}</option>
<optgroup label="Groupes"> <optgroup label="Groupes">
<option ng-repeat="group in groups" ng-value="group.id" ng-selected="plan.group_id == group.id">{{group.name}}</option> <option ng-repeat="group in groups" ng-value="group.id" ng-selected="plan.group_id == group.id">{{group.name}}</option>
</optgroup> </optgroup>
</select> </select>
<span class="help-block" ng-show="planForm['plan[group_id]'].$dirty && planForm['plan[group_id]'].$error.required" translate>{{ 'group_is_required' }}</span> <span class="help-block" ng-show="planForm['plan[group_id]'].$dirty && planForm['plan[group_id]'].$error.required" translate>{{ 'plan_form.group_is_required' }}</span>
</div> </div>
<div class="form-group" ng-class="{'has-error': planForm['plan[interval]'].$dirty && planForm['plan[interval]'].$invalid}"> <div class="form-group" ng-class="{'has-error': planForm['plan[interval]'].$dirty && planForm['plan[interval]'].$invalid}">
<label for="plan[interval]">{{ 'period' | translate }} *</label> <label for="plan[interval]">{{ 'plan_form.period' | translate }} *</label>
<select id="plan[interval]" <select id="plan[interval]"
name="plan[interval]" name="plan[interval]"
class="form-control" class="form-control"
ng-model="plan.interval" ng-model="plan.interval"
ng-disabled="method == 'PATCH'" ng-disabled="method == 'PATCH'"
required="required"> required="required">
<option value="month" ng-selected="plan.interval == 'month'" translate>{{ 'month' }}</option> <option value="month" ng-selected="plan.interval == 'month'" translate>{{ 'plan_form.month' }}</option>
<option value="year" ng-selected="plan.interval == 'year'" translate>{{ 'year' }}</option> <option value="year" ng-selected="plan.interval == 'year'" translate>{{ 'plan_form.year' }}</option>
</select> </select>
<span class="help-block" ng-show="planForm['plan[interval]'].$dirty && planForm['plan[interval]'].$error.required" translate>{{ 'period_is_required' }}</span> <span class="help-block" ng-show="planForm['plan[interval]'].$dirty && planForm['plan[interval]'].$error.required" translate>{{ 'plan_form.period_is_required' }}</span>
</div> </div>
<div class="form-group" ng-class="{'has-error': planForm['plan[interval_count]'].$dirty && planForm['plan[interval_count]'].$invalid}"> <div class="form-group" ng-class="{'has-error': planForm['plan[interval_count]'].$dirty && planForm['plan[interval_count]'].$invalid}">
<label for="plan[interval]">{{ 'number_of_periods' | translate }} *</label> <label for="plan[interval]">{{ 'plan_form.number_of_periods' | translate }} *</label>
<input id="plan[interval_count]" <input id="plan[interval_count]"
name="plan[interval_count]" name="plan[interval_count]"
class="form-control" class="form-control"
@ -65,12 +65,12 @@
ng-disabled="method == 'PATCH'" ng-disabled="method == 'PATCH'"
required="required" required="required"
min="1"/> min="1"/>
<span class="help-block" ng-show="planForm['plan[interval_count]'].$dirty && planForm['plan[interval_count]'].$error.required" translate>{{ 'number_of_periods_is_required' }}</span> <span class="help-block" ng-show="planForm['plan[interval_count]'].$dirty && planForm['plan[interval_count]'].$error.required" translate>{{ 'plan_form.number_of_periods_is_required' }}</span>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="input-group" ng-class="{'has-error': planForm['plan[amount]'].$dirty && planForm['plan[amount]'].$invalid}"> <div class="input-group" ng-class="{'has-error': planForm['plan[amount]'].$dirty && planForm['plan[amount]'].$invalid}">
<label for="plan[amount]">{{ 'subscription_price' | translate }} *</label> <label for="plan[amount]">{{ 'plan_form.subscription_price' | translate }} *</label>
<div class="input-group"> <div class="input-group">
<span class="input-group-addon">{{currencySymbol}}</span> <span class="input-group-addon">{{currencySymbol}}</span>
<input id="plan[amount]" <input id="plan[amount]"
@ -80,24 +80,24 @@
ng-required="true" ng-required="true"
ng-model="plan.amount"/> ng-model="plan.amount"/>
</div> </div>
<span class="help-block" ng-show="planForm['plan[amount]'].$dirty && planForm['plan[amount]'].$error.required" translate>{{ 'price_is_required' }}</span> <span class="help-block" ng-show="planForm['plan[amount]'].$dirty && planForm['plan[amount]'].$error.required" translate>{{ 'plan_form.price_is_required' }}</span>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label translate>{{ 'visual_prominence_of_the_subscription' }}</label> <label translate>{{ 'plan_form.visual_prominence_of_the_subscription' }}</label>
<input ng-model="plan.ui_weight" <input ng-model="plan.ui_weight"
type="number" type="number"
name="plan[ui_weight]" name="plan[ui_weight]"
class="form-control"> class="form-control">
<span class="help-block"> <span class="help-block">
{{ 'on_the_subscriptions_page_the_most_prominent_subscriptions_will_be_placed_at_the_top_of_the_list' | translate }} {{ 'plan_form.on_the_subscriptions_page_the_most_prominent_subscriptions_will_be_placed_at_the_top_of_the_list' | translate }}
{{ 'an_evelated_number_means_a_higher_prominence' | translate }} {{ 'plan_form.an_evelated_number_means_a_higher_prominence' | translate }}
</span> </span>
</div> </div>
<div class="input-group m-t-md"> <div class="input-group m-t-md">
<label for="plan[is_rolling]" class="control-label m-r-md">{{ 'rolling_subscription' | translate }} *</label> <label for="plan[is_rolling]" class="control-label m-r-md">{{ 'plan_form.rolling_subscription' | translate }} *</label>
<input bs-switch <input bs-switch
ng-model="plan.isRolling" ng-model="plan.isRolling"
id="plan[is_rolling]" id="plan[is_rolling]"
@ -112,8 +112,8 @@
<span ng-if="method == 'PATCH'">{{ (plan.is_rolling ? 'yes' : 'no') | translate }}</span> <span ng-if="method == 'PATCH'">{{ (plan.is_rolling ? 'yes' : 'no') | translate }}</span>
<input type="hidden" name="plan[is_rolling]" value="{{plan.isRolling}}"/> <input type="hidden" name="plan[is_rolling]" value="{{plan.isRolling}}"/>
<span class="help-block"> <span class="help-block">
{{ 'a_rolling_subscription_will_begin_the_day_of_the_first_training' | translate }} {{ 'plan_form.a_rolling_subscription_will_begin_the_day_of_the_first_training' | translate }}
{{ 'otherwise_it_will_begin_as_soon_as_it_is_bought' | translate }} {{ 'plan_form.otherwise_it_will_begin_as_soon_as_it_is_bought' | translate }}
</span> </span>
</div> </div>
@ -121,12 +121,12 @@
<!-- PDF description attachement --> <!-- PDF description attachement -->
<input type="hidden" ng-model="plan.plan_file_attributes.id" name="plan[plan_file_attributes][id]" ng-value="plan.plan_file_attributes.id" /> <input type="hidden" ng-model="plan.plan_file_attributes.id" name="plan[plan_file_attributes][id]" ng-value="plan.plan_file_attributes.id" />
<input type="hidden" ng-model="plan.plan_file_attributes._destroy" name="plan[plan_file_attributes][_destroy]" ng-value="plan.plan_file_attributes._destroy"/> <input type="hidden" ng-model="plan.plan_file_attributes._destroy" name="plan[plan_file_attributes][_destroy]" ng-value="plan.plan_file_attributes._destroy"/>
<label class="m-t-md" translate>{{ 'information_sheet' }}</label> <label class="m-t-md" translate>{{ 'plan_form.information_sheet' }}</label>
<div class="fileinput input-group" data-provides="fileinput" ng-class="fileinputClass(plan.plan_file_attributes)"> <div class="fileinput input-group" data-provides="fileinput" ng-class="fileinputClass(plan.plan_file_attributes)">
<div class="form-control" data-trigger="fileinput"> <div class="form-control" data-trigger="fileinput">
<i class="glyphicon glyphicon-file fileinput-exists"></i> <span class="fileinput-filename">{{file.attachment || plan.plan_file_attributes.attachment_identifier}}</span> <i class="glyphicon glyphicon-file fileinput-exists"></i> <span class="fileinput-filename">{{file.attachment || plan.plan_file_attributes.attachment_identifier}}</span>
</div> </div>
<span class="input-group-addon btn btn-default btn-file"><span class="fileinput-new" translate>{{ 'attach_an_information_sheet' }}</span> <span class="input-group-addon btn btn-default btn-file"><span class="fileinput-new" translate>{{ 'plan_form.attach_an_information_sheet' }}</span>
<span class="fileinput-exists" translate>{{ 'change' }}</span><input type="file" <span class="fileinput-exists" translate>{{ 'change' }}</span><input type="file"
name="plan[plan_file_attributes][attachment]" name="plan[plan_file_attributes][attachment]"
accept="image/*, application/pdf"></span> accept="image/*, application/pdf"></span>
@ -135,7 +135,7 @@
<div class="form-group m-t-md" ng-show="plan.type == 'PartnerPlan' && method != 'PATCH'"> <div class="form-group m-t-md" ng-show="plan.type == 'PartnerPlan' && method != 'PATCH'">
<input type="hidden" ng-model="plan.partnerId" name="plan[partner_id]" ng-value="plan.partnerId" /> <input type="hidden" ng-model="plan.partnerId" name="plan[partner_id]" ng-value="plan.partnerId" />
<label for="plan[partner_id]">{{ 'notified_partner' | translate }} *</label> <label for="plan[partner_id]">{{ 'plan_form.notified_partner' | translate }} *</label>
<div class="input-group"> <div class="input-group">
<select class="form-control" <select class="form-control"
ng-model="plan.partnerId" ng-model="plan.partnerId"
@ -144,10 +144,10 @@
<option value=""></option> <option value=""></option>
</select> </select>
<span class="input-group-btn"> <span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="openPartnerNewModal()"><i class="fa fa-user-plus"></i> {{ 'new_user' | translate }}</button> <button class="btn btn-default" type="button" ng-click="openPartnerNewModal()"><i class="fa fa-user-plus"></i> {{ 'plan_form.new_user' | translate }}</button>
</span> </span>
</div> </div>
<span class="help-block" translate>{{ 'as_part_of_a_partner_subscription_some_notifications_may_be_sent_to_this_user' }}</span> <span class="help-block" translate>{{ 'plan_form.as_part_of_a_partner_subscription_some_notifications_may_be_sent_to_this_user' }}</span>
</div> </div>
<div class="form-group" ng-show="plan.partners"> <div class="form-group" ng-show="plan.partners">

View File

@ -7,7 +7,7 @@
</div> </div>
<div class="col-xs-10 col-sm-10 col-md-8 b-l"> <div class="col-xs-10 col-sm-10 col-md-8 b-l">
<section class="heading-title"> <section class="heading-title">
<h1>{{ 'subscription_plan' | translate }} {{ plan.base_name }}</h1> <h1>{{ 'edit_plan.subscription_plan' | translate }} {{ plan.base_name }}</h1>
</section> </section>
</div> </div>
@ -30,23 +30,23 @@
<ng-include src="'<%= asset_path 'admin/plans/_form.html' %>'"></ng-include> <ng-include src="'<%= asset_path 'admin/plans/_form.html' %>'"></ng-include>
<h2 class="m-t-xl" translate>{{ 'prices' }}</h2> <h2 class="m-t-xl" translate>{{ 'edit_plan.prices' }}</h2>
<div class="form-group col-md-6 col-lg-offset-6"> <div class="form-group col-md-6 col-lg-offset-6">
<input type="hidden" ng-model="plan.parent" name="plan[parent_id]" ng-value="plan.parent"/> <input type="hidden" ng-model="plan.parent" name="plan[parent_id]" ng-value="plan.parent"/>
<label for="parentPlan" translate>{{ 'copy_prices_from' }}</label> <label for="parentPlan" translate>{{ 'edit_plan.copy_prices_from' }}</label>
<select id="parentPlan" ng-options="plan.id as humanReadablePlanName(plan, groups) for plan in plans" ng-model="plan.parent" ng-change="copyPricesFromPlan()" class="form-control"> <select id="parentPlan" ng-options="plan.id as humanReadablePlanName(plan, groups) for plan in plans" ng-model="plan.parent" ng-change="copyPricesFromPlan()" class="form-control">
<option value=""></option> <option value=""></option>
</select> </select>
</div> </div>
<h3 translate>{{ 'machines' }}</h3> <h3 translate>{{ 'edit_plan.machines' }}</h3>
<table class="table"> <table class="table">
<thead> <thead>
<th translate>{{ 'machine' }}</th> <th translate>{{ 'edit_plan.machine' }}</th>
<th translate>{{ 'hourly_rate' }}</th> <th translate>{{ 'edit_plan.hourly_rate' }}</th>
</thead> </thead>
<tbody> <tbody>
<tr ng-repeat="price in plan.prices"> <tr ng-repeat="price in plan.prices" ng-if="price.priceable_type === 'Machine'">
<td style="width: 60%;">{{ getMachineName(price.priceable_id) }} (id {{ price.priceable_id }}) *</td> <td style="width: 60%;">{{ getMachineName(price.priceable_id) }} (id {{ price.priceable_id }}) *</td>
<td> <td>
<div class="input-group" ng-class="{'has-error': planForm['plan[prices_attributes][][amount]'].$dirty && planForm['plan[prices_attributes][][amount]'].$invalid}"> <div class="input-group" ng-class="{'has-error': planForm['plan[prices_attributes][][amount]'].$dirty && planForm['plan[prices_attributes][][amount]'].$invalid}">
@ -59,6 +59,27 @@
</tbody> </tbody>
</table> </table>
<h3 ng-hide="fablabWithoutSpaces" translate>{{ 'edit_plan.spaces' }}</h3>
<table class="table" ng-hide="fablabWithoutSpaces">
<thead>
<th translate>{{ 'edit_plan.space' }}</th>
<th translate>{{ 'edit_plan.hourly_rate' }}</th>
</thead>
<tbody>
<tr ng-repeat="price in plan.prices" ng-if="price.priceable_type === 'Space'">
<td style="width: 60%;">{{ getSpaceName(price.priceable_id) }} *</td>
<td>
<div class="input-group" ng-class="{'has-error': planForm['plan[prices_attributes][][amount]'].$dirty && planForm['plan[prices_attributes][][amount]'].$invalid}">
<span class="input-group-addon">{{currencySymbol}}</span>
<input type="number" class="form-control" name="plan[prices_attributes][][amount]" ng-value="price.amount" required="required"/>
<input type="hidden" class="form-control" name="plan[prices_attributes][][id]" ng-value="price.id"/>
</div>
</td>
</tr>
</tbody>
</table>
<div class="panel-footer no-padder"> <div class="panel-footer no-padder">
<input type="submit" value="{{ 'confirm_changes' | translate }}" class="r-b btn-valid btn btn-warning btn-block p-lg btn-lg text-u-c" ng-disabled="planForm.$invalid"/> <input type="submit" value="{{ 'confirm_changes' | translate }}" class="r-b btn-valid btn btn-warning btn-block p-lg btn-lg text-u-c" ng-disabled="planForm.$invalid"/>
</div> </div>

View File

@ -7,7 +7,7 @@
</div> </div>
<div class="col-xs-10 col-sm-10 col-md-8 b-l"> <div class="col-xs-10 col-sm-10 col-md-8 b-l">
<section class="heading-title"> <section class="heading-title">
<h1 translate>{{ 'add_a_subscription_plan' }}</h1> <h1 translate>{{ 'new_plan.add_a_subscription_plan' }}</h1>
</section> </section>
</div> </div>

View File

@ -1,13 +1,13 @@
<h2 translate>{{ 'list_of_the_coupons' }}</h2> <h2 translate>{{ 'pricing.list_of_the_coupons' }}</h2>
<button type="button" class="btn btn-warning m-t-lg m-b" ui-sref="app.admin.coupons_new" translate>{{ 'add_a_new_coupon' }}</button> <button type="button" class="btn btn-warning m-t-lg m-b" ui-sref="app.admin.coupons_new" translate>{{ 'pricing.add_a_new_coupon' }}</button>
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
<th translate>{{ 'name' }}</th> <th translate>{{ 'pricing.name' }}</th>
<th translate>{{ 'discount' }}</th> <th translate>{{ 'pricing.discount' }}</th>
<th translate>{{ 'nb_of_usages' }}</th> <th translate>{{ 'pricing.nb_of_usages' }}</th>
<th translate>{{ 'status' }}</th> <th translate>{{ 'pricing.status' }}</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@ -19,7 +19,7 @@
<span ng-show="coupon.type == 'amount_off'">{{coupon.amount_off}} {{currencySymbol}}</span> <span ng-show="coupon.type == 'amount_off'">{{coupon.amount_off}} {{currencySymbol}}</span>
</td> </td>
<td>{{coupon.usages}}</td> <td>{{coupon.usages}}</td>
<td translate>{{coupon.status}}</td> <td translate>{{'pricing.'+coupon.status}}</td>
<td> <td>
<button type="button" class="btn btn-default" ng-click="sendCouponToUser(coupon)"><i class="fa fa-send-o"></i> </button> <button type="button" class="btn btn-default" ng-click="sendCouponToUser(coupon)"><i class="fa fa-send-o"></i> </button>
<button type="button" class="btn btn-default" ui-sref="app.admin.coupons_edit({id:coupon.id})"><i class="fa fa-pencil-square-o"></i></button> <button type="button" class="btn btn-default" ui-sref="app.admin.coupons_edit({id:coupon.id})"><i class="fa fa-pencil-square-o"></i></button>

View File

@ -1,10 +1,10 @@
<h2 class="m-t-lg" translate>{{ 'trainings' }}</h2> <h2 class="m-t-lg" translate>{{ 'pricing.trainings' }}</h2>
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
<th style="width:20%" translate>{{ 'subscription' }}</th> <th style="width:20%" translate>{{ 'pricing.subscription' }}</th>
<th style="width:10%" translate>{{ 'credits' }}</th> <th style="width:10%" translate>{{ 'pricing.credits' }}</th>
<th style="width:50%" translate>{{ 'related_trainings' }}</th> <th style="width:50%" translate>{{ 'pricing.related_trainings' }}</th>
<th style="width:20%"></th> <th style="width:20%"></th>
</tr> </tr>
</thead> </thead>
@ -43,17 +43,17 @@
</tbody> </tbody>
</table> </table>
<h2 class="m-t-lg" translate>{{ 'machines' }}</h2> <h2 class="m-t-lg" translate>{{ 'pricing.machines' }}</h2>
<div class="btn-group m-t-md m-b-md"> <div class="btn-group m-t-md m-b-md">
<button type="button" class="btn btn-warning" ng-click="addMachineCredit($event)" translate>{{ 'add_a_machine_credit' }}</button> <button type="button" class="btn btn-warning" ng-click="addMachineCredit($event)" translate>{{ 'pricing.add_a_machine_credit' }}</button>
</div> </div>
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
<th style="width:20%" translate>{{ 'machine' }}</th> <th style="width:20%" translate>{{ 'pricing.machine' }}</th>
<th style="width:10%" translate>{{ 'hours' }}</th> <th style="width:10%" translate>{{ 'pricing.hours' }}</th>
<th style="width:50%" translate>{{ 'related_subscriptions' }}</th> <th style="width:50%" translate>{{ 'pricing.related_subscriptions' }}</th>
<th style="width:20%"></th> <th style="width:20%"></th>
</tr> </tr>
</thead> </thead>
@ -94,4 +94,56 @@
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table>
<h2 class="m-t-lg" translate>{{ 'pricing.spaces' }}</h2>
<div class="btn-group m-t-md m-b-md">
<button type="button" class="btn btn-warning" ng-click="addSpaceCredit($event)" translate>{{ 'pricing.add_a_space_credit' }}</button>
</div>
<table class="table">
<thead>
<tr>
<th style="width:20%" translate>{{ 'pricing.space' }}</th>
<th style="width:10%" translate>{{ 'pricing.hours' }}</th>
<th style="width:50%" translate>{{ 'pricing.related_subscriptions' }}</th>
<th style="width:20%"></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="sc in spaceCredits">
<td>
<span editable-select="sc.creditable_id" e-name="creditable_id" e-form="rowform" e-ng-options="s.id as s.name for s in spaces" e-required>
{{ showCreditableName(sc) }}
</span>
</td>
<td>
<span editable-number="sc.hours" e-name="hours" e-form="rowform" e-required>
{{ sc.hours }}
</span>
</td>
<td>
<span editable-select="sc.plan_id" e-ng-options="p.id as humanReadablePlanName(p, groups, 'short') for p in plans" e-name="plan_id" e-form="rowform">
{{ getPlanFromId(sc.plan_id) | humanReadablePlanName: groups: 'short' }}
</span>
</td>
<td>
<form editable-form name="rowform" onbeforesave="saveSpaceCredit($data, sc.id)" ng-show="rowform.$visible" class="form-buttons form-inline" shown="inserted == sc">
<button type="submit" ng-disabled="rowform.$waiting" class="btn btn-warning">
<i class="fa fa-check"></i>
</button>
<button type="button" ng-disabled="rowform.$waiting" ng-click="cancelSpaceCredit(rowform, $index)" class="btn btn-default">
<i class="fa fa-times"></i>
</button>
</form>
<div class="buttons" ng-show="!rowform.$visible">
<button class="btn btn-default" ng-click="rowform.$show()">
<i class="fa fa-edit"></i> {{ 'edit' | translate }}
</button>
<button class="btn btn-danger" ng-click="removeSpaceCredit($index)">
<i class="fa fa-trash-o"></i> {{ 'delete' | translate }} (!)
</button>
</div>
</td>
</tr>
</tbody>
</table> </table>

View File

@ -7,7 +7,7 @@
</div> </div>
<div class="col-xs-10 col-sm-10 col-md-8 b-l"> <div class="col-xs-10 col-sm-10 col-md-8 b-l">
<section class="heading-title"> <section class="heading-title">
<h1 translate>{{ 'pricing_management' }}</h1> <h1 translate>{{ 'pricing.pricing_management' }}</h1>
</section> </section>
</div> </div>
@ -21,23 +21,27 @@
<div class="col-md-12"> <div class="col-md-12">
<uib-tabset justified="true"> <uib-tabset justified="true">
<uib-tab heading="{{ 'subscriptions' | translate }}"> <uib-tab heading="{{ 'pricing.subscriptions' | translate }}">
<ng-include src="'<%= asset_path 'admin/pricing/subscriptions.html' %>'"></ng-include> <ng-include src="'<%= asset_path 'admin/pricing/subscriptions.html' %>'"></ng-include>
</uib-tab> </uib-tab>
<uib-tab heading="{{ 'trainings' | translate }}"> <uib-tab heading="{{ 'pricing.trainings' | translate }}">
<ng-include src="'<%= asset_path 'admin/pricing/trainings.html' %>'"></ng-include> <ng-include src="'<%= asset_path 'admin/pricing/trainings.html' %>'"></ng-include>
</uib-tab> </uib-tab>
<uib-tab heading="{{ 'machine_hours' | translate }}"> <uib-tab heading="{{ 'pricing.machine_hours' | translate }}">
<ng-include src="'<%= asset_path 'admin/pricing/machine_hours.html' %>'"></ng-include> <ng-include src="'<%= asset_path 'admin/pricing/machine_hours.html' %>'"></ng-include>
</uib-tab> </uib-tab>
<uib-tab heading="{{ 'credits' | translate }}"> <uib-tab heading="{{ 'pricing.spaces' | translate }}" ng-hide="fablabWithoutSpaces">
<ng-include src="'<%= asset_path 'admin/pricing/spaces.html' %>'"></ng-include>
</uib-tab>
<uib-tab heading="{{ 'pricing.credits' | translate }}">
<ng-include src="'<%= asset_path 'admin/pricing/credits.html' %>'"></ng-include> <ng-include src="'<%= asset_path 'admin/pricing/credits.html' %>'"></ng-include>
</uib-tab> </uib-tab>
<uib-tab heading="{{ 'coupons' | translate }}"> <uib-tab heading="{{ 'pricing.coupons' | translate }}">
<ng-include src="'<%= asset_path 'admin/pricing/coupons.html' %>'"></ng-include> <ng-include src="'<%= asset_path 'admin/pricing/coupons.html' %>'"></ng-include>
</uib-tab> </uib-tab>
</uib-tabset> </uib-tabset>

View File

@ -1,10 +1,10 @@
<div class="alert alert-warning m-t"> <div class="alert alert-warning m-t">
{{ 'these_prices_match_machine_hours_rates_' | translate }} <span class="font-bold" translate>{{ '_without_subscriptions' }}</span>. {{ 'pricing.these_prices_match_machine_hours_rates_' | translate }} <span class="font-bold" translate>{{ 'pricing._without_subscriptions' }}</span>.
</div> </div>
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
<th style="width:20%" translate>{{ 'machines' }}</th> <th style="width:20%" translate>{{ 'pricing.machines' }}</th>
<th style="width:20%" ng-repeat="group in groups"> <th style="width:20%" ng-repeat="group in groups">
<span class="text-u-c text-sm">{{group.name}}</span> <span class="text-u-c text-sm">{{group.name}}</span>
</th> </th>

View File

@ -1,11 +1,11 @@
<div class="modal-header"> <div class="modal-header">
<h3 class="text-center red" translate>{{ 'send_a_coupon' }}</h3> <h3 class="text-center red" translate>{{ 'pricing.send_a_coupon' }}</h3>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<select-member></select-member> <select-member></select-member>
<div class="widget panel b-a m"> <div class="widget panel b-a m">
<div class="panel-heading b-b small"> <div class="panel-heading b-b small">
<h3 class="panel-title" translate>{{ 'coupon' }}</h3> <h3 class="panel-title" translate>{{ 'pricing.coupon' }}</h3>
</div> </div>
<div class="widget-content no-bg auto wrapper"> <div class="widget-content no-bg auto wrapper">
<table> <table>
@ -13,12 +13,12 @@
<tr><th style="width:60%"></th></tr> <tr><th style="width:60%"></th></tr>
</thead> </thead>
<tbody> <tbody>
<tr><td translate>{{'code'}}</td> <td>{{coupon.code}}</td></tr> <tr><td translate>{{'pricing.code'}}</td> <td>{{coupon.code}}</td></tr>
<tr><td translate>{{'discount'}}</td> <td><span ng-show="coupon.type == 'percent_off'">{{coupon.percent_off}} %</span><span ng-show="coupon.type == 'amount_off'">{{coupon.amount_off}} {{currencySymbol}}</span></td></tr> <tr><td translate>{{'pricing.discount'}}</td> <td><span ng-show="coupon.type == 'percent_off'">{{coupon.percent_off}} %</span><span ng-show="coupon.type == 'amount_off'">{{coupon.amount_off}} {{currencySymbol}}</span></td></tr>
<tr><td translate>{{'validity_per_user'}}</td> <td translate>{{coupon.validity_per_user}}</td></tr> <tr><td translate>{{'pricing.validity_per_user'}}</td> <td translate>{{'pricing.'+coupon.validity_per_user}}</td></tr>
<tr><td translate>{{'valid_until'}}</td> <td>{{coupon.valid_until | amDateFormat:'L'}}</td></tr> <tr><td translate>{{'pricing.valid_until'}}</td> <td>{{coupon.valid_until | amDateFormat:'L'}}</td></tr>
<tr><td translate>{{'usages'}}</td> <td>{{coupon.usages}} / {{coupon.max_usages | maxCount}}</td></tr> <tr><td translate>{{'pricing.usages'}}</td> <td>{{coupon.usages}} / {{coupon.max_usages | maxCount}}</td></tr>
<tr><td translate>{{'enabled'}}</td> <td>{{coupon.active | booleanFormat}}</td></tr> <tr><td translate>{{'pricing.enabled'}}</td> <td>{{coupon.active | booleanFormat}}</td></tr>
</tbody> </tbody>
</table> </table>
</div> </div>

View File

@ -0,0 +1,26 @@
<div class="alert alert-warning m-t">
{{ 'pricing.these_prices_match_space_hours_rates_' | translate }} <span class="font-bold" translate>{{ 'pricing._without_subscriptions' }}</span>.
</div>
<table class="table">
<thead>
<tr>
<th style="width:20%" translate>{{ 'pricing.spaces' }}</th>
<th style="width:20%" ng-repeat="group in groups">
<span class="text-u-c text-sm">{{group.name}}</span>
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="space in spaces">
<td>
{{ space.name }}
</td>
<td ng-repeat="group in groups">
<span editable-number="findPriceBy(spacesPrices, space.id, group.id).amount"
onbeforesave="updatePrice($data, findPriceBy(spacesPrices, space.id, group.id))">
{{ findPriceBy(spacesPrices, space.id, group.id).amount | currency}}
</span>
</td>
</tr>
</tbody>
</table>

View File

@ -1,21 +1,21 @@
<h2 translate>{{ 'list_of_the_subscription_plans' }}</h2> <h2 translate>{{ 'pricing.list_of_the_subscription_plans' }}</h2>
<div ng-show="fablabWithoutPlans" class="alert alert-warning m-t"> <div ng-show="fablabWithoutPlans" class="alert alert-warning m-t">
{{ 'beware_the_subscriptions_are_disabled_on_this_application' | translate }} {{ 'pricing.beware_the_subscriptions_are_disabled_on_this_application' | translate }}
{{ 'you_can_create_some_but_they_wont_be_available_until_the_project_is_redeployed_by_the_server_manager' | translate }} {{ 'pricing.you_can_create_some_but_they_wont_be_available_until_the_project_is_redeployed_by_the_server_manager' | translate }}
<br>{{ 'for_safety_reasons_please_dont_create_subscriptions_if_you_dont_want_intend_to_use_them_later' | translate }} <br>{{ 'pricing.for_safety_reasons_please_dont_create_subscriptions_if_you_dont_want_intend_to_use_them_later' | translate }}
</div> </div>
<button type="button" class="btn btn-warning m-t-lg m-b" ui-sref="app.admin.plans.new" translate>{{ 'add_a_new_subscription_plan' }}</button> <button type="button" class="btn btn-warning m-t-lg m-b" ui-sref="app.admin.plans.new" translate>{{ 'pricing.add_a_new_subscription_plan' }}</button>
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
<th><a href="" ng-click="setOrderPlans('type')">{{ 'type' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderPlans=='type', 'fa fa-sort-alpha-desc': orderPlans=='-type', 'fa fa-arrows-v': orderPlans }"></i></a></th> <th><a href="" ng-click="setOrderPlans('type')">{{ 'pricing.type' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderPlans=='type', 'fa fa-sort-alpha-desc': orderPlans=='-type', 'fa fa-arrows-v': orderPlans }"></i></a></th>
<th><a href="" ng-click="setOrderPlans('name')">{{ 'name' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderPlans=='name', 'fa fa-sort-alpha-desc': orderPlans=='-name', 'fa fa-arrows-v': orderPlans }"></i></a></th> <th><a href="" ng-click="setOrderPlans('name')">{{ 'pricing.name' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderPlans=='name', 'fa fa-sort-alpha-desc': orderPlans=='-name', 'fa fa-arrows-v': orderPlans }"></i></a></th>
<th><a href="" ng-click="setOrderPlans('interval')">{{ 'duration' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-amount-asc': orderPlans=='interval', 'fa fa-sort-amount-desc': orderPlans=='-interval', 'fa fa-arrows-v': orderPlans }"></i></a></th> <th><a href="" ng-click="setOrderPlans('interval')">{{ 'pricing.duration' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-amount-asc': orderPlans=='interval', 'fa fa-sort-amount-desc': orderPlans=='-interval', 'fa fa-arrows-v': orderPlans }"></i></a></th>
<th><a href="" ng-click="setOrderPlans('group_id')">{{ 'group' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderPlans=='group_id', 'fa fa-sort-alpha-desc': orderPlans=='-group_id', 'fa fa-arrows-v': orderPlans }"></i></a></th> <th><a href="" ng-click="setOrderPlans('group_id')">{{ 'pricing.group' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderPlans=='group_id', 'fa fa-sort-alpha-desc': orderPlans=='-group_id', 'fa fa-arrows-v': orderPlans }"></i></a></th>
<th class="hidden-xs"><a href="" ng-click="setOrderPlans('ui_weight')">{{ 'prominence' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': orderPlans=='ui_weight', 'fa fa-sort-numeric-desc': orderPlans=='-ui_weight', 'fa fa-arrows-v': orderPlans }"></i></a></th> <th class="hidden-xs"><a href="" ng-click="setOrderPlans('pricing.ui_weight')">{{ 'pricing.prominence' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': orderPlans=='ui_weight', 'fa fa-sort-numeric-desc': orderPlans=='-ui_weight', 'fa fa-arrows-v': orderPlans }"></i></a></th>
<th><a href="" ng-click="setOrderPlans('amount')">{{ 'price' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': orderPlans=='amount', 'fa fa-sort-numeric-desc': orderPlans=='-amount', 'fa fa-arrows-v': orderPlans }"></i></a></th> <th><a href="" ng-click="setOrderPlans('amount')">{{ 'pricing.price' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': orderPlans=='amount', 'fa fa-sort-numeric-desc': orderPlans=='-amount', 'fa fa-arrows-v': orderPlans }"></i></a></th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>

View File

@ -1,7 +1,7 @@
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
<th style="width:20%" translate>{{ 'trainings' }}</th> <th style="width:20%" translate>{{ 'pricing.trainings' }}</th>
<th style="width:20%" ng-repeat="group in groups"> <th style="width:20%" ng-repeat="group in groups">
<span class="text-u-c text-sm">{{group.name}}</span> <span class="text-u-c text-sm">{{group.name}}</span>
</th> </th>

View File

@ -3,8 +3,8 @@
<div class="row m-t-lg m-b-lg"> <div class="row m-t-lg m-b-lg">
<div class="col-sm-offset-4 col-sm-4"> <div class="col-sm-offset-4 col-sm-4">
<h1 ng-model="aboutTitleSetting.value" medium-editor options='{"placeholder": "{{ "title_of_the_about_page" | translate }}", "disableToolbar": true, "disableReturn": false}' class="text-u-c"></h1> <h1 ng-model="aboutTitleSetting.value" medium-editor options='{"placeholder": "{{ "settings.title_of_the_about_page" | translate }}", "disableToolbar": true, "disableReturn": false}' class="text-u-c"></h1>
<span class="help-block text-info text-xs"><i class="fa fa-lightbulb-o"></i> {{ 'shift_enter_to_force_carriage_return' | translate }}</span> <span class="help-block text-info text-xs"><i class="fa fa-lightbulb-o"></i> {{ 'settings.shift_enter_to_force_carriage_return' | translate }}</span>
<button name="button" class="btn btn-warning" ng-click="save(aboutTitleSetting)" translate>{{ 'save' }}</button> <button name="button" class="btn btn-warning" ng-click="save(aboutTitleSetting)" translate>{{ 'save' }}</button>
</div> </div>
@ -12,7 +12,7 @@
<div class="row"> <div class="row">
<div class="col-md-4 col-md-offset-1"> <div class="col-md-4 col-md-offset-1">
<div class="text-justify" ng-model="aboutBodySetting.value" medium-editor options='{"placeholder": "{{ "input_the_main_content" | translate }}", <div class="text-justify" ng-model="aboutBodySetting.value" medium-editor options='{"placeholder": "{{ "settings.input_the_main_content" | translate }}",
"buttons": ["bold", "italic", "anchor", "header1", "header2" ] "buttons": ["bold", "italic", "anchor", "header1", "header2" ]
}'> }'>
@ -20,12 +20,12 @@
<button name="button" class="btn btn-warning" ng-click="save(aboutBodySetting)" translate>{{ 'save' }}</button> <button name="button" class="btn btn-warning" ng-click="save(aboutBodySetting)" translate>{{ 'save' }}</button>
</div> </div>
<div class="col-md-4 col-md-offset-2"> <div class="col-md-4 col-md-offset-2">
<div ng-model="aboutContactsSetting.value" medium-editor options='{"placeholder": "{{ "input_the_fablab_contacts" | translate }}", <div ng-model="aboutContactsSetting.value" medium-editor options='{"placeholder": "{{ "settings.input_the_fablab_contacts" | translate }}",
"buttons": ["bold", "italic", "anchor", "header1", "header2" ] "buttons": ["bold", "italic", "anchor", "header1", "header2" ]
}'> }'>
</div> </div>
<span class="help-block text-info text-xs"><i class="fa fa-lightbulb-o"></i> {{ 'shift_enter_to_force_carriage_return' | translate }}</span> <span class="help-block text-info text-xs"><i class="fa fa-lightbulb-o"></i> {{ 'settings.shift_enter_to_force_carriage_return' | translate }}</span>
<button name="button" class="btn btn-warning" ng-click="save(aboutContactsSetting)" translate>{{ 'save' }}</button> <button name="button" class="btn btn-warning" ng-click="save(aboutContactsSetting)" translate>{{ 'save' }}</button>
</div> </div>

View File

@ -1,16 +1,16 @@
<div class="panel panel-default m-t-md"> <div class="panel panel-default m-t-md">
<div class="panel-heading"> <div class="panel-heading">
<span class="font-sbold" translate>{{ 'title' }}</span> <span class="font-sbold" translate>{{ 'settings.title' }}</span>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<div class="row m-t-lg"> <div class="row m-t-lg">
<div class="col-md-4"> <div class="col-md-4">
<form role="form" novalidate> <form role="form" novalidate>
<label for="fablabName" class="control-label m-r" translate>{{ 'fablab_title' }}</label> <label for="fablabName" class="control-label m-r" translate>{{ 'settings.fablab_title' }}</label>
<div class="form-group"> <div class="form-group">
<div class="input-group"> <div class="input-group">
<div class="input-group-addon"><i class="fa fa-font"></i></div> <div class="input-group-addon"><i class="fa fa-font"></i></div>
<input type="text" id="fablabName" ng-model="fablabName.value" class="form-control" placeholder="{{ 'fablab_name' | translate }}"/> <input type="text" id="fablabName" ng-model="fablabName.value" class="form-control" placeholder="{{ 'settings.fablab_name' | translate }}"/>
</div> </div>
</div> </div>
<button name="button" class="btn btn-warning" ng-click="save(fablabName)" translate>{{ 'save' }}</button> <button name="button" class="btn btn-warning" ng-click="save(fablabName)" translate>{{ 'save' }}</button>
@ -19,13 +19,13 @@
<div class="col-md-4 col-md-offset-1"> <div class="col-md-4 col-md-offset-1">
<form role="form" novalidate> <form role="form" novalidate>
<h4 class="control-label m-r" translate>{{ 'title_concordance' }}</h4> <h4 class="control-label m-r" translate>{{ 'settings.title_concordance' }}</h4>
<div class="form-group"> <div class="form-group">
<input type="radio" name="nameGenre" id="nameGenreMale" ng-model="nameGenre.value" ng-value="'male'" /> <input type="radio" name="nameGenre" id="nameGenreMale" ng-model="nameGenre.value" ng-value="'male'" />
<label for="nameGenreMale">{{ 'male' | translate }} <span style="font-weight: normal">{{ 'eg' | translate }} <cite>{{ 'about' | translate }} <strong translate>{{ 'male_preposition' }}</strong> {{fablabName.value}}</cite></span></label> <label for="nameGenreMale">{{ 'settings.male' | translate }} <span style="font-weight: normal">{{ 'settings.eg' | translate }} <cite>{{ 'settings.about' | translate }} <strong translate>{{ 'settings.male_preposition' }}</strong> {{fablabName.value}}</cite></span></label>
<br/> <br/>
<input type="radio" name="nameGenre" id="nameGenreFemale" ng-model="nameGenre.value" ng-value="'female'" /> <input type="radio" name="nameGenre" id="nameGenreFemale" ng-model="nameGenre.value" ng-value="'female'" />
<label for="nameGenreFemale">{{ 'female' | translate }} <span style="font-weight: normal">{{ 'eg' | translate }} <cite>{{ 'about' | translate }} <strong translate>{{ 'female_preposition' }}</strong> {{fablabName.value}}</cite></span></label> <label for="nameGenreFemale">{{ 'settings.female' | translate }} <span style="font-weight: normal">{{ 'settings.eg' | translate }} <cite>{{ 'settings.about' | translate }} <strong translate>{{ 'settings.female_preposition' }}</strong> {{fablabName.value}}</cite></span></label>
</div> </div>
<button name="button" class="btn btn-warning" ng-click="save(nameGenre)" translate>{{ 'save' }}</button> <button name="button" class="btn btn-warning" ng-click="save(nameGenre)" translate>{{ 'save' }}</button>
</form> </form>
@ -36,15 +36,15 @@
<div class="panel panel-default m-t-lg"> <div class="panel panel-default m-t-lg">
<div class="panel-heading"> <div class="panel-heading">
<span class="font-sbold" translate>{{ 'customize_information_messages' }}</span> <span class="font-sbold" translate>{{ 'settings.customize_information_messages' }}</span>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<div class="row"> <div class="row">
<div class="col-md-3"> <div class="col-md-3">
<h4 translate>{{ 'message_of_the_machine_booking_page' }}</h4> <h4 translate>{{ 'settings.message_of_the_machine_booking_page' }}</h4>
<div ng-model="machineExplicationsAlert.value" medium-editor options='{"placeholder": "{{ "type_the_message_content" | translate }}", <div ng-model="machineExplicationsAlert.value" medium-editor options='{"placeholder": "{{ "settings.type_the_message_content" | translate }}",
"buttons": ["bold", "italic", "unorderedlist", "header2" ] "buttons": ["bold", "italic", "unorderedlist", "header2" ]
}'> }'>
@ -52,8 +52,8 @@
<button name="button" class="btn btn-warning" ng-click="save(machineExplicationsAlert)" translate>{{ 'save' }}</button> <button name="button" class="btn btn-warning" ng-click="save(machineExplicationsAlert)" translate>{{ 'save' }}</button>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<h4 translate>{{ 'warning_message_of_the_training_booking_page'}}</h4> <h4 translate>{{ 'settings.warning_message_of_the_training_booking_page'}}</h4>
<div ng-model="trainingExplicationsAlert.value" medium-editor options='{"placeholder": "{{ "type_the_message_content" | translate }}", <div ng-model="trainingExplicationsAlert.value" medium-editor options='{"placeholder": "{{ "settings.type_the_message_content" | translate }}",
"buttons": ["bold", "italic", "unorderedlist", "header2" ] "buttons": ["bold", "italic", "unorderedlist", "header2" ]
}'> }'>
@ -61,8 +61,8 @@
<button name="button" class="btn btn-warning" ng-click="save(trainingExplicationsAlert)" translate>{{ 'save' }}</button> <button name="button" class="btn btn-warning" ng-click="save(trainingExplicationsAlert)" translate>{{ 'save' }}</button>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<h4 translate>{{ 'information_message_of_the_training_reservation_page'}}</h4> <h4 translate>{{ 'settings.information_message_of_the_training_reservation_page'}}</h4>
<div ng-model="trainingInformationMessage.value" medium-editor options='{"placeholder": "{{ "type_the_message_content" | translate }}", <div ng-model="trainingInformationMessage.value" medium-editor options='{"placeholder": "{{ "settings.type_the_message_content" | translate }}",
"buttons": ["bold", "italic", "unorderedlist", "header2" ] "buttons": ["bold", "italic", "unorderedlist", "header2" ]
}'> }'>
@ -70,38 +70,46 @@
<button name="button" class="btn btn-warning" ng-click="save(trainingInformationMessage)" translate>{{ 'save' }}</button> <button name="button" class="btn btn-warning" ng-click="save(trainingInformationMessage)" translate>{{ 'save' }}</button>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<h4 translate>{{ 'message_of_the_subscriptions_page' }}</h4> <h4 translate>{{ 'settings.message_of_the_subscriptions_page' }}</h4>
<div ng-model="subscriptionExplicationsAlert.value" medium-editor options='{"placeholder": "{{ "type_the_message_content" | translate }}", <div ng-model="subscriptionExplicationsAlert.value" medium-editor options='{"placeholder": "{{ "settings.type_the_message_content" | translate }}",
"buttons": ["bold", "italic", "unorderedlist", "header2" ] "buttons": ["bold", "italic", "unorderedlist", "header2" ]
}'> }'>
</div> </div>
<button name="button" class="btn btn-warning" ng-click="save(subscriptionExplicationsAlert)" translate>{{ 'save' }}</button> <button name="button" class="btn btn-warning" ng-click="save(subscriptionExplicationsAlert)" translate>{{ 'save' }}</button>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<h4 translate>{{ 'message_of_the_events_page' }}</h4> <h4 translate>{{ 'settings.message_of_the_events_page' }}</h4>
<div ng-model="eventExplicationsAlert.value" medium-editor options='{"placeholder": "{{ "type_the_message_content" | translate }}", <div ng-model="eventExplicationsAlert.value" medium-editor options='{"placeholder": "{{ "settings.type_the_message_content" | translate }}",
"buttons": ["bold", "italic", "unorderedlist", "header2" ] "buttons": ["bold", "italic", "unorderedlist", "header2" ]
}'> }'>
</div> </div>
<button name="button" class="btn btn-warning" ng-click="save(eventExplicationsAlert)" translate>{{ 'save' }}</button> <button name="button" class="btn btn-warning" ng-click="save(eventExplicationsAlert)" translate>{{ 'save' }}</button>
</div> </div>
<div class="col-md-3" ng-hide="fablabWithoutSpaces">
<h4 translate>{{ 'settings.message_of_the_spaces_page' }}</h4>
<div ng-model="spaceExplicationsAlert.value" medium-editor options='{"placeholder": "{{ "settings.type_the_message_content" | translate }}",
"buttons": ["bold", "italic", "unorderedlist", "header2" ]
}'>
</div>
<button name="button" class="btn btn-warning" ng-click="save(spaceExplicationsAlert)" translate>{{ 'save' }}</button>
</div>
</div> </div>
</div> </div>
</div> </div>
<div class="panel panel-default m-t-lg"> <div class="panel panel-default m-t-lg">
<div class="panel-heading"> <div class="panel-heading">
<span class="font-sbold" translate>{{ 'legal_documents'}}</span> <span class="font-sbold" translate>{{ 'settings.legal_documents'}}</span>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<div class="alert alert-warning m-t" translate> <div class="alert alert-warning m-t" translate>
{{ 'if_these_documents_are_not_filled_no_consent_about_them_will_be_asked_to_the_user' }} {{ 'settings.if_these_documents_are_not_filled_no_consent_about_them_will_be_asked_to_the_user' }}
</div> </div>
<div class="row"> <div class="row">
<form class="col-md-6" method="post" action="{{actionUrl.cgv}}" novalidate name="cgvForm" ng-upload="submited(content)" ng-submit="addLoader('cgv')" upload-options-enable-rails-csrf="true" unsaved-warning-form> <form class="col-md-6" method="post" action="{{actionUrl.cgv}}" novalidate name="cgvForm" ng-upload="submited(content)" ng-submit="addLoader('cgv')" upload-options-enable-rails-csrf="true" unsaved-warning-form>
<input type="hidden" name="custom_asset[name]" value="cgv-file"> <input type="hidden" name="custom_asset[name]" value="cgv-file">
<input name="_method" type="hidden" ng-value="methods.cgv"> <input name="_method" type="hidden" ng-value="methods.cgv">
<label for="tnc_file" class="control-label m-r" translate>{{ 'general_terms_and_conditions_(T&C)' }}</label> <label for="tnc_file" class="control-label m-r" translate>{{ 'settings.general_terms_and_conditions_(T&C)' }}</label>
<div class="form-group"> <div class="form-group">
<div class="fileinput input-group" data-provides="fileinput" ng-class="fileinputClass(cgvFile.custom_asset_file_attributes.attachment)"> <div class="fileinput input-group" data-provides="fileinput" ng-class="fileinputClass(cgvFile.custom_asset_file_attributes.attachment)">
<div class="form-control" data-trigger="fileinput"> <div class="form-control" data-trigger="fileinput">
@ -125,7 +133,7 @@
<form class="col-md-6" method="post" action="{{actionUrl.cgu}}" novalidate name="cguForm" ng-upload="submited(content)" ng-submit="addLoader('cgu')" upload-options-enable-rails-csrf="true" unsaved-warning-form> <form class="col-md-6" method="post" action="{{actionUrl.cgu}}" novalidate name="cguForm" ng-upload="submited(content)" ng-submit="addLoader('cgu')" upload-options-enable-rails-csrf="true" unsaved-warning-form>
<input type="hidden" name="custom_asset[name]" value="cgu-file"> <input type="hidden" name="custom_asset[name]" value="cgu-file">
<input name="_method" type="hidden" ng-value="methods.cgu"> <input name="_method" type="hidden" ng-value="methods.cgu">
<label for="tos_file" class="control-label m-r" translate>{{ 'terms_of_service_(TOS)' }}</label> <label for="tos_file" class="control-label m-r" translate>{{ 'settings.terms_of_service_(TOS)' }}</label>
<div class="form-group"> <div class="form-group">
<div class="fileinput input-group" data-provides="fileinput" ng-class="fileinputClass(cguFile.custom_asset_file_attributes.attachment)"> <div class="fileinput input-group" data-provides="fileinput" ng-class="fileinputClass(cguFile.custom_asset_file_attributes.attachment)">
<div class="form-control" data-trigger="fileinput"> <div class="form-control" data-trigger="fileinput">
@ -150,21 +158,21 @@
<div class="panel panel-default m-t-lg"> <div class="panel panel-default m-t-lg">
<div class="panel-heading"> <div class="panel-heading">
<span class="font-sbold" translate>{{ 'customize_the_graphics' }}</span> <span class="font-sbold" translate>{{ 'settings.customize_the_graphics' }}</span>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<div class="alert alert-warning m-t"> <div class="alert alert-warning m-t">
<span translate>{{ 'for_an_optimal_rendering_the_logo_image_must_be_at_the_PNG_format_with_a_transparent_background_and_with_an_aspect_ratio_3.5_times_wider_than_the_height' }}</span><br/> <span translate>{{ 'settings.for_an_optimal_rendering_the_logo_image_must_be_at_the_PNG_format_with_a_transparent_background_and_with_an_aspect_ratio_3.5_times_wider_than_the_height' }}</span><br/>
<span translate>{{ 'concerning_the_favicon_it_must_be_at_ICO_format_with_a_size_of_16x16_pixels' }}</span><br/> <span translate>{{ 'settings.concerning_the_favicon_it_must_be_at_ICO_format_with_a_size_of_16x16_pixels' }}</span><br/>
<br/> <br/>
<span translate>{{ 'remember_to_refresh_the_page_for_the_changes_to_take_effect' }}</span> <span translate>{{ 'settings.remember_to_refresh_the_page_for_the_changes_to_take_effect' }}</span>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-4"> <div class="col-md-4">
<form class="custom-logo-container" method="post" action="{{actionUrl.logo}}" novalidate name="logoForm" ng-upload="submited(content)" upload-options-enable-rails-csrf="true" unsaved-warning-form> <form class="custom-logo-container" method="post" action="{{actionUrl.logo}}" novalidate name="logoForm" ng-upload="submited(content)" upload-options-enable-rails-csrf="true" unsaved-warning-form>
<input type="hidden" name="custom_asset[name]" value="logo-file"> <input type="hidden" name="custom_asset[name]" value="logo-file">
<input name="_method" type="hidden" ng-value="methods.logo"> <input name="_method" type="hidden" ng-value="methods.logo">
<h3 class="m-l" translate>{{ 'logo_(white_background)' }}</h3> <h3 class="m-l" translate>{{ 'settings.logo_(white_background)' }}</h3>
<div class="custom-logo" style="background-image: url({{customLogo}});"> <div class="custom-logo" style="background-image: url({{customLogo}});">
<img src="data:image/png;base64," data-src="holder.js/100%x100%/text:&#xf03e;/font:FontAwesome/icon-xs" bs-holder ng-show="!customLogo" class="img-responsive"> <img src="data:image/png;base64," data-src="holder.js/100%x100%/text:&#xf03e;/font:FontAwesome/icon-xs" bs-holder ng-show="!customLogo" class="img-responsive">
<img base-sixty-four-image="customLogo" ng-show="customLogo && customLogo.base64"> <img base-sixty-four-image="customLogo" ng-show="customLogo && customLogo.base64">
@ -172,7 +180,7 @@
<div class="tools-box"> <div class="tools-box">
<div class="btn-group"> <div class="btn-group">
<div class="btn btn-default btn-file"> <div class="btn btn-default btn-file">
<i class="fa fa-edit"></i> {{ 'change_the_logo' | translate }} <i class="fa fa-edit"></i> {{ 'settings.change_the_logo' | translate }}
<input type="file" <input type="file"
accept="image/png,image/x-png" accept="image/png,image/x-png"
name="custom_asset[custom_asset_file_attributes][attachment]" name="custom_asset[custom_asset_file_attributes][attachment]"
@ -190,7 +198,7 @@
<form class="custom-logo-container" method="post" action="{{actionUrl.logoBlack}}" novalidate name="logoBlackForm" ng-upload="submited(content)" upload-options-enable-rails-csrf="true" unsaved-warning-form> <form class="custom-logo-container" method="post" action="{{actionUrl.logoBlack}}" novalidate name="logoBlackForm" ng-upload="submited(content)" upload-options-enable-rails-csrf="true" unsaved-warning-form>
<input type="hidden" name="custom_asset[name]" value="logo-black-file"> <input type="hidden" name="custom_asset[name]" value="logo-black-file">
<input name="_method" type="hidden" ng-value="methods.logoBlack"> <input name="_method" type="hidden" ng-value="methods.logoBlack">
<h3 class="m-l" translate>{{ 'logo_(black_background)' }}</h3> <h3 class="m-l" translate>{{ 'settings.logo_(black_background)' }}</h3>
<div class="custom-logo bg-dark" style="background-image: url({{customLogoBlack}});"> <div class="custom-logo bg-dark" style="background-image: url({{customLogoBlack}});">
<img src="data:image/png;base64," data-src="holder.js/100%x100%/text:&#xf03e;/font:FontAwesome/icon-black-xs" bs-holder ng-show="!customLogoBlack" class="img-responsive"> <img src="data:image/png;base64," data-src="holder.js/100%x100%/text:&#xf03e;/font:FontAwesome/icon-black-xs" bs-holder ng-show="!customLogoBlack" class="img-responsive">
<img base-sixty-four-image="customLogoBlack" ng-show="customLogoBlack && customLogoBlack.base64"> <img base-sixty-four-image="customLogoBlack" ng-show="customLogoBlack && customLogoBlack.base64">
@ -198,7 +206,7 @@
<div class="tools-box"> <div class="tools-box">
<div class="btn-group"> <div class="btn-group">
<div class="btn btn-default btn-file"> <div class="btn btn-default btn-file">
<i class="fa fa-edit"></i> {{ 'change_the_logo' | translate }} <i class="fa fa-edit"></i> {{ 'settings.change_the_logo' | translate }}
<input type="file" <input type="file"
accept="image/png,image/x-png" accept="image/png,image/x-png"
name="custom_asset[custom_asset_file_attributes][attachment]" name="custom_asset[custom_asset_file_attributes][attachment]"
@ -216,7 +224,7 @@
<form class="custom-favicon-container" method="post" action="{{actionUrl.favicon}}" novalidate name="faviconForm" ng-upload="submited(content)" upload-options-enable-rails-csrf="true" unsaved-warning-form> <form class="custom-favicon-container" method="post" action="{{actionUrl.favicon}}" novalidate name="faviconForm" ng-upload="submited(content)" upload-options-enable-rails-csrf="true" unsaved-warning-form>
<input type="hidden" name="custom_asset[name]" value="favicon-file"> <input type="hidden" name="custom_asset[name]" value="favicon-file">
<input name="_method" type="hidden" ng-value="methods.favicon"> <input name="_method" type="hidden" ng-value="methods.favicon">
<h3 class="m-l" translate>{{ 'favicon' }}</h3> <h3 class="m-l" translate>{{ 'settings.favicon' }}</h3>
<div class="custom-favicon" style="background-image: url({{customFavicon}});"> <div class="custom-favicon" style="background-image: url({{customFavicon}});">
<img src="data:image/png;base64," data-src="holder.js/32x32/text:&#xf03e;/font:FontAwesome/icon-xs" bs-holder ng-show="!customFavicon" class="img-responsive"> <img src="data:image/png;base64," data-src="holder.js/32x32/text:&#xf03e;/font:FontAwesome/icon-xs" bs-holder ng-show="!customFavicon" class="img-responsive">
<img base-sixty-four-image="customFavicon" ng-show="customFavicon && customFavicon.base64"> <img base-sixty-four-image="customFavicon" ng-show="customFavicon && customFavicon.base64">
@ -224,7 +232,7 @@
<div class="tools-box"> <div class="tools-box">
<div class="btn-group"> <div class="btn-group">
<div class="btn btn-default btn-file"> <div class="btn btn-default btn-file">
<i class="fa fa-edit"></i> {{ 'change_the_favicon' | translate }} <i class="fa fa-edit"></i> {{ 'settings.change_the_favicon' | translate }}
<input type="file" <input type="file"
accept="image/png,image/x-png,image/x-icon,image/ico,image/vnd.microsoft.icon" accept="image/png,image/x-png,image/x-icon,image/ico,image/vnd.microsoft.icon"
name="custom_asset[custom_asset_file_attributes][attachment]" name="custom_asset[custom_asset_file_attributes][attachment]"
@ -241,14 +249,14 @@
</div> </div>
<div class="row m-t m-l-xs"> <div class="row m-t m-l-xs">
<div class="col-md-4"> <div class="col-md-4">
<h4 translate>{{ 'main_colour' }}</h4> <h4 translate>{{ 'settings.main_colour' }}</h4>
<form role="form" class="form-inline" name="mainColorForm" novalidate> <form role="form" class="form-inline" name="mainColorForm" novalidate>
<div class="form-group"> <div class="form-group">
<div class="input-group"> <div class="input-group">
<div class="input-group-addon"> <div class="input-group-addon">
<i class="fa fa-paint-brush"></i> <i class="fa fa-paint-brush"></i>
</div> </div>
<input type="text" minicolors ng-model="mainColorSetting.value" class="form-control" placeholder="{{ 'primary' | translate}}"/> <input type="text" minicolors ng-model="mainColorSetting.value" class="form-control" placeholder="{{ 'settings.primary' | translate}}"/>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
@ -257,14 +265,14 @@
</form> </form>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<h4 translate>{{ 'secondary_colour' }}</h4> <h4 translate>{{ 'settings.secondary_colour' }}</h4>
<form role="form" class="form-inline" name="secondColorForm" novalidate> <form role="form" class="form-inline" name="secondColorForm" novalidate>
<div class="form-group"> <div class="form-group">
<div class="input-group"> <div class="input-group">
<div class="input-group-addon"> <div class="input-group-addon">
<i class="fa fa-paint-brush"></i> <i class="fa fa-paint-brush"></i>
</div> </div>
<input type="text" minicolors ng-model="secondColorSetting.value" class="form-control" placeholder="{{ 'secondary' | translate}}"/> <input type="text" minicolors ng-model="secondColorSetting.value" class="form-control" placeholder="{{ 'settings.secondary' | translate}}"/>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
@ -278,7 +286,7 @@
<form class="custom-profile-image-container" method="post" action="{{actionUrl.profileImage}}" novalidate name="profileImageForm" ng-upload="submited(content)" upload-options-enable-rails-csrf="true" unsaved-warning-form> <form class="custom-profile-image-container" method="post" action="{{actionUrl.profileImage}}" novalidate name="profileImageForm" ng-upload="submited(content)" upload-options-enable-rails-csrf="true" unsaved-warning-form>
<input type="hidden" name="custom_asset[name]" value="profile-image-file"> <input type="hidden" name="custom_asset[name]" value="profile-image-file">
<input name="_method" type="hidden" ng-value="methods.profileImage"> <input name="_method" type="hidden" ng-value="methods.profileImage">
<h3 class="m-l" translate>{{ 'background_picture_of_the_profile_banner' }}</h3> <h3 class="m-l" translate>{{ 'settings.background_picture_of_the_profile_banner' }}</h3>
<div class="custom-profile-image" style="background-image: url({{profileImage}});"> <div class="custom-profile-image" style="background-image: url({{profileImage}});">
<img src="data:image/png;base64," data-src="holder.js/100%x100%/text:&#xf03e;/font:FontAwesome/icon-xs" bs-holder ng-show="!profileImage" class="img-responsive"> <img src="data:image/png;base64," data-src="holder.js/100%x100%/text:&#xf03e;/font:FontAwesome/icon-xs" bs-holder ng-show="!profileImage" class="img-responsive">
<img base-sixty-four-image="profileImage" ng-show="profileImage && profileImage.base64"> <img base-sixty-four-image="profileImage" ng-show="profileImage && profileImage.base64">
@ -286,7 +294,7 @@
<div class="tools-box"> <div class="tools-box">
<div class="btn-group"> <div class="btn-group">
<div class="btn btn-default btn-file"> <div class="btn btn-default btn-file">
<i class="fa fa-edit"></i> {{ 'change_the_profile_banner' | translate }} <i class="fa fa-edit"></i> {{ 'settings.change_the_profile_banner' | translate }}
<input type="file" <input type="file"
accept="image/png,image/x-png" accept="image/png,image/x-png"
name="custom_asset[custom_asset_file_attributes][attachment]" name="custom_asset[custom_asset_file_attributes][attachment]"

View File

@ -2,21 +2,21 @@
<div class="panel-body"> <div class="panel-body">
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<h4 translate>{{ 'news_of_the_home_page' }}</h4> <h4 translate>{{ 'settings.news_of_the_home_page' }}</h4>
<div ng-model="homeBlogpostSetting.value" class="well" medium-editor options='{"placeholder": "{{ "type_your_news_here" | translate }}", <div ng-model="homeBlogpostSetting.value" class="well" medium-editor options='{"placeholder": "{{ "settings.type_your_news_here" | translate }}",
"buttons": ["bold", "italic", "anchor", "header1", "header2" ]}'></div> "buttons": ["bold", "italic", "anchor", "header1", "header2" ]}'></div>
<span class="help-block text-info text-xs"><i class="fa fa-lightbulb-o"></i> {{ 'leave_it_empty_to_not_bring_up_any_news_on_the_home_page' | translate }}</span> <span class="help-block text-info text-xs"><i class="fa fa-lightbulb-o"></i> {{ 'settings.leave_it_empty_to_not_bring_up_any_news_on_the_home_page' | translate }}</span>
<button name="button" class="btn btn-warning" ng-click="save(homeBlogpostSetting)" translate>{{ 'save' }}</button> <button name="button" class="btn btn-warning" ng-click="save(homeBlogpostSetting)" translate>{{ 'save' }}</button>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<h4 translate>{{ 'twitter_stream' }}</h4> <h4 translate>{{ 'settings.twitter_stream' }}</h4>
<form role="form" class="form-inline" name="twitterForm" novalidate> <form role="form" class="form-inline" name="twitterForm" novalidate>
<div class="form-group"> <div class="form-group">
<div class="input-group"> <div class="input-group">
<div class="input-group-addon"> <div class="input-group-addon">
<i class="fa fa-twitter"></i> <i class="fa fa-twitter"></i>
</div> </div>
<input type="text" ng-model="twitterSetting.value" class="form-control" placeholder="{{ 'name_of_the_twitter_account' | translate }}"/> <input type="text" ng-model="twitterSetting.value" class="form-control" placeholder="{{ 'settings.name_of_the_twitter_account' | translate }}"/>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">

View File

@ -7,7 +7,7 @@
</div> </div>
<div class="col-xs-10 col-sm-10 col-md-8 b-l"> <div class="col-xs-10 col-sm-10 col-md-8 b-l">
<section class="heading-title"> <section class="heading-title">
<h1 translate>{{ 'customize_the_application' }}</h1> <h1 translate>{{ 'settings.customize_the_application' }}</h1>
</section> </section>
</div> </div>
@ -20,18 +20,18 @@
<div class="col-md-12"> <div class="col-md-12">
<uib-tabset justified="true"> <uib-tabset justified="true">
<uib-tab heading="{{ 'general' | translate }}"> <uib-tab heading="{{ 'settings.general' | translate }}">
<ng-include src="'<%= asset_path 'admin/settings/general.html' %>'"></ng-include> <ng-include src="'<%= asset_path 'admin/settings/general.html' %>'"></ng-include>
</uib-tab> </uib-tab>
<uib-tab heading="{{ 'home_page' | translate }}"> <uib-tab heading="{{ 'settings.home_page' | translate }}">
<ng-include src="'<%= asset_path 'admin/settings/home_page.html' %>'"></ng-include> <ng-include src="'<%= asset_path 'admin/settings/home_page.html' %>'"></ng-include>
</uib-tab> </uib-tab>
<uib-tab heading="{{ 'about' | translate }}"> <uib-tab heading="{{ 'settings.about' | translate }}">
<ng-include src="'<%= asset_path 'admin/settings/about.html' %>'"></ng-include> <ng-include src="'<%= asset_path 'admin/settings/about.html' %>'"></ng-include>
</uib-tab> </uib-tab>
<uib-tab heading="{{ 'reservations' | translate }}"> <uib-tab heading="{{ 'settings.reservations' | translate }}">
<ng-include src="'<%= asset_path 'admin/settings/reservations.html' %>'"></ng-include> <ng-include src="'<%= asset_path 'admin/settings/reservations.html' %>'"></ng-include>
</uib-tab> </uib-tab>
</uib-tabset> </uib-tabset>

View File

@ -1,20 +1,20 @@
<div class="panel panel-default m-t-lg"> <div class="panel panel-default m-t-lg">
<div class="panel-heading"> <div class="panel-heading">
<span class="font-sbold" translate>{{ 'reservations_parameters' }}</span> <span class="font-sbold" translate>{{ 'settings.reservations_parameters' }}</span>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<div> <div>
<div class="row"> <div class="row">
<h3 class="m-l" translate>{{ 'confine_the_booking_agenda' }}</h3> <h3 class="m-l" translate>{{ 'settings.confine_the_booking_agenda' }}</h3>
<div class="col-md-2"> <div class="col-md-2">
<h4 translate>{{ 'opening_time' }}</h4> <h4 translate>{{ 'settings.opening_time' }}</h4>
<uib-timepicker ng-model="windowStart.value" hour-step="timepicker.hstep" minute-step="timepicker.mstep" show-meridian="false"></uib-timepicker> <uib-timepicker ng-model="windowStart.value" hour-step="timepicker.hstep" minute-step="timepicker.mstep" show-meridian="false"></uib-timepicker>
</div> </div>
<div class="col-md-4 m-t"> <div class="col-md-4 m-t">
<button name="button" class="btn btn-warning m-l" ng-click="save(windowStart)" translate>{{ 'save' }}</button> <button name="button" class="btn btn-warning m-l" ng-click="save(windowStart)" translate>{{ 'save' }}</button>
</div> </div>
<div class="col-md-2"> <div class="col-md-2">
<h4 translate>{{ 'closing_time' }}</h4> <h4 translate>{{ 'settings.closing_time' }}</h4>
<uib-timepicker ng-model="windowEnd.value" hour-step="timepicker.hstep" minute-step="timepicker.mstep" show-meridian="false"></uib-timepicker> <uib-timepicker ng-model="windowEnd.value" hour-step="timepicker.hstep" minute-step="timepicker.mstep" show-meridian="false"></uib-timepicker>
</div> </div>
<div class="col-md-4 m-t"> <div class="col-md-4 m-t">
@ -22,23 +22,23 @@
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<h3 class="m-l" translate>{{ 'ability_for_the_users_to_move_their_reservations' }}</h3> <h3 class="m-l" translate>{{ 'settings.ability_for_the_users_to_move_their_reservations' }}</h3>
<div class="form-group m-l"> <div class="form-group m-l">
<label for="enableMove" class="control-label m-r" translate>{{ 'reservations_shifting' }}</label> <label for="enableMove" class="control-label m-r" translate>{{ 'settings.reservations_shifting' }}</label>
<input bs-switch <input bs-switch
ng-model="enableMove.value" ng-model="enableMove.value"
id="enableMove" id="enableMove"
type="checkbox" type="checkbox"
class="form-control" class="form-control"
switch-on-text="{{ 'enabled' | translate }}" switch-on-text="{{ 'settings.enabled' | translate }}"
switch-off-text="{{ 'disabled' | translate }}" switch-off-text="{{ 'settings.disabled' | translate }}"
switch-animate="true"/> switch-animate="true"/>
<button name="button" class="btn btn-warning m-l" ng-click="save(enableMove)" translate>{{ 'save' }}</button> <button name="button" class="btn btn-warning m-l" ng-click="save(enableMove)" translate>{{ 'save' }}</button>
</div> </div>
</div> </div>
<div class="row" ng-show="enableMove.value"> <div class="row" ng-show="enableMove.value">
<form class="col-md-4" name="moveDelayForm"> <form class="col-md-4" name="moveDelayForm">
<label for="moveDelay" class="control-label m-r" translate>{{ 'prior_period_(hours)' }}</label> <label for="moveDelay" class="control-label m-r" translate>{{ 'settings.prior_period_(hours)' }}</label>
<div class="form-group"> <div class="form-group">
<div class="input-group"> <div class="input-group">
<div class="input-group-addon"> <div class="input-group-addon">
@ -51,23 +51,23 @@
</form> </form>
</div> </div>
<div class="row"> <div class="row">
<h3 class="m-l" translate>{{ 'ability_for_the_users_to_cancel_their_reservations' }}</h3> <h3 class="m-l" translate>{{ 'settings.ability_for_the_users_to_cancel_their_reservations' }}</h3>
<div class="form-group m-l"> <div class="form-group m-l">
<label for="enableCancel" class="control-label m-r" translate>{{ 'reservations_cancelling' }}</label> <label for="enableCancel" class="control-label m-r" translate>{{ 'settings.reservations_cancelling' }}</label>
<input bs-switch <input bs-switch
ng-model="enableCancel.value" ng-model="enableCancel.value"
id="enableCancel" id="enableCancel"
type="checkbox" type="checkbox"
class="form-control" class="form-control"
switch-on-text="{{ 'enabled' | translate }}" switch-on-text="{{ 'settings.enabled' | translate }}"
switch-off-text="{{ 'disabled' | translate }}" switch-off-text="{{ 'settings.disabled' | translate }}"
switch-animate="true"/> switch-animate="true"/>
<button name="button" class="btn btn-warning m-l" ng-click="save(enableCancel)" translate>{{ 'save' }}</button> <button name="button" class="btn btn-warning m-l" ng-click="save(enableCancel)" translate>{{ 'save' }}</button>
</div> </div>
</div> </div>
<div class="row" ng-show="enableCancel.value"> <div class="row" ng-show="enableCancel.value">
<form class="col-md-4" name="cancelDelayForm"> <form class="col-md-4" name="cancelDelayForm">
<label for="cancelDelay" class="control-label m-r" translate>{{ 'prior_period_(hours)' }}</label> <label for="cancelDelay" class="control-label m-r" translate>{{ 'settings.prior_period_(hours)' }}</label>
<div class="form-group"> <div class="form-group">
<div class="input-group"> <div class="input-group">
<div class="input-group-addon"> <div class="input-group-addon">
@ -85,27 +85,27 @@
<div class="panel panel-default m-t-lg"> <div class="panel panel-default m-t-lg">
<div class="panel-heading"> <div class="panel-heading">
<span class="font-sbold" translate>{{ 'reservations_reminders' }}</span> <span class="font-sbold" translate>{{ 'settings.reservations_reminders' }}</span>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<div class="row"> <div class="row">
<h3 class="m-l" translate>{{ 'notification_sending_before_the_reservation_occurs' }}</h3> <h3 class="m-l" translate>{{ 'settings.notification_sending_before_the_reservation_occurs' }}</h3>
<div class="form-group m-l"> <div class="form-group m-l">
<label for="enableReminder" class="control-label m-r" translate>{{ 'reservations_reminders' }}</label> <label for="enableReminder" class="control-label m-r" translate>{{ 'settings.reservations_reminders' }}</label>
<input bs-switch <input bs-switch
ng-model="enableReminder.value" ng-model="enableReminder.value"
id="enableReminder" id="enableReminder"
type="checkbox" type="checkbox"
class="form-control" class="form-control"
switch-on-text="{{ 'enabled' | translate }}" switch-on-text="{{ 'settings.enabled' | translate }}"
switch-off-text="{{ 'disabled' | translate }}" switch-off-text="{{ 'settings.disabled' | translate }}"
switch-animate="true"/> switch-animate="true"/>
<button name="button" class="btn btn-warning m-l" ng-click="save(enableReminder)" translate>{{ 'save' }}</button> <button name="button" class="btn btn-warning m-l" ng-click="save(enableReminder)" translate>{{ 'save' }}</button>
</div> </div>
</div> </div>
<div class="row" ng-show="enableReminder.value"> <div class="row" ng-show="enableReminder.value">
<form class="col-md-4" name="reminderDelayForm"> <form class="col-md-4" name="reminderDelayForm">
<label for="reminderDelay" class="control-label m-r" translate>{{ 'prior_period_(hours)' }}</label> <label for="reminderDelay" class="control-label m-r" translate>{{ 'settings.prior_period_(hours)' }}</label>
<div class="form-group"> <div class="form-group">
<div class="input-group"> <div class="input-group">
<div class="input-group-addon"> <div class="input-group-addon">
@ -114,7 +114,7 @@
<input type="number" class="form-control" id="reminderDelay" ng-model="reminderDelay.value" min="0"> <input type="number" class="form-control" id="reminderDelay" ng-model="reminderDelay.value" min="0">
</div> </div>
<span class="help-block text-info text-xs"> <span class="help-block text-info text-xs">
<i class="fa fa-lightbulb-o"></i> {{ 'default_value_is_24_hours' | translate }} <i class="fa fa-lightbulb-o"></i> {{ 'settings.default_value_is_24_hours' | translate }}
</span> </span>
</div> </div>
<button name="button" class="btn btn-warning" ng-click="save(reminderDelay)" ng-disabled="reminderDelayForm.$invalid" translate>{{ 'save' }}</button> <button name="button" class="btn btn-warning" ng-click="save(reminderDelay)" ng-disabled="reminderDelayForm.$invalid" translate>{{ 'save' }}</button>

View File

@ -26,7 +26,7 @@
<div class="col-md-12"> <div class="col-md-12">
<uib-tabset justified="true"> <uib-tabset justified="true">
<uib-tab ng-repeat="stat in statistics" heading="{{stat.label}}" select="setActiveTab(stat)" ng-if="stat.table && !(stat.es_type_key == 'subscription' && fablabWithoutPlans)"> <uib-tab ng-repeat="stat in statistics" heading="{{stat.label}}" select="setActiveTab(stat)" ng-hide="hiddenTab(stat)">
<form id="filters_form" name="filters_form" class="form-inline m-t-md m-b-lg" novalidate="novalidate"> <form id="filters_form" name="filters_form" class="form-inline m-t-md m-b-lg" novalidate="novalidate">
<div id="agePickerPane" class="form-group datepicker-container" style="z-index:102;"> <div id="agePickerPane" class="form-group datepicker-container" style="z-index:102;">
<button id="agePickerExpand" class="btn btn-default" type="button" ng-click="agePicker.show = !agePicker.show"> <button id="agePickerExpand" class="btn btn-default" type="button" ng-click="agePicker.show = !agePicker.show">

View File

@ -7,14 +7,14 @@
</div> </div>
<div class="col-xs-10 col-sm-10 col-md-8 b-l b-r-md hide-b-r-lg"> <div class="col-xs-10 col-sm-10 col-md-8 b-l b-r-md hide-b-r-lg">
<section class="heading-title"> <section class="heading-title">
<h1 translate>{{ 'calendar' }}</h1> <h1 translate>{{ 'calendar.calendar' }}</h1>
</section> </section>
</div> </div>
<div class="col-xs-12 col-sm-12 col-md-3 b-t hide-b-md hidden-lg"> <div class="col-xs-12 col-sm-12 col-md-3 b-t hide-b-md hidden-lg">
<div class="heading-actions wrapper"> <div class="heading-actions wrapper">
<button type="button" class="btn btn-default m-t m-b" ng-click="openFilterAside()"> <button type="button" class="btn btn-default m-t m-b" ng-click="openFilterAside()">
<span class="fa fa-filter"></span> {{ 'filter-calendar' | translate }} <span class="fa fa-filter"></span> {{ 'calendar.filter_calendar' | translate }}
</button> </button>
</div> </div>
</div> </div>
@ -38,7 +38,7 @@
<div class="col-lg-3 hidden-md hidden-sm hidden-xs"> <div class="col-lg-3 hidden-md hidden-sm hidden-xs">
<div class="widget panel b-a m m-t-lg"> <div class="widget panel b-a m m-t-lg">
<div class="panel-heading b-b small"> <div class="panel-heading b-b small">
<h3 translate>{{ 'filter-calendar' }}</h3> <h3 translate>{{ 'calendar.filter_calendar' }}</h3>
</div> </div>
<div class="widget-content no-bg auto wrapper calendar-filter"> <div class="widget-content no-bg auto wrapper calendar-filter">
<ng-include src="'<%= asset_path 'calendar/filter.html' %>'"></ng-include> <ng-include src="'<%= asset_path 'calendar/filter.html' %>'"></ng-include>
@ -53,7 +53,7 @@
<div class="widget"> <div class="widget">
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" ng-click="close($event)"><span>&times;</span></button> <button type="button" class="close" ng-click="close($event)"><span>&times;</span></button>
<h1 class="modal-title" translate>{{ 'filter-calendar' }}</h1> <h1 class="modal-title" translate>{{ 'calendar.filter_calendar' }}</h1>
</div> </div>
<div class="modal-body widget-content calendar-filter calendar-filter-aside"> <div class="modal-body widget-content calendar-filter calendar-filter-aside">
<ng-include src="'<%= asset_path 'calendar/filter.html' %>'"></ng-include> <ng-include src="'<%= asset_path 'calendar/filter.html' %>'"></ng-include>

View File

@ -1,6 +1,6 @@
<div> <div>
<div class="row"> <div class="row">
<h3 class="col-md-11 col-sm-11 col-xs-11 text-purple" translate>{{ 'trainings' }}</h3> <h3 class="col-md-11 col-sm-11 col-xs-11 text-purple" translate>{{ 'calendar.trainings' }}</h3>
<input class="col-md-1 col-sm-1 col-xs-1" type="checkbox" ng-model="filter.trainings" ng-change="toggleFilter('trainings', filter)"> <input class="col-md-1 col-sm-1 col-xs-1" type="checkbox" ng-model="filter.trainings" ng-change="toggleFilter('trainings', filter)">
</div> </div>
<div ng-repeat="t in trainings" class="row"> <div ng-repeat="t in trainings" class="row">
@ -10,7 +10,7 @@
</div> </div>
<div class="m-t"> <div class="m-t">
<div class="row"> <div class="row">
<h3 class="col-md-11 col-sm-11 col-xs-11 text-beige" translate>{{ 'machines' }}</h3> <h3 class="col-md-11 col-sm-11 col-xs-11 text-beige" translate>{{ 'calendar.machines' }}</h3>
<input class="col-md-1 col-sm-1 col-xs-1" type="checkbox" ng-model="filter.machines" ng-change="toggleFilter('machines', filter)"> <input class="col-md-1 col-sm-1 col-xs-1" type="checkbox" ng-model="filter.machines" ng-change="toggleFilter('machines', filter)">
</div> </div>
<div ng-repeat="m in machines" class="row"> <div ng-repeat="m in machines" class="row">
@ -18,11 +18,21 @@
<input class="col-md-1 col-sm-1 col-xs-1" type="checkbox" ng-model="m.checked" ng-change="filterAvailabilities(filter)"> <input class="col-md-1 col-sm-1 col-xs-1" type="checkbox" ng-model="m.checked" ng-change="filterAvailabilities(filter)">
</div> </div>
</div> </div>
<div class="m-t">
<div class="row">
<h3 class="col-md-11 col-sm-11 col-xs-11 text-cyan" translate>{{ 'calendar.spaces' }}</h3>
<input class="col-md-1 col-sm-1 col-xs-1" type="checkbox" ng-model="filter.spaces" ng-change="toggleFilter('spaces', filter)">
</div>
<div ng-repeat="s in spaces" class="row">
<span class="col-md-11 col-sm-11 col-xs-11">{{::s.name}}</span>
<input class="col-md-1 col-sm-1 col-xs-1" type="checkbox" ng-model="s.checked" ng-change="filterAvailabilities(filter)">
</div>
</div>
<div class="m-t row"> <div class="m-t row">
<h3 class="col-md-11 col-sm-11 col-xs-11 text-japonica" translate>{{ 'events' }}</h3> <h3 class="col-md-11 col-sm-11 col-xs-11 text-japonica" translate>{{ 'calendar.events' }}</h3>
<input class="col-md-1 col-sm-1 col-xs-1" type="checkbox" ng-model="filter.evt" ng-change="filterAvailabilities(filter)"> <input class="col-md-1 col-sm-1 col-xs-1" type="checkbox" ng-model="filter.evt" ng-change="filterAvailabilities(filter)">
</div> </div>
<div class="m-t row"> <div class="m-t row">
<h3 class="col-md-11 col-sm-11 col-xs-11 text-black" translate>{{ 'show_no_disponible' }}</h3> <h3 class="col-md-11 col-sm-11 col-xs-11 text-black" translate>{{ 'calendar.show_unavailables' }}</h3>
<input class="col-md-1 col-sm-1 col-xs-1" type="checkbox" ng-model="filter.dispo" ng-change="filterAvailabilities(filter)"> <input class="col-md-1 col-sm-1 col-xs-1" type="checkbox" ng-model="filter.dispo" ng-change="filterAvailabilities(filter)">
</div> </div>

View File

@ -29,178 +29,25 @@
<select-member></select-member> <select-member></select-member>
</div> </div>
<div class="widget panel b-a m m-t-lg" ng-show="!ctrl.member && currentUser.role == 'admin' && eventsReserved.length == 0 && (!paidMachineSlots || paidMachineSlots.length == 0) && !slotToModify && !modifiedSlots"> <cart slot="selectedEvent"
<div class="panel-heading b-b small"> slot-selection-time="selectionTime"
<h3 translate>{{ 'summary' }}</h3> events="events"
</div> user="ctrl.member"
<div class="widget-content no-bg auto wrapper"> mode-plans="plansAreShown"
<p class="font-felt fleche-left text-lg"><%= image_tag("fleche-left.png", class: 'fleche-left visible-lg') %> plan="selectedPlan"
{{ 'select_one_or_more_slots_in_the_calendar' | translate }}</p> plan-selection-time="planSelectionTime"
</div> settings="settings"
</div> on-slot-added-to-cart="markSlotAsAdded"
on-slot-removed-from-cart="markSlotAsRemoved"
on-slot-start-to-modify="markSlotAsModifying"
<div class="widget panel b-a m m-t-lg" ng-if="ctrl.member && !slotToModify && !modifiedSlots"> on-slot-modify-success="modifyMachineSlot"
<div class="panel-heading b-b small"> on-slot-modify-cancel="cancelModifyMachineSlot"
<h3 translate>{{ 'summary' }}</h3> on-slot-modify-unselect="changeModifyMachineSlot"
</div> on-slot-cancel-success="slotCancelled"
after-payment="afterPayment"
<div class="widget-content no-bg auto wrapper" ng-show="eventsReserved.length == 0 && (!paidMachineSlots || paidMachineSlots.length == 0)"> reservable-id="{{machine.id}}"
<p class="font-felt fleche-left text-lg"><%= image_tag("fleche-left.png", class: 'fleche-left visible-lg') %> reservable-type="Machine"
{{ 'select_one_or_more_slots_in_the_calendar' | translate }}</p> reservable-name="{{machine.name}}"></cart>
</div>
<div class="widget-content no-bg auto wrapper" ng-if="eventsReserved.length > 0">
<div class="font-sbold m-b-sm " translate>{{ 'you_ve_just_selected_the_slot' }}</div>
<div class="panel panel-default bg-light" ng-repeat="machineSlot in eventsReserved">
<div class="panel-body">
<div class="font-sbold text-u-c">{{ 'datetime_to_time' | translate:{START_DATETIME:(machineSlot.start | amDateFormat:'LLLL'), END_TIME:(machineSlot.end | amDateFormat:'LT') } }}</div>
<div class="text-base">{{ 'cost_of_a_machine_hour' | translate }} <span ng-class="{'text-blue': !machineSlot.promo, 'red': machineSlot.promo}">{{machineSlot.price | currency}}</span></div>
<div ng-show="currentUser.role == 'admin'" class="m-t">
<label for="offerSlot" class="control-label m-r" translate>{{ 'offer_this_slot' }}</label>
<input bs-switch
ng-model="machineSlot.offered"
id="offerSlot"
type="checkbox"
class="form-control"
switch-on-text="{{ 'yes' | translate}}"
switch-off-text="{{ 'no' | translate}}"
switch-animate="true"
switch-readonly="{{machineSlot.isValid}}"/>
</div>
</div>
<div>
<button class="btn btn-valid btn-warning btn-block text-u-c r-b" ng-click="validMachineSlot(machineSlot)" ng-if="!machineSlot.isValid" translate>{{ 'confirm_this_slot' }}</button>
</div>
<div class="clear"><a class="pull-right m-b-sm text-u-l ng-scope m-r-sm" href="#" ng-click="removeMachineSlot(machineSlot, $event)" ng-if="machineSlot.isValid" translate>{{ 'remove_this_slot' }}</a></div>
</div>
<coupon show="machineSlotsValid() && (!plansAreShown || selectedPlan)" coupon="coupon.applied" total="totalNoCoupon" user-id="{{ctrl.member.id}}"></coupon>
<span ng-hide="fablabWithoutPlans">
<div ng-if="machineSlotsValid() && !ctrl.member.subscribed_plan" ng-show="!plansAreShown">
<p class="font-sbold text-base l-h-2x" translate>{{ 'to_benefit_from_attractive_prices' }}</p>
<div><button class="btn btn-warning-full rounded btn-block text-xs" ng-click="showPlans()" translate>{{ 'view_our_subscriptions' }}</button></div>
<p class="font-bold text-base text-u-c text-center m-b-xs" translate>{{ 'or' }}</p>
</div>
<div ng-if="selectedPlan">
<div class="m-t-md m-b-sm text-base">{{ 'you_ve_just_selected_a_' | translate }} <br> <span class="font-sbold" translate>{{ '_subscription' }}</span> :</div>
<div class="panel panel-default bg-light m-n">
<div class="panel-body m-b-md">
<div class="font-sbold text-u-c">{{selectedPlan | humanReadablePlanName }}</div>
<div class="text-base">{{ 'cost_of_the_subscription' | translate }} <span class="text-blue">{{selectedPlan.amount | currency}}</span></div>
</div>
</div>
</div>
</span>
</div>
<div class="panel-footer no-padder" ng-if="eventsReserved.length > 0">
<button class="btn btn-valid btn-info btn-block p-l btn-lg text-u-c r-b text-base" ng-click="payMachine()" ng-if="machineSlotsValid() && (!plansAreShown || selectedPlan)">{{ 'confirm_and_pay' | translate }} {{amountTotal | currency}}</button>
</div>
<div class="widget-content no-bg auto wrapper" ng-if="paidMachineSlots">
{{ 'you_have_settled_the_following_machine_hours' | translate }} <strong>{{machine.name}}</strong>:
<div class="well well-warning m-t-sm" ng-repeat="paidSlot in paidMachineSlots">
<i class="font-sbold text-u-c">{{ 'datetime_to_time' | translate:{START_DATETIME:(paidSlot.start | amDateFormat:'LLLL'), END_TIME:(paidSlot.end | amDateFormat:'LT') } }}</i>
<div class="font-sbold">{{ 'cost_of_a_machine_hour' | translate }} {{paidSlot.machine.amount() | currency}}</div>
</div>
<div ng-if="selectedPlan">
<div class="m-t-md m-b-sm text-base">{{ 'you_have_settled_a_' | translate }} <br> <span class="font-sbold" translate>{{ '_subscription' }}</span> :</div>
<div class="well well-warning m-t-sm">
<i class="font-sbold text-u-c">{{selectedPlan | humanReadablePlanName }}</i>
<div class="font-sbold">{{ 'cost_of_the_subscription' | translate }} {{selectedPlan.amount | currency}}</div>
</div>
</div>
<div class="m-t-md font-sbold">{{ 'total_' | translate }} {{amountTotal | currency}}</div>
<div class="alert alert-success" ng-if="ctrl.member.subscribed_plan">{{ 'thank_you_your_payment_has_been_successfully_registered' | translate }}<br>
{{ 'your_invoice_will_be_available_soon_from_your_' | translate }} <a ui-sref="app.logged.dashboard.invoices" translate>{{ 'dashboard' }}</a>
</div>
</div>
</div>
<div class="widget panel b-a m m-t-lg" ng-if="slotToModify || modifiedSlots">
<div class="panel-heading b-b small">
<h3 translate>{{ 'summary' }}</h3>
</div>
<div class="widget-content no-bg auto wrapper" ng-if="slotToModify">
<div class="font-sbold m-b-sm " translate>{{ 'i_want_to_change_the_following_reservation' }}</div>
<div class="panel panel-warning bg-yellow">
<div class="panel-body">
<div class="font-sbold text-u-c">{{ 'datetime_to_time' | translate:{START_DATETIME:(slotToModify.start | amDateFormat:'LLLL'), END_TIME:(slotToModify.end | amDateFormat:'LT') } }}</div>
</div>
<div class="clear"><a class="pull-right m-b-sm text-u-l ng-scope m-r-sm" href="#" ng-click="removeSlotToModify($event)" translate>{{ 'cancel_my_modification' }}</a></div>
</div>
<div class="widget-content no-bg">
<p class="font-felt fleche-left text-lg"><%= image_tag("fleche-left.png", class: 'fleche-left visible-lg') %>
{{ 'select_a_new_slot_in_the_calendar' | translate }}</p>
</div>
<div class="panel panel-info bg-info text-white" ng-if="slotToPlace">
<div class="panel-body">
<div class="font-sbold text-u-c">{{ 'datetime_to_time' | translate:{START_DATETIME:(slotToPlace.start | amDateFormat:'LLLL'), END_TIME:(slotToPlace.end | amDateFormat:'LT') } }}</div>
</div>
<div class="clear"><a class="pull-right m-b-sm text-u-l ng-scope m-r-sm" href="#" ng-click="removeSlotToPlace($event)" translate>{{ 'cancel_my_selection' }}</a></div>
</div>
<div ng-if="slotToPlace && slotToModify.tags.length > 0 && slotToPlace.tags.length > 0" ng-class="{'panel panel-danger bg-red': tagMissmatch()}">
<div class="panel-body">
<div id="fromTags">
{{ 'tags_of_the_original_slot' | translate }}<br/>
<span ng-repeat="tag in slotToModify.tags">
<span class='label label-success text-white' title="{{tag.name}}">{{tag.name}}</span>
</span>
</div><br/>
<div id="toTags">
{{ 'tags_of_the_destination_slot' | translate }}<br/>
<span ng-repeat="tag in slotToPlace.tags">
<span class='label label-success text-white' title="{{tag.name}}">{{tag.name}}</span>
</span>
</div>
</div>
</div>
</div>
<div class="panel-footer no-padder" ng-if="slotToModify && slotToPlace">
<button class="btn btn-invalid btn-default btn-block p-l btn-lg text-u-c r-n text-base" ng-click="cancelModifyMachineSlot()" translate>{{ 'cancel' }}</button>
<div>
<button class="btn btn-valid btn-info btn-block p-l btn-lg text-u-c r-b text-base" ng-click="modifyMachineSlot()" translate>{{ 'confirm_my_modification' }}</button>
</div>
</div>
<div class="widget-content no-bg auto wrapper" ng-if="modifiedSlots">
<div class="font-sbold m-b-sm " translate>{{ 'your_booking_slot_was_successfully_moved_from_' }}</div>
<div class="panel panel-default bg-light">
<div class="panel-body">
<div class="font-sbold text-u-c">{{ 'datetime_to_time' | translate:{START_DATETIME:(modifiedSlots.oldReservedSlot.start | amDateFormat:'LLLL'), END_TIME:(modifiedSlots.oldReservedSlot.end | amDateFormat:'LT') } }}</div>
</div>
</div>
<p class="text-center font-bold m-b-sm text-u-c" translate>{{ 'to_date' }}</p>
<div class="panel panel-success bg-success bg-light">
<div class="panel-body">
<div class="font-sbold text-u-c">{{ 'datetime_to_time' | translate:{START_DATETIME:(modifiedSlots.newReservedSlot.start | amDateFormat:'LLLL'), END_TIME:(modifiedSlots.newReservedSlot.end | amDateFormat:'LT') } }}</div>
</div>
</div>
</div>
</div>
<uib-alert type="warning m"> <uib-alert type="warning m">
<p class="text-sm"> <p class="text-sm">

View File

@ -0,0 +1,167 @@
<div class="widget panel b-a m m-t-lg" ng-if="user && !events.modifiable && !events.moved">
<div class="panel-heading b-b small">
<h3 translate>{{ 'cart.summary' }}</h3>
</div>
<div class="widget-content no-bg auto wrapper" ng-show="events.reserved.length == 0 && (!events.paid || events.paid.length == 0)">
<p class="font-felt fleche-left text-lg"><%= image_tag('fleche-left.png', class: 'fleche-left visible-lg') %>
{{ 'cart.select_one_or_more_slots_in_the_calendar' | translate:{SINGLE:limitToOneSlot}:"messageformat" }}</p>
</div>
<div class="widget-content no-bg auto wrapper" ng-if="events.reserved.length > 0">
<div class="font-sbold m-b-sm " translate>{{ 'cart.you_ve_just_selected_the_slot' }}</div>
<div class="panel panel-default bg-light" ng-repeat="slot in events.reserved">
<div class="panel-body">
<div class="font-sbold text-u-c">{{ 'cart.datetime_to_time' | translate:{START_DATETIME:(slot.start | amDateFormat:'LLLL'), END_TIME:(slot.end | amDateFormat:'LT') } }}</div>
<div class="text-base">{{ 'cart.cost_of_TYPE' | translate:{TYPE:reservableType}:"messageformat" }} <span ng-class="{'text-blue': !slot.promo, 'red': slot.promo}">{{slot.price | currency}}</span></div>
<div ng-show="isAdmin()" class="m-t">
<label for="offerSlot" class="control-label m-r" translate>{{ 'cart.offer_this_slot' }}</label>
<input bs-switch
ng-model="slot.offered"
id="offerSlot"
type="checkbox"
class="form-control"
switch-on-text="{{ 'yes' | translate}}"
switch-off-text="{{ 'no' | translate}}"
switch-animate="true"
switch-readonly="{{slot.isValid}}"/>
</div>
</div>
<div>
<button class="btn btn-valid btn-warning btn-block text-u-c r-b" ng-click="validateSlot(slot)" ng-if="!slot.isValid" translate>{{ 'cart.confirm_this_slot' }}</button>
</div>
<div class="clear"><a class="pull-right m-b-sm text-u-l ng-scope m-r-sm" href="#" ng-click="removeSlot(slot, $index, $event)" ng-if="slot.isValid" translate>{{ 'cart.remove_this_slot' }}</a></div>
</div>
<coupon show="isSlotsValid() && (!modePlans || selectedPlan)" coupon="coupon.applied" total="totalNoCoupon" user-id="{{user.id}}"></coupon>
<div ng-hide="fablabWithoutPlans">
<div ng-if="isSlotsValid() && !user.subscribed_plan" ng-show="!modePlans">
<p class="font-sbold text-base l-h-2x" translate>{{ 'cart.to_benefit_from_attractive_prices' }}</p>
<div><button class="btn btn-warning-full rounded btn-block text-xs" ng-click="showPlans()" translate>{{ 'cart.view_our_subscriptions' }}</button></div>
<p class="font-bold text-base text-u-c text-center m-b-xs" translate>{{ 'cart.or' }}</p>
</div>
<div ng-if="selectedPlan">
<div class="m-t-md m-b-sm text-base">{{ 'cart.you_ve_just_selected_a_' | translate }} <br> <span class="font-sbold" translate>{{ 'cart._subscription' }}</span> :</div>
<div class="panel panel-default bg-light m-n">
<div class="panel-body m-b-md">
<div class="font-sbold text-u-c">{{selectedPlan | humanReadablePlanName }}</div>
<div class="text-base">{{ 'cart.cost_of_the_subscription' | translate }} <span class="text-blue">{{selectedPlan.amount | currency}}</span></div>
</div>
</div>
</div>
</div>
</div>
<div class="panel-footer no-padder" ng-if="events.reserved.length > 0">
<button class="btn btn-valid btn-info btn-block p-l btn-lg text-u-c r-b text-base" ng-click="payCart()" ng-if="isSlotsValid() && (!modePlans || selectedPlan)">{{ 'cart.confirm_and_pay' | translate }} {{amountTotal | currency}}</button>
</div>
<div class="widget-content no-bg auto wrapper" ng-show="events.paid && events.paid.length > 0">
{{ 'cart.you_have_settled_the_following_TYPE' | translate:{TYPE:reservableType}:"messageformat" }} <strong>{{reservableName}}</strong>:
<div class="well well-warning m-t-sm" ng-repeat="paidSlot in events.paid">
<i class="font-sbold text-u-c">{{ 'cart.datetime_to_time' | translate:{START_DATETIME:(paidSlot.start | amDateFormat:'LLLL'), END_TIME:(paidSlot.end | amDateFormat:'LT') } }}</i>
<div class="font-sbold">{{ 'cart.cost_of_TYPE' | translate:{TYPE:reservableType}:"messageformat" }} {{paidSlot.price | currency}}</div>
</div>
<div ng-if="selectedPlan">
<div class="m-t-md m-b-sm text-base">{{ 'cart.you_have_settled_a_' | translate }} <br> <span class="font-sbold" translate>{{ 'cart._subscription' }}</span> :</div>
<div class="well well-warning m-t-sm">
<i class="font-sbold text-u-c">{{selectedPlan | humanReadablePlanName }}</i>
<div class="font-sbold">{{ 'cart.cost_of_the_subscription' | translate }} {{selectedPlan.amount | currency}}</div>
</div>
</div>
<div class="m-t-md font-sbold">{{ 'cart.total_' | translate }} {{amountTotal | currency}}</div>
<div class="alert alert-success" ng-if="user.subscribed_plan">{{ 'cart.thank_you_your_payment_has_been_successfully_registered' | translate }}<br>
{{ 'cart.your_invoice_will_be_available_soon_from_your_' | translate }} <a ui-sref="app.logged.dashboard.invoices" translate>{{ 'cart.dashboard' }}</a>
</div>
</div>
</div>
<div class="widget panel b-a m m-t-lg" ng-if="events.modifiable || events.moved">
<div class="panel-heading b-b small">
<h3 translate>{{ 'cart.summary' }}</h3>
</div>
<div class="widget-content no-bg auto wrapper" ng-if="events.modifiable">
<div class="font-sbold m-b-sm " translate>{{ 'cart.i_want_to_change_the_following_reservation' }}</div>
<div class="panel panel-warning bg-yellow">
<div class="panel-body">
<div class="font-sbold text-u-c">{{ 'cart.datetime_to_time' | translate:{START_DATETIME:(events.modifiable.start | amDateFormat:'LLLL'), END_TIME:(events.modifiable.end | amDateFormat:'LT') } }}</div>
</div>
<div class="clear"><a class="pull-right m-b-sm text-u-l ng-scope m-r-sm" href="#" ng-click="cancelModifySlot($event)" translate>{{ 'cart.cancel_my_modification' }}</a></div>
</div>
<div class="widget-content no-bg">
<p class="font-felt fleche-left text-lg"><%= image_tag('fleche-left.png', class: 'fleche-left visible-lg') %>
{{ 'cart.select_a_new_slot_in_the_calendar' | translate }}</p>
</div>
<div class="panel panel-info bg-info text-white" ng-if="events.placable">
<div class="panel-body">
<div class="font-sbold text-u-c">{{ 'cart.datetime_to_time' | translate:{START_DATETIME:(events.placable.start | amDateFormat:'LLLL'), END_TIME:(events.placable.end | amDateFormat:'LT') } }}</div>
</div>
<div class="clear"><a class="pull-right m-b-sm text-u-l ng-scope m-r-sm" href="#" ng-click="removeSlotToPlace($event)" translate>{{ 'cart.cancel_my_selection' }}</a></div>
</div>
<div ng-if="events.placable && (events.modifiable.tags.length > 0 || events.placable.tags.length > 0)" ng-class="{'panel panel-danger bg-red': tagMissmatch()}">
<div class="panel-body">
<div id="fromTags">
{{ 'cart.tags_of_the_original_slot' | translate }}<br/>
<span ng-repeat="tag in events.modifiable.tags">
<span class='label label-success text-white' title="{{tag.name}}">{{tag.name}}</span>
</span>
<span ng-show="events.modifiable.tags.length == 0">
<span class='label label-warning text-white' title="{{ 'cart.none' | translate }}" translate>{{ 'cart.none' }}</span>
</span>
</div><br/>
<div id="toTags">
{{ 'cart.tags_of_the_destination_slot' | translate }}<br/>
<span ng-repeat="tag in events.placable.tags">
<span class='label label-success text-white' title="{{tag.name}}">{{tag.name}}</span>
</span>
<span ng-show="events.placable.tags.length == 0">
<span class='label label-warning text-white' title="{{ 'cart.none' | translate }}" translate>{{ 'cart.none' }}</span>
</span>
</div>
</div>
</div>
</div>
<div class="panel-footer no-padder" ng-if="events.modifiable && events.placable">
<button class="btn btn-invalid btn-default btn-block p-l btn-lg text-u-c r-n text-base" ng-click="cancelModifySlot()" translate>{{ 'cancel' }}</button>
<div>
<button class="btn btn-valid btn-info btn-block p-l btn-lg text-u-c r-b text-base" ng-click="modifySlot()" translate>{{ 'cart.confirm_my_modification' }}</button>
</div>
</div>
<div class="widget-content no-bg auto wrapper" ng-show="events.moved">
<div class="font-sbold m-b-sm " translate>{{ 'cart.your_booking_slot_was_successfully_moved_from_' }}</div>
<div class="panel panel-default bg-light">
<div class="panel-body">
<div class="font-sbold text-u-c">{{ 'cart.datetime_to_time' | translate:{START_DATETIME:(events.moved.oldSlot.start | amDateFormat:'LLLL'), END_TIME:(events.moved.oldSlot.end | amDateFormat:'LT') } }}</div>
</div>
</div>
<p class="text-center font-bold m-b-sm text-u-c" translate>{{ 'cart.to_date' }}</p>
<div class="panel panel-success bg-success bg-light">
<div class="panel-body">
<div class="font-sbold text-u-c">{{ 'cart.datetime_to_time' | translate:{START_DATETIME:(events.moved.newSlot.start | amDateFormat:'LLLL'), END_TIME:(events.moved.newSlot.end | amDateFormat:'LT') } }}</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,109 @@
<uib-alert ng-repeat="alert in alerts" type="{{alert.type}}" close="closeAlert($index)">{{alert.msg}}</uib-alert>
<div class="form-group m-b-lg" ng-class="{'has-error': spaceForm['space[name]'].$dirty && spaceForm['space[name]'].$invalid}">
<label for="space_name" class="col-sm-2 control-label">{{ 'space.name' | translate }} *</label>
<div class="col-sm-4">
<input ng-model="space.name"
type="text"
name="space[name]"
class="form-control"
id="space_name"
placeholder="{{'space.name' | translate}}"
required>
<span class="help-block" ng-show="spaceForm['space[name]'].$dirty && spaceForm['space[name]'].$error.required" translate>{{ 'space.name_is_required' }}</span>
</div>
</div>
<div class="form-group m-b-lg">
<label for="space_image" class="col-sm-2 control-label">{{ 'space.illustration' | translate }} *</label>
<div class="col-sm-10">
<div class="fileinput" data-provides="fileinput" ng-class="fileinputClass(space.space_image)">
<div class="fileinput-new thumbnail" style="width: 334px; height: 250px;">
<img src="data:image/png;base64," data-src="holder.js/100%x100%/text:&#xf03e;/font:FontAwesome/icon" bs-holder ng-if="!space.space_image">
</div>
<div class="fileinput-preview fileinput-exists thumbnail" style="max-width: 334px;">
<img ng-src="{{ space.space_image }}" alt="" />
</div>
<div>
<span class="btn btn-default btn-file">
<span class="fileinput-new">{{ 'space.add_an_illustration' | translate }} <i class="fa fa-upload fa-fw"></i></span>
<span class="fileinput-exists" translate>{{ 'change' }}</span>
<input type="file"
id="space_image"
ng-model="space.space_image"
name="space[space_image_attributes][attachment]"
accept="image/*"
required
bs-jasny-fileinput>
</span>
<a href="#" class="btn btn-danger fileinput-exists" data-dismiss="fileinput" translate>{{ 'delete' }}</a>
</div>
</div>
</div>
</div>
<div class="form-group m-b-xl" ng-class="{'has-error': spaceForm['space[default_places]'].$dirty && spaceForm['space[default_places]'].$invalid}">
<label for="default_places" class="col-sm-2 control-label">{{ 'space.default_places' | translate }} *</label>
<div class="col-sm-10">
<input type="number"
name="space[default_places]"
ng-model="space.default_places"
id="default_places"
class="form-control"
required>
<span class="help-block" ng-show="spaceForm['space[default_places]'].$dirty && spaceForm['space[default_places]'].$error.required" translate>{{ 'space.default_places_is_required' }}</span>
</div>
</div>
<div class="form-group m-b-xl">
<label for="space_description" class="col-sm-2 control-label" translate>{{ 'space.description' }}</label>
<div class="col-sm-10">
<input type="hidden"
name="space[description]"
ng-value="space.description" />
<summernote ng-model="space.description"
id="space_description"
placeholder=""
config="summernoteOpts"
name="space[description]">
</summernote>
</div>
</div>
<div class="form-group m-b-xl">
<label for="space_characteristics" class="col-sm-2 control-label" translate>{{ 'space.characteristics' }}</label>
<div class="col-sm-10">
<input type="hidden"
name="space[characteristics]"
ng-value="space.characteristics" />
<summernote ng-model="space.characteristics"
id="space_characteristics"
placeholder=""
config="summernoteOpts"
name="space[characteristics]">
</summernote>
</div>
</div>
<div class="form-group m-b-xl">
<label class="col-sm-2 control-label" translate>{{ 'space.attached_files_(pdf)' }}</label>
<div class="col-sm-10">
<div ng-repeat="file in space.space_files_attributes" ng-show="!file._destroy">
<input type="hidden" ng-model="file.id" name="space[space_files_attributes][][id]" ng-value="file.id" />
<input type="hidden" ng-model="file._destroy" name="space[space_files_attributes][][_destroy]" ng-value="file._destroy"/>
<div class="fileinput input-group" data-provides="fileinput" ng-class="fileinputClass(file.attachment)">
<div class="form-control" data-trigger="fileinput">
<i class="glyphicon glyphicon-file fileinput-exists"></i> <span class="fileinput-filename">{{file.attachment}}</span>
</div>
<span class="input-group-addon btn btn-default btn-file"><span class="fileinput-new" translate>{{ 'space.attach_a_file' }}</span>
<span class="fileinput-exists" translate>{{ 'change' }}</span><input type="file" name="space[space_files_attributes][][attachment]" accept=".pdf"></span>
<a class="input-group-addon btn btn-danger fileinput-exists" data-dismiss="fileinput" ng-click="deleteFile(file)"><i class="fa fa-trash-o"></i></a>
</div>
</div>
<a class="btn btn-default" ng-click="addFile()" role="button"> {{ 'space.add_an_attachment' | translate }} <i class="fa fa-file-o fa-fw"></i></a>
</div>
</div>

View File

@ -0,0 +1,50 @@
<section class="heading b-b">
<div class="row no-gutter">
<div class="col-md-1 hidden-xs">
<section class="heading-btn">
<a href="#" ng-click="backPrevLocation($event)"><i class="fa fa-long-arrow-left "></i></a>
</section>
</div>
<div class="col-md-8 b-l b-r">
<section class="heading-title">
<h1 translate translate-values="{NAME: space.name}">{{ 'space_edit.edit_the_space_NAME' }}</h1>
</section>
</div>
</div>
</section>
<div class="row no-gutter" >
<div class="col-md-9 b-r nopadding">
<form role="form"
name="spaceForm"
class="form-horizontal"
action="{{ actionUrl }}"
ng-upload="submited(content)"
upload-options-enable-rails-csrf="true"
unsaved-warning-form
novalidate>
<input name="_method" type="hidden" ng-value="method">
<section class="panel panel-default bg-light m-lg">
<div class="panel-body m-r">
<ng-include src="'<%= asset_path 'spaces/_form.html' %>'"></ng-include>
</div>
<div class="panel-footer no-padder">
<input type="submit"
value="{{ 'space_edit.validate_the_changes' | translate }}"
class="r-b btn-valid btn btn-warning btn-block p-lg btn-lg text-u-c"
ng-disabled="spaceForm.$invalid"/>
</div>
</section>
</form>
</div>
<div class="col-md-3"/>
</div>

View File

@ -0,0 +1,63 @@
<section class="heading b-b">
<div class="row no-gutter">
<div class="col-xs-2 col-sm-2 col-md-1">
<section class="heading-btn">
<a href="#" ng-click="backPrevLocation($event)"><i class="fa fa-long-arrow-left "></i></a>
</section>
</div>
<div class="col-xs-10 col-sm-10 col-md-8 b-l b-r-md">
<section class="heading-title">
<h1 translate>{{ 'the_spaces' }}</h1>
</section>
</div>
<div class="col-xs-12 col-sm-12 col-md-3 b-t hide-b-md" ng-if="isAuthorized('admin')">
<section class="heading-actions wrapper">
<a class="btn btn-lg btn-warning bg-white b-2x rounded m-t-xs" ui-sref="app.admin.space_new" role="button" translate>{{ 'add_a_space' }}</a>
</section>
</div>
</div>
</section>
<section class="m-lg">
<div class="row" ng-repeat="space in (spaces.length/3 | array)">
<div class="col-xs-12 col-sm-6 col-md-4" ng-repeat="space in spaces.slice(3*$index, 3*$index + 3)">
<div class="widget panel panel-default">
<div class="panel-heading picture" ng-if="!space.space_image" ng-click="showMachine(space)">
<img src="data:image/png;base64," data-src="holder.js/100%x100%/text:&#xf03e;/font:FontAwesome/icon" bs-holder class="img-responsive">
</div>
<div class="panel-heading picture" style="background-image:url({{space.space_image}})" ng-if="space.space_image" ng-click="showMachine(space)">
</div>
<div class="panel-body">
<h1 class="text-center m-b">{{space.name}}</h1>
</div>
<div class="panel-footer no-padder">
<div class="text-center clearfix">
<div class="col-sm-6 b-r no-padder">
<div class="btn btn-default btn-block no-b padder-v red" ng-click="reserveSpace(space, $event)">
<i class="fa fa-bookmark"></i> {{ 'book' | translate }}
</div>
</div>
<div class="col-sm-6 no-padder">
<div class="btn btn-default btn-block padder-v no-b red" ng-click="showSpace(space)">
<i class="fa fa-eye"></i> {{ 'consult' | translate }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>

View File

@ -0,0 +1,57 @@
<section class="heading b-b">
<div class="row no-gutter">
<div class="col-md-1 hidden-xs">
<section class="heading-btn">
<a href="#" ng-click="backPrevLocation($event)"><i class="fa fa-long-arrow-left "></i></a>
</section>
</div>
<div class="col-md-8 b-l b-r">
<section class="heading-title">
<h1 translate>{{ 'space_new.add_a_new_space' }}</h1>
</section>
</div>
</div>
</section>
<div class="row no-gutter" >
<div class="col-md-9 b-r nopadding">
<div class="m-lg alert alert-warning" role="alert">
{{ 'space_new.watch_out_when_creating_a_new_space_its_prices_are_initialized_at_0_for_all_subscriptions' | translate}}
{{ 'space_new.consider_changing_its_prices_before_creating_any_reservation_slot' | translate }}
</div>
<form role="form"
name="spaceForm"
class="form-horizontal"
action="{{ actionUrl }}"
ng-upload="submited(content)"
upload-options-enable-rails-csrf="true"
unsaved-warning-form
novalidate>
<input name="_method" type="hidden" ng-value="method">
<section class="panel panel-default bg-light m-lg">
<div class="panel-body m-r">
<ng-include src="'<%= asset_path 'spaces/_form.html' %>'"></ng-include>
</div>
<div class="panel-footer no-padder">
<input type="submit"
value="{{ 'space_new.add_this_space' | translate }}"
class="r-b btn-valid btn btn-warning btn-block p-lg btn-lg text-u-c"
ng-disabled="spaceForm.$invalid"/>
</div>
</section>
</form>
</div>
<div class="col-md-3"/>
</div>

View File

@ -0,0 +1,59 @@
<section class="heading b-b">
<div class="row no-gutter">
<div class="col-xs-2 col-sm-2 col-md-1">
<section class="heading-btn">
<a href="#" ng-click="backPrevLocation($event)"><i class="fa fa-long-arrow-left "></i></a>
</section>
</div>
<div class="col-xs-10 col-sm-10 col-md-8 b-l b-r-md">
<section class="heading-title">
<h1 translate translate-values="{NAME:space.name}">{{ 'space_reserve.planning_of_space_NAME' }}</h1>
</section>
</div>
</div>
</section>
<div class="row no-gutter training-reserve">
<div class="col-sm-12 col-md-12 col-lg-9">
<div ui-calendar="calendarConfig" ng-model="eventSources" calendar="calendar" class="wrapper-lg" ng-show="!plansAreShown"></div>
<ng-include ng-if="!fablabWithoutPlans" src="'<%= asset_path 'plans/_plan.html' %>'"></ng-include>
</div>
<div class="col-sm-12 col-md-12 col-lg-3">
<div ng-if="currentUser.role === 'admin'">
<select-member></select-member>
</div>
<cart slot="selectedEvent"
slot-selection-time="selectionTime"
events="events"
user="ctrl.member"
mode-plans="plansAreShown"
plan="selectedPlan"
plan-selection-time="planSelectionTime"
settings="settings"
on-slot-added-to-cart="markSlotAsAdded"
on-slot-removed-from-cart="markSlotAsRemoved"
on-slot-start-to-modify="markSlotAsModifying"
on-slot-modify-success="modifyTrainingSlot"
on-slot-modify-cancel="cancelModifyTrainingSlot"
on-slot-modify-unselect="changeModifyTrainingSlot"
on-slot-cancel-success="slotCancelled"
after-payment="afterPayment"
reservable-id="{{space.id}}"
reservable-type="Space"
reservable-name="{{space.name}}"></cart>
<uib-alert type="warning m" ng-show="spaceExplicationsAlert">
<p class="text-sm">
<i class="fa fa-warning"></i>
<span ng-bind-html="spaceExplicationsAlert"></span>
</p>
</uib-alert>
</div>
</div>

View File

@ -0,0 +1,85 @@
<div>
<section class="heading b-b">
<div class="row no-gutter">
<div class="col-xs-2 col-sm-2 col-md-1">
<section class="heading-btn">
<a href="#" ng-click="backPrevLocation($event)"><i class="fa fa-long-arrow-left "></i></a>
</section>
</div>
<div class="col-xs-10 col-sm-10 col-md-7 b-l b-r-md">
<section class="heading-title">
<h1>{{ space.name }}</h1>
</section>
</div>
<div class="col-xs-12 col-sm-12 col-md-4 b-t hide-b-md">
<section class="heading-actions wrapper">
<a ng-click="reserveSpace($event)" class="btn btn-lg btn-warning bg-white b-2x rounded m-t-xs" ng-if="!isAuthorized('admin')" translate>{{ 'space_show.book_this_space' }}</a>
<a ui-sref="app.admin.space_edit({id:space.slug})" ng-if="isAuthorized('admin')" class="btn btn-lg btn-warning bg-white b-2x rounded m-t-xs"><i class="fa fa-edit"></i> {{ 'edit' | translate }}</a>
<a ng-click="deleteSpace($event)" ng-if="isAuthorized('admin')" class="btn btn-lg btn-danger b-2x rounded no-b m-t-xs"><i class="fa fa-trash-o"></i></a>
</section>
</div>
</div>
</section>
<div class="row no-gutter">
<div class="col-sm-12 col-md-12 col-lg-8 b-r-lg">
<div class="article wrapper-lg">
<div class="article-thumbnail" ng-if="space.space_image">
<img ng-src="{{space.space_image}}" alt="{{space.name}}" class="img-responsive">
</div>
<p class="intro" ng-bind-html="space.description | toTrusted"></p>
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-4">
<div class="widget panel b-a m m-t-lg">
<div class="panel-heading b-b small">
<h3 translate>{{ 'space_show.characteristics' }}</h3>
</div>
<div class="widget-content no-bg wrapper">
<h3></h3>
<p ng-bind-html="space.characteristics | toTrusted"></p>
</div>
</div>
<section class="widget panel b-a m" ng-if="space.space_files_attributes">
<div class="panel-heading b-b">
<span class="badge bg-warning pull-right">{{space.space_files_attributes.length}}</span>
<h3 translate>{{ 'space_show.files_to_download' }}</h3>
</div>
<ul class="widget-content list-group list-group-lg no-bg auto">
<li ng-repeat="file in space.space_files_attributes" class="list-group-item no-b clearfix">
<a target="_blank" ng-href="{{file.attachment_url}}"><i class="fa fa-arrow-circle-o-down"> </i> {{file.attachment | humanize : 25}}</a>
</li>
</ul>
</section>
<section class="widget panel b-a m" ng-if="space.space_projects">
<div class="panel-heading b-b">
<h3 translate>{{ 'space_show.projects_using_the_space' }}</h3>
</div>
<ul class="widget-content list-group list-group-lg no-bg auto">
<li ng-repeat="project in space.space_projects" class="list-group-item no-b clearfix">
<a ui-sref="app.public.projects_show({id:project.slug})"><i class="fa"> </i> {{project.name}}</a>
</li>
</ul>
</section>
</div>
</div>
</div>

View File

@ -7,15 +7,15 @@
</div> </div>
<div class="col-xs-10 col-sm-10 col-md-8 b-l b-r-md"> <div class="col-xs-10 col-sm-10 col-md-8 b-l b-r-md">
<section class="heading-title"> <section class="heading-title">
<h1 ng-hide="training" translate>{{ 'trainings_planning' }}</h1> <h1 ng-show="mode == 'all'" translate>{{ 'trainings_planning' }}</h1>
<h1 ng-show="training"><span translate>{{ 'planning_of' }}</span> {{training.name}}</h1> <h1 ng-hide="mode == 'all'"><span translate>{{ 'planning_of' }}</span> {{training.name}}</h1>
</section> </section>
</div> </div>
<div class="col-xs-12 col-sm-12 col-md-3 b-t hide-b-md"> <div class="col-xs-12 col-sm-12 col-md-3 b-t hide-b-md">
<section class="heading-actions wrapper"> <section class="heading-actions wrapper">
<a class="btn btn-lg btn-warning bg-white b-2x rounded m-t-xs" <a class="btn btn-lg btn-warning bg-white b-2x rounded m-t-xs"
ui-sref="app.logged.trainings_reserve({id:'all'})" ui-sref="app.logged.trainings_reserve({id:'all'})"
ng-show="training" ng-hide="mode == 'all'"
role="button" role="button"
translate>{{ 'all_trainings' }}</a> translate>{{ 'all_trainings' }}</a>
</section> </section>
@ -33,165 +33,34 @@
<div class="col-sm-12 col-md-12 col-lg-3"> <div class="col-sm-12 col-md-12 col-lg-3">
<div class="text-center m-t">
</div>
<div ng-if="currentUser.role === 'admin'"> <div ng-if="currentUser.role === 'admin'">
<select-member></select-member> <select-member></select-member>
</div> </div>
<div class="widget panel b-a m m-t-lg" ng-if="ctrl.member && !slotToModify && !modifiedSlots">
<div class="panel-heading b-b small">
<h3 translate>{{ 'summary' }}</h3>
</div>
<div class="widget-content no-bg auto wrapper" ng-show="!selectedTraining && !paidTraining">
<p class="font-felt fleche-left text-lg"><%= image_tag("fleche-left.png", class: 'fleche-left visible-lg') %>
{{ 'select_a_slot_in_the_calendar' | translate }}</p>
</div>
<div class="widget-content no-bg auto wrapper" ng-if="selectedTraining"> <cart slot="selectedEvent"
<div class="font-sbold m-b-sm " translate>{{ 'you_ve_just_selected_the_slot' }}</div> slot-selection-time="selectionTime"
events="events"
user="ctrl.member"
mode-plans="plansAreShown"
plan="selectedPlan"
plan-selection-time="planSelectionTime"
settings="settings"
on-slot-added-to-cart="markSlotAsAdded"
on-slot-removed-from-cart="markSlotAsRemoved"
on-slot-start-to-modify="markSlotAsModifying"
on-slot-modify-success="modifyTrainingSlot"
on-slot-modify-cancel="cancelModifyTrainingSlot"
on-slot-modify-unselect="changeModifyTrainingSlot"
on-slot-cancel-success="slotCancelled"
after-payment="afterPayment"
reservable-id="{{training.id}}"
reservable-type="Training"
reservable-name="{{training.name}}"
limit-to-one-slot="true"></cart>
<div class="panel panel-default bg-light m-n">
<div class="panel-body">
<div class="font-sbold text-u-c">{{ 'datetime_to_time' | translate:{START_DATETIME:(selectedTraining.start | amDateFormat:'LLLL'), END_TIME:(selectedTraining.end | amDateFormat:'LT')} }}</div>
<div class="text-base">{{ 'training_cost_' | translate }} <span ng-class="{'text-blue': selectedTraining.training.amount == selectedTrainingAmount, 'red': selectedTraining.training.amount != selectedTrainingAmount}">{{selectedTrainingAmount | currency}}</span></div>
<div ng-show="currentUser.role == 'admin'" class="m-t">
<label for="offerSlot" class="control-label m-r" translate>{{ 'offer_this_training' }}</label>
<input bs-switch
ng-model="selectedTraining.offered"
id="offerSlot"
type="checkbox"
class="form-control"
switch-on-text="{{ 'yes' | translate }}"
switch-off-text="{{ 'no' | translate }}"
switch-animate="true"
switch-readonly="{{trainingIsValid}}"
ng-change="updatePrices()"/>
</div>
</div>
<div class="panel-footer no-padder">
<button class="btn btn-valid btn-warning btn-block text-u-c r-b" ng-click="validTraining()" ng-if="!trainingIsValid" translate>{{ 'confirm_this_slot' }}</button>
</div>
</div>
<div class="clear">
<a class="pull-right m-t-xs text-u-l" href="#" ng-click="removeTraining($event)" ng-if="trainingIsValid" translate>{{ 'remove_this_slot' }}</a>
</div>
<coupon show="trainingIsValid && (!plansIsShow || selectedPlan)" coupon="coupon.applied" user-id="{{ctrl.member.id}}" total="totalNoCoupon"></coupon>
<span ng-hide="fablabWithoutPlans">
<div ng-if="trainingIsValid && !ctrl.member.subscribed_plan" ng-show="!plansIsShow">
<p class="font-sbold text-base l-h-2x" translate>{{ 'to_benefit_from_attractive_prices_and_a_free_training' }}</p>
<div><button class="btn btn-warning-full rounded btn-block text-xs" ng-click="showPlans()" translate>{{ 'view_our_subscriptions' }}</button></div>
<p class="font-bold text-base text-u-c text-center m-b-xs">ou</p>
</div>
<div ng-if="selectedPlan">
<div class="m-t-md m-b-sm text-base">{{ 'you_ve_just_selected_a_' | translate }} <br> <span class="font-sbold" translate>{{ '_subscription' }}</span> :</div>
<div class="panel panel-default bg-light m-n">
<div class="panel-body m-b-md">
<div class="font-sbold text-u-c">{{ selectedPlan | humanReadablePlanName }}</div>
<div class="text-base">{{ 'subscription_cost' | translate }} <span class="text-blue">{{selectedPlan.amount | currency}}</span></div>
</div>
</div>
</div>
</span>
</div>
<div class="panel-footer no-padder" ng-if="selectedTraining">
<button class="btn btn-valid btn-info btn-block p-l btn-lg text-u-c r-b text-base" ng-click="payTraining()" ng-if="trainingIsValid && (!plansIsShow || selectedPlan)">{{ 'confirm_and_pay' | translate }} {{amountTotal | currency}}</button>
</div>
<div class="widget-content no-bg auto wrapper" ng-if="paidTraining">
<div class="m-t-md m-b-sm text-base">{{ 'you_have_settled_the_training' | translate }} <br> <span class="font-sbold">{{paidTraining.training.name}}</span> :
</div>
<div class="well well-warning m-t-sm">
<i class="font-sbold text-u-c">{{ 'datetime_to_time' | translate:{START_DATETIME:(paidTraining.start | amDateFormat:'LLLL'), END_TIME:(paidTraining.end | amDateFormat:'LT') } }} </i>
<div class="font-sbold">{{ 'training_cost_' | translate }} {{paidTraining.training.amount | currency}}</div>
</div>
<div ng-if="selectedPlan">
<div class="m-t-md m-b-sm text-base">{{ 'you_have_settled_a_' | translate }} <br> <span class="font-sbold" translate>{{ '_subscription' }}</span> :</div>
<div class="well well-warning m-t-sm">
<i class="font-sbold text-u-c">{{selectedPlan | humanReadablePlanName }}</i>
<div class="font-sbold">{{ 'subscription_cost' | translate }} {{selectedPlan.amount | currency}}</div>
</div>
</div>
<div class="m-t-md font-sbold">{{ 'total_' | translate }} {{amountTotal | currency}}</div>
<div class="alert alert-success" ng-if="ctrl.member.subscribed_plan">{{ 'thank_you_your_payment_has_been_successfully_registered' | translate }}<br>
{{ 'your_invoice_will_be_available_soon_from_your_' | translate }} <a ui-sref="app.logged.dashboard.invoices" translate>{{ 'dashboard' }}</a>
</div>
</div>
</div>
<div class="widget panel b-a m m-t-lg" ng-if="slotToModify || modifiedSlots">
<div class="panel-heading b-b small">
<h3 translate>{{ 'summary' }}</h3>
</div>
<div class="widget-content no-bg auto wrapper" ng-if="slotToModify">
<div class="font-sbold m-b-sm " translate>{{ 'i_want_to_change_the_following_reservation' }}</div>
<div class="panel panel-warning bg-yellow">
<div class="panel-body">
<div class="font-sbold text-u-c">{{ 'datetime_to_time' | translate:{START_DATETIME:(slotToModify.start | amDateFormat:'LLLL'), END_TIME:(slotToModify.end | amDateFormat:'LT') } }}</div>
</div>
<div class="clear"><a class="pull-right m-b-sm text-u-l ng-scope m-r-sm" href="#" ng-click="removeSlotToModify($event)" translate>{{ 'cancel_my_modification' }}</a></div>
</div>
<div class="widget-content no-bg">
<p class="font-felt fleche-left text-lg"><%= image_tag("fleche-left.png", class: 'fleche-left visible-lg') %>
{{ 'select_a_new_slot_in_the_calendar' | translate }}</p>
</div>
<div class="panel panel-info bg-info text-white" ng-if="slotToPlace">
<div class="panel-body">
<div class="font-sbold text-u-c">{{ 'datetime_to_time' | translate:{START_DATETIME:(slotToPlace.start | amDateFormat:'LLLL'), END_TIME:(slotToPlace.end | amDateFormat:'LT') } }}</div>
</div>
<div class="clear"><a class="pull-right m-b-sm text-u-l ng-scope m-r-sm" href="#" ng-click="removeSlotToPlace($event)" translate>{{ 'cancel_my_selection' }}</a></div>
</div>
</div>
<div class="panel-footer no-padder" ng-if="slotToModify && slotToPlace">
<button class="btn btn-invalid btn-default btn-block p-l btn-lg text-u-c r-n text-base" ng-click="cancelModifyMachineSlot()" translate>{{ 'cancel' }}</button>
<div>
<button class="btn btn-valid btn-info btn-block p-l btn-lg text-u-c r-b text-base" ng-click="modifyTrainingSlot()" translate>{{ 'confirm_my_modification' }}</button>
</div>
</div>
<div class="widget-content no-bg auto wrapper" ng-if="modifiedSlots">
<div class="font-sbold m-b-sm " translate>{{ 'your_booking_slot_was_successfully_moved_from_' }}</div>
<div class="panel panel-default bg-light">
<div class="panel-body">
<div class="font-sbold text-u-c">{{ 'datetime_to_time' | translate:{START_DATETIME:(modifiedSlots.oldReservedSlot.start | amDateFormat:'LLLL'), END_TIME:(modifiedSlots.oldReservedSlot.end | amDateFormat:'LT') } }}</div>
</div>
</div>
<p class="text-center font-bold m-b-sm text-u-c" translate>{{ 'to_date' }}</p>
<div class="panel panel-success bg-success bg-light">
<div class="panel-body">
<div class="font-sbold text-u-c">{{ 'datetime_to_time' | translate:{START_DATETIME:(modifiedSlots.newReservedSlot.start | amDateFormat:'LLLL'), END_TIME:(modifiedSlots.newReservedSlot.end | amDateFormat:'LT') } }}</div>
</div>
</div>
</div>
</div>
<uib-alert type="info m"> <uib-alert type="info m">
<p class="text-sm font-bold"> <p class="text-sm font-bold">
<i class="fa fa-lightbulb-o"></i> <i class="fa fa-lightbulb-o"></i>

View File

@ -1,26 +1,36 @@
class API::AvailabilitiesController < API::ApiController class API::AvailabilitiesController < API::ApiController
include FablabConfiguration
before_action :authenticate_user!, except: [:public] before_action :authenticate_user!, except: [:public]
before_action :set_availability, only: [:show, :update, :destroy, :reservations] before_action :set_availability, only: [:show, :update, :destroy, :reservations]
respond_to :json respond_to :json
## machine availabilities are divided in multiple slots of 60 minutes ## machine/spaces availabilities are divided in multiple slots of 60 minutes
SLOT_DURATION = 60 SLOT_DURATION = 60
def index def index
authorize Availability authorize Availability
start_date = ActiveSupport::TimeZone[params[:timezone]].parse(params[:start]) start_date = ActiveSupport::TimeZone[params[:timezone]].parse(params[:start])
end_date = ActiveSupport::TimeZone[params[:timezone]].parse(params[:end]).end_of_day end_date = ActiveSupport::TimeZone[params[:timezone]].parse(params[:end]).end_of_day
@availabilities = Availability.includes(:machines,:tags,:trainings).where.not(available_type: 'event') @availabilities = Availability.includes(:machines, :tags, :trainings, :spaces).where.not(available_type: 'event')
.where('start_at >= ? AND end_at <= ?', start_date, end_date) .where('start_at >= ? AND end_at <= ?', start_date, end_date)
if fablab_spaces_deactivated?
@availabilities = @availabilities.where.not(available_type: 'space')
end
end end
def public def public
start_date = ActiveSupport::TimeZone[params[:timezone]].parse(params[:start]) start_date = ActiveSupport::TimeZone[params[:timezone]].parse(params[:start])
end_date = ActiveSupport::TimeZone[params[:timezone]].parse(params[:end]).end_of_day end_date = ActiveSupport::TimeZone[params[:timezone]].parse(params[:end]).end_of_day
@reservations = Reservation.includes(:slots, user: [:profile]).references(:slots, :user).where('slots.start_at >= ? AND slots.end_at <= ?', start_date, end_date) @reservations = Reservation.includes(:slots, user: [:profile]).references(:slots, :user).where('slots.start_at >= ? AND slots.end_at <= ?', start_date, end_date)
# request for 1 single day
if in_same_day(start_date, end_date) if in_same_day(start_date, end_date)
@training_and_event_availabilities = Availability.includes(:tags, :trainings, :event, :slots).where(available_type: ['training', 'event']) # trainings, events
@training_and_event_availabilities = Availability.includes(:tags, :trainings, :event, :slots).where(available_type: %w(training event))
.where('start_at >= ? AND end_at <= ?', start_date, end_date) .where('start_at >= ? AND end_at <= ?', start_date, end_date)
# machines
@machine_availabilities = Availability.includes(:tags, :machines).where(available_type: 'machines') @machine_availabilities = Availability.includes(:tags, :machines).where(available_type: 'machines')
.where('start_at >= ? AND end_at <= ?', start_date, end_date) .where('start_at >= ? AND end_at <= ?', start_date, end_date)
@machine_slots = [] @machine_slots = []
@ -35,14 +45,38 @@ class API::AvailabilitiesController < API::ApiController
end end
end end
end end
@availabilities = [].concat(@training_and_event_availabilities).concat(@machine_slots)
# spaces
@space_availabilities = Availability.includes(:tags, :spaces).where(available_type: 'space')
.where('start_at >= ? AND end_at <= ?', start_date, end_date)
if params[:s]
@space_availabilities.where(available_id: params[:s])
end
@space_slots = []
@space_availabilities.each do |a|
space = a.spaces.first
((a.end_at - a.start_at)/SLOT_DURATION.minutes).to_i.times do |i|
if (a.start_at + (i * SLOT_DURATION).minutes) > Time.now
slot = Slot.new(start_at: a.start_at + (i*SLOT_DURATION).minutes, end_at: a.start_at + (i*SLOT_DURATION).minutes + SLOT_DURATION.minutes, availability_id: a.id, availability: a, space: space, title: space.name)
slot = verify_space_is_reserved(slot, @reservations, current_user, '')
@space_slots << slot
end
end
end
@availabilities = [].concat(@training_and_event_availabilities).concat(@machine_slots).concat(@space_slots)
# request for many days (week or month)
else else
@availabilities = Availability.includes(:tags, :machines, :trainings, :event, :slots) @availabilities = Availability.includes(:tags, :machines, :trainings, :spaces, :event, :slots)
.where('start_at >= ? AND end_at <= ?', start_date, end_date) .where('start_at >= ? AND end_at <= ?', start_date, end_date)
@availabilities.each do |a| @availabilities.each do |a|
if a.available_type != 'machines' if a.available_type == 'training' or a.available_type == 'event'
a = verify_training_event_is_reserved(a, @reservations) a = verify_training_event_is_reserved(a, @reservations, current_user)
elsif a.available_type == 'space'
a.is_reserved = is_reserved_availability(a, current_user.id)
end end
end end
end end
@ -90,7 +124,7 @@ class API::AvailabilitiesController < API::ApiController
@user = current_user @user = current_user
end end
@current_user_role = current_user.is_admin? ? 'admin' : 'user' @current_user_role = current_user.is_admin? ? 'admin' : 'user'
@machine = Machine.find(params[:machine_id]) @machine = Machine.friendly.find(params[:machine_id])
@slots = [] @slots = []
@reservations = Reservation.where('reservable_type = ? and reservable_id = ?', @machine.class.to_s, @machine.id).includes(:slots, user: [:profile]).references(:slots, :user).where('slots.start_at > ?', Time.now) @reservations = Reservation.where('reservable_type = ? and reservable_id = ?', @machine.class.to_s, @machine.id).includes(:slots, user: [:profile]).references(:slots, :user).where('slots.start_at > ?', Time.now)
if @user.is_admin? if @user.is_admin?
@ -135,7 +169,7 @@ class API::AvailabilitiesController < API::ApiController
# who made the request? # who made the request?
# 1) an admin (he can see all future availabilities) # 1) an admin (he can see all future availabilities)
if @user.is_admin? if current_user.is_admin?
@availabilities = @availabilities.includes(:tags, :slots, trainings: [:machines]).where('availabilities.start_at > ?', Time.now) @availabilities = @availabilities.includes(:tags, :slots, trainings: [:machines]).where('availabilities.start_at > ?', Time.now)
# 2) an user (he cannot see availabilities further than 1 (or 3) months) # 2) an user (he cannot see availabilities further than 1 (or 3) months)
else else
@ -146,13 +180,46 @@ class API::AvailabilitiesController < API::ApiController
# finally, we merge the availabilities with the reservations # finally, we merge the availabilities with the reservations
@availabilities.each do |a| @availabilities.each do |a|
a = verify_training_event_is_reserved(a, @reservations) a = verify_training_event_is_reserved(a, @reservations, @user)
end
end
def spaces
if params[:member_id]
@user = User.find(params[:member_id])
else
@user = current_user
end
@current_user_role = current_user.is_admin? ? 'admin' : 'user'
@space = Space.friendly.find(params[:space_id])
@slots = []
@reservations = Reservation.where('reservable_type = ? and reservable_id = ?', @space.class.to_s, @space.id).includes(:slots, user: [:profile]).references(:slots, :user).where('slots.start_at > ?', Time.now)
if @user.is_admin?
@availabilities = @space.availabilities.includes(:tags).where("end_at > ? AND available_type = 'space'", Time.now)
else
end_at = 1.month.since
end_at = 3.months.since if is_subscription_year(@user)
@availabilities = @space.availabilities.includes(:tags).where("end_at > ? AND end_at < ? AND available_type = 'space'", Time.now, end_at).where('availability_tags.tag_id' => @user.tag_ids.concat([nil]))
end
@availabilities.each do |a|
((a.end_at - a.start_at)/SLOT_DURATION.minutes).to_i.times do |i|
if (a.start_at + (i * SLOT_DURATION).minutes) > Time.now
slot = Slot.new(start_at: a.start_at + (i*SLOT_DURATION).minutes, end_at: a.start_at + (i*SLOT_DURATION).minutes + SLOT_DURATION.minutes, availability_id: a.id, availability: a, space: @space, title: '')
slot = verify_space_is_reserved(slot, @reservations, @user, @current_user_role)
@slots << slot
end
end
end
@slots.each do |s|
if s.is_complete? and not s.is_reserved
s.title = t('availabilities.not_available')
end
end end
end end
def reservations def reservations
authorize Availability authorize Availability
@reservation_slots = @availability.slots.includes(reservation: [user: [:profile]]).order('slots.start_at ASC') @reservation_slots = @availability.slots.includes(reservations: [user: [:profile]]).order('slots.start_at ASC')
end end
private private
@ -161,10 +228,20 @@ class API::AvailabilitiesController < API::ApiController
end end
def availability_params def availability_params
params.require(:availability).permit(:start_at, :end_at, :available_type, :machine_ids, :training_ids, :nb_total_places, machine_ids: [], training_ids: [], tag_ids: [], params.require(:availability).permit(:start_at, :end_at, :available_type, :machine_ids, :training_ids, :nb_total_places, machine_ids: [], training_ids: [], space_ids: [], tag_ids: [],
:machines_attributes => [:id, :_destroy]) :machines_attributes => [:id, :_destroy])
end end
def is_reserved_availability(availability, user_id)
reserved_slots = []
availability.slots.each do |s|
if s.canceled_at.nil?
reserved_slots << s
end
end
reserved_slots.map(&:reservations).flatten.map(&:user_id).include? user_id
end
def is_reserved(start_at, reservations) def is_reserved(start_at, reservations)
is_reserved = false is_reserved = false
reservations.each do |r| reservations.each do |r|
@ -184,7 +261,7 @@ class API::AvailabilitiesController < API::ApiController
slot.is_reserved = true slot.is_reserved = true
slot.title = "#{slot.machine.name} - #{t('availabilities.not_available')}" slot.title = "#{slot.machine.name} - #{t('availabilities.not_available')}"
slot.can_modify = true if user_role === 'admin' slot.can_modify = true if user_role === 'admin'
slot.reservation = r slot.reservations.push r
end end
if s.start_at == slot.start_at and r.user == user and s.canceled_at == nil if s.start_at == slot.start_at and r.user == user and s.canceled_at == nil
slot.title = "#{slot.machine.name} - #{t('availabilities.i_ve_reserved')}" slot.title = "#{slot.machine.name} - #{t('availabilities.i_ve_reserved')}"
@ -197,8 +274,27 @@ class API::AvailabilitiesController < API::ApiController
slot slot
end end
def verify_training_event_is_reserved(availability, reservations) def verify_space_is_reserved(slot, reservations, user, user_role)
user = current_user reservations.each do |r|
r.slots.each do |s|
if slot.space.id == r.reservable_id
if s.start_at == slot.start_at and s.canceled_at == nil
slot.can_modify = true if user_role === 'admin'
slot.reservations.push r
end
if s.start_at == slot.start_at and r.user == user and s.canceled_at == nil
slot.id = s.id
slot.title = t('availabilities.i_ve_reserved')
slot.can_modify = true
slot.is_reserved = true
end
end
end
end
slot
end
def verify_training_event_is_reserved(availability, reservations, user)
reservations.each do |r| reservations.each do |r|
r.slots.each do |s| r.slots.each do |s|
if ((availability.available_type == 'training' and availability.trainings.first.id == r.reservable_id) or (availability.available_type == 'event' and availability.event.id == r.reservable_id)) and s.start_at == availability.start_at and s.canceled_at == nil if ((availability.available_type == 'training' and availability.trainings.first.id == r.reservable_id) or (availability.available_type == 'event' and availability.event.id == r.reservable_id)) and s.start_at == availability.start_at and s.canceled_at == nil
@ -239,6 +335,12 @@ class API::AvailabilitiesController < API::ApiController
availabilities_filtered << a availabilities_filtered << a
end end
end end
# space
if params[:s] and a.available_type == 'space'
if params[:s].include?(a.spaces.first.id.to_s)
availabilities_filtered << a
end
end
# machines # machines
if params[:m] and a.available_type == 'machines' if params[:m] and a.available_type == 'machines'
if (params[:m].map(&:to_i) & a.machine_ids).any? if (params[:m].map(&:to_i) & a.machine_ids).any?

View File

@ -44,7 +44,7 @@ class API::PricesController < API::ApiController
@amount = {elements: nil, total: 0, before_coupon: 0} @amount = {elements: nil, total: 0, before_coupon: 0}
else else
_reservable = _price_params[:reservable_type].constantize.find(_price_params[:reservable_id]) _reservable = _price_params[:reservable_type].constantize.find(_price_params[:reservable_id])
@amount = Price.compute(current_user.is_admin?, _user, _reservable, _price_params[:slots_attributes], _price_params[:plan_id], _price_params[:nb_reserve_places], _price_params[:tickets_attributes], coupon_params[:coupon_code]) @amount = Price.compute(current_user.is_admin?, _user, _reservable, _price_params[:slots_attributes] || [], _price_params[:plan_id], _price_params[:nb_reserve_places], _price_params[:tickets_attributes], coupon_params[:coupon_code])
end end

View File

@ -0,0 +1,49 @@
class API::SpacesController < API::ApiController
before_action :authenticate_user!, except: [:index, :show]
respond_to :json
def index
@spaces = Space.includes(:space_image)
end
def show
@space = Space.includes(:space_files, :projects).friendly.find(params[:id])
end
def create
authorize Space
@space = Space.new(space_params)
if @space.save
render :show, status: :created, location: @space
else
render json: @space.errors, status: :unprocessable_entity
end
end
def update
authorize Space
@space = get_space
if @space.update(space_params)
render :show, status: :ok, location: @space
else
render json: @space.errors, status: :unprocessable_entity
end
end
def destroy
@space = get_space
authorize @space
@space.destroy
head :no_content
end
private
def get_space
Space.friendly.find(params[:id])
end
def space_params
params.require(:space).permit(:name, :description, :characteristics, :default_places, space_image_attributes: [:attachment],
space_files_attributes: [:id, :attachment, :_destroy])
end
end

View File

@ -6,7 +6,7 @@ class API::StatisticsController < API::ApiController
@statistics = StatisticIndex.all @statistics = StatisticIndex.all
end end
%w(account event machine project subscription training user).each do |path| %w(account event machine project subscription training user space).each do |path|
class_eval %{ class_eval %{
def #{path} def #{path}
authorize :statistic, :#{path}? authorize :statistic, :#{path}?

View File

@ -2,4 +2,8 @@ module FablabConfiguration
def fablab_plans_deactivated? def fablab_plans_deactivated?
Rails.application.secrets.fablab_without_plans Rails.application.secrets.fablab_without_plans
end end
def fablab_spaces_deactivated?
Rails.application.secrets.fablab_without_spaces
end
end end

View File

@ -1,23 +1,41 @@
module AvailabilityHelper module AvailabilityHelper
MACHINE_COLOR = '#e4cd78' MACHINE_COLOR = '#e4cd78'
TRAINING_COLOR = '#bd7ae9' TRAINING_COLOR = '#bd7ae9'
SPACE_COLOR = '#3fc7ff'
EVENT_COLOR = '#dd7e6b' EVENT_COLOR = '#dd7e6b'
IS_RESERVED_BY_CURRENT_USER = '#b2e774' IS_RESERVED_BY_CURRENT_USER = '#b2e774'
MACHINE_IS_RESERVED_BY_USER = '#1d98ec' MACHINE_IS_RESERVED_BY_USER = '#1d98ec'
IS_COMPLETED = '#eeeeee' IS_COMPLETED = '#eeeeee'
def availability_border_color(availability) def availability_border_color(availability)
if availability.available_type == 'machines' case availability.available_type
MACHINE_COLOR when 'machines'
elsif availability.available_type == 'training' MACHINE_COLOR
TRAINING_COLOR when 'training'
else TRAINING_COLOR
EVENT_COLOR when 'space'
SPACE_COLOR
else
EVENT_COLOR
end end
end end
def machines_slot_border_color(slot) def machines_slot_border_color(slot)
slot.is_reserved ? (slot.is_reserved_by_current_user ? IS_RESERVED_BY_CURRENT_USER : IS_COMPLETED) : MACHINE_COLOR if slot.is_reserved
slot.is_reserved_by_current_user ? IS_RESERVED_BY_CURRENT_USER : IS_COMPLETED
else
MACHINE_COLOR
end
end
def space_slot_border_color(slot)
if slot.is_reserved
IS_RESERVED_BY_CURRENT_USER
elsif slot.is_complete?
IS_COMPLETED
else
SPACE_COLOR
end
end end
def trainings_events_border_color(availability) def trainings_events_border_color(availability)
@ -26,10 +44,15 @@ module AvailabilityHelper
elsif availability.is_completed elsif availability.is_completed
IS_COMPLETED IS_COMPLETED
else else
if availability.available_type == 'training' case availability.available_type
TRAINING_COLOR when 'training'
else TRAINING_COLOR
EVENT_COLOR when 'event'
EVENT_COLOR
when 'space'
SPACE_COLOR
else
'#000'
end end
end end
end end

View File

@ -1,5 +1,8 @@
class Availability < ActiveRecord::Base class Availability < ActiveRecord::Base
## machine/spaces availabilities are divided in multiple slots of 60 minutes
SLOT_DURATION = 60
# elastic initialisations # elastic initialisations
include Elasticsearch::Model include Elasticsearch::Model
index_name 'fablab' index_name 'fablab'
@ -12,6 +15,9 @@ class Availability < ActiveRecord::Base
has_many :trainings_availabilities, dependent: :destroy has_many :trainings_availabilities, dependent: :destroy
has_many :trainings, through: :trainings_availabilities has_many :trainings, through: :trainings_availabilities
has_many :spaces_availabilities, dependent: :destroy
has_many :spaces, through: :spaces_availabilities
has_many :slots has_many :slots
has_many :reservations, through: :slots has_many :reservations, through: :slots
@ -23,6 +29,7 @@ class Availability < ActiveRecord::Base
scope :machines, -> { where(available_type: 'machines') } scope :machines, -> { where(available_type: 'machines') }
scope :trainings, -> { includes(:trainings).where(available_type: 'training') } scope :trainings, -> { includes(:trainings).where(available_type: 'training') }
scope :spaces, -> { includes(:spaces).where(available_type: 'space') }
attr_accessor :is_reserved, :slot_id, :can_modify attr_accessor :is_reserved, :slot_id, :can_modify
@ -43,10 +50,18 @@ class Availability < ActiveRecord::Base
end end
def safe_destroy def safe_destroy
if available_type == 'machines' case available_type
reservations = Reservation.where(reservable_type: 'Machine', reservable_id: machine_ids).joins(:slots).where('slots.availability_id = ?', id) when 'machines'
else reservations = Reservation.where(reservable_type: 'Machine', reservable_id: machine_ids).joins(:slots).where('slots.availability_id = ?', id)
reservations = Reservation.where(reservable_type: 'Training', reservable_id: training_ids).joins(:slots).where('slots.availability_id = ?', id) when 'training'
reservations = Reservation.where(reservable_type: 'Training', reservable_id: training_ids).joins(:slots).where('slots.availability_id = ?', id)
when 'space'
reservations = Reservation.where(reservable_type: 'Space', reservable_id: space_ids).joins(:slots).where('slots.availability_id = ?', id)
when 'event'
reservations = Reservation.where(reservable_type: 'Event', reservable_id: event&.id).joins(:slots).where('slots.availability_id = ?', id)
else
STDERR.puts "[safe_destroy] Availability with unknown type #{available_type}"
reservations = []
end end
if reservations.size == 0 if reservations.size == 0
# this update may not call any rails callbacks, that's why we use direct SQL update # this update may not call any rails callbacks, that's why we use direct SQL update
@ -57,16 +72,29 @@ class Availability < ActiveRecord::Base
end end
end end
## compute the total number of places over the whole space availability
def available_space_places
if available_type === 'space'
((end_at - start_at)/SLOT_DURATION.minutes).to_i * nb_total_places
end
end
def title(filter = {}) def title(filter = {})
if available_type == 'machines' case available_type
if filter[:machine_ids] when 'machines'
return machines.to_ary.delete_if {|m| !filter[:machine_ids].include?(m.id)}.map(&:name).join(' - ') if filter[:machine_ids]
end return machines.to_ary.delete_if {|m| !filter[:machine_ids].include?(m.id)}.map(&:name).join(' - ')
return machines.map(&:name).join(' - ') end
elsif available_type == 'event' return machines.map(&:name).join(' - ')
event.name when 'event'
else event.name
trainings.map(&:name).join(' - ') when 'training'
trainings.map(&:name).join(' - ')
when 'space'
spaces.map(&:name).join(' - ')
else
STDERR.puts "[title] Availability with unknown type #{available_type}"
'???'
end end
end end
@ -74,23 +102,23 @@ class Availability < ActiveRecord::Base
# if haven't defined a nb_total_places, places are unlimited # if haven't defined a nb_total_places, places are unlimited
def is_completed def is_completed
return false if nb_total_places.blank? return false if nb_total_places.blank?
if available_type == 'training' if available_type == 'training' || available_type == 'space'
nb_total_places <= slots.to_a.select {|s| s.canceled_at == nil }.size nb_total_places <= slots.to_a.select {|s| s.canceled_at == nil }.size
elsif available_type == 'event' elsif available_type == 'event'
event.nb_free_places == 0 event.nb_free_places == 0
end end
end end
# TODO: refactoring this function for avoid N+1 query
def nb_total_places def nb_total_places
if available_type == 'training' case available_type
if read_attribute(:nb_total_places).present? when 'training'
read_attribute(:nb_total_places) super.presence || trainings.map {|t| t.nb_total_places}.reduce(:+)
when 'event'
event.nb_total_places
when 'space'
super.presence || spaces.map {|s| s.default_places}.reduce(:+)
else else
trainings.first.nb_total_places unless trainings.empty? nil
end
elsif available_type == 'event'
event.nb_total_places
end end
end end
@ -98,12 +126,17 @@ class Availability < ActiveRecord::Base
def as_indexed_json def as_indexed_json
json = JSON.parse(to_json) json = JSON.parse(to_json)
json['hours_duration'] = (end_at - start_at) / (60 * 60) json['hours_duration'] = (end_at - start_at) / (60 * 60)
if available_type == 'machines' case available_type
json['subType'] = machines_availabilities.map{|ma| ma.machine.friendly_id} when 'machines'
elsif available_type == 'training' json['subType'] = machines_availabilities.map{|ma| ma.machine.friendly_id}
json['subType'] = trainings_availabilities.map{|ta| ta.training.friendly_id} when'training'
elsif available_type == 'event' json['subType'] = trainings_availabilities.map{|ta| ta.training.friendly_id}
json['subType'] = [event.category.friendly_id] when 'event'
json['subType'] = [event.category.friendly_id]
when 'space'
json['subType'] = spaces_availabilities.map{|sa| sa.space.friendly_id}
else
json['subType'] = []
end end
json['bookable_hours'] = json['hours_duration'] * json['subType'].length json['bookable_hours'] = json['hours_duration'] * json['subType'].length
json['date'] = start_at.to_date json['date'] = start_at.to_date

View File

@ -204,7 +204,9 @@ class Invoice < ActiveRecord::Base
private private
def generate_and_send_invoice def generate_and_send_invoice
puts "Creating an InvoiceWorker job to generate the following invoice: id(#{id}), invoiced_id(#{invoiced_id}), invoiced_type(#{invoiced_type}), user_id(#{user_id})" unless Rails.env.test?
puts "Creating an InvoiceWorker job to generate the following invoice: id(#{id}), invoiced_id(#{invoiced_id}), invoiced_type(#{invoiced_type}), user_id(#{user_id})"
end
InvoiceWorker.perform_async(id) InvoiceWorker.perform_async(id)
end end

View File

@ -4,7 +4,7 @@ class MachinesAvailability < ActiveRecord::Base
after_destroy :cleanup_availability after_destroy :cleanup_availability
# when the MachinesAvailability is deleted (from Machine destroy cascade), we delete the corresponding # when the MachinesAvailability is deleted (from Machine destroy cascade), we delete the corresponding
# availability if the deleted machine was the last is this availability slot and teh availability is not # availability if the deleted machine was the last of this availability slot, and the availability is not
# currently being destroyed. # currently being destroyed.
def cleanup_availability def cleanup_availability
unless availability.destroying unless availability.destroying

View File

@ -4,6 +4,7 @@ class Plan < ActiveRecord::Base
has_many :credits, dependent: :destroy has_many :credits, dependent: :destroy
has_many :training_credits, -> {where(creditable_type: 'Training')}, class_name: 'Credit' has_many :training_credits, -> {where(creditable_type: 'Training')}, class_name: 'Credit'
has_many :machine_credits, -> {where(creditable_type: 'Machine')}, class_name: 'Credit' has_many :machine_credits, -> {where(creditable_type: 'Machine')}, class_name: 'Credit'
has_many :space_credits, -> {where(creditable_type: 'Space')}, class_name: 'Credit'
has_many :subscriptions has_many :subscriptions
has_one :plan_image, as: :viewable, dependent: :destroy has_one :plan_image, as: :viewable, dependent: :destroy
has_one :plan_file, as: :viewable, dependent: :destroy has_one :plan_file, as: :viewable, dependent: :destroy
@ -18,6 +19,7 @@ class Plan < ActiveRecord::Base
after_update :update_stripe_plan, if: :amount_changed? after_update :update_stripe_plan, if: :amount_changed?
after_create :create_stripe_plan, unless: :skip_create_stripe_plan after_create :create_stripe_plan, unless: :skip_create_stripe_plan
after_create :create_machines_prices after_create :create_machines_prices
after_create :create_spaces_prices
after_create :create_statistic_type after_create :create_statistic_type
after_destroy :delete_stripe_plan after_destroy :delete_stripe_plan
@ -56,6 +58,12 @@ class Plan < ActiveRecord::Base
end end
end end
def create_spaces_prices
Space.all.each do |space|
Price.create(priceable: space, plan: self, group_id: self.group_id, amount: 0)
end
end
def duration def duration
interval_count.send(interval) interval_count.send(interval)
end end

View File

@ -31,6 +31,7 @@ class Price < ActiveRecord::Base
new_plan_being_bought = true new_plan_being_bought = true
else else
plan = nil plan = nil
new_plan_being_bought = false
end end
# === compute reservation price === # === compute reservation price ===
@ -41,13 +42,13 @@ class Price < ActiveRecord::Base
when Machine when Machine
base_amount = reservable.prices.find_by(group_id: user.group_id, plan_id: plan.try(:id)).amount base_amount = reservable.prices.find_by(group_id: user.group_id, plan_id: plan.try(:id)).amount
if plan if plan
machine_credit = plan.machine_credits.select {|credit| credit.creditable_id == reservable.id}.first space_credit = plan.machine_credits.select {|credit| credit.creditable_id == reservable.id}.first
if machine_credit if space_credit
hours_available = machine_credit.hours hours_available = space_credit.hours
if !new_plan_being_bought unless new_plan_being_bought
user_credit = user.users_credits.find_by(credit_id: machine_credit.id) user_credit = user.users_credits.find_by(credit_id: space_credit.id)
if user_credit if user_credit
hours_available = machine_credit.hours - user_credit.hours_used hours_available = space_credit.hours - user_credit.hours_used
end end
end end
slots.each_with_index do |slot, index| slots.each_with_index do |slot, index|
@ -69,18 +70,18 @@ class Price < ActiveRecord::Base
amount = reservable.amount_by_group(user.group_id).amount amount = reservable.amount_by_group(user.group_id).amount
if plan if plan
# Return True if the subscription link a training credit for training reserved by the user # Return True if the subscription link a training credit for training reserved by the user
training_is_creditable = plan.training_credits.select {|credit| credit.creditable_id == reservable.id}.size > 0 space_is_creditable = plan.training_credits.select {|credit| credit.creditable_id == reservable.id}.size > 0
# Training reserved by the user is free when : # Training reserved by the user is free when :
# |-> the user already has a current subscription and if training_is_creditable is true and has at least one credit available. # |-> the user already has a current subscription and if space_is_creditable is true and has at least one credit available.
if !new_plan_being_bought if !new_plan_being_bought
if user.training_credits.size < plan.training_credit_nb and training_is_creditable if user.training_credits.size < plan.training_credit_nb and space_is_creditable
amount = 0 amount = 0
end end
# |-> the user buys a new subscription and if training_is_creditable is true. # |-> the user buys a new subscription and if space_is_creditable is true.
else else
if training_is_creditable if space_is_creditable
amount = 0 amount = 0
end end
end end
@ -99,6 +100,34 @@ class Price < ActiveRecord::Base
_amount += get_slot_price(amount, slot, admin, _elements) _amount += get_slot_price(amount, slot, admin, _elements)
end end
# Space reservation
when Space
base_amount = reservable.prices.find_by(group_id: user.group_id, plan_id: plan.try(:id)).amount
if plan
space_credit = plan.space_credits.select {|credit| credit.creditable_id == reservable.id}.first
if space_credit
hours_available = space_credit.hours
unless new_plan_being_bought
user_credit = user.users_credits.find_by(credit_id: space_credit.id)
if user_credit
hours_available = space_credit.hours - user_credit.hours_used
end
end
slots.each_with_index do |slot, index|
_amount += get_slot_price(base_amount, slot, admin, _elements, (index < hours_available))
end
else
slots.each do |slot|
_amount += get_slot_price(base_amount, slot, admin, _elements)
end
end
else
slots.each do |slot|
_amount += get_slot_price(base_amount, slot, admin, _elements)
end
end
# Unknown reservation type # Unknown reservation type
else else
raise NotImplementedError raise NotImplementedError

View File

@ -21,6 +21,7 @@ class Project < ActiveRecord::Base
accepts_nested_attributes_for :project_caos, allow_destroy: true, reject_if: :all_blank accepts_nested_attributes_for :project_caos, allow_destroy: true, reject_if: :all_blank
has_and_belongs_to_many :machines, join_table: :projects_machines has_and_belongs_to_many :machines, join_table: :projects_machines
has_and_belongs_to_many :spaces, join_table: :projects_spaces
has_and_belongs_to_many :components, join_table: :projects_components has_and_belongs_to_many :components, join_table: :projects_components
has_and_belongs_to_many :themes, join_table: :projects_themes has_and_belongs_to_many :themes, join_table: :projects_themes

View File

@ -2,7 +2,10 @@ class Reservation < ActiveRecord::Base
include NotifyWith::NotificationAttachedObject include NotifyWith::NotificationAttachedObject
belongs_to :user belongs_to :user
has_many :slots, dependent: :destroy
has_many :slots_reservations, dependent: :destroy
has_many :slots, through: :slots_reservations
accepts_nested_attributes_for :slots, allow_destroy: true accepts_nested_attributes_for :slots, allow_destroy: true
belongs_to :reservable, polymorphic: true belongs_to :reservable, polymorphic: true
@ -126,6 +129,34 @@ class Reservation < ActiveRecord::Base
self.invoice.invoice_items.push InvoiceItem.new(amount: ii_amount, stp_invoice_item_id: (ii.id if ii), description: description) self.invoice.invoice_items.push InvoiceItem.new(amount: ii_amount, stp_invoice_item_id: (ii.id if ii), description: description)
end end
# === Space reservation ===
when Space
base_amount = reservable.prices.find_by(group_id: user.group_id, plan_id: plan.try(:id)).amount
users_credits_manager = UsersCredits::Manager.new(reservation: self, plan: plan)
slots.each_with_index do |slot, index|
description = reservable.name + " #{I18n.l slot.start_at, format: :long} - #{I18n.l slot.end_at, format: :hour_minute}"
ii_amount = base_amount # ii_amount default to base_amount
if users_credits_manager.will_use_credits?
ii_amount = (index < users_credits_manager.free_hours_count) ? 0 : base_amount
end
ii_amount = 0 if slot.offered and on_site # if it's a local payment and slot is offered free
unless on_site # if it's local payment then do not create Stripe::InvoiceItem
ii = Stripe::InvoiceItem.create(
customer: user.stp_customer_id,
amount: ii_amount,
currency: Rails.application.secrets.stripe_currency,
description: description
)
invoice_items << ii
end
self.invoice.invoice_items.push InvoiceItem.new(amount: ii_amount, stp_invoice_item_id: (ii.id if ii), description: description)
end
# === Unknown reservation type === # === Unknown reservation type ===
else else
raise NotImplementedError raise NotImplementedError
@ -164,7 +195,7 @@ class Reservation < ActiveRecord::Base
if @wallet_amount_debit != 0 and !on_site if @wallet_amount_debit != 0 and !on_site
invoice_items << Stripe::InvoiceItem.create( invoice_items << Stripe::InvoiceItem.create(
customer: user.stp_customer_id, customer: user.stp_customer_id,
amount: -@wallet_amount_debit, amount: -@wallet_amount_debit.to_i,
currency: Rails.application.secrets.stripe_currency, currency: Rails.application.secrets.stripe_currency,
description: "wallet -#{@wallet_amount_debit / 100.0}" description: "wallet -#{@wallet_amount_debit / 100.0}"
) )
@ -347,7 +378,7 @@ class Reservation < ActiveRecord::Base
def machine_not_already_reserved def machine_not_already_reserved
already_reserved = false already_reserved = false
self.slots.each do |slot| self.slots.each do |slot|
same_hour_slots = Slot.joins(:reservation).where( same_hour_slots = Slot.joins(:reservations).where(
reservations: { reservable_type: self.reservable_type, reservations: { reservable_type: self.reservable_type,
reservable_id: self.reservable_id reservable_id: self.reservable_id
}, },

View File

@ -31,7 +31,8 @@ class Setting < ActiveRecord::Base
name_genre name_genre
reminder_enable reminder_enable
reminder_delay reminder_delay
event_explications_alert ) event_explications_alert
space_explications_alert )
} }
after_update :update_stylesheet if :value_changed? after_update :update_stylesheet if :value_changed?

View File

@ -1,16 +1,31 @@
class Slot < ActiveRecord::Base class Slot < ActiveRecord::Base
include NotifyWith::NotificationAttachedObject include NotifyWith::NotificationAttachedObject
belongs_to :reservation has_many :slots_reservations, dependent: :destroy
has_many :reservations, through: :slots_reservations
belongs_to :availability belongs_to :availability
attr_accessor :is_reserved, :machine, :title, :can_modify, :is_reserved_by_current_user attr_accessor :is_reserved, :machine, :space, :title, :can_modify, :is_reserved_by_current_user
after_update :set_ex_start_end_dates_attrs, if: :dates_were_modified? after_update :set_ex_start_end_dates_attrs, if: :dates_were_modified?
after_update :notify_member_and_admin_slot_is_modified, if: :dates_were_modified? after_update :notify_member_and_admin_slot_is_modified, if: :dates_were_modified?
after_update :notify_member_and_admin_slot_is_canceled, if: :canceled? after_update :notify_member_and_admin_slot_is_canceled, if: :canceled?
# for backward compatibility
def reservation
reservations.first
end
def destroy
update_column(:destroying, true)
super
end
def is_complete?
reservations.length >= availability.nb_total_places
end
private private
def notify_member_and_admin_slot_is_modified def notify_member_and_admin_slot_is_modified
NotificationCenter.call type: 'notify_member_slot_is_modified', NotificationCenter.call type: 'notify_member_slot_is_modified',

View File

@ -0,0 +1,13 @@
class SlotsReservation < ActiveRecord::Base
belongs_to :slot
belongs_to :reservation
after_destroy :cleanup_slots
# when the SlotsReservation is deleted (from Reservation destroy cascade), we delete the
# corresponding slot
def cleanup_slots
unless slot.destroying
slot.destroy
end
end
end

58
app/models/space.rb Normal file
View File

@ -0,0 +1,58 @@
class Space < ActiveRecord::Base
extend FriendlyId
friendly_id :name, use: :slugged
validates :name, :default_places, presence: true
has_one :space_image, as: :viewable, dependent: :destroy
accepts_nested_attributes_for :space_image, allow_destroy: true
has_many :space_files, as: :viewable, dependent: :destroy
accepts_nested_attributes_for :space_files, allow_destroy: true, reject_if: :all_blank
has_and_belongs_to_many :projects, join_table: :projects_spaces
has_many :spaces_availabilities
has_many :availabilities, through: :spaces_availabilities, dependent: :destroy
has_many :reservations, as: :reservable, dependent: :destroy
has_many :prices, as: :priceable, dependent: :destroy
has_many :credits, as: :creditable, dependent: :destroy
after_create :create_statistic_subtype
after_create :create_space_prices
after_update :update_statistic_subtype, if: :name_changed?
after_destroy :remove_statistic_subtype
def create_statistic_subtype
index = StatisticIndex.find_by(es_type_key: 'space')
StatisticSubType.create!({statistic_types: index.statistic_types, key: self.slug, label: self.name})
end
def update_statistic_subtype
index = StatisticIndex.find_by(es_type_key: 'space')
subtype = StatisticSubType.joins(statistic_type_sub_types: :statistic_type).find_by(key: self.slug, statistic_types: { statistic_index_id: index.id })
subtype.label = self.name
subtype.save!
end
def remove_statistic_subtype
subtype = StatisticSubType.find_by(key: self.slug)
subtype.destroy!
end
def create_space_prices
Group.all.each do |group|
Price.create(priceable: self, group: group, amount: 0)
end
Plan.all.includes(:group).each do |plan|
Price.create(group: plan.group, plan: plan, priceable: self, amount: 0)
end
end
def destroyable?
reservations.empty?
end
end

3
app/models/space_file.rb Normal file
View File

@ -0,0 +1,3 @@
class SpaceFile < Asset
mount_uploader :attachment, SpaceFileUploader
end

View File

@ -0,0 +1,4 @@
class SpaceImage < Asset
mount_uploader :attachment, SpaceImageUploader
end

View File

@ -0,0 +1,14 @@
class SpacesAvailability < ActiveRecord::Base
belongs_to :space
belongs_to :availability
after_destroy :cleanup_availability
# when the SpacesAvailability is deleted (from Space destroy cascade), we delete the corresponding
# availability. We don't use 'dependent: destroy' as we need to prevent conflicts if the destroy came from
# the Availability destroy cascade.
def cleanup_availability
unless availability.destroying
availability.safe_destroy
end
end
end

View File

@ -0,0 +1,9 @@
module Stats
class Space
include Elasticsearch::Persistence::Model
include StatConcern
include StatReservationConcern
attribute :spaceId, Integer
end
end

View File

@ -1,4 +1,4 @@
class TrainingImage < Asset class TrainingImage < Asset
mount_uploader :attachment, MachineImageUploader mount_uploader :attachment, TrainingImageUploader
end end

View File

@ -125,6 +125,8 @@ module PDF
### Machine reservation ### Machine reservation
when 'Machine' when 'Machine'
details += I18n.t('invoices.machine_reservation_DESCRIPTION', DESCRIPTION: item.description) details += I18n.t('invoices.machine_reservation_DESCRIPTION', DESCRIPTION: item.description)
when 'Space'
details += I18n.t('invoices.space_reservation_DESCRIPTION', DESCRIPTION: item.description)
### Training reservation ### Training reservation
when 'Training' when 'Training'
details += I18n.t('invoices.training_reservation_DESCRIPTION', DESCRIPTION: item.description) details += I18n.t('invoices.training_reservation_DESCRIPTION', DESCRIPTION: item.description)

View File

@ -0,0 +1,13 @@
class SpacePolicy < ApplicationPolicy
def create?
user.is_admin?
end
def update?
user.is_admin?
end
def destroy?
user.is_admin? and record.destroyable?
end
end

View File

@ -1,6 +1,6 @@
class StatisticPolicy < ApplicationPolicy class StatisticPolicy < ApplicationPolicy
%w(index account event machine project subscription training user scroll export_subscription export_machine %w(index account event machine project subscription training user space scroll export_subscription export_machine
export_training export_event export_account export_project export_global).each do |action| export_training export_event export_account export_project export_space export_global).each do |action|
define_method "#{action}?" do define_method "#{action}?" do
user.is_admin? user.is_admin?
end end

View File

@ -39,6 +39,23 @@ class StatisticService
end end
end end
# space list
reservations_space_list(options).each do |r|
%w(booking hour).each do |type|
stat = Stats::Space.new({
date: format_date(r.date),
type: type,
subType: r.space_type,
ca: r.ca,
spaceId: r.space_id,
name: r.space_name,
reservationId: r.reservation_id
}.merge(user_info_stat(r)))
stat.stat = (type == 'booking' ? 1 : r.nb_hours)
stat.save
end
end
# training list # training list
reservations_training_list(options).each do |r| reservations_training_list(options).each do |r|
%w(booking hour).each do |type| %w(booking hour).each do |type|
@ -170,6 +187,27 @@ class StatisticService
result result
end end
def reservations_space_list(options = default_options)
result = []
Reservation
.where("reservable_type = 'Space' AND reservations.created_at >= :start_date AND reservations.created_at <= :end_date", options)
.eager_load(:slots, user: [:profile, :group], invoice: [:invoice_items])
.each do |r|
u = r.user
result.push OpenStruct.new({
date: options[:start_date].to_date,
reservation_id: r.id,
space_id: r.reservable.id,
space_name: r.reservable.name,
space_type: r.reservable.slug,
nb_hours: r.slots.size,
ca: calcul_ca(r.invoice)
}.merge(user_info(u))) if r.reservable
end
result
end
def reservations_training_list(options = default_options) def reservations_training_list(options = default_options)
result = [] result = []
Reservation Reservation
@ -298,7 +336,7 @@ class StatisticService
def clean_stat(options = default_options) def clean_stat(options = default_options)
client = Elasticsearch::Model.client client = Elasticsearch::Model.client
%w{Account Event Machine Project Subscription Training User}.each do |o| %w{Account Event Machine Project Subscription Training User Space}.each do |o|
model = "Stats::#{o}".constantize model = "Stats::#{o}".constantize
client.delete_by_query(index: model.index_name, type: model.document_type, body: {query: {match: {date: format_date(options[:start_date])}}}) client.delete_by_query(index: model.index_name, type: model.document_type, body: {query: {match: {date: format_date(options[:start_date])}}})
end end

View File

@ -40,7 +40,7 @@ class StatisticsExportService
File.open(export.file,"w+b") {|f| f.puts content } File.open(export.file,"w+b") {|f| f.puts content }
end end
%w(account event machine project subscription training).each do |path| %w(account event machine project subscription training space).each do |path|
class_eval %{ class_eval %{
def export_#{path}(export) def export_#{path}(export)

View File

@ -11,17 +11,19 @@ module UsersCredits
if user if user
@manager = Managers::User.new(user) @manager = Managers::User.new(user)
elsif reservation elsif reservation
if reservation.reservable_type == "Training" if reservation.reservable_type == 'Training'
@manager = Managers::Training.new(reservation, plan) @manager = Managers::Training.new(reservation, plan)
elsif reservation.reservable_type == "Machine" elsif reservation.reservable_type == 'Machine'
@manager = Managers::Machine.new(reservation, plan) @manager = Managers::Machine.new(reservation, plan)
elsif reservation.reservable_type == "Event" elsif reservation.reservable_type == 'Event'
@manager = Managers::Event.new(reservation, plan) @manager = Managers::Event.new(reservation, plan)
elsif reservation.reservable_type == 'Space'
@manager = Managers::Space.new(reservation, plan)
else else
raise ArgumentError, "reservation.reservable_type must be Training, Machine or Event" raise ArgumentError, 'reservation.reservable_type must be Training, Machine, Space or Event'
end end
else else
raise ArgumentError, "you have to pass either a reservation or a user to initialize a UsersCredits::Manager" raise ArgumentError, 'you have to pass either a reservation or a user to initialize a UsersCredits::Manager'
end end
end end
@ -152,5 +154,52 @@ module UsersCredits
def update_credits def update_credits
end end
end end
class Space < Reservation
def will_use_credits? # to known if a credit will be used in the context of the given reservation
_will_use_credits?[0]
end
def free_hours_count
_will_use_credits?[1]
end
def update_credits
super
will_use_credits, free_hours_count, space_credit = _will_use_credits?
if will_use_credits
users_credit = user.users_credits.find_or_initialize_by(credit_id: space_credit.id)
if users_credit.new_record?
users_credit.hours_used = free_hours_count
else
users_credit.hours_used += free_hours_count
end
users_credit.save!
end
end
private
def _will_use_credits?
return false, 0 unless plan
if space_credit = plan.space_credits.find_by(creditable_id: reservation.reservable_id)
users_credit = user.users_credits.find_by(credit_id: space_credit.id)
already_used_hours = users_credit ? users_credit.hours_used : 0
remaining_hours = space_credit.hours - already_used_hours
free_hours_count = [remaining_hours, reservation.slots.size].min
if free_hours_count > 0
return true, free_hours_count, space_credit
else
return false, free_hours_count, space_credit
end
end
return false, 0
end
end
end end
end end

View File

@ -0,0 +1,49 @@
class SpaceFileUploader < CarrierWave::Uploader::Base
# Include RMagick or MiniMagick support:
# include CarrierWave::RMagick
#include CarrierWave::MiniMagick
include UploadHelper
# Choose what kind of storage to use for this uploader:
storage :file
after :remove, :delete_empty_dirs
# storage :fog
# Override the directory where uploaded files will be stored.
# This is a sensible default for uploaders that are meant to be mounted:
def store_dir
"#{base_store_dir}/#{model.id}"
end
def base_store_dir
"uploads/#{model.class.to_s.underscore}"
end
# Provide a default URL as a default if there hasn't been a file uploaded:
# def default_url
# # For Rails 3.1+ asset pipeline compatibility:
# # ActionController::Base.helpers.asset_path("fallback/" + [version_name, "default.png"].compact.join('_'))
#
# "/images/fallback/" + [version_name, "default.png"].compact.join('_')
# end
# Process files as they are uploaded:
# process :scale => [200, 300]
#
# def scale(width, height)
# # do something
# end
# Add a white list of extensions which are allowed to be uploaded.
# For images you might use something like this:
def extension_white_list
%w(pdf)
end
# Override the filename of the uploaded files:
# Avoid using model.id or version_name here, see uploader/store.rb for details.
#def filename
#"avatar.#{file.extension}" if original_filename
#end
end

View File

@ -0,0 +1,58 @@
class SpaceImageUploader < CarrierWave::Uploader::Base
# Include RMagick or MiniMagick support:
# include CarrierWave::RMagick
include CarrierWave::MiniMagick
include UploadHelper
# Choose what kind of storage to use for this uploader:
storage :file
after :remove, :delete_empty_dirs
# storage :fog
# Override the directory where uploaded files will be stored.
# This is a sensible default for uploaders that are meant to be mounted:
def store_dir
"#{base_store_dir}/#{model.id}"
end
def base_store_dir
"uploads/#{model.class.to_s.underscore}"
end
# Provide a default URL as a default if there hasn't been a file uploaded:
# def default_url
# # For Rails 3.1+ asset pipeline compatibility:
# # ActionController::Base.helpers.asset_path("fallback/" + [version_name, "default.png"].compact.join('_'))
#
# "/images/fallback/" + [version_name, "default.png"].compact.join('_')
# end
# Process files as they are uploaded:
# process :scale => [200, 300]
#
# def scale(width, height)
# # do something
# end
# Create different versions of your uploaded files:
version :large do
process :resize_to_fit => [1000, 700]
end
version :medium do
process :resize_to_fit => [700, 400]
end
# Add a white list of extensions which are allowed to be uploaded.
# For images you might use something like this:
def extension_white_list
%w(jpg jpeg gif png)
end
# Override the filename of the uploaded files:
# Avoid using model.id or version_name here, see uploader/store.rb for details.
def filename
"space_image.#{file.extension}" if original_filename
end
end

View File

@ -0,0 +1,58 @@
class TrainingImageUploader < CarrierWave::Uploader::Base
# Include RMagick or MiniMagick support:
# include CarrierWave::RMagick
include CarrierWave::MiniMagick
include UploadHelper
# Choose what kind of storage to use for this uploader:
storage :file
after :remove, :delete_empty_dirs
# storage :fog
# Override the directory where uploaded files will be stored.
# This is a sensible default for uploaders that are meant to be mounted:
def store_dir
"#{base_store_dir}/#{model.id}"
end
def base_store_dir
"uploads/#{model.class.to_s.underscore}"
end
# Provide a default URL as a default if there hasn't been a file uploaded:
# def default_url
# # For Rails 3.1+ asset pipeline compatibility:
# # ActionController::Base.helpers.asset_path("fallback/" + [version_name, "default.png"].compact.join('_'))
#
# "/images/fallback/" + [version_name, "default.png"].compact.join('_')
# end
# Process files as they are uploaded:
# process :scale => [200, 300]
#
# def scale(width, height)
# # do something
# end
# Create different versions of your uploaded files:
version :large do
process :resize_to_fit => [1000, 700]
end
version :medium do
process :resize_to_fit => [700, 400]
end
# Add a white list of extensions which are allowed to be uploaded.
# For images you might use something like this:
def extension_white_list
%w(jpg jpeg gif png)
end
# Override the filename of the uploaded files:
# Avoid using model.id or version_name here, see uploader/store.rb for details.
def filename
"training_image.#{file.extension}" if original_filename
end
end

View File

@ -5,7 +5,7 @@ json.array!(@availabilities) do |availability|
json.textColor 'black' json.textColor 'black'
json.backgroundColor 'white' json.backgroundColor 'white'
# availability object # availability object
if availability.try(:available_type) if availability.instance_of? Availability
json.title availability.title(@title_filter) json.title availability.title(@title_filter)
if availability.available_type == 'event' if availability.available_type == 'event'
json.event_id availability.event.id json.event_id availability.event.id
@ -20,7 +20,7 @@ json.array!(@availabilities) do |availability|
json.name t.name json.name t.name
end end
if availability.available_type != 'machines' if availability.available_type == 'training' or availability.available_type == 'event'
json.borderColor trainings_events_border_color(availability) json.borderColor trainings_events_border_color(availability)
if availability.is_reserved if availability.is_reserved
json.is_reserved true json.is_reserved true
@ -29,20 +29,43 @@ json.array!(@availabilities) do |availability|
json.is_completed true json.is_completed true
json.title "#{availability.title} - #{t('trainings.completed')}" json.title "#{availability.title} - #{t('trainings.completed')}"
end end
elsif availability.available_type == 'space'
complete = availability.slots.length >= availability.available_space_places
json.is_completed complete
json.borderColor availability_border_color(availability)
if complete
json.title "#{availability.title} - #{t('trainings.completed')}"
json.borderColor AvailabilityHelper::IS_COMPLETED
end
if availability.is_reserved
json.is_reserved true
json.title "#{availability.title} - #{t('trainings.i_ve_reserved')}"
end
else else
json.borderColor availability_border_color(availability) json.borderColor availability_border_color(availability)
end end
# machine slot object ( here => availability = slot ) # slot object ( here => availability = slot )
else # -> machines / spaces
elsif availability.instance_of? Slot
json.title availability.title json.title availability.title
json.machine_id availability.machine.id
json.borderColor machines_slot_border_color(availability)
json.tag_ids availability.availability.tag_ids json.tag_ids availability.availability.tag_ids
json.tags availability.availability.tags do |t| json.tags availability.availability.tags do |t|
json.id t.id json.id t.id
json.name t.name json.name t.name
end end
json.is_reserved availability.is_reserved if availability.try(:machine)
json.machine_id availability.machine.id
json.borderColor machines_slot_border_color(availability)
json.is_reserved availability.is_reserved
elsif availability.try(:space)
json.space_id availability.space.id
json.borderColor space_slot_border_color(availability)
json.is_completed availability.is_complete?
else
json.title 'Unknown slot'
end
else
json.title 'Unknown object'
end end
end end

View File

@ -0,0 +1,27 @@
json.array!(@slots) do |slot|
json.id slot.id if slot.id
json.can_modify slot.can_modify
json.title slot.title
json.start slot.start_at.iso8601
json.end slot.end_at.iso8601
json.is_reserved slot.is_reserved
json.is_completed slot.is_complete?
json.backgroundColor 'white'
json.borderColor space_slot_border_color(slot)
json.availability_id slot.availability_id
json.space do
json.id slot.space.id
json.name slot.space.name
end
# the user who booked the slot ...
json.user do
json.id slot.reservation.user.id
json.name slot.reservation.user.profile.full_name
end if @current_user_role == 'admin' and slot.reservation # ... if the slot was reserved
json.tag_ids slot.availability.tag_ids
json.tags slot.availability.tags do |t|
json.id t.id
json.name t.name
end
end

View File

@ -1,6 +1,5 @@
json.array!(@availabilities) do |a| json.array!(@availabilities) do |a|
json.id a.id json.id a.slot_id if a.slot_id
json.slot_id a.slot_id if a.slot_id
if a.is_reserved if a.is_reserved
json.is_reserved true json.is_reserved true
json.title "#{a.trainings[0].name}' - #{t('trainings.i_ve_reserved')}" json.title "#{a.trainings[0].name}' - #{t('trainings.i_ve_reserved')}"
@ -16,6 +15,7 @@ json.array!(@availabilities) do |a|
json.backgroundColor 'white' json.backgroundColor 'white'
json.can_modify a.can_modify json.can_modify a.can_modify
json.nb_total_places a.nb_total_places json.nb_total_places a.nb_total_places
json.availability_id a.id
json.training do json.training do
json.id a.trainings.first.id json.id a.trainings.first.id

View File

@ -1,4 +1,4 @@
json.extract! @machine, :id, :name, :description, :spec, :created_at, :updated_at json.extract! @machine, :id, :name, :description, :spec, :created_at, :updated_at, :slug
json.machine_image @machine.machine_image.attachment.large.url if @machine.machine_image json.machine_image @machine.machine_image.attachment.large.url if @machine.machine_image
json.machine_files_attributes @machine.machine_files do |f| json.machine_files_attributes @machine.machine_files do |f|
json.id f.id json.id f.id

View File

@ -0,0 +1,4 @@
json.array!(@spaces) do |space|
json.extract! space, :id, :name, :description, :slug, :default_places
json.space_image space.space_image.attachment.medium.url if space.space_image
end

View File

@ -0,0 +1,12 @@
json.extract! @space, :id, :name, :description, :characteristics, :created_at, :updated_at, :slug, :default_places
json.space_image @space.space_image.attachment.large.url if @space.space_image
json.space_files_attributes @space.space_files do |f|
json.id f.id
json.attachment f.attachment_identifier
json.attachment_url f.attachment_url
end
# Unused for the moment. May be used to show a list of projects
# using the space in the space_show screen
# json.space_projects @space.projects do |p|
# json.extract! p, :slug, :name
# end

View File

@ -4,7 +4,7 @@ json.availabilities @availabilities do |a|
json.start_at a.start_at.iso8601 json.start_at a.start_at.iso8601
json.end_at a.end_at.iso8601 json.end_at a.end_at.iso8601
json.reservation_users a.slots.map do |slot| json.reservation_users a.slots.map do |slot|
json.id slot.reservation.user.id json.id slot.reservation.user_id
json.full_name slot.reservation.user.profile.full_name json.full_name slot.reservation.user.profile.full_name
json.is_valid slot.reservation.user.trainings.include?(@training) json.is_valid slot.reservation.user.trainings.include?(@training)
end end

View File

@ -24,6 +24,7 @@
var Fablab = Fablab || {}; var Fablab = Fablab || {};
Fablab.withoutPlans = ('<%= Rails.application.secrets.fablab_without_plans %>' == 'true'); Fablab.withoutPlans = ('<%= Rails.application.secrets.fablab_without_plans %>' == 'true');
Fablab.withoutSpaces = ('<%= Rails.application.secrets.fablab_without_spaces %>' != 'false');
Fablab.disqusShortname = "<%= Rails.application.secrets.disqus_shortname %>"; Fablab.disqusShortname = "<%= Rails.application.secrets.disqus_shortname %>";
Fablab.defaultHost = "<%= Rails.application.secrets.default_host %>"; Fablab.defaultHost = "<%= Rails.application.secrets.default_host %>";
Fablab.gaId = "<%= Rails.application.secrets.google_analytics_id %>"; Fablab.gaId = "<%= Rails.application.secrets.google_analytics_id %>";

View File

@ -68,7 +68,7 @@ date = wb.styles.add_style :format_code => Rails.application.secrets.excel_date_
end end
end end
# proceed teh 'ca' field if requested # proceed the 'ca' field if requested
if index.ca if index.ca
data.push hit['_source']['ca'] data.push hit['_source']['ca']
styles.push nil styles.push nil

View File

@ -15,7 +15,7 @@ class StatisticsExportWorker
service = StatisticsExportService.new service = StatisticsExportService.new
method_name = "export_#{export.export_type}" method_name = "export_#{export.export_type}"
if %w(account event machine project subscription training global).include?(export.export_type) and service.respond_to?(method_name) if %w(account event machine project subscription training space global).include?(export.export_type) and service.respond_to?(method_name)
service.public_send(method_name, export) service.public_send(method_name, export)
NotificationCenter.call type: :notify_admin_export_complete, NotificationCenter.call type: :notify_admin_export_complete,

View File

@ -12,6 +12,7 @@ STRIPE_CURRENCY: 'eur'
INVOICE_PREFIX: Demo-FabLab-facture INVOICE_PREFIX: Demo-FabLab-facture
FABLAB_WITHOUT_PLANS: 'false' FABLAB_WITHOUT_PLANS: 'false'
FABLAB_WITHOUT_SPACES: 'true'
DEFAULT_MAIL_FROM: Fab Manager Demo <noreply@fab-manager.com> DEFAULT_MAIL_FROM: Fab Manager Demo <noreply@fab-manager.com>

View File

@ -13,28 +13,38 @@ en:
calendar: calendar:
# manage the trainings & machines slots # manage the trainings & machines slots
calendar_management: "Calendar management" admin_calendar:
ongoing_reservations: "Ongoing reservations" calendar_management: "Calendar management"
no_reservations: "No reservations" trainings: "Trainings"
do_you_really_want_to_cancel_the_USER_s_reservation_the_DATE_at_TIME_concerning_RESERVATION: "Do you really want to cancel the {USER}'s reservation, the {DATE} at {TIME}, concerning {RESERVATION}?" # messageFormat interpolation machines: "Machines"
reservation_cancellation_failed: "Reservation cancellation failed." spaces: "Spaces"
unable_to_remove_the_last_machine_of_the_slot_delete_the_slot_rather: "Unable to remove the last machine of the slot. Delete the slot rather." ongoing_reservations: "Ongoing reservations"
do_you_really_want_to_remove_MACHINE_from_this_slot: "Do you really want to remove \"{MACHINE}\" from this slot?" # messageFormat interpolation no_reservations: "No reservations"
this_will_prevent_any_new_reservation_on_this_slot_but_wont_cancel_those_existing: "This will prevent any new reservation on this slot but won't cancel those existing." confirmation_required: "Confirmation required"
beware_this_cannot_be_reverted: "Beware: this cannot be reverted." do_you_really_want_to_cancel_the_USER_s_reservation_the_DATE_at_TIME_concerning_RESERVATION: "Do you really want to cancel the {USER}'s reservation, the {DATE} at {TIME}, concerning {RESERVATION}?" # messageFormat interpolation
the_machine_was_successfully_removed_from_the_slot: "The machine was successfully removed from the slot." reservation_was_successfully_cancelled: "Reservation was successfully cancelled."
deletion_failed: "Deletion failed." reservation_cancellation_failed: "Reservation cancellation failed."
DATE_slot: "{{DATE}} slot:" # angular interpolation unable_to_remove_the_last_machine_of_the_slot_delete_the_slot_rather: "Unable to remove the last machine of the slot. Delete the slot rather."
you_can_define_a_training_on_that_slot: "You can define a training on that slot:" do_you_really_want_to_remove_MACHINE_from_this_slot: "Do you really want to remove \"{MACHINE}\" from this slot?" # messageFormat interpolation
link_a_training: "Link a training" this_will_prevent_any_new_reservation_on_this_slot_but_wont_cancel_those_existing: "This will prevent any new reservation on this slot but won't cancel those existing."
or_: "Or" beware_this_cannot_be_reverted: "Beware: this cannot be reverted."
_select_some_machines: "select some machines" the_machine_was_successfully_removed_from_the_slot: "The machine was successfully removed from the slot."
number_of_tickets: "Number of tickets: " deletion_failed: "Deletion failed."
adjust_the_opening_hours: "Adjust the opening hours" DATE_slot: "{{DATE}} slot:" # angular interpolation
restrict_this_slot_with_labels_(optional): "Restrict this slot with labels (optional)" what_kind_of_slot_do_you_want_to_create: "What kind of slot do you want to create?"
the_slot_START-END_has_been_successfully_deleted: "The slot {{START}} - {{END}} has been successfully deleted" # angular interpolation training: "Training"
unable_to_delete_the_slot_START-END_because_it_s_already_reserved_by_a_member: "Unable to delete the slot {{START}} - {{END}} because it's already reserved by a member" # angular interpolation machine: "Machine"
you_should_link_a_training_or_a_machine_to_this_slot: "You should link a training or a machine to this slot." space: "Space"
next: "Next >"
previous: "< Previous"
select_some_machines: "Select some machines"
number_of_tickets: "Number of tickets: "
adjust_the_opening_hours: "Adjust the opening hours"
to_time: "to" # context: time. eg. "from 18:00 to 21:00"
restrict_this_slot_with_labels_(optional): "Restrict this slot with labels (optional)"
the_slot_START-END_has_been_successfully_deleted: "The slot {{START}} - {{END}} has been successfully deleted" # angular interpolation
unable_to_delete_the_slot_START-END_because_it_s_already_reserved_by_a_member: "Unable to delete the slot {{START}} - {{END}} because it's already reserved by a member" # angular interpolation
you_should_select_at_least_a_machine: "You should select at least one machine on this slot."
project_elements: project_elements:
# management of the projects' components # management of the projects' components
@ -132,55 +142,78 @@ en:
pricing: pricing:
# subscriptions, prices, credits and coupons management # subscriptions, prices, credits and coupons management
pricing_management: "Pricing management" pricing:
list_of_the_subscription_plans: "List of the subscription plans" pricing_management: "Pricing management"
beware_the_subscriptions_are_disabled_on_this_application: "Beware, the subscriptions are disabled on this application." subscriptions: "Subscriptions"
you_can_create_some_but_they_wont_be_available_until_the_project_is_redeployed_by_the_server_manager: "You can create some but they won't be available until the project is redeployed by the server manager." trainings: "Trainings"
for_safety_reasons_please_dont_create_subscriptions_if_you_dont_want_intend_to_use_them_later: "For safety reasons, please don't create subscriptions if you don't intend to use them later." list_of_the_subscription_plans: "List of the subscription plans"
add_a_new_subscription_plan: "Add a new subscription plan" beware_the_subscriptions_are_disabled_on_this_application: "Beware, the subscriptions are disabled on this application."
duration: "Duration" you_can_create_some_but_they_wont_be_available_until_the_project_is_redeployed_by_the_server_manager: "You can create some but they won't be available until the project is redeployed by the server manager."
prominence: "Prominence" for_safety_reasons_please_dont_create_subscriptions_if_you_dont_want_intend_to_use_them_later: "For safety reasons, please don't create subscriptions if you don't intend to use them later."
machine_hours: "Machine hours" add_a_new_subscription_plan: "Add a new subscription plan"
these_prices_match_machine_hours_rates_: "These prices match machine hours rates" type: "Type"
_without_subscriptions: "without subscriptions" partner: "Partner"
credits: "Credits" standard: "Standard"
related_trainings: "Related trainings" name: "Name"
add_a_machine_credit: "Add a machine credit" duration: "Duration"
machine: "Machine" group: "Group"
hours: "Hours" prominence: "Prominence"
related_subscriptions: "Related subscriptions" price: "Price"
please_specify_a_number: "Please specify a number." machine_hours: "Machine hours"
none: "None" # grammar note: concordance with "training". these_prices_match_machine_hours_rates_: "These prices match machine hours rates"
an_error_occurred_while_saving_the_number_of_credits: "An error occurred while saving the number of credits." _without_subscriptions: "without subscriptions"
an_error_occurred_while_deleting_credit_with_the_TRAINING: "An error occurred while deleting credit with the {{TRAINING}}." # angular interpolation machines: "Machines"
an_error_occurred_unable_to_find_the_credit_to_revoke: "An error occurred : unable to find the credit to revoke." credits: "Credits"
an_error_occurred_while_creating_credit_with_the_TRAINING: "An error occurred while creating credit with the {{TRAINING}}." # angular interpolation subscription: "Subscription"
not_set: "Not set" related_trainings: "Related trainings"
error_a_credit_linking_this_machine_with_that_subscription_already_exists: "Error : a credit linking this machine with that subscription already exists." add_a_machine_credit: "Add a machine credit"
changes_have_been_successfully_saved: "Changes have been successfully saved." machine: "Machine"
credit_was_successfully_saved: "Credit was successfully saved." hours: "Hours"
do_you_really_want_to_delete_this_subscription_plan: "Do you really want to delete this subscription plan?" related_subscriptions: "Related subscriptions"
subscription_plan_was_successfully_deleted: "Subscription plan was successfully deleted." please_specify_a_number: "Please specify a number."
unable_to_delete_the_specified_subscription_an_error_occurred: "Unable to delete the specified subscription, an error occurred." none: "None" # grammar note: concordance with "training".
coupons: "Coupons" an_error_occurred_while_saving_the_number_of_credits: "An error occurred while saving the number of credits."
list_of_the_coupons: "List of the coupons" an_error_occurred_while_deleting_credit_with_the_TRAINING: "An error occurred while deleting credit with the {{TRAINING}}." # angular interpolation
discount: "Discount" an_error_occurred_unable_to_find_the_credit_to_revoke: "An error occurred : unable to find the credit to revoke."
nb_of_usages: "Number of usages" an_error_occurred_while_creating_credit_with_the_TRAINING: "An error occurred while creating credit with the {{TRAINING}}." # angular interpolation
status: "Status" not_set: "Not set"
add_a_new_coupon: "Add a new coupon" error_a_credit_linking_this_machine_with_that_subscription_already_exists: "Error : a credit linking this machine with that subscription already exists."
disabled: "Disabled" changes_have_been_successfully_saved: "Changes have been successfully saved."
expired: "Expired" credit_was_successfully_saved: "Credit was successfully saved."
sold_out: "Sold out" do_you_really_want_to_delete_this_subscription_plan: "Do you really want to delete this subscription plan?"
active: "Active" subscription_plan_was_successfully_deleted: "Subscription plan was successfully deleted."
do_you_really_want_to_delete_this_coupon: "Do you really want to delete this coupon?" unable_to_delete_the_specified_subscription_an_error_occurred: "Unable to delete the specified subscription, an error occurred."
coupon_was_successfully_deleted: "Coupon was successfully deleted." coupons: "Coupons"
unable_to_delete_the_specified_coupon_already_in_use: "Unable to delete the specified coupon: it is already used with some invoices." list_of_the_coupons: "List of the coupons"
unable_to_delete_the_specified_coupon_an_unexpected_error_occurred: "Unable to delete the specified coupon: an unexpected error occurred." discount: "Discount"
send_a_coupon: "Send a coupon" nb_of_usages: "Number of usages"
coupon: "Coupon" status: "Status"
usages: "Usages" add_a_new_coupon: "Add a new coupon"
coupon_successfully_sent_to_USER: "Coupon successfully sent to {{USER}}" # angular interpolation disabled: "Disabled"
an_error_occurred_unable_to_send_the_coupon: "An unexpected error prevent from sending the coupon." expired: "Expired"
sold_out: "Sold out"
active: "Active"
confirmation_required: "Confirmation required"
do_you_really_want_to_delete_this_coupon: "Do you really want to delete this coupon?"
coupon_was_successfully_deleted: "Coupon was successfully deleted."
unable_to_delete_the_specified_coupon_already_in_use: "Unable to delete the specified coupon: it is already used with some invoices."
unable_to_delete_the_specified_coupon_an_unexpected_error_occurred: "Unable to delete the specified coupon: an unexpected error occurred."
send_a_coupon: "Send a coupon"
coupon: "Coupon"
usages: "Usages"
coupon_successfully_sent_to_USER: "Coupon successfully sent to {{USER}}" # angular interpolation
an_error_occurred_unable_to_send_the_coupon: "An unexpected error prevent from sending the coupon."
code: "Code"
enabled: "Enabled"
validity_per_user: "Validity per user"
once: "Just once"
forever: "Each use"
valid_until: "Valid until (included)"
spaces: "Spaces"
these_prices_match_space_hours_rates_: "These prices match space hours rates"
add_a_space_credit: "Add a Space credit"
space: "Espace"
error_a_credit_linking_this_space_with_that_subscription_already_exists: "Error : a credit linking this space with that subscription already exists."
coupons_new: coupons_new:
# ajouter un code promotionnel # ajouter un code promotionnel
@ -194,19 +227,24 @@ en:
plans: plans:
new: new:
# add a subscription plan on the platform # add a subscription plan on the platform
add_a_subscription_plan: "Add a subscription plan" new_plan:
unable_to_create_the_subscription_please_try_again: "Unable to create the subscription plan. Please try again." add_a_subscription_plan: "Add a subscription plan"
successfully_created_subscription(s)_dont_forget_to_redefine_prices: "Subscription(s) successfully created. Don't forget to redefine prices." unable_to_create_the_subscription_please_try_again: "Unable to create the subscription plan. Please try again."
unable_to_save_this_user_check_that_there_isnt_an_already_a_user_with_the_same_name: "Unable to save this user. Check that there isn't an already defined user with the same name." successfully_created_subscription(s)_dont_forget_to_redefine_prices: "Subscription(s) successfully created. Don't forget to redefine prices."
unable_to_save_this_user_check_that_there_isnt_an_already_a_user_with_the_same_name: "Unable to save this user. Check that there isn't an already defined user with the same name."
edit: edit:
# edit a subscription plan / machine hours prices # edit a subscription plan / machine hours prices
subscription_plan: "Subscription plan:" edit_plan:
prices: "Prices" subscription_plan: "Subscription plan:"
copy_prices_from: "Copy prices from" prices: "Prices"
machine: "Machine" copy_prices_from: "Copy prices from"
hourly_rate: "Hourly rate" machines: "Machines"
unable_to_save_subscription_changes_please_try_again: "Unable to save subscription changes. Please try again." machine: "Machine"
subscription_successfully_changed: "Subscription successfully changed." hourly_rate: "Hourly rate"
spaces: "Spaces"
space: "Space"
unable_to_save_subscription_changes_please_try_again: "Unable to save subscription changes. Please try again."
subscription_successfully_changed: "Subscription successfully changed."
invoices: invoices:
# list of all invoices & invoicing parameters # list of all invoices & invoicing parameters
@ -476,90 +514,95 @@ en:
settings: settings:
# global application parameters and customization # global application parameters and customization
customize_the_application: "Customize the application" settings:
general: "General" title: "Title"
fablab_title: "FabLab title" customize_the_application: "Customize the application"
fablab_name: "FabLab name" general: "General"
title_concordance: "Title concordance" fablab_title: "FabLab title"
male: "Male." fablab_name: "FabLab name"
female: "Female." title_concordance: "Title concordance"
eg: "eg:" male: "Male."
about: "About" female: "Female."
male_preposition: "the" eg: "eg:"
female_preposition: "the" about: "About"
customize_information_messages: "Customize information messages" male_preposition: "the"
message_of_the_machine_booking_page: "Message of the machine booking page:" female_preposition: "the"
type_the_message_content: "Type the message content" customize_information_messages: "Customize information messages"
warning_message_of_the_training_booking_page: "Warning message of the training booking page:" message_of_the_machine_booking_page: "Message of the machine booking page:"
information_message_of_the_training_reservation_page: "Information message of the training reservation page:" type_the_message_content: "Type the message content"
message_of_the_subscriptions_page: "Message of the subscriptions page:" warning_message_of_the_training_booking_page: "Warning message of the training booking page:"
message_of_the_events_page: "Message of the events page:" information_message_of_the_training_reservation_page: "Information message of the training reservation page:"
legal_documents: "Legal documents" message_of_the_subscriptions_page: "Message of the subscriptions page:"
if_these_documents_are_not_filled_no_consent_about_them_will_be_asked_to_the_user: "If these documents are not filled, no consent about them will be asked." message_of_the_events_page: "Message of the events page:"
general_terms_and_conditions_(T&C): "General terms and conditions (T&C)" message_of_the_spaces_page: "Message of the spaces page:"
terms_of_service_(TOS): "Terms of service (TOS)" legal_documents: "Legal documents"
customize_the_graphics: "Customize the graphics" if_these_documents_are_not_filled_no_consent_about_them_will_be_asked_to_the_user: "If these documents are not filled, no consent about them will be asked."
for_an_optimal_rendering_the_logo_image_must_be_at_the_PNG_format_with_a_transparent_background_and_with_an_aspect_ratio_3.5_times_wider_than_the_height: "For an optimal rendering, the logo image must be at the PNG format with a transparent background and an aspect ratio 3.5 wider than the height." general_terms_and_conditions_(T&C): "General terms and conditions (T&C)"
concerning_the_favicon_it_must_be_at_ICO_format_with_a_size_of_16x16_pixels: "Concerning the favicon, it must be at ICO format with a size of 16x16 pixels." terms_of_service_(TOS): "Terms of service (TOS)"
remember_to_refresh_the_page_for_the_changes_to_take_effect: "Remember to refresh the page for the changes to take effect." customize_the_graphics: "Customize the graphics"
logo_(white_background): "Logo (white background)" for_an_optimal_rendering_the_logo_image_must_be_at_the_PNG_format_with_a_transparent_background_and_with_an_aspect_ratio_3.5_times_wider_than_the_height: "For an optimal rendering, the logo image must be at the PNG format with a transparent background and an aspect ratio 3.5 wider than the height."
change_the_logo: "Change the logo" concerning_the_favicon_it_must_be_at_ICO_format_with_a_size_of_16x16_pixels: "Concerning the favicon, it must be at ICO format with a size of 16x16 pixels."
logo_(black_background): "Logo (black background)" remember_to_refresh_the_page_for_the_changes_to_take_effect: "Remember to refresh the page for the changes to take effect."
favicon: "Favicon" logo_(white_background): "Logo (white background)"
change_the_favicon: "Change the favicon" change_the_logo: "Change the logo"
main_colour: "Main colour:" logo_(black_background): "Logo (black background)"
primary: "Primary" favicon: "Favicon"
secondary_colour: "Secondary colour:" change_the_favicon: "Change the favicon"
secondary: "Secondary" main_colour: "Main colour:"
background_picture_of_the_profile_banner: "Background picture of the profile banner" primary: "Primary"
change_the_profile_banner: "Change the profile banner" secondary_colour: "Secondary colour:"
home_page: "Home page" secondary: "Secondary"
news_of_the_home_page: "News of the home page:" background_picture_of_the_profile_banner: "Background picture of the profile banner"
type_your_news_here: "Type your news here" change_the_profile_banner: "Change the profile banner"
leave_it_empty_to_not_bring_up_any_news_on_the_home_page: "Leave it empty to not bring up any news on the home page" home_page: "Home page"
twitter_stream: "Twitter Stream:" news_of_the_home_page: "News of the home page:"
name_of_the_twitter_account: "Name of the Twitter account" type_your_news_here: "Type your news here"
title_of_the_about_page: "Title of the About page" leave_it_empty_to_not_bring_up_any_news_on_the_home_page: "Leave it empty to not bring up any news on the home page"
shift_enter_to_force_carriage_return: "SHIFT + ENTER to force carriage return" twitter_stream: "Twitter Stream:"
input_the_main_content: "Input the main content" name_of_the_twitter_account: "Name of the Twitter account"
input_the_fablab_contacts: "Input the FabLab contacts" title_of_the_about_page: "Title of the About page"
reservations: "Reservations" shift_enter_to_force_carriage_return: "SHIFT + ENTER to force carriage return"
reservations_parameters: "Reservations parameters" input_the_main_content: "Input the main content"
confine_the_booking_agenda: "Confine the booking agenda" input_the_fablab_contacts: "Input the FabLab contacts"
opening_time: "Opening time" reservations: "Reservations"
closing_time: "Closing time" reservations_parameters: "Reservations parameters"
ability_for_the_users_to_move_their_reservations: "Ability for the users to move their reservations" confine_the_booking_agenda: "Confine the booking agenda"
reservations_shifting: "Reservations shifting" opening_time: "Opening time"
prior_period_(hours): "Prior period (hours)" closing_time: "Closing time"
enabled: "Enabled" ability_for_the_users_to_move_their_reservations: "Ability for the users to move their reservations"
disabled: "Disabled" reservations_shifting: "Reservations shifting"
ability_for_the_users_to_cancel_their_reservations: "Ability for the users to cancel their reservations" prior_period_(hours): "Prior period (hours)"
reservations_cancelling: "Reservations cancelling" enabled: "Enabled"
reservations_reminders: "Reservations reminders" disabled: "Disabled"
notification_sending_before_the_reservation_occurs: "Notification sending before the reservation occurs" ability_for_the_users_to_cancel_their_reservations: "Ability for the users to cancel their reservations"
customization_of_SETTING_successfully_saved: "Customization of the {{SETTING}} successfully saved." # angular interpolation reservations_cancelling: "Reservations cancelling"
file_successfully_updated: "File successfully updated." reservations_reminders: "Reservations reminders"
name_genre: "title concordance" notification_sending_before_the_reservation_occurs: "Notification sending before the reservation occurs"
machine_explications_alert: "explanation message on the machine reservation page" customization_of_SETTING_successfully_saved: "Customization of the {{SETTING}} successfully saved." # angular interpolation
training_explications_alert: "explanation message on the training reservation page" file_successfully_updated: "File successfully updated."
training_information_message: "information message on the machine reservation page" name_genre: "title concordance"
subscription_explications_alert: "explanation message on the subscription page" machine_explications_alert: "explanation message on the machine reservation page"
main_color: "main colour" training_explications_alert: "explanation message on the training reservation page"
secondary_color: "secondary colour" training_information_message: "information message on the machine reservation page"
home_blogpost: "homepage's brief" subscription_explications_alert: "explanation message on the subscription page"
twitter_name: "Twitter feed name" event_explications_alert: "explanation message on the event reservation page"
about_title: "\"About\" page title" space_explications_alert: "explanation message on the space reservation page"
about_body: "\"About\" page content" main_color: "main colour"
about_contacts: "\"About\" page contacts" secondary_color: "secondary colour"
booking_window_start: "opening time" home_blogpost: "homepage's brief"
booking_window_end: "closing time" twitter_name: "Twitter feed name"
booking_move_enable: "reservation moving enabling" about_title: "\"About\" page title"
booking_move_delay: "preventive delay of moving" about_body: "\"About\" page content"
booking_cancel_enable: "reservation canceling enabling" about_contacts: "\"About\" page contacts"
booking_cancel_delay: "preventive delay of canceling" booking_window_start: "opening time"
reminder_enable: "reservation reminding enabling" booking_window_end: "closing time"
reminder_delay: "delay before sending the reminder" booking_move_enable: "reservation moving enabling"
default_value_is_24_hours: "If the field is leaved empty: 24 hours." booking_move_delay: "preventive delay of moving"
booking_cancel_enable: "reservation canceling enabling"
booking_cancel_delay: "preventive delay of canceling"
reminder_enable: "reservation reminding enabling"
reminder_delay: "delay before sending the reminder"
default_value_is_24_hours: "If the field is leaved empty: 24 hours."
open_api_clients: open_api_clients:
add_new_client: "Create new API client" add_new_client: "Create new API client"
@ -575,3 +618,17 @@ en:
client_successfully_updated: "Client successfully updated." client_successfully_updated: "Client successfully updated."
client_successfully_deleted: "Client successfully deleted." client_successfully_deleted: "Client successfully deleted."
access_successfully_revoked: "Access successfully revoked." access_successfully_revoked: "Access successfully revoked."
space_new:
# create a new space
space_new:
add_a_new_space: "Add a new space"
watch_out_when_creating_a_new_space_its_prices_are_initialized_at_0_for_all_subscriptions: "Watch out! When creating a new space, its prices are initialized at 0 for all subscriptions."
consider_changing_its_prices_before_creating_any_reservation_slot: "Consider changing its prices before creating any reservation slot."
add_this_space: "Add this space"
space_edit:
# modify an exiting space
space_edit:
edit_the_space_NAME: "Edit the space: {{NAME}}" # angular interpolation
validate_the_changes: "Validate the changes"

View File

@ -4,7 +4,7 @@ fr:
machines_new: machines_new:
# ajout d'une nouvelle machine # ajout d'une nouvelle machine
declare_a_new_machine: "Déclarer une nouvelle machine" declare_a_new_machine: "Déclarer une nouvelle machine"
watch_out_when_creating_a_new_machine_its_prices_are_initialized_at_0_for_all_subscriptions: "Attention, lors de la création d'une machine, ses tarifs de réservation sont initialisés à zero pour tout les abonnements." watch_out_when_creating_a_new_machine_its_prices_are_initialized_at_0_for_all_subscriptions: "Attention, lors de la création d'une machine, ses tarifs de réservation sont initialisés à zero pour tous les abonnements."
consider_changing_them_before_creating_any_reservation_slot: "Pensez à les modifier avant de créer des créneaux pour cette machine." consider_changing_them_before_creating_any_reservation_slot: "Pensez à les modifier avant de créer des créneaux pour cette machine."
machines_edit: machines_edit:
@ -13,28 +13,38 @@ fr:
calendar: calendar:
# gestion des créneaux machines et formations # gestion des créneaux machines et formations
calendar_management: "Gestion du calendrier" admin_calendar:
ongoing_reservations: "Réservations en cours" calendar_management: "Gestion du calendrier"
no_reservations: "Aucune réservation" trainings: "Formations"
do_you_really_want_to_cancel_the_USER_s_reservation_the_DATE_at_TIME_concerning_RESERVATION: "Êtes-vous {GENDER, select, female{sûre} other{sûr}} de vouloir annuler la réservation de {USER}, le {DATE} à {TIME}, concernant {RESERVATION} ?" # messageFormat interpolation machines: "Machines"
reservation_cancellation_failed: "L'annulation de la réservation a échouée." spaces: "Espaces"
unable_to_remove_the_last_machine_of_the_slot_delete_the_slot_rather: "Impossible de supprimer la dernière machine du créneau. Supprimez plutôt le créneau." ongoing_reservations: "Réservations en cours"
do_you_really_want_to_remove_MACHINE_from_this_slot: "Êtes-vous {GENDER, select, female{sûre} other{sûr}} de vouloir retirer \"{MACHINE}\" de ce créneau ?" # messageFormat interpolation no_reservations: "Aucune réservation"
this_will_prevent_any_new_reservation_on_this_slot_but_wont_cancel_those_existing: "Ceci interdira toute nouvelle réservation de cette machine sur ce créneau mais n'annulera pas les réservation existantes." confirmation_required: "Confirmation requise"
beware_this_cannot_be_reverted: "Attention : ceci n'est pas réversible." do_you_really_want_to_cancel_the_USER_s_reservation_the_DATE_at_TIME_concerning_RESERVATION: "Êtes-vous {GENDER, select, female{sûre} other{sûr}} de vouloir annuler la réservation de {USER}, le {DATE} à {TIME}, concernant {RESERVATION} ?" # messageFormat interpolation
the_machine_was_successfully_removed_from_the_slot: "La machine a bien été supprimée du créneau." reservation_was_successfully_cancelled: "La réservation a bien été annulée."
deletion_failed: "La suppression a échouée." reservation_cancellation_failed: "L'annulation de la réservation a échouée."
DATE_slot: "Créneau du {{DATE}} :" # angular interpolation unable_to_remove_the_last_machine_of_the_slot_delete_the_slot_rather: "Impossible de supprimer la dernière machine du créneau. Supprimez plutôt le créneau."
you_can_define_a_training_on_that_slot: "Vous pouvez définir une formation sur ce créneau :" do_you_really_want_to_remove_MACHINE_from_this_slot: "Êtes-vous {GENDER, select, female{sûre} other{sûr}} de vouloir retirer \"{MACHINE}\" de ce créneau ?" # messageFormat interpolation
link_a_training: "Associer une formation" this_will_prevent_any_new_reservation_on_this_slot_but_wont_cancel_those_existing: "Ceci interdira toute nouvelle réservation de cette machine sur ce créneau mais n'annulera pas les réservation existantes."
or_: "Ou" beware_this_cannot_be_reverted: "Attention : ceci n'est pas réversible."
_select_some_machines: "sélectionner des machines" the_machine_was_successfully_removed_from_the_slot: "La machine a bien été supprimée du créneau."
number_of_tickets: "Nombre de places : " deletion_failed: "La suppression a échouée."
adjust_the_opening_hours: "Ajuster l'horaire" DATE_slot: "Créneau du {{DATE}} :" # angular interpolation
restrict_this_slot_with_labels_(optional): "Restreindre ce créneau avec des étiquettes (optionnel)" what_kind_of_slot_do_you_want_to_create: "Quel type de créneau voulez-vous créer ?"
the_slot_START-END_has_been_successfully_deleted: "Le créneau {{START}} - {{END}} a bien été supprimé" # angular interpolation training: "Formation"
unable_to_delete_the_slot_START-END_because_it_s_already_reserved_by_a_member: "Le créneau {{START}} - {{END}} n'a pu être supprimé car il est déjà réservé par un membre" # angular interpolation machine: "Machine"
you_should_link_a_training_or_a_machine_to_this_slot: "Vous devriez associer une formation ou une machine à ce créneau." space: "Espace"
next: "Suivant >"
previous: "< Précédent"
select_some_machines: "Sélectionnez des machines"
number_of_tickets: "Nombre de places : "
adjust_the_opening_hours: "Ajuster l'horaire"
to_time: "à" # context: time. eg. "from 18:00 to 21:00"
restrict_this_slot_with_labels_(optional): "Restreindre ce créneau avec des étiquettes (optionnel)"
the_slot_START-END_has_been_successfully_deleted: "Le créneau {{START}} - {{END}} a bien été supprimé" # angular interpolation
unable_to_delete_the_slot_START-END_because_it_s_already_reserved_by_a_member: "Le créneau {{START}} - {{END}} n'a pu être supprimé car il est déjà réservé par un membre" # angular interpolation
you_should_select_at_least_a_machine: "Vous devriez sélectionne au moins une machine pour ce créneau."
project_elements: project_elements:
# gestion des éléments constituant les projets # gestion des éléments constituant les projets
@ -132,55 +142,78 @@ fr:
pricing: pricing:
# gestion des abonnements, des tarifs, des crédits et des codes promo # gestion des abonnements, des tarifs, des crédits et des codes promo
pricing_management: "Gestion de la tarification" pricing:
list_of_the_subscription_plans: "Liste des formules d'abonnements" pricing_management: "Gestion de la tarification"
beware_the_subscriptions_are_disabled_on_this_application: "Attention, les abonnements sont désactivés sur cette application." subscriptions: "Abonnements"
you_can_create_some_but_they_wont_be_available_until_the_project_is_redeployed_by_the_server_manager: "Vous pouvez tout de même en créer mais ils ne seront disponibles qu'après un redéploiement du projet par le responsable du serveur." trainings: "Formations"
for_safety_reasons_please_dont_create_subscriptions_if_you_dont_want_intend_to_use_them_later: "Pour des raisons de sécurité, veuillez ne pas créer d'abonnements si vous ne comptez pas les utiliser par la suite." list_of_the_subscription_plans: "Liste des formules d'abonnements"
add_a_new_subscription_plan: "Ajouter une nouvelle formule d'abonnement" beware_the_subscriptions_are_disabled_on_this_application: "Attention, les abonnements sont désactivés sur cette application."
duration: "Durée" you_can_create_some_but_they_wont_be_available_until_the_project_is_redeployed_by_the_server_manager: "Vous pouvez tout de même en créer mais ils ne seront disponibles qu'après un redéploiement du projet par le responsable du serveur."
prominence: "Importance" for_safety_reasons_please_dont_create_subscriptions_if_you_dont_want_intend_to_use_them_later: "Pour des raisons de sécurité, veuillez ne pas créer d'abonnements si vous ne comptez pas les utiliser par la suite."
machine_hours: "Heures machines" add_a_new_subscription_plan: "Ajouter une nouvelle formule d'abonnement"
these_prices_match_machine_hours_rates_: "Ces tarifs correspondent au prix d'une heure machine" type: "Type"
_without_subscriptions: "sans abonnement" partner: "Partenaire"
credits: "Crédits" standard: "Standard"
related_trainings: "Formations associées" name: "Nom"
add_a_machine_credit: "Ajouter un crédit Machine" duration: "Durée"
machine: "Machine" group: "Groupe"
hours: "Heures" prominence: "Importance"
related_subscriptions: "Abonnements associés" price: "Prix"
please_specify_a_number: "Veuillez spécifier un nombre." machine_hours: "Heures machines"
none: "Aucune" # grammar note: concordance with "training". these_prices_match_machine_hours_rates_: "Ces tarifs correspondent au prix d'une heure machine"
an_error_occurred_while_saving_the_number_of_credits: "Une erreur est survenue lors de l'enregistrement du nombre de crédits." _without_subscriptions: "sans abonnement"
an_error_occurred_while_deleting_credit_with_the_TRAINING: "Une erreur est survenue lors de la suppression du crédit avec la {{TRAINING}}." # angular interpolation machines: "Machines"
an_error_occurred_unable_to_find_the_credit_to_revoke: "Une erreur est survenue : impossible de retrouver le crédit à enlever." credits: "Crédits"
an_error_occurred_while_creating_credit_with_the_TRAINING: "Une erreur est survenue lors de la création du crédit avec la {{TRAINING}}." # angular interpolation subscription: "Abonnement"
not_set: "Non défini" related_trainings: "Formations associées"
error_a_credit_linking_this_machine_with_that_subscription_already_exists: "Erreur : un crédit associant cette machine et cet abonnement existe déjà." add_a_machine_credit: "Ajouter un crédit Machine"
changes_have_been_successfully_saved: "Les modifications ont bien été enregistrées." machine: "Machine"
credit_was_successfully_saved: "Le crédit a bien été enregistré." hours: "Heures"
do_you_really_want_to_delete_this_subscription_plan: "Êtes-vous sûr(e) de vouloir supprimer cette formule d'abonnement ?" related_subscriptions: "Abonnements associés"
subscription_plan_was_successfully_deleted: "La formule d'abonnement a bien été supprimée." please_specify_a_number: "Veuillez spécifier un nombre."
unable_to_delete_the_specified_subscription_an_error_occurred: "Impossible de supprimer l'abonnement spécifié, une erreur s'est produite." none: "Aucune" # grammar note: concordance with "training".
coupons: "Codes promotionnels" an_error_occurred_while_saving_the_number_of_credits: "Une erreur est survenue lors de l'enregistrement du nombre de crédits."
list_of_the_coupons: "Liste des codes promotionnels" an_error_occurred_while_deleting_credit_with_the_TRAINING: "Une erreur est survenue lors de la suppression du crédit avec la {{TRAINING}}." # angular interpolation
discount: "Réduction" an_error_occurred_unable_to_find_the_credit_to_revoke: "Une erreur est survenue : impossible de retrouver le crédit à enlever."
nb_of_usages: "Nombre d'utilisations" an_error_occurred_while_creating_credit_with_the_TRAINING: "Une erreur est survenue lors de la création du crédit avec la {{TRAINING}}." # angular interpolation
status: "Statut" not_set: "Non défini"
add_a_new_coupon: "Ajouter un code promotionnel" error_a_credit_linking_this_machine_with_that_subscription_already_exists: "Erreur : un crédit associant cette machine et cet abonnement existe déjà."
disabled: "Désactivé" changes_have_been_successfully_saved: "Les modifications ont bien été enregistrées."
expired: "Expiré" credit_was_successfully_saved: "Le crédit a bien été enregistré."
sold_out: "Épuisé" do_you_really_want_to_delete_this_subscription_plan: "Êtes-vous sûr(e) de vouloir supprimer cette formule d'abonnement ?"
active: "Actif" subscription_plan_was_successfully_deleted: "La formule d'abonnement a bien été supprimée."
do_you_really_want_to_delete_this_coupon: "Êtes-vous sûr(e) de vouloir supprimer ce code promotionnel ?" unable_to_delete_the_specified_subscription_an_error_occurred: "Impossible de supprimer l'abonnement spécifié, une erreur s'est produite."
coupon_was_successfully_deleted: "Le code promotionnel a bien été supprimé." coupons: "Codes promotionnels"
unable_to_delete_the_specified_coupon_already_in_use: "Impossible de supprimer le code promotionnel : il est utilisé dans des factures." list_of_the_coupons: "Liste des codes promotionnels"
unable_to_delete_the_specified_coupon_an_unexpected_error_occurred: "Impossible de supprimer le code promotionnel : une erreur inattendue s'est produite." discount: "Réduction"
send_a_coupon: "Envoyer un code promo" nb_of_usages: "Nombre d'utilisations"
coupon: "Code promo" status: "Statut"
usages: "Utilisations" add_a_new_coupon: "Ajouter un code promotionnel"
coupon_successfully_sent_to_USER: "Le code promotionnel a bien été envoyé à {{USER}}" # angular interpolation disabled: "Désactivé"
an_error_occurred_unable_to_send_the_coupon: "Une erreur inattendue a empêché l'envoi du code promotionnel." expired: "Expiré"
sold_out: "Épuisé"
active: "Actif"
confirmation_required: "Confirmation requise"
do_you_really_want_to_delete_this_coupon: "Êtes-vous sûr(e) de vouloir supprimer ce code promotionnel ?"
coupon_was_successfully_deleted: "Le code promotionnel a bien été supprimé."
unable_to_delete_the_specified_coupon_already_in_use: "Impossible de supprimer le code promotionnel : il est utilisé dans des factures."
unable_to_delete_the_specified_coupon_an_unexpected_error_occurred: "Impossible de supprimer le code promotionnel : une erreur inattendue s'est produite."
send_a_coupon: "Envoyer un code promo"
coupon: "Code promo"
usages: "Utilisations"
coupon_successfully_sent_to_USER: "Le code promotionnel a bien été envoyé à {{USER}}" # angular interpolation
an_error_occurred_unable_to_send_the_coupon: "Une erreur inattendue a empêché l'envoi du code promotionnel."
code: "Code"
enabled: "Activé"
validity_per_user: "Validité par utilisateur"
once: "Une seule fois"
forever: "À chaque utilisation"
valid_until: "Valable jusqu'au (inclus)"
spaces: "Espaces"
these_prices_match_space_hours_rates_: "Ces tarifs correspondent au prix d'une heure espace"
add_a_space_credit: "Ajouter un crédit Espace"
space: "Espace"
error_a_credit_linking_this_space_with_that_subscription_already_exists: "Erreur : un crédit associant cet espace et cet abonnement existe déjà."
coupons_new: coupons_new:
# ajouter un code promotionnel # ajouter un code promotionnel
@ -194,19 +227,24 @@ fr:
plans: plans:
new: new:
# ajouter une formule d'abonnement sur la plate-forme # ajouter une formule d'abonnement sur la plate-forme
add_a_subscription_plan: "Ajouter une formule d'abonnement" new_plan:
unable_to_create_the_subscription_please_try_again: "L'abonnement n'a pas pu être créé. Veuillez réessayer." add_a_subscription_plan: "Ajouter une formule d'abonnement"
successfully_created_subscription(s)_dont_forget_to_redefine_prices: "Création du/des abonnement(s) réussie. N'oubliez pas de redéfinir les tarifs." unable_to_create_the_subscription_please_try_again: "L'abonnement n'a pas pu être créé. Veuillez réessayer."
unable_to_save_this_user_check_that_there_isnt_an_already_a_user_with_the_same_name: "Impossible d'enregistrer cet utilisateur. Vérifiez qu'il n'existe pas déjà un utilisateur du même nom." successfully_created_subscription(s)_dont_forget_to_redefine_prices: "Création du/des abonnement(s) réussie. N'oubliez pas de redéfinir les tarifs."
unable_to_save_this_user_check_that_there_isnt_an_already_a_user_with_the_same_name: "Impossible d'enregistrer cet utilisateur. Vérifiez qu'il n'existe pas déjà un utilisateur du même nom."
edit: edit:
# modifier une formule d'abonnement / les prix des heures machines # modifier une formule d'abonnement / les prix des heures machines
subscription_plan: "Formule d'abonnement :" edit_plan:
prices: "Tarifs" subscription_plan: "Formule d'abonnement :"
copy_prices_from: "Copier les prix depuis" prices: "Tarifs"
machine: "Machine" copy_prices_from: "Copier les prix depuis"
hourly_rate: "Tarif horaire" machines: "Machines"
unable_to_save_subscription_changes_please_try_again: "Les modifications de l'abonnement n'ont pas pu être enregistrées. Veuillez réessayer." machine: "Machine"
subscription_successfully_changed: "Modification de l'abonnement réussie." hourly_rate: "Tarif horaire"
spaces: "Espaces"
space: "Espace"
unable_to_save_subscription_changes_please_try_again: "Les modifications de l'abonnement n'ont pas pu être enregistrées. Veuillez réessayer."
subscription_successfully_changed: "Modification de l'abonnement réussie."
invoices: invoices:
# liste de toutes les factures & paramètres de facturation # liste de toutes les factures & paramètres de facturation
@ -476,90 +514,95 @@ fr:
settings: settings:
# paramètres globaux de l'application et personnalisation # paramètres globaux de l'application et personnalisation
customize_the_application: "Personnalisation de l'application" settings:
general: "Général" title: "Titre"
fablab_title: "Titre du FabLab" customize_the_application: "Personnalisation de l'application"
fablab_name: "Nom du FabLab" general: "Général"
title_concordance: "Accord du titre" fablab_title: "Titre du FabLab"
male: "Masculin." fablab_name: "Nom du FabLab"
female: "Féminin." title_concordance: "Accord du titre"
eg: "ex :" male: "Masculin."
about: "A propos" female: "Féminin."
male_preposition: "du" eg: "ex :"
female_preposition: "de la" about: "A propos"
customize_information_messages: "Personnaliser les messages d'informations" male_preposition: "du"
message_of_the_machine_booking_page: "Message sur la page de réservation d'une machine :" female_preposition: "de la"
type_the_message_content: "Saisir le contenu du message" customize_information_messages: "Personnaliser les messages d'informations"
warning_message_of_the_training_booking_page: "Message d'avertissement sur la page de réservation d'une formation :" message_of_the_machine_booking_page: "Message sur la page de réservation d'une machine :"
information_message_of_the_training_reservation_page: "Message d'information sur la page de réservation d'une formation :" type_the_message_content: "Saisir le contenu du message"
message_of_the_subscriptions_page: "Message sur la page des abonnements :" warning_message_of_the_training_booking_page: "Message d'avertissement sur la page de réservation d'une formation :"
message_of_the_events_page: "Message sur la page des évènements :" information_message_of_the_training_reservation_page: "Message d'information sur la page de réservation d'une formation :"
legal_documents: "Documents légaux" message_of_the_subscriptions_page: "Message sur la page des abonnements :"
if_these_documents_are_not_filled_no_consent_about_them_will_be_asked_to_the_user: "Si ces documents ne sont pas renseignés, aucun consentement à leur sujet ne sera demandé à l'utilisateur." message_of_the_events_page: "Message sur la page des évènements :"
general_terms_and_conditions_(T&C): "Conditions générales de vente (CGV)" message_of_the_spaces_page: "Message sur la page des espaces :"
terms_of_service_(TOS): "Conditions générales d'utilisation (CGU)" legal_documents: "Documents légaux"
customize_the_graphics: "Personnaliser la charte graphique" if_these_documents_are_not_filled_no_consent_about_them_will_be_asked_to_the_user: "Si ces documents ne sont pas renseignés, aucun consentement à leur sujet ne sera demandé à l'utilisateur."
for_an_optimal_rendering_the_logo_image_must_be_at_the_PNG_format_with_a_transparent_background_and_with_an_aspect_ratio_3.5_times_wider_than_the_height: "Pour un rendu optimal, l'image du logo doit être au format PNG avec un fond transparent et d'un aspect environ 3,5 fois plus long que haut." general_terms_and_conditions_(T&C): "Conditions générales de vente (CGV)"
concerning_the_favicon_it_must_be_at_ICO_format_with_a_size_of_16x16_pixels: "La favicon devrait quant à elle être au format ICO et d'une taille de 16x16 pixels." terms_of_service_(TOS): "Conditions générales d'utilisation (CGU)"
remember_to_refresh_the_page_for_the_changes_to_take_effect: "Pensez à rafraîchir la page pour que les modifications prennent effet." customize_the_graphics: "Personnaliser la charte graphique"
logo_(white_background): "Logo (fond blanc)" for_an_optimal_rendering_the_logo_image_must_be_at_the_PNG_format_with_a_transparent_background_and_with_an_aspect_ratio_3.5_times_wider_than_the_height: "Pour un rendu optimal, l'image du logo doit être au format PNG avec un fond transparent et d'un aspect environ 3,5 fois plus long que haut."
change_the_logo: "Changer le logo" concerning_the_favicon_it_must_be_at_ICO_format_with_a_size_of_16x16_pixels: "La favicon devrait quant à elle être au format ICO et d'une taille de 16x16 pixels."
logo_(black_background): "Logo (font noir)" remember_to_refresh_the_page_for_the_changes_to_take_effect: "Pensez à rafraîchir la page pour que les modifications prennent effet."
favicon: "Favicon" logo_(white_background): "Logo (fond blanc)"
change_the_favicon: "Changer la favicon" change_the_logo: "Changer le logo"
main_colour: "Couleur principale :" logo_(black_background): "Logo (font noir)"
primary: "Primaire" favicon: "Favicon"
secondary_colour: "Couleur secondaire :" change_the_favicon: "Changer la favicon"
secondary: "Secondaire" main_colour: "Couleur principale :"
background_picture_of_the_profile_banner: "Image de fond du bandeau de profil" primary: "Primaire"
change_the_profile_banner: "Changer le bandeau de profil" secondary_colour: "Couleur secondaire :"
home_page: "Page d'accueil" secondary: "Secondaire"
news_of_the_home_page: "Brève de la page d'accueil :" background_picture_of_the_profile_banner: "Image de fond du bandeau de profil"
type_your_news_here: "Saisir votre brève ici" change_the_profile_banner: "Changer le bandeau de profil"
leave_it_empty_to_not_bring_up_any_news_on_the_home_page: "Laisser vide pour ne pas faire apparaître de brève sur la page d'accueil" home_page: "Page d'accueil"
twitter_stream: "Flux Twitter :" news_of_the_home_page: "Brève de la page d'accueil :"
name_of_the_twitter_account: "Nom du compte Twitter" type_your_news_here: "Saisir votre brève ici"
title_of_the_about_page: "Titre page A propos" leave_it_empty_to_not_bring_up_any_news_on_the_home_page: "Laisser vide pour ne pas faire apparaître de brève sur la page d'accueil"
shift_enter_to_force_carriage_return: "MAJ. + ENTRÉE pour forcer le retour à la ligne" twitter_stream: "Flux Twitter :"
input_the_main_content: "Saisir le contenu principal" name_of_the_twitter_account: "Nom du compte Twitter"
input_the_fablab_contacts: "Saisir les Contacts du FabLab" title_of_the_about_page: "Titre page A propos"
reservations: "Réservations" shift_enter_to_force_carriage_return: "MAJ. + ENTRÉE pour forcer le retour à la ligne"
reservations_parameters: "Paramètres des réservations" input_the_main_content: "Saisir le contenu principal"
confine_the_booking_agenda: "Borner l'agenda de réservation" input_the_fablab_contacts: "Saisir les Contacts du FabLab"
opening_time: "Heure d'ouverture" reservations: "Réservations"
closing_time: "Heure de fermeture" reservations_parameters: "Paramètres des réservations"
ability_for_the_users_to_move_their_reservations: "Possibilité pour l'utilisateur de déplacer ses réservations" confine_the_booking_agenda: "Borner l'agenda de réservation"
reservations_shifting: "Déplacement des réservations" opening_time: "Heure d'ouverture"
prior_period_(hours): "Délai préalable (en heures)" closing_time: "Heure de fermeture"
enabled: "Activé" ability_for_the_users_to_move_their_reservations: "Possibilité pour l'utilisateur de déplacer ses réservations"
disabled: "Désactivé" reservations_shifting: "Déplacement des réservations"
ability_for_the_users_to_cancel_their_reservations: "Possibilité pour l'utilisateur d'annuler ses réservations" prior_period_(hours): "Délai préalable (en heures)"
reservations_cancelling: "Annulation des réservations" enabled: "Activé"
reservations_reminders: "Rappel des réservations" disabled: "Désactivé"
notification_sending_before_the_reservation_occurs: "Envoi de notification avant l'avènement de la réservation" ability_for_the_users_to_cancel_their_reservations: "Possibilité pour l'utilisateur d'annuler ses réservations"
customization_of_SETTING_successfully_saved: "La personnalisation de {{SETTING}} a bien été enregistrée." # angular interpolation reservations_cancelling: "Annulation des réservations"
file_successfully_updated: "Le fichier a bien été mis à jour." reservations_reminders: "Rappel des réservations"
name_genre: "l'accord du nom" notification_sending_before_the_reservation_occurs: "Envoi de notification avant l'avènement de la réservation"
machine_explications_alert: "l'explication sur la page de réservation d'une machine" customization_of_SETTING_successfully_saved: "La personnalisation de {{SETTING}} a bien été enregistrée." # angular interpolation
training_explications_alert: "l'explication sur la page de réservation d'une formation" file_successfully_updated: "Le fichier a bien été mis à jour."
training_information_message: "l'information sur la page de réservation d'une formation" name_genre: "l'accord du nom"
subscription_explications_alert: "l'explication sur la page de souscription à un abonnement" machine_explications_alert: "l'explication sur la page de réservation d'une machine"
main_color: "la couleur principale" training_explications_alert: "l'explication sur la page de réservation d'une formation"
secondary_color: "la couleur secondaire" training_information_message: "l'information sur la page de réservation d'une formation"
home_blogpost: "la brève de la page d'accueil" subscription_explications_alert: "l'explication sur la page de souscription à un abonnement"
twitter_name: "nom du flux Twitter" event_explications_alert: "l'explication sur la page de réservation d'un évènement"
about_title: "titre de la page \"À propos\"" space_explications_alert: "l'explication sur la page de réservation d'un espace"
about_body: "corps de la page \"À propos\"" main_color: "la couleur principale"
about_contacts: "contacts sur la page \"À propos\"" secondary_color: "la couleur secondaire"
booking_window_start: "l'heure d'ouverture" home_blogpost: "la brève de la page d'accueil"
booking_window_end: "l'heure de fermeture" twitter_name: "nom du flux Twitter"
booking_move_enable: "l'activation du déplacement de réservation" about_title: "titre de la page \"À propos\""
booking_move_delay: "délai préventif de déplacement" about_body: "corps de la page \"À propos\""
booking_cancel_enable: "l'activation de l'annulation de réservation" about_contacts: "contacts sur la page \"À propos\""
booking_cancel_delay: "délai préventif d'annulation" booking_window_start: "l'heure d'ouverture"
reminder_enable: "l'activation du rappel de réservation" booking_window_end: "l'heure de fermeture"
reminder_delay: "délai avant envoi de la notification de rappel" booking_move_enable: "l'activation du déplacement de réservation"
default_value_is_24_hours: "Si aucune valeur n'est renseignée : 24 heures." booking_move_delay: "délai préventif de déplacement"
booking_cancel_enable: "l'activation de l'annulation de réservation"
booking_cancel_delay: "délai préventif d'annulation"
reminder_enable: "l'activation du rappel de réservation"
reminder_delay: "délai avant envoi de la notification de rappel"
default_value_is_24_hours: "Si aucune valeur n'est renseignée : 24 heures."
open_api_clients: open_api_clients:
add_new_client: "Créer un compte client" add_new_client: "Créer un compte client"
@ -575,3 +618,17 @@ fr:
client_successfully_updated: "Les modifications ont été enregistrées." client_successfully_updated: "Les modifications ont été enregistrées."
client_successfully_deleted: "Le compte client a bien été supprimé." client_successfully_deleted: "Le compte client a bien été supprimé."
access_successfully_revoked: "L'accès a bien été revoqué." access_successfully_revoked: "L'accès a bien été revoqué."
space_new:
# créer un nouvel espace
space_new:
add_a_new_space: "Ajouter un nouvel espace"
watch_out_when_creating_a_new_space_its_prices_are_initialized_at_0_for_all_subscriptions: "Attention, lors de la création d'un espace, ses tarifs de réservation sont initialisés à zero pour tous les abonnements."
consider_changing_its_prices_before_creating_any_reservation_slot: "Pensez à modifier ces prix avant de créer des créneaux pour cet espace."
add_this_space: "Ajouter cet espace"
space_edit:
# modifier un espace existant
space_edit:
edit_the_space_NAME: "Modifier l'espace : {{NAME}}" # angular interpolation
validate_the_changes: "Valider les modifications"

View File

@ -98,67 +98,27 @@ en:
machines_reserve: machines_reserve:
# book a machine # book a machine
machine_planning: "Machine planning" machine_planning: "Machine planning"
select_one_or_more_slots_in_the_calendar: "Select one or more slots in the calendar" i_ve_reserved: "I've reserved"
you_ve_just_selected_the_slot: "You've just selected the slot:"
datetime_to_time: "{{START_DATETIME}} to {{END_TIME}}" # angular interpolation, eg: Thursday, September 4 1986 8:30 PM to 10:00 PM
cost_of_a_machine_hour: "Cost of a machine hour"
offer_this_slot: "Offer this slot"
confirm_this_slot: "Confirm this slot"
remove_this_slot: "Remove this slot"
to_benefit_from_attractive_prices: "To benefit from attractive prices"
view_our_subscriptions: "View our subscriptions"
cost_of_the_subscription: "Cost of the subscription"
you_have_settled_the_following_machine_hours: "You have settled the following machine hours:"
you_have_settled_a_: "You have settled a"
i_want_to_change_the_following_reservation: "I want to change the following reservation:"
cancel_my_modification: "Cancel my modification"
select_a_new_slot_in_the_calendar: "Select a new slot in the calendar"
cancel_my_selection: "Cancel my selection"
tags_of_the_original_slot: "Tags of the original slot:"
tags_of_the_destination_slot: "Tags of the destination slot:"
confirm_my_modification: "Confirm my modification"
your_booking_slot_was_successfully_moved_from_: "Your booking slot was successfully moved from"
i_ve_reserved: "J'ai réservé"
not_available: "Not available" not_available: "Not available"
unable_to_change_the_reservation: "Unable to change the reservation"
i_reserve: "I reserve" i_reserve: "I reserve"
i_shift: "I shift" i_shift: "I shift"
i_change: "I change" i_change: "I change"
do_you_really_want_to_cancel_this_reservation: "So you really want to cancel this reservation?"
reservation_was_cancelled_successfully: "Reservation was cancelled successfully."
cancellation_failed: "Cancellation failed."
a_problem_occured_during_the_payment_process_please_try_again_later: "A problem occurred during the payment process. Please try again later."
trainings_reserve: trainings_reserve:
# book a training # book a training
trainings_planning: "Trainings planning" trainings_planning: "Trainings planning"
planning_of: "Planning of" # followed by the training name (eg. "Planning of 3d printer training") planning_of: "Planning of" # followed by the training name (eg. "Planning of 3d printer training")
all_trainings: "All trainings" all_trainings: "All trainings"
select_a_slot_in_the_calendar: "Select a slot in the calendar"
you_ve_just_selected_the_slot: "You've just selected the slot:"
datetime_to_time: "{{START_DATETIME}} to {{END_TIME}}" # angular interpolation, eg: Thursday, September 4 1986 8:30 PM to 10:00 PM
offer_this_training: "Offer this training"
confirm_this_slot: "Confirm this slot"
remove_this_slot: "Remove this slot"
to_benefit_from_attractive_prices_and_a_free_training: "To benefit from attractives prices and a free training"
view_our_subscriptions: "View our subscriptions"
subscription_cost: "Subscription cost"
you_have_settled_the_training: "You have settled the training"
training_cost_: "Training cost:"
you_have_settled_a_: "You have settled a"
i_want_to_change_the_following_reservation: "I want to change the following reservation:"
cancel_my_modification: "Cancel my modification"
select_a_new_slot_in_the_calendar: "Select a new slot in the calendar"
cancel_my_selection: "Cancel my selection" cancel_my_selection: "Cancel my selection"
confirm_my_modification: "Confirm my modification"
your_booking_slot_was_successfully_moved_from_: "Your booking slot was successfully moved from"
i_ve_reserved: "I've reserved" i_ve_reserved: "I've reserved"
an_error_occured_preventing_the_booked_slot_from_being_modified: "An error occurred preventing the booked slot from being modified."
i_shift: "I shift" space_reserve:
i_change: "I change" # book a space
do_you_really_want_to_cancel_this_reservation: "Do you really want to cancel this reservation?" space_reserve:
cancellation_failed: "Cancellation failed." planning_of_space_NAME: "Planning of the {{NAME}} space" # angular interpolation
a_problem_occured_during_the_payment_process_please_try_again_later: "A problem occured during the payment process. Please try again later." i_ve_reserved: "I've reserved"
i_shift: "I shift"
i_change: "I change"
notifications: notifications:
notifications_center: "Notifications center" notifications_center: "Notifications center"

View File

@ -98,67 +98,27 @@ fr:
machines_reserve: machines_reserve:
# réserver une machine # réserver une machine
machine_planning: "Planning machine" machine_planning: "Planning machine"
select_one_or_more_slots_in_the_calendar: "Sélectionnez un ou plusieurs créneaux dans le calendrier"
you_ve_just_selected_the_slot: "Vous venez de sélectionner le créneau :"
datetime_to_time: "{{START_DATETIME}} à {{END_TIME}}" # angular interpolation, eg: Thursday, September 4 1986 8:30 PM to 10:00 PM
cost_of_a_machine_hour: "Coût de l'heure machine"
offer_this_slot: "Offrir ce créneau"
confirm_this_slot: "Valider ce créneau"
remove_this_slot: "Supprimer ce créneau"
to_benefit_from_attractive_prices: "Pour bénéficier de prix avantageux"
view_our_subscriptions: "Consultez nos abonnements"
cost_of_the_subscription: "Coût de l'abonnement"
you_have_settled_the_following_machine_hours: "Vous avez réglé les heures machines suivantes :"
you_have_settled_a_: "Vous avez réglé un"
i_want_to_change_the_following_reservation: "Je souhaite modifier ma réservation suivante :"
cancel_my_modification: "Annuler ma modification"
select_a_new_slot_in_the_calendar: "Sélectionnez un nouveau créneau dans le calendrier"
cancel_my_selection: "Annuler ma sélection"
tags_of_the_original_slot: "Étiquettes du créneau d'origine :"
tags_of_the_destination_slot: "Étiquettes du créneau de destination :"
confirm_my_modification: "Valider ma modification"
your_booking_slot_was_successfully_moved_from_: "Votre créneau de réservation a bien été déplacé du"
i_ve_reserved: "J'ai réservé" i_ve_reserved: "J'ai réservé"
not_available: "Non disponible" not_available: "Non disponible"
unable_to_change_the_reservation: "Impossible de modifier la réservation"
i_reserve: "Je réserve" i_reserve: "Je réserve"
i_shift: "Je déplace" i_shift: "Je déplace"
i_change: "Je change" i_change: "Je change"
do_you_really_want_to_cancel_this_reservation: "Êtes-vous sur de vouloir annuler cette réservation ?"
reservation_was_cancelled_successfully: "La réservation a bien été annulée."
cancellation_failed: "L'annulation a échoué."
a_problem_occured_during_the_payment_process_please_try_again_later: "Il y a eu un problème lors de la procédure de paiement. Veuillez réessayer plus tard."
trainings_reserve: trainings_reserve:
# réserver une formation # réserver une formation
trainings_planning: "Planning formations" trainings_planning: "Planning formations"
planning_of: "Planning de la" # suivi du nom de la formation (eg. "Planning de la formation imprimante 3d") planning_of: "Planning de la" # suivi du nom de la formation (eg. "Planning de la formation imprimante 3d")
all_trainings: "Toutes les formations" all_trainings: "Toutes les formations"
select_a_slot_in_the_calendar: "Sélectionnez un créneau dans le calendrier"
you_ve_just_selected_the_slot: "Vous venez de sélectionner le créneau :"
datetime_to_time: "{{START_DATETIME}} à {{END_TIME}}" # angular interpolation, eg: Thursday, September 4 1986 8:30 PM to 10:00 PM
offer_this_training: "Offrir cette formation"
confirm_this_slot: "Valider ce créneau"
remove_this_slot: "Supprimer ce créneau"
to_benefit_from_attractive_prices_and_a_free_training: "Pour bénéficier de prix avantageux et d'une formation offerte"
view_our_subscriptions: "Consultez nos abonnements"
subscription_cost: "Coût de l'abonnement"
you_have_settled_the_training: "Vous avez réglé la formation"
training_cost_: "Coût de la formation :"
you_have_settled_a_: "Vous avez réglé un"
i_want_to_change_the_following_reservation: "Je souhaite modifier ma réservation suivante :"
cancel_my_modification: "Annuler ma modification"
select_a_new_slot_in_the_calendar: "Sélectionnez un nouveau créneau dans le calendrier"
cancel_my_selection: "Annuler ma sélection" cancel_my_selection: "Annuler ma sélection"
confirm_my_modification: "Valider ma modification"
your_booking_slot_was_successfully_moved_from_: "Votre créneau de réservation a bien été déplacé du"
i_ve_reserved: "J'ai réservé" i_ve_reserved: "J'ai réservé"
an_error_occured_preventing_the_booked_slot_from_being_modified: "Une erreur est survenue, empêchant la modification du créneau réservé."
i_shift: "Je déplace" space_reserve:
i_change: "Je change" # réserver un espace
do_you_really_want_to_cancel_this_reservation: "Êtes-vous sur de vouloir annuler cette réservation ?" space_reserve:
cancellation_failed: "L'annulation a échoué." planning_of_space_NAME: "Planning de l'espace {{NAME}}" # angular interpolation
a_problem_occured_during_the_payment_process_please_try_again_later: "Il y a eu un problème lors de la procédure de paiement. Veuillez réessayer plus tard." i_ve_reserved: "J'ai réservé"
i_shift: "Je déplace"
i_change: "Je change"
notifications: notifications:
notifications_center: "Centre de notifications" notifications_center: "Centre de notifications"

View File

@ -31,6 +31,7 @@ en:
reserve_a_machine: "Reserve a Machine" reserve_a_machine: "Reserve a Machine"
trainings_registrations: "Trainings registrations" trainings_registrations: "Trainings registrations"
events_registrations: "Events registrations" events_registrations: "Events registrations"
reserve_a_space: "Reserve a Space"
projects_gallery: "Projects gallery" projects_gallery: "Projects gallery"
subscriptions: "Subscriptions" subscriptions: "Subscriptions"
public_calendar: "Calendar" public_calendar: "Calendar"
@ -44,6 +45,7 @@ en:
subscriptions_and_prices: "Subscriptions and Prices" subscriptions_and_prices: "Subscriptions and Prices"
manage_the_events: "Manage the events" manage_the_events: "Manage the events"
manage_the_machines: "Manage the Machines" manage_the_machines: "Manage the Machines"
manage_the_spaces: "Manage the Spaces"
manage_the_projects_elements: "Manage the Projects Elements" manage_the_projects_elements: "Manage the Projects Elements"
statistics: "Statistics" statistics: "Statistics"
customization: "Customization" customization: "Customization"
@ -246,6 +248,29 @@ en:
you_can_shift_this_reservation_on_the_following_slots: "You can shift this reservation on the following slots:" you_can_shift_this_reservation_on_the_following_slots: "You can shift this reservation on the following slots:"
calendar: calendar:
calendar: "Calendar" # public calendar
show_no_disponible: "Show the slots no disponibles" calendar:
filter-calendar: "Filter calendar" calendar: "Calendar"
show_unavailables: "Show unavailable slots"
filter_calendar: "Filter calendar"
trainings: "Trainings"
machines: "Machines"
spaces: "Spaces"
events: "Events"
spaces_list:
# list of spaces
the_spaces: "The spaces"
add_a_space: "Add a space"
space_show:
# display the details of a space
space_show:
book_this_space: "Book this space"
unauthorized_operation: "Unauthorized operation"
confirmation_required: "Confirmation required"
do_you_really_want_to_delete_this_space: "Do you really want to delete this space?"
the_space_cant_be_deleted_because_it_is_already_reserved_by_some_users: "Unable to delete this space, because it is already reserved by some users."
characteristics: "Characteristics"
files_to_download: "Files to download"
projects_using_the_space: "Projects using the space"

View File

@ -31,6 +31,7 @@ fr:
reserve_a_machine: "Réserver une machine" reserve_a_machine: "Réserver une machine"
trainings_registrations: "Inscriptions formations" trainings_registrations: "Inscriptions formations"
events_registrations: "Inscriptions aux évènements" events_registrations: "Inscriptions aux évènements"
reserve_a_space: "Réserver un espace"
projects_gallery: "Galerie de projets" projects_gallery: "Galerie de projets"
subscriptions: "Abonnements" subscriptions: "Abonnements"
public_calendar: "Calendrier" public_calendar: "Calendrier"
@ -44,6 +45,7 @@ fr:
subscriptions_and_prices: "Abonnements & Tarifs" subscriptions_and_prices: "Abonnements & Tarifs"
manage_the_events: "Gérer les évènements" manage_the_events: "Gérer les évènements"
manage_the_machines: "Gérer les machines" manage_the_machines: "Gérer les machines"
manage_the_spaces: "Gérer les espaces"
manage_the_projects_elements: "Gérer les éléments projets" manage_the_projects_elements: "Gérer les éléments projets"
statistics: "Statistiques" statistics: "Statistiques"
customization: "Personnalisation" customization: "Personnalisation"
@ -248,6 +250,29 @@ fr:
you_can_shift_this_reservation_on_the_following_slots: "Vous pouvez déplacer cette réservation sur les créneaux suivants :" you_can_shift_this_reservation_on_the_following_slots: "Vous pouvez déplacer cette réservation sur les créneaux suivants :"
calendar: calendar:
calendar: "Calendrier" # calendrier publique
show_no_disponible: "Afficher les crénaux non disponibles" calendar:
filter-calendar: "Filtrer le calendrier" calendar: "Calendrier"
show_unavailables: "Afficher les créneaux non disponibles"
filter_calendar: "Filtrer le calendrier"
trainings: "Formations"
machines: "Machines"
spaces: "Espaces"
events: "Évènements"
spaces_list:
# liste des espaces
the_spaces: "Les espaces"
add_a_space: "Ajouter un espace"
space_show:
# affichage des détails d'un espace
space_show:
book_this_space: "Réserver cet espace"
unauthorized_operation: "Opération non autorisée"
confirmation_required: "Confirmation requise"
do_you_really_want_to_delete_this_space: "Voulez-vous vraiment supprimer cet espace ?"
the_space_cant_be_deleted_because_it_is_already_reserved_by_some_users: "L'espace ne peut pas être supprimé car il a déjà été réservé par des utilisateurs."
characteristics: "Caractéristiques"
files_to_download: "Fichiers à télécharger"
projects_using_the_space: "Projets utilisant l'espace"

Some files were not shown because too many files have changed in this diff Show More