} expected: $scope.provider.providable_attributes.o_auth2_mappings_attributes
+# @returns {Boolean} true if the mapping is declared
+##
+check_oauth2_id_is_mapped = (mappings) ->
+ for mapping in mappings
+ if mapping.local_model == 'user' and mapping.local_field == 'uid' and not mapping._destroy
+ return true
+ return false
+
+
+
+##
+# Page listing all authentication providers
+##
+Application.Controllers.controller "AuthentificationController", ["$scope", "$state", "$rootScope", "dialogs", "growl", "authProvidersPromise", 'AuthProvider', '_t'
+, ($scope, $state, $rootScope, dialogs, growl, authProvidersPromise, AuthProvider, _t) ->
+
+ ### PUBLIC SCOPE ###
+
+ ## full list of authentication providers
+ $scope.providers = authProvidersPromise
+
+
+
+ ##
+ # Translate the classname into an explicit textual message
+ # @param type {string} Ruby polymorphic model classname
+ # @returns {string}
+ ##
+ $scope.getType = (type) ->
+ text = METHODS[type]
+ if typeof text != 'undefined'
+ return text
+ else
+ return _t('unknown')+type
+
+
+
+ ##
+ # Translate the status string into an explicit textual message
+ # @param status {string} active | pending | previous
+ # @returns {string}
+ ##
+ $scope.getState = (status) ->
+ switch status
+ when 'active' then _t('active')
+ when 'pending' then _t('pending')
+ when 'previous' then _t('previous_provider')
+ else _t('unknown')+status
+
+
+
+ ##
+ # Ask for confirmation then delete the specified provider
+ # @param providers {Array} full list of authentication providers
+ # @param provider {Object} provider to delete
+ ##
+ $scope.destroyProvider = (providers, provider) ->
+ dialogs.confirm
+ resolve:
+ object: ->
+ title: _t('confirmation_required')
+ msg: _t('do_you_really_want_to_delete_the_TYPE_authentication_provider_NAME', {TYPE:$scope.getType(provider.providable_type), NAME:provider.name})
+ , ->
+ # the admin has confirmed, delete
+ AuthProvider.delete id: provider.id
+ , ->
+ providers.splice(findIdxById(providers, provider.id), 1)
+ growl.success(_t('authentication_provider_successfully_deleted'))
+ , ->
+ growl.error(_t('an_error_occurred_unable_to_delete_the_specified_provider'))
+
+]
+
+
+
+##
+# Page to add a new authentication provider
+##
+Application.Controllers.controller "NewAuthenticationController", ["$scope", "$state", "$rootScope", "dialogs", "growl", "mappingFieldsPromise", "authProvidersPromise", "AuthProvider", '_t'
+, ($scope, $state, $rootScope, dialogs, growl, mappingFieldsPromise, authProvidersPromise, AuthProvider, _t) ->
+
+ $scope.authMethods = METHODS
+
+ $scope.mappingFields = mappingFieldsPromise
+
+ $scope.mode = 'creation'
+
+ $scope.provider = {
+ name: '',
+ providable_type: '',
+ providable_attributes: {}
+ }
+
+
+ ##
+ # Initialize some provider's specific properties when selecting the provider type
+ ##
+ $scope.updateProvidable = ->
+ # === OAuth2Provider ===
+ if $scope.provider.providable_type == 'OAuth2Provider'
+ if typeof $scope.provider.providable_attributes.o_auth2_mappings_attributes == 'undefined'
+ $scope.provider.providable_attributes['o_auth2_mappings_attributes'] = []
+ # Add others providers initializers here if needed ...
+
+
+
+ ##
+ # Validate and save the provider parameters in database
+ ##
+ $scope.registerProvider = ->
+ # === DatabaseProvider ===
+ if $scope.provider.providable_type == 'DatabaseProvider'
+ # prevent from adding mode than 1
+ for provider in authProvidersPromise
+ if provider.providable_type == 'DatabaseProvider'
+ growl.error _t('a_local_database_provider_already_exists_unable_to_create_another')
+ return false
+ AuthProvider.save auth_provider: $scope.provider, (provider) ->
+ growl.success _t('local_provider_successfully_saved')
+ $state.go('app.admin.members')
+ # === OAuth2Provider ===
+ else if $scope.provider.providable_type == 'OAuth2Provider'
+ # check the ID mapping
+ unless check_oauth2_id_is_mapped($scope.provider.providable_attributes.o_auth2_mappings_attributes)
+ growl.error(_t('it_is_required_to_set_the_matching_between_User.uid_and_the_API_to_add_this_provider'))
+ return false
+ # discourage the use of unsecure SSO
+ unless $scope.provider.providable_attributes.base_url.indexOf('https://') > -1
+ dialogs.confirm
+ size: 'l'
+ resolve:
+ object: ->
+ title: _t('security_issue_detected')
+ msg: _t('beware_the_oauth2_authenticatoin_provider_you_are_about_to_add_isnt_using_HTTPS') +
+ _t('this_is_a_serious_security_issue_on_internet_and_should_never_be_used_except_for_testing_purposes') +
+ _t('do_you_really_want_to_continue')
+ , -> # unsecured http confirmed
+ AuthProvider.save auth_provider: $scope.provider, (provider) ->
+ growl.success _t('unsecured_oauth2_provider_successfully_added')
+ $state.go('app.admin.members')
+ else
+ AuthProvider.save auth_provider: $scope.provider, (provider) ->
+ growl.success _t('oauth2_provider_successfully_added')
+ $state.go('app.admin.members')
+
+
+
+ ##
+ # Changes the admin's view to the members list page
+ ##
+ $scope.cancel = ->
+ $state.go('app.admin.members')
+
+]
+
+
+
+##
+# Page to edit an already added authentication provider
+##
+Application.Controllers.controller "EditAuthenticationController", ["$scope", "$state", "$stateParams", "$rootScope", "dialogs", "growl", 'providerPromise', 'mappingFieldsPromise', 'AuthProvider', '_t'
+, ($scope, $state, $stateParams, $rootScope, dialogs, growl, providerPromise, mappingFieldsPromise, AuthProvider, _t) ->
+
+ $scope.provider = providerPromise
+
+ $scope.authMethods = METHODS
+
+ $scope.mode = 'edition'
+
+ $scope.mappingFields = mappingFieldsPromise
+
+ ##
+ # Update the current provider with the new inputs
+ ##
+ $scope.updateProvider = ->
+ # check the ID mapping
+ unless check_oauth2_id_is_mapped($scope.provider.providable_attributes.o_auth2_mappings_attributes)
+ growl.error(_t('it_is_required_to_set_the_matching_between_User.uid_and_the_API_to_add_this_provider'))
+ return false
+ AuthProvider.update {id: $scope.provider.id}, {auth_provider: $scope.provider}, (provider) ->
+ growl.success(_t('provider_successfully_updated'))
+ $state.go('app.admin.members')
+ , ->
+ growl.error(_t('an_error_occurred_unable_to_update_the_provider'))
+
+ ##
+ # Changes the admin's view to the members list page
+ ##
+ $scope.cancel = ->
+ $state.go('app.admin.members')
+
+]
\ No newline at end of file
diff --git a/app/assets/javascripts/controllers/admin/calendar.coffee.erb b/app/assets/javascripts/controllers/admin/calendar.coffee.erb
new file mode 100644
index 000000000..81e38b54f
--- /dev/null
+++ b/app/assets/javascripts/controllers/admin/calendar.coffee.erb
@@ -0,0 +1,403 @@
+'use strict'
+
+##
+# Controller used in the calendar management page
+##
+
+Application.Controllers.controller "AdminCalendarController", ["$scope", "$state", "$uibModal", "moment", "Availability", 'Slot', 'Setting', 'growl', 'dialogs', 'availabilitiesPromise', 'bookingWindowStart', 'bookingWindowEnd', 'machinesPromise', '_t'
+($scope, $state, $uibModal, moment, Availability, Slot, Setting, growl, dialogs, availabilitiesPromise, bookingWindowStart, bookingWindowEnd, machinesPromise, _t) ->
+
+
+
+ ### PRIVATE STATIC CONSTANTS ###
+
+ # The calendar is divided in slots of 30 minutes
+ BASE_SLOT = '00:30:00'
+
+ # The bookings can be positioned every half hours
+ BOOKING_SNAP = '00:30:00'
+
+ # The calendar will be initialized positioned under 9:00 AM
+ DEFAULT_CALENDAR_POSITION = '09:00:00'
+
+ # We do not allow the creation of slots that are not a multiple of 60 minutes
+ SLOT_MULTIPLE = 60
+
+
+
+ ### PUBLIC SCOPE ###
+
+ ## list of the FabLab machines
+ $scope.machines = machinesPromise
+
+ ## currently selected availability
+ $scope.availability = null
+
+ ## bind the availabilities slots with full-Calendar events
+ $scope.eventSources = []
+ $scope.eventSources.push
+ events: availabilitiesPromise
+ textColor: 'black'
+
+ ## after fullCalendar loads, provides access to its methods through $scope.calendar.fullCalendar()
+ $scope.calendar = null
+
+ ## fullCalendar (v2) configuration
+ $scope.calendarConfig =
+ timezone: Fablab.timezone
+ lang: Fablab.fullcalendar_locale
+ header:
+ left: 'month agendaWeek'
+ center: 'title'
+ right: 'today prev,next'
+ firstDay: 1 # Week start on monday (France)
+ scrollTime: DEFAULT_CALENDAR_POSITION
+ slotDuration: BASE_SLOT
+ snapDuration: BOOKING_SNAP
+ allDayDefault: false
+ minTime: "00:00:00"
+ maxTime: "24:00:00"
+ height: 'auto'
+ buttonIcons:
+ prev: 'left-single-arrow'
+ next: 'right-single-arrow'
+ timeFormat:
+ agenda:'H:mm'
+ month: 'H(:mm)'
+ axisFormat: 'H:mm'
+
+ allDaySlot: false
+ defaultView: 'agendaWeek'
+ selectable: true
+ selecHelper: true
+ select: (start, end, jsEvent, view) ->
+ calendarSelectCb(start, end, jsEvent, view)
+ eventClick: (event, jsEvent, view)->
+ calendarEventClickCb(event, jsEvent, view)
+ eventRender: (event, element, view) ->
+ eventRenderCb(event, element)
+
+ ## fullCalendar time bounds (up & down)
+ $scope.calendarConfig.minTime = moment.duration(moment(bookingWindowStart.setting.value).format('HH:mm:ss'))
+ $scope.calendarConfig.maxTime = moment.duration(moment(bookingWindowEnd.setting.value).format('HH:mm:ss'))
+
+
+
+ ##
+ # Open a confirmation modal to cancel the booking of a user for the currently selected event.
+ # @param slot {Object} reservation slot of a user, inherited from $resource
+ ##
+ $scope.cancelBooking = (slot) ->
+ # open a confirmation dialog
+ dialogs.confirm
+ resolve:
+ object: ->
+ title: _t('confirmation_required')
+ msg: _t("do_you_really_want_to_cancel_the_USER_s_reservation_the_DATE_at_TIME_concerning_RESERVATION"
+ , { 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')
+ , ->
+ # the admin has confirmed, cancel the subscription
+ Slot.cancel {id: slot.slot_id}
+ , (data, status) -> # success
+ # update the canceled_at attribute
+ for resa in $scope.reservations
+ if resa.slot_id == data.id
+ resa.canceled_at = data.canceled_at
+ break
+ # notify the admin
+ growl.success(_t('reservation_was_successfully_cancelled'))
+ , (data, status) -> # failed
+ growl.error(_t('reservation_cancellation_failed'))
+
+
+
+ ##
+ # Open a confirmation modal to remove a machine for the currently selected availability,
+ # except if it is the last machine of the reservation.
+ # @param machine {Object} must contain the machine ID and name
+ ##
+ $scope.removeMachine = (machine) ->
+ if $scope.availability.machine_ids.length == 1
+ growl.error(_t('unable_to_remove_the_last_machine_of_the_slot_delete_the_slot_rather'))
+ else
+ # open a confirmation dialog
+ dialogs.confirm
+ resolve:
+ object: ->
+ title: _t('confirmation_required')
+ msg: _t('do_you_really_want_to_remove_MACHINE_from_this_slot', {GENDER:getGender($scope.currentUser), MACHINE:machine.name}, "messageformat") + ' ' +
+ _t('this_will_prevent_any_new_reservation_on_this_slot_but_wont_cancel_those_existing') + ' ' +
+ _t('beware_this_cannot_be_reverted')
+ , ->
+ # the admin has confirmed, remove the machine
+ machines = $scope.availability.machine_ids
+ for key, m_id in machines
+ if m_id == machine.id
+ machines.splice(key, 1)
+
+ Availability.update {id: $scope.availability.id}, {availability: {machines_attributes: [{id: machine.id, _destroy: true}]}}
+ , (data, status) -> # success
+ # update the machine_ids attribute
+ $scope.availability.machine_ids = data.machine_ids
+ $scope.availability.title = data.title
+ $scope.calendar.fullCalendar 'rerenderEvents'
+ # notify the admin
+ growl.success(_t('the_machine_was_successfully_removed_from_the_slot'))
+ , (data, status) -> # failed
+ growl.error(_t('deletion_failed'))
+
+
+
+ ### PRIVATE SCOPE ###
+
+ ##
+ # Return an enumerable meaninful string for the gender of the provider user
+ # @param user {Object} Database user record
+ # @return {string} 'male' or 'female'
+ ##
+ getGender = (user) ->
+ if user.profile
+ if user.profile.gender == "true" then 'male' else 'female'
+ else 'other'
+
+ # Triggered when the admin drag on the agenda to create a new reservable slot.
+ # @see http://fullcalendar.io/docs/selection/select_callback/
+ ##
+ calendarSelectCb = (start, end, jsEvent, view) ->
+ start = moment.tz(start.toISOString(), Fablab.timezone)
+ end = moment.tz(end.toISOString(), Fablab.timezone)
+ # first we check that the selected slot is an N-hours multiple (ie. not decimal)
+ if Number.isInteger(parseInt((end.valueOf() - start.valueOf()) / (SLOT_MULTIPLE * 1000), 10)/SLOT_MULTIPLE)
+ today = new Date()
+ if (parseInt((start.valueOf() - today) / (60 * 1000), 10) >= 0)
+ # then we open a modal window to let the admin specify the slot type
+ modalInstance = $uibModal.open
+ templateUrl: '<%= asset_path "admin/calendar/eventModal.html" %>'
+ controller: 'CreateEventModalController'
+ resolve:
+ start: -> start
+ end: -> end
+ # when the modal is closed, we send the slot to the server for saving
+ modalInstance.result.then (availability) ->
+ $scope.calendar.fullCalendar 'renderEvent',
+ id: availability.id
+ title: availability.title,
+ start: availability.start_at
+ end: availability.end_at
+ textColor: 'black'
+ backgroundColor: availability.backgroundColor
+ borderColor: availability.borderColor
+ tag_ids: availability.tag_ids
+ machine_ids: availability.machine_ids
+ , true
+ , ->
+ $scope.calendar.fullCalendar('unselect')
+
+ $scope.calendar.fullCalendar('unselect')
+
+
+
+ ##
+ # Triggered when the admin clicks on a availability slot in the agenda.
+ # @see http://fullcalendar.io/docs/mouse/eventClick/
+ ##
+ calendarEventClickCb = (event, jsEvent, view) ->
+
+ $scope.availability = event
+
+ # if the user has clicked on the delete event button, delete the event
+ if ($(jsEvent.target).hasClass('remove-event'))
+ Availability.delete id: event.id, ->
+ $scope.calendar.fullCalendar 'removeEvents', event.id
+ for _event, i in $scope.eventSources[0].events
+ if _event.id == event.id
+ $scope.eventSources[0].events.splice(i,1)
+
+ 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.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')}))
+ # if the user has only clicked on the event, display its reservations
+ else
+ Availability.reservations {id: event.id}, (reservations) ->
+ $scope.reservations = reservations
+
+
+
+
+ ##
+ # 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) ->
+ if event.tag_ids.length > 0
+ Availability.get {id: event.id}, (avail) ->
+ html = ''
+ for tag in avail.tags
+ html += "#{tag.name} "
+ element.find('.fc-title').append(" "+html)
+
+]
+
+
+
+##
+# 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) ->
+
+ ## $uibModal parameter
+ $scope.start = start
+
+ ## $uibModal parameter
+ $scope.end = end
+
+ ## machines list
+ $scope.machines = []
+
+ ## trainings list
+ $scope.trainings = []
+
+ ## machines associated with the created slot
+ $scope.selectedMachines = []
+
+ ## the user is not able to edit the ending time of the availability, unless he set the type to 'training'
+ $scope.endDateReadOnly = true
+
+ ## timepickers configuration
+ $scope.timepickers =
+ start:
+ hstep: 1
+ mstep: 5
+ end:
+ hstep: 1
+ mstep: 5
+
+ ## slot details
+ $scope.availability =
+ start_at: start
+ end_at: end
+ available_type: 'machines' # default
+
+
+
+ ##
+ # Adds or removes the provided machine from the current slot
+ # @param machine {Object}
+ ##
+ $scope.toggleSelection = (machine)->
+ index = $scope.selectedMachines.indexOf(machine)
+ if index > -1
+ $scope.selectedMachines.splice(index, 1)
+ else
+ $scope.selectedMachines.push(machine)
+
+
+
+ ##
+ # Callback for the modal window validation: save the slot and closes the modal
+ ##
+ $scope.ok = ->
+ if $scope.availability.available_type == "machines"
+ if $scope.selectedMachines.length > 0
+ $scope.availability.machine_ids = $scope.selectedMachines.map (m) -> m.id
+ else
+ growl.error(_t('you_should_link_a_training_or_a_machine_to_this_slot'))
+ return
+ else
+ $scope.availability.training_ids = [$scope.selectedTraining.id]
+ Availability.save
+ availability: $scope.availability
+ , (availability) ->
+ $uibModalInstance.close(availability)
+
+
+
+ ##
+ # Callback to cancel the slot creation
+ ##
+ $scope.cancel = ->
+ $uibModalInstance.dismiss('cancel')
+
+
+
+ ##
+ # Switches the slot type : machine availability or training availability
+ ##
+ $scope.changeAvailableType = ->
+ if $scope.availability.available_type == "machines"
+ $scope.availability.available_type = "training"
+ else
+ $scope.availability.available_type = "machines"
+
+
+
+ ##
+ # For training avaiabilities, set the maximum number of people allowed to register on this slot
+ ##
+ $scope.setNbTotalPlaces = ->
+ $scope.availability.nb_total_places = $scope.selectedTraining.nb_total_places
+
+
+ ### PRIVATE SCOPE ###
+
+ ##
+ # Kind of constructor: these actions will be realized first when the controller is loaded
+ ##
+ initialize = ->
+ Machine.query().$promise.then (data)->
+ $scope.machines = data.map (d) ->
+ id: d.id
+ name: d.name
+ Training.query().$promise.then (data)->
+ $scope.trainings = data.map (d) ->
+ id: d.id
+ name: d.name
+ nb_total_places: d.nb_total_places
+ if $scope.trainings.length > 0
+ $scope.selectedTraining = $scope.trainings[0]
+ $scope.setNbTotalPlaces()
+ Tag.query().$promise.then (data) ->
+ $scope.tags = data
+
+ ## When we configure a machine availability, do not let the user change the end time, as the total
+ ## time must be dividable by 60 minutes (base slot duration). For training availabilities, the user
+ ## can configure any duration as it does not matters.
+ $scope.$watch 'availability.available_type', (newValue, oldValue, scope) ->
+ if newValue == 'machines'
+ $scope.endDateReadOnly = true
+ diff = moment($scope.end).diff($scope.start, 'hours') # the result is rounded down by moment.js
+ $scope.end = moment($scope.start).add(diff, 'hours').toDate()
+ $scope.availability.end_at = $scope.end
+ else
+ $scope.endDateReadOnly = false
+
+ ## When the start date is changed, if we are configuring a machine availability,
+ ## maintain the relative length of the slot (ie. change the end time accordingly)
+ $scope.$watch 'start', (newValue, oldValue, scope) ->
+ # for machine availabilities, adjust the end time
+ if $scope.availability.available_type == 'machines'
+ end = moment($scope.end)
+ end.add(moment(newValue).diff(oldValue), 'milliseconds')
+ $scope.end = end.toDate()
+ else # for training availabilities
+ # prevent the admin from setting the begining after the and
+ if moment(newValue).add(1, 'hour').isAfter($scope.end)
+ $scope.start = oldValue
+ # update availability object
+ $scope.availability.start_at = $scope.start
+
+ ## Maintain consistency between the end time and the date object in the availability object
+ $scope.$watch 'end', (newValue, oldValue, scope) ->
+ ## we prevent the admin from setting the end of the availability before its begining
+ if moment($scope.start).add(1, 'hour').isAfter(newValue)
+ $scope.end = oldValue
+ # update availability object
+ $scope.availability.end_at = $scope.end
+
+
+
+ ## !!! MUST BE CALLED AT THE END of the controller
+ initialize()
+]
diff --git a/app/assets/javascripts/controllers/admin/events.coffee b/app/assets/javascripts/controllers/admin/events.coffee
index 5d782709d..50503beb0 100644
--- a/app/assets/javascripts/controllers/admin/events.coffee
+++ b/app/assets/javascripts/controllers/admin/events.coffee
@@ -14,8 +14,8 @@
# - $scope.addFile()
# - $scope.deleteFile(file)
# - $scope.fileinputClass(v)
-# - $scope.openStartDatePicker($event)
-# - $scope.openEndDatePicker($event)
+# - $scope.toggleStartDatePicker($event)
+# - $scope.toggleEndDatePicker($event)
# - $scope.toggleRecurrenceEnd(e)
#
# Requires :
@@ -23,7 +23,7 @@
# - $state (Ui-Router) [ 'app.public.events_list' ]
##
class EventsController
- constructor: ($scope, $state, Event, Category) ->
+ constructor: ($scope, $state, $locale, Event, Category) ->
## Retrieve the list of categories from the server (stage, atelier, ...)
Category.query().$promise.then (data)->
@@ -33,12 +33,12 @@ class EventsController
## default parameters for AngularUI-Bootstrap datepicker
$scope.datePicker =
- format: 'dd/MM/yyyy'
+ format: $locale.DATETIME_FORMATS.shortDate
startOpened: false # default: datePicker is not shown
endOpened: false
recurrenceEndOpened: false
options:
- startingDay: 1 # France: the week starts on monday
+ startingDay: Fablab.weekStartingDay
@@ -136,21 +136,20 @@ class EventsController
##
# Controller used in the events listing page (admin view)
##
-Application.Controllers.controller "adminEventsController", ["$scope", "$state", 'Event', ($scope, $state, Event) ->
+Application.Controllers.controller "AdminEventsController", ["$scope", "$state", 'Event', 'eventsPromise', ($scope, $state, Event, eventsPromise) ->
### PUBLIC SCOPE ###
- ## The events displayed on the page
- $scope.events = []
-
## By default, the pagination mode is activated to limit the page size
$scope.paginateActive = true
- ## The currently displayed page number
- $scope.page = 1
+ ## The events displayed on the page
+ $scope.events = eventsPromise
+ ## Current virtual page
+ $scope.page = 2
##
# Adds a bucket of events to the bottom of the page, grouped by month
@@ -158,10 +157,7 @@ Application.Controllers.controller "adminEventsController", ["$scope", "$state",
$scope.loadMoreEvents = ->
Event.query {page: $scope.page}, (data)->
$scope.events = $scope.events.concat data
- if data.length
- $scope.paginateActive = false if $scope.events.length >= data[0].nb_total_events
- else
- $scope.paginateActive = false
+ paginationCheck(data, $scope.events)
$scope.page += 1
@@ -172,10 +168,40 @@ Application.Controllers.controller "adminEventsController", ["$scope", "$state",
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
- $scope.loadMoreEvents()
+ paginationCheck(eventsPromise, $scope.events)
- ## !!! MUST BE CALLED AT THE END of the controller
+
+ ##
+ # Check if all events are already displayed OR if the button 'load more events'
+ # is required
+ # @param lastEvents {Array} last events loaded onto the diplay (ie. last "page")
+ # @param events {Array} full list of events displayed on the page (not only the last retrieved)
+ ##
+ paginationCheck = (lastEvents, events)->
+ if lastEvents.length > 0
+ $scope.paginateActive = false if events.length >= lastEvents[0].nb_total_events
+ else
+ $scope.paginateActive = false
+
+
+
+ # init the controller (call at the end !)
initialize()
+
+]
+
+
+
+##
+# Controller used in the reservations listing page for a specific event
+##
+Application.Controllers.controller "ShowEventReservationsController", ["$scope", 'eventPromise', 'reservationsPromise', ($scope, eventPromise, reservationsPromise) ->
+
+ ## retrieve the event from the ID provided in the current URL
+ $scope.event = eventPromise
+
+ ## list of reservations for the current event
+ $scope.reservations = reservationsPromise
]
@@ -183,7 +209,8 @@ Application.Controllers.controller "adminEventsController", ["$scope", "$state",
##
# Controller used in the event creation page
##
-Application.Controllers.controller "newEventController", ["$scope", "$state", 'Event', 'Category', 'CSRF', ($scope, $state, Event, Category, CSRF) ->
+Application.Controllers.controller "NewEventController", ["$scope", "$state", "$locale", 'Event', 'Category', 'CSRF', '_t'
+, ($scope, $state, $locale, Event, Category, CSRF, _t) ->
CSRF.setMetaTags()
## API URL where the form will be posted
@@ -204,15 +231,18 @@ Application.Controllers.controller "newEventController", ["$scope", "$state", 'E
## Possible types of recurrences for an event
$scope.recurrenceTypes = [
- {label: 'Aucune', value: 'none'},
- {label: 'Tous les jours', value: 'day'},
- {label: 'Chaque semaine', value: 'week'},
- {label: 'Chaque mois', value: 'month'},
- {label: 'Chaque année', value: 'year'}
+ {label: _t('none'), value: 'none'},
+ {label: _t('every_days'), value: 'day'},
+ {label: _t('every_week'), value: 'week'},
+ {label: _t('every_month'), value: 'month'},
+ {label: _t('every_year'), value: 'year'}
]
+ ## currency symbol for the current locale (cf. angular-i18n)
+ $scope.currencySymbol = $locale.NUMBER_FORMATS.CURRENCY_SYM;
+
## Using the EventsController
- new EventsController($scope, $state, Event, Category)
+ new EventsController($scope, $state, $locale, Event, Category)
]
@@ -220,8 +250,12 @@ Application.Controllers.controller "newEventController", ["$scope", "$state", 'E
##
# Controller used in the events edition page
##
-Application.Controllers.controller "editEventController", ["$scope", "$state", "$stateParams", 'Event', 'Category', 'CSRF', ($scope, $state, $stateParams, Event, Category, CSRF) ->
- CSRF.setMetaTags()
+Application.Controllers.controller "EditEventController", ["$scope", "$state", "$stateParams", "$locale", 'Event', 'Category', 'CSRF', 'eventPromise'
+, ($scope, $state, $stateParams, $locale, Event, Category, CSRF, eventPromise) ->
+
+ ### PUBLIC SCOPE ###
+
+
## API URL where the form will be posted
$scope.actionUrl = "/api/events/" + $stateParams.id
@@ -230,13 +264,32 @@ Application.Controllers.controller "editEventController", ["$scope", "$state", "
$scope.method = 'put'
## Retrieve the event details, in case of error the user is redirected to the events listing
- Event.get {id: $stateParams.id}
- , (event)->
- $scope.event = event
- return
- , ->
- $state.go('app.public.events_list')
+ $scope.event = eventPromise
- ## Using the EventsController
- new EventsController($scope, $state, Event, Category)
+ ## currency symbol for the current locale (cf. angular-i18n)
+ $scope.currencySymbol = $locale.NUMBER_FORMATS.CURRENCY_SYM;
+
+
+
+ ### PRIVATE SCOPE ###
+
+
+
+ ##
+ # Kind of constructor: these actions will be realized first when the controller is loaded
+ ##
+ initialize = ->
+ CSRF.setMetaTags()
+
+ # init the dates to JS objects
+ $scope.event.start_date = moment($scope.event.start_date).toDate()
+ $scope.event.end_date = moment($scope.event.end_date).toDate()
+
+ ## Using the EventsController
+ new EventsController($scope, $state, $locale, Event, Category)
+
+
+
+ ## !!! MUST BE CALLED AT THE END of the controller
+ initialize()
]
diff --git a/app/assets/javascripts/controllers/admin/graphs.coffee b/app/assets/javascripts/controllers/admin/graphs.coffee
new file mode 100644
index 000000000..03a7b542b
--- /dev/null
+++ b/app/assets/javascripts/controllers/admin/graphs.coffee
@@ -0,0 +1,648 @@
+'use strict'
+
+Application.Controllers.controller "GraphsController", ["$scope", "$state", '$locale', "$rootScope", 'es', 'Statistics', '_t'
+, ($scope, $state, $locale, $rootScope, es, Statistics, _t) ->
+
+
+
+ ### PRIVATE STATIC CONSTANTS ###
+
+ ## height of the HTML/SVG charts elements in pixels
+ CHART_HEIGHT = 500
+
+ ## Label of the charts' horizontal axes
+ X_AXIS_LABEL = _t('date')
+
+ ## Label of the charts' vertical axes
+ Y_AXIS_LABEL = _t('number')
+
+ ## Colors for the line charts. Each new line uses the next color in this array
+ CHART_COLORS = ['#b35a94', '#1c5794', '#00b49e', '#6fac48', '#ebcf4a', '#fd7e33', '#ca3436', '#a26e3a']
+
+
+
+ ### PUBLIC SCOPE ###
+
+ ## ui-view transitions optimization: if true, the charts will never be refreshed
+ $scope.preventRefresh = false
+
+ ## statistics structure in elasticSearch
+ $scope.statistics = []
+
+ ## statistics data recovered from elasticSearch
+ $scope.data = null
+
+ ## default interval: one day
+ $scope.display =
+ interval: 'week'
+
+ ## active tab will be set here
+ $scope.selectedIndex = null
+
+ ## for palmares graphs, filters values are stored here
+ $scope.ranking =
+ sortCriterion: 'ca'
+ groupCriterion: 'subType'
+
+ ## default: we do not open the datepicker menu
+ $scope.datePicker =
+ show: false
+
+ ## datePicker parameters for interval beginning
+ $scope.datePickerStart =
+ format: $locale.DATETIME_FORMATS.shortDate
+ opened: false # default: datePicker is not shown
+ minDate: null
+ maxDate: moment().subtract(1, 'day').toDate()
+ selected: moment().utc().subtract(1, 'months').subtract(1, 'day').startOf('day').toDate()
+ options:
+ startingDay: Fablab.weekStartingDay
+
+ ## datePicker parameters for interval ending
+ $scope.datePickerEnd =
+ format: $locale.DATETIME_FORMATS.shortDate
+ opened: false # default: datePicker is not shown
+ minDate: null
+ maxDate: moment().subtract(1, 'day').toDate()
+ selected: moment().subtract(1, 'day').endOf('day').toDate()
+ options:
+ startingDay: Fablab.weekStartingDay
+
+
+
+ ##
+ # Callback to open the datepicker (interval start)
+ # @param {Object} jQuery event object
+ ##
+ $scope.toggleStartDatePicker = ($event) ->
+ toggleDatePicker($event, $scope.datePickerStart)
+
+
+
+ ##
+ # Callback to open the datepicker (interval end)
+ # @param {Object} jQuery event object
+ ##
+ $scope.toggleEndDatePicker = ($event) ->
+ toggleDatePicker($event, $scope.datePickerEnd)
+
+
+
+ ##
+ # Callback called when the active tab is changed.
+ # Recover the current tab and store its value in $scope.selectedIndex
+ # @param tab {Object} elasticsearch statistic structure
+ ##
+ $scope.setActiveTab = (tab) ->
+ $scope.selectedIndex = tab
+ $scope.ranking.groupCriterion = 'subType'
+ if tab.ca
+ $scope.ranking.sortCriterion = 'ca'
+ else
+ $scope.ranking.sortCriterion = tab.types[0].key
+ refreshChart()
+
+
+
+ ##
+ # Callback to close the date-picking popup and refresh the results
+ ##
+ $scope.validateDateChange = ->
+ $scope.datePicker.show = false
+ refreshChart()
+
+
+
+ ### PRIVATE SCOPE ###
+
+ ##
+ # Kind of constructor: these actions will be realized first when the controller is loaded
+ ##
+ initialize = ->
+ Statistics.query (stats) ->
+ $scope.statistics = stats
+ # watch the interval changes to refresh the graph
+ $scope.$watch (scope) ->
+ return scope.display.interval
+ , (newValue, oldValue) ->
+ refreshChart()
+ $scope.$watch (scope) ->
+ return scope.ranking.sortCriterion
+ , (newValue, oldValue) ->
+ refreshChart()
+ $scope.$watch (scope) ->
+ return scope.ranking.groupCriterion
+ , (newValue, oldValue) ->
+ refreshChart()
+ refreshChart()
+
+ # workaround for angular-bootstrap::tabs behavior: on tab deletion, another tab will be selected
+ # which will cause every tabs to reload, one by one, when the view is closed
+ $rootScope.$on '$stateChangeStart', (event, toState, toParams, fromState, fromParams) ->
+ if fromState.name == 'app.admin.stats_graphs' and Object.keys(fromParams).length == 0
+ $scope.preventRefresh = true
+
+
+
+ ##
+ # Generic function to toggle a bootstrap datePicker
+ # @param $event {Object} jQuery event object
+ # @param datePicker {Object} settings object of the concerned datepicker. Must have an 'opened' property
+ ##
+ toggleDatePicker = ($event, datePicker) ->
+ $event.preventDefault()
+ $event.stopPropagation()
+ datePicker.opened = !datePicker.opened
+
+
+
+ ##
+ # Query elasticSearch according to the current parameters and update the chart
+ ##
+ refreshChart = ->
+ if $scope.selectedIndex and !$scope.preventRefresh
+ query $scope.selectedIndex, (aggregations, error)->
+ if error
+ console.error(error)
+ else
+ if $scope.selectedIndex.graph.chart_type != 'discreteBarChart'
+ $scope.data = formatAggregations(aggregations)
+ angular.forEach $scope.data, (datum, key) ->
+ updateChart($scope.selectedIndex.graph.chart_type, datum, key)
+ else
+ $scope.data = formatRankingAggregations(aggregations, $scope.selectedIndex.graph.limit, $scope.ranking.groupCriterion)
+ updateChart($scope.selectedIndex.graph.chart_type, $scope.data.ranking, $scope.selectedIndex.es_type_key)
+
+
+
+ ##
+ # Callback used in NVD3 to print timestamps as literal dates on the X axis
+ ##
+ xAxisTickFormatFunction = (d, x, y) ->
+ ### WARNING !! These tests (typeof/instanceof) may become broken on nvd3 update ###
+ if $scope.display.interval == 'day'
+ if typeof d == 'number' or d instanceof Date
+ d3.time.format(Fablab.d3DateFormat) moment(d).toDate()
+ else # typeof d == 'string'
+ d
+ else if $scope.display.interval == 'week'
+ if typeof x == 'number' or d instanceof Date
+ d3.time.format(_t('week_short')+' %U') moment(d).toDate()
+ else if typeof d == 'number'
+ _t('week_of_START_to_END', {START:moment(d).format('L'), END:moment(d).add(6, 'days').format('L')})
+ else # typeof d == 'string'
+ d
+ else if $scope.display.interval == 'month'
+ if typeof d == 'number'
+ label = moment(d).format('MMMM YYYY')
+ label.substr(0,1).toUpperCase()+label.substr(1).toLowerCase()
+ else # typeof d == 'string'
+ d
+
+
+
+ ##
+ # Format aggregations as retuned by elasticSearch to an understandable format for NVD3
+ # @param aggs {Object} as returned by elasticsearch
+ ##
+ formatAggregations = (aggs) ->
+ format = {}
+
+ angular.forEach aggs, (type, type_key) -> # go through aggs[$TYPE] where $TYPE = month|year|hour|booking|...
+ format[type_key] = []
+ if type.subgroups
+ angular.forEach type.subgroups.buckets, (subgroup) -> # go through aggs.$TYPE.subgroups.buckets where each bucket represent a $SUBTYPE
+ angular.forEach $scope.selectedIndex.types, (cur_type) -> # in the mean time, go through the types of the current index (active tab) ...
+ if cur_type.key == type_key # ... looking for the type matching $TYPE
+ for it_st in [0.. cur_type.subtypes.length-1] by 1 # when we've found it, iterate over its subtypes ...
+ cur_subtype = cur_type.subtypes[it_st]
+ if subgroup.key == cur_subtype.key # ... which match $SUBTYPE
+ # then we construct NVD3 dataSource according to these informations
+ dataSource =
+ values: []
+ key: cur_subtype.label
+ total : 0
+ color: CHART_COLORS[it_st]
+ area: true
+ # finally, we iterate over 'intervals' buckets witch contains
+ # per date aggregations for our current dataSource
+ angular.forEach subgroup.intervals.buckets, (interval) ->
+ dataSource.values.push
+ x: interval.key
+ y: interval.total.value
+ dataSource.total += parseInt(interval.total.value)
+ dataSource.key += ' (' + dataSource.total + ')'
+ format[type_key].push dataSource
+ format
+
+
+
+ ##
+ # Format aggregations for ranking charts to an understandable format for NVD3
+ # @param aggs {Object} as returned by elasticsearch
+ # @param limit {number} limit the number of stats in the bar chart
+ # @param typeKey {String} field name witch results are grouped by
+ ##
+ formatRankingAggregations = (aggs, limit, typeKey) ->
+ format =
+ ranking: []
+
+ it = 0
+ while (it < aggs.subgroups.buckets.length)
+ bucket = aggs.subgroups.buckets[it]
+ dataSource =
+ values: []
+ key: getRankingLabel(bucket.key, typeKey)
+ color: CHART_COLORS[it]
+ area: true
+ dataSource.values.push
+ x: getRankingLabel(bucket.key, typeKey)
+ y: bucket.total.value
+ format.ranking.push(dataSource)
+ it++
+ getY = (object)->
+ object.values[0].y
+ format.ranking = stableSort(format.ranking, 'DESC', getY).slice(0, limit)
+ for i in [0..format.ranking.length] by 1
+ if typeof format.ranking[i] == 'undefined' then format.ranking.splice(i,1)
+ format
+
+
+
+ ##
+ # For BarCharts, return the label for a given bar
+ # @param key {string} raw value of the label
+ # @param typeKey {string} name of the field the results are grouped by
+ ##
+ getRankingLabel = (key, typeKey) ->
+ if $scope.selectedIndex
+ if (typeKey == 'subType')
+ for type in $scope.selectedIndex.types
+ for subtype in type.subtypes
+ if (subtype.key == key)
+ return subtype.label
+ else
+ for field in $scope.selectedIndex.additional_fields
+ if (field.key == typeKey)
+ switch field.data_type
+ when 'date' then return moment(key).format('LL')
+ when 'list' then return key.name
+ else return key
+
+
+
+ ##
+ # Prepare the elasticSearch query for the stats matching the current controller's parameters
+ # @param index {{id:{number}, es_type_key:{string}, label:{string}, table:{boolean}, additional_fields:{Array},
+ # types:{Array}, graph:{Object}}} elasticSearch type in stats index to query
+ # @param callback {function} function be to run after results were retrieved,
+ # it will receive two parameters : results {Array}, error {String} (if any)
+ ##
+ query = (index, callback) ->
+ # invalid callback handeling
+ if typeof(callback) != "function"
+ console.error('[graphsController::query] Error: invalid callback provided')
+ return
+ if !index
+ callback([], '[graphsController::query] Error: invalid index provided')
+ return
+
+ if index.graph.chart_type != 'discreteBarChart'
+ # list statistics types
+ stat_types = []
+ for t in index.types
+ if t.graph
+ stat_types.push(t.key)
+
+ # exception handeling
+ if stat_types.length == 0
+ callback([], "Error: Unable to retrieve any graphical statistic types in the provided index")
+
+ type_it = 0
+ results = {}
+ error = ''
+ recursiveCb = ->
+ if type_it < stat_types.length
+ queryElasticStats index.es_type_key, stat_types[type_it], (prevResults, prevError)->
+ if (prevError)
+ console.error('[graphsController::query] '+prevError)
+ error += '\n'+prevError
+ results[stat_types[type_it]] = prevResults
+ type_it++
+ recursiveCb()
+ else
+ callback(results)
+ recursiveCb()
+ else # palmares (ranking)
+ queryElasticRanking index.es_type_key, $scope.ranking.groupCriterion, $scope.ranking.sortCriterion, index.graph.limit, (results, error) ->
+ if (error)
+ callback([], error)
+ else
+ callback(results)
+
+
+
+ ##
+ # Run the elasticSearch query to retreive the /stats/type aggregations
+ # @param esType {String} elasticSearch document type (subscription|machine|training|...)
+ # @param statType {String} statistics type (year|month|hour|booking|...)
+ # @param callback {function} function be to run after results were retrieved,
+ # it will receive two parameters : results {Array}, error {String} (if any)
+ ##
+ queryElasticStats = (esType, statType, callback) ->
+ # handle invalid callback
+ if typeof(callback) != "function"
+ console.error('[graphsController::queryElasticStats] Error: invalid callback provided')
+ return
+ if !esType or !statType
+ callback([], '[graphsController::queryElasticStats] Error: invalid parameters provided')
+
+ # run query
+ es.search
+ "index": "stats"
+ "type": esType
+ "searchType": "count"
+ "body": buildElasticAggregationsQuery(statType, $scope.display.interval, moment($scope.datePickerStart.selected), moment($scope.datePickerEnd.selected))
+ , (error, response) ->
+ if (error)
+ callback([], "Error: something unexpected occurred during elasticSearch query: "+error)
+ else
+ callback(response.aggregations)
+
+
+
+ ##
+ # For ranking displays, run the elasticSearch query to retreive the /stats/type aggregations
+ # @param esType {String} elasticSearch document type (subscription|machine|training|...)
+ # @param statType {String} statistics type (year|month|hour|booking|...)
+ # @param callback {function} function be to run after results were retrieved,
+ # it will receive two parameters : results {Array}, error {String} (if any)
+ ##
+ queryElasticRanking = (esType, groupKey, sortKey, limit, callback) ->
+ # handle invalid callback
+ if typeof(callback) != "function"
+ console.error('[graphsController::queryElasticRanking] Error: invalid callback provided')
+ return
+ if !esType or !groupKey or !sortKey or typeof limit != 'number'
+ callback([], '[graphsController::queryElasticRanking] Error: invalid parameters provided')
+
+ # run query
+ es.search
+ "index": "stats"
+ "type": esType
+ "searchType": "count"
+ "body": buildElasticAggregationsRankingQuery(groupKey, sortKey, moment($scope.datePickerStart.selected), moment($scope.datePickerEnd.selected))
+ , (error, response) ->
+ if (error)
+ callback([], "Error: something unexpected occurred during elasticSearch query: "+error)
+ else
+ callback(response.aggregations)
+
+
+
+ ##
+ # Parse a final elastic results bucket and return a D3 compatible object
+ # @param bucket {{key_as_string:{String}, key:{Number}, doc_count:{Number}, total:{{value:{Number}}}}} interval bucket
+ ##
+ parseElasticBucket = (bucket) ->
+ [ bucket.key, bucket.total.value ]
+
+
+
+ ##
+ # Build an object representing the content of the REST-JSON query to elasticSearch, based on the parameters
+ # currently defined for data aggegations.
+ # @param type {String} statistics type (visit|rdv|rating|ca|plan|account|search|...)
+ # @param interval {String} statistics interval (year|quarter|month|week|day|hour|minute|second)
+ # @param intervalBegin {moment} statitics interval beginning (moment.js type)
+ # @param intervalEnd {moment} statitics interval ending (moment.js type)
+ ##
+ buildElasticAggregationsQuery = (type, interval, intervalBegin, intervalEnd) ->
+ q =
+ "query":
+ "bool":
+ "must": [
+ {
+ "match":
+ "type": type
+ }
+ {
+ "range":
+ "date":
+ "gte": intervalBegin.format()
+ "lte": intervalEnd.format()
+ }
+ ]
+ "aggregations":
+ "subgroups":
+ "terms":
+ "field": "subType" #TODO allow aggregate by custom field
+ "aggregations":
+ "intervals":
+ "date_histogram":
+ "field": "date"
+ "interval": interval
+ "min_doc_count": 0
+ "extended_bounds":
+ "min": intervalBegin.valueOf()
+ "max": intervalEnd.valueOf()
+ "aggregations":
+ "total":
+ "sum":
+ "field": "stat"
+
+ # scale weeks on sunday as nvd3 supports only these weeks
+ if interval == 'week'
+ q.aggregations.subgroups.aggregations.intervals.date_histogram['post_offset'] = '-1d'
+ q.aggregations.subgroups.aggregations.intervals.date_histogram['pre_offset'] = '-1d'
+ # scale days to UTC time
+ else if interval == 'day'
+ offset = moment().utcOffset()
+ q.aggregations.subgroups.aggregations.intervals.date_histogram['post_offset'] = (-offset)+'m'
+ q
+
+
+
+ ##
+ # Build an object representing the content of the REST-JSON query to elasticSearch, based on the parameters
+ # currently defined for data aggegations.
+ # @param groupKey {String} statistics subtype or custom field
+ # @param sortKey {String} statistics type or 'ca'
+ # @param intervalBegin {moment} statitics interval beginning (moment.js type)
+ # @param intervalEnd {moment} statitics interval ending (moment.js type)
+ ##
+ buildElasticAggregationsRankingQuery = (groupKey, sortKey, intervalBegin, intervalEnd) ->
+ q =
+ "query":
+ "bool":
+ "must": [
+ {
+ "range":
+ "date":
+ "gte": intervalBegin.format()
+ "lte": intervalEnd.format()
+ }
+ {
+ "term":
+ "type": "booking"
+ }
+ ]
+ "aggregations":
+ "subgroups":
+ "terms":
+ "field": "subType"
+ "aggregations":
+ "total":
+ "sum":
+ "field": "stat"
+
+ # we group the results by the custom given key (eg. by event date)
+ q.aggregations.subgroups.terms =
+ field: groupKey
+ size: 0
+
+ # results must be sorted and limited later by angular
+ if sortKey != 'ca'
+ angular.forEach q.query.bool.must, (must) ->
+ if must.term
+ must.term.type = sortKey
+ else
+ q.aggregations.subgroups.aggregations.total.sum.field = sortKey
+
+ q
+
+
+
+ ##
+ # Redraw the NDV3 chart using the provided data
+ # @param chart_type {String} stackedAreaChart|discreteBarChart|lineChart
+ # @param data {Array} array of NVD3 dataSources
+ # @param type {String} which chart to update (statistic type key)
+ ##
+ updateChart = (chart_type, data, type) ->
+
+ id = "#chart-"+type+" svg"
+
+ # clean old charts
+ d3.selectAll(id+" > *").remove()
+
+ nv.addGraph ->
+ # no data or many dates, display line charts
+ if data.length == 0 or (data[0].values.length > 1 and (chart_type != 'discreteBarChart'))
+ if chart_type == 'stackedAreaChart'
+ chart = nv.models.stackedAreaChart().useInteractiveGuideline(true)
+ else
+ chart = nv.models.lineChart().useInteractiveGuideline(true)
+
+ if data.length > 0
+ if $scope.display.interval == 'day'
+ setTimeScale(chart.xAxis, chart.xScale, [d3.time.day, data[0].values.length])
+ else if $scope.display.interval == 'week'
+ setTimeScale(chart.xAxis, chart.xScale, [d3.time.week, data[0].values.length])
+ else if $scope.display.interval == 'month'
+ setTimeScale(chart.xAxis, chart.xScale, [d3.time.month, data[0].values.length])
+
+ chart.xAxis.tickFormat(xAxisTickFormatFunction)
+ chart.yAxis.tickFormat(d3.format('d'))
+
+ chart.xAxis.axisLabel(X_AXIS_LABEL)
+ chart.yAxis.axisLabel(Y_AXIS_LABEL)
+
+ # only one date, display histograms
+ else
+ chart = nv.models.discreteBarChart()
+ chart.tooltip.enabled(false)
+ chart.showValues(true)
+ chart.x (d) -> d.label
+ chart.y (d) -> d.value
+ data = prepareDataForBarChart(data, type)
+
+ # common for each charts
+ chart.margin({left: 100, right: 100})
+ chart.noData(_t('no_data_for_this_period'))
+ chart.height( CHART_HEIGHT )
+
+ # add new chart to the page
+ d3.select(id).datum(data).transition().duration(350).call(chart)
+
+ # resize the graph when the page is resized
+ nv.utils.windowResize(chart.update)
+ # return the chart
+ chart
+
+
+
+ ##
+ # Given an NVD3 line chart axis, scale it to display ordinated dates, according to the given arguments
+ ##
+ setTimeScale = (nvd3Axis, nvd3Scale, argsArray) ->
+ scale = d3.time.scale()
+
+ nvd3Axis.scale(scale)
+ nvd3Scale(scale)
+
+ if (not argsArray and not argsArray.length)
+ oldTicks = nvd3Axis.axis.ticks
+ nvd3Axis.axis.ticks = ->
+ oldTicks.apply(nvd3Axis.axis, argsArray)
+
+
+
+ ##
+ # Translate line chart data in dates row to bar chart data, one bar per type.
+ ##
+ prepareDataForBarChart = (data, type) ->
+ newData = [
+ key: type
+ values: []
+ ]
+ for info in data
+ if info
+ newData[0].values.push
+ "label": info.key
+ "value": info.values[0].y
+ "color": info.color
+
+ newData
+
+
+
+ ##
+ # Sort the provided array, in the specified order, on the value returned by the callback.
+ # This is a stable-sorting algorithm implementation, ie. two call with the same array will return the same results
+ # orders, especially with equal values.
+ # @param array {Array} the array to sort
+ # @param order {string} 'ASC' or 'DESC'
+ # @param getValue {function} the callback which will return the value on which the sort will occurs
+ # @returns {Array}
+ ##
+ stableSort = (array, order, getValue) ->
+ # prepare sorting
+ keys_order = []
+ result = []
+ for i in [0..array.length] by 1
+ keys_order[array[i]] = i;
+ result.push(array[i]);
+
+ # callback for javascript native Array.sort()
+ sort_fc = (a, b) ->
+ val_a = getValue(a)
+ val_b = getValue(b)
+ if val_a == val_b
+ return keys_order[a] - keys_order[b]
+ if val_a < val_b
+ if order == 'ASC' then return -1
+ else return 1
+ else
+ if order == 'ASC' then return 1
+ else return -1
+
+ # finish the sort
+ result.sort(sort_fc)
+ return result
+
+
+
+ ## !!! MUST BE CALLED AT THE END of the controller
+ initialize()
+]
diff --git a/app/assets/javascripts/controllers/admin/groups.coffee.erb b/app/assets/javascripts/controllers/admin/groups.coffee.erb
new file mode 100644
index 000000000..87fd816cc
--- /dev/null
+++ b/app/assets/javascripts/controllers/admin/groups.coffee.erb
@@ -0,0 +1,64 @@
+Application.Controllers.controller "GroupsController", ["$scope", 'groupsPromise', 'Group', 'growl', '_t', ($scope, groupsPromise, Group, growl, _t) ->
+
+ ## List of users groups
+ $scope.groups = groupsPromise
+
+
+
+ ##
+ # Removes the newly inserted but not saved group / Cancel the current group modification
+ # @param rowform {Object} see http://vitalets.github.io/angular-xeditable/
+ # @param index {number} group index in the $scope.groups array
+ ##
+ $scope.cancelGroup = (rowform, index) ->
+ if $scope.groups[index].id?
+ rowform.$cancel()
+ else
+ $scope.groups.splice(index, 1)
+
+
+
+ ##
+ # Creates a new empty entry in the $scope.groups array
+ ##
+ $scope.addGroup = ->
+ $scope.inserted =
+ name: ''
+ $scope.groups.push($scope.inserted)
+
+
+
+ ##
+ # Saves a new group / Update an existing group to the server (form validation callback)
+ # @param data {Object} group name
+ # @param [data] {number} group id, in case of update
+ ##
+ $scope.saveGroup = (data, id) ->
+ if id?
+ Group.update {id: id}, { group: data }, (response) ->
+ growl.success(_t('changes_successfully_saved'))
+ , (error) ->
+ growl.error(_t('an_error_occurred_while_saving_changes'))
+ else
+ Group.save { group: data }, (resp)->
+ growl.success(_t('new_group_successfully_saved'))
+ $scope.groups[$scope.groups.length-1].id = resp.id
+ , (error) ->
+ growl.error(_t('an_error_occurred_when_saving_the_new_group'))
+ $scope.groups.splice($scope.groups.length-1, 1)
+
+
+
+ ##
+ # Deletes the group at the specified index
+ # @param index {number} group index in the $scope.groups array
+ ##
+ $scope.removeGroup = (index) ->
+ Group.delete { id: $scope.groups[index].id }, (resp) ->
+ growl.success(_t('group_successfully_deleted'))
+ $scope.groups.splice(index, 1)
+ , (error) ->
+ growl.error(_t('unable_to_delete_group_because_some_users_and_or_groups_are_still_linked_to_it'))
+
+
+]
diff --git a/app/assets/javascripts/controllers/admin/invoices.coffee.erb b/app/assets/javascripts/controllers/admin/invoices.coffee.erb
new file mode 100644
index 000000000..21727dfe7
--- /dev/null
+++ b/app/assets/javascripts/controllers/admin/invoices.coffee.erb
@@ -0,0 +1,492 @@
+'use strict'
+
+##
+# Controller used in the admin invoices listing page
+##
+Application.Controllers.controller "InvoicesController", ["$scope", "$state", 'Invoice', '$uibModal', "growl", "$filter", 'Setting', 'settings', '_t'
+, ($scope, $state, Invoice, $uibModal, growl, $filter, Setting, settings, _t) ->
+
+
+
+ ### PUBLIC SCOPE ###
+
+ ## List of all users invoices
+ $scope.invoices = Invoice.query()
+
+ ## Default invoices ordering/sorting
+ $scope.orderInvoice = '-reference'
+
+ ## Invoices parameters
+ $scope.invoice =
+ logo: null
+ reference:
+ model: ''
+ help: null
+ templateUrl: 'editReference.html'
+ code:
+ model: ''
+ active: true
+ templateUrl: 'editCode.html'
+ number:
+ model: ''
+ help: null
+ templateUrl: 'editNumber.html'
+ VAT:
+ rate: 19.6
+ active: false
+ templateUrl: 'editVAT.html'
+ text:
+ content: ''
+ legals:
+ content: ''
+
+ ## Placeholding date for the invoice creation
+ $scope.today = moment()
+
+ ## Placeholding date for the reservation begin
+ $scope.inOneWeek = moment().add(1, 'week').startOf('hour')
+
+ ## Placeholding date for the reservation end
+ $scope.inOneWeekAndOneHour = moment().add(1, 'week').add(1, 'hour').startOf('hour')
+
+
+
+ ##
+ # Change the invoices ordering criterion to the one provided
+ # @param orderBy {string} ordering criterion
+ ##
+ $scope.setOrderInvoice = (orderBy)->
+ if $scope.orderInvoice == orderBy
+ $scope.orderInvoice = '-'+orderBy
+ else
+ $scope.orderInvoice = orderBy
+
+
+
+ ##
+ # Open a modal window asking the admin the details to refund the user about the provided invoice
+ # @param invoice {Object} invoice inherited from angular's $resource
+ ##
+ $scope.generateAvoirForInvoice = (invoice)->
+ # open modal
+ modalInstance = $uibModal.open
+ templateUrl: '<%= asset_path "admin/invoices/avoirModal.html" %>'
+ controller: 'AvoirModalController'
+ resolve:
+ invoice: -> invoice
+
+ # once done, update the invoice model and inform the admin
+ modalInstance.result.then (res) ->
+ $scope.invoices.unshift res.avoir
+ Invoice.get {id: invoice.id}, (data) ->
+ invoice.has_avoir = data.has_avoir
+ growl.success(_t('refund_invoice_successfully_created'))
+
+
+
+ ##
+ # Generate an invoice reference sample from the parametrized model
+ # @returns {string} invoice reference sample
+ ##
+ $scope.mkReference = ->
+ sample = $scope.invoice.reference.model
+ if sample
+ # invoice number per day (dd..dd)
+ sample = sample.replace(/d+(?![^\[]*])/g, (match, offset, string) ->
+ padWithZeros(2, match.length)
+ )
+ # invoice number per month (mm..mm)
+ sample = sample.replace(/m+(?![^\[]*])/g, (match, offset, string) ->
+ padWithZeros(12, match.length)
+ )
+ # invoice number per year (yy..yy)
+ sample = sample.replace(/y+(?![^\[]*])/g, (match, offset, string) ->
+ padWithZeros(8, match.length)
+ )
+ # date informations
+ sample = sample.replace(/[YMD]+(?![^\[]*])/g, (match, offset, string) ->
+ $scope.today.format(match)
+ )
+ # information about online selling (X[text])
+ sample = sample.replace(/X\[([^\]]+)\]/g, (match, p1, offset, string) ->
+ p1
+ )
+ # information about refunds (R[text]) - does not apply here
+ sample = sample.replace(/R\[([^\]]+)\]/g, "")
+ sample
+
+
+ ##
+ # Generate an order nmuber sample from the parametrized model
+ # @returns {string} invoice reference sample
+ ##
+ $scope.mkNumber = ->
+ sample = $scope.invoice.number.model
+ if sample
+ # global order number (nn..nn)
+ sample = sample.replace(/n+(?![^\[]*])/g, (match, offset, string) ->
+ padWithZeros(327, match.length)
+ )
+ # order number per year (yy..yy)
+ sample = sample.replace(/y+(?![^\[]*])/g, (match, offset, string) ->
+ padWithZeros(8, match.length)
+ )
+ # order number per month (mm..mm)
+ sample = sample.replace(/m+(?![^\[]*])/g, (match, offset, string) ->
+ padWithZeros(12, match.length)
+ )
+ # order number per day (dd..dd)
+ sample = sample.replace(/d+(?![^\[]*])/g, (match, offset, string) ->
+ padWithZeros(2, match.length)
+ )
+ # date informations
+ sample = sample.replace(/[YMD]+(?![^\[]*])/g, (match, offset, string) ->
+ $scope.today.format(match)
+ )
+ sample
+
+
+
+ ##
+ # Open a modal dialog allowing the user to edit the invoice reference generation template
+ ##
+ $scope.openEditReference = ->
+ modalInstance = $uibModal.open
+ animation: true,
+ templateUrl: $scope.invoice.reference.templateUrl,
+ size: 'lg',
+ resolve:
+ model: ->
+ $scope.invoice.reference.model
+ controller: ($scope, $uibModalInstance, model) ->
+ $scope.model = model
+ $scope.ok = ->
+ $uibModalInstance.close($scope.model)
+ $scope.cancel = ->
+ $uibModalInstance.dismiss('cancel')
+
+ modalInstance.result.then (model) ->
+ Setting.update { name: 'invoice_reference' }, { value: model }, (data)->
+ $scope.invoice.reference.model = model
+ growl.success(_t('invoice_reference_successfully_saved'))
+ , (error)->
+ growl.error(_t('an_error_occurred_while_saving_invoice_reference'))
+ console.error(error)
+
+
+
+
+ ##
+ # Open a modal dialog allowing the user to edit the invoice code
+ ##
+ $scope.openEditCode = ->
+ modalInstance = $uibModal.open
+ animation: true,
+ templateUrl: $scope.invoice.code.templateUrl,
+ size: 'lg',
+ resolve:
+ model: ->
+ $scope.invoice.code.model
+ active: ->
+ $scope.invoice.code.active
+ controller: ($scope, $uibModalInstance, model, active) ->
+ $scope.codeModel = model
+ $scope.isSelected = active
+
+
+ $scope.ok = ->
+ $uibModalInstance.close({model: $scope.codeModel, active: $scope.isSelected})
+ $scope.cancel = ->
+ $uibModalInstance.dismiss('cancel')
+
+ modalInstance.result.then (result) ->
+ Setting.update { name: 'invoice_code-value' }, { value: result.model }, (data)->
+ $scope.invoice.code.model = result.model
+ if result.active
+ growl.success(_t('invoicing_code_succesfully_saved'))
+ , (error)->
+ growl.error(_t('an_error_occurred_while_saving_the_invoicing_code'))
+ console.error(error)
+
+ Setting.update { name: 'invoice_code-active' }, { value: if result.active then "true" else "false" }, (data)->
+ $scope.invoice.code.active = result.active
+ if result.active
+ growl.success(_t('code_successfully_activated'))
+ else
+ growl.success(_t('code_successfully_disabled'))
+ , (error)->
+ growl.error(_t('an_error_occurred_while_activating_the_invoicing_code'))
+ console.error(error)
+
+
+
+
+ ##
+ # Open a modal dialog allowing the user to edit the invoice number
+ ##
+ $scope.openEditInvoiceNb = ->
+ modalInstance = $uibModal.open
+ animation: true,
+ templateUrl: $scope.invoice.number.templateUrl,
+ size: 'lg',
+ resolve:
+ model: ->
+ $scope.invoice.number.model
+ controller: ($scope, $uibModalInstance, model) ->
+ $scope.model = model
+ $scope.ok = ->
+ $uibModalInstance.close($scope.model)
+ $scope.cancel = ->
+ $uibModalInstance.dismiss('cancel')
+
+ modalInstance.result.then (model) ->
+ Setting.update { name: 'invoice_order-nb' }, { value: model }, (data)->
+ $scope.invoice.number.model = model
+ growl.success(_t('order_number_successfully_saved'))
+ , (error)->
+ growl.error(_t('an_error_occurred_while_saving_the_order_number'))
+ console.error(error)
+
+
+
+
+ ##
+ # Open a modal dialog allowing the user to edit the VAT parameters for the invoices
+ # The VAT can be disabled and its rate can be configured
+ ##
+ $scope.openEditVAT = ->
+ modalInstance = $uibModal.open
+ animation: true,
+ templateUrl: $scope.invoice.VAT.templateUrl,
+ size: 'lg',
+ resolve:
+ rate: ->
+ $scope.invoice.VAT.rate
+ active: ->
+ $scope.invoice.VAT.active
+ controller: ($scope, $uibModalInstance, rate, active) ->
+ $scope.rate = rate
+ $scope.isSelected = active
+
+
+ $scope.ok = ->
+ $uibModalInstance.close({rate: $scope.rate, active: $scope.isSelected})
+ $scope.cancel = ->
+ $uibModalInstance.dismiss('cancel')
+
+ modalInstance.result.then (result) ->
+ Setting.update { name: 'invoice_VAT-rate' }, { value: result.rate+"" }, (data)->
+ $scope.invoice.VAT.rate = result.rate
+ if result.active
+ growl.success(_t('VAT_rate_successfully_saved'))
+ , (error)->
+ growl.error(_t('an_error_occurred_while_saving_the_VAT_rate'))
+ console.error(error)
+
+ Setting.update { name: 'invoice_VAT-active' }, { value: if result.active then "true" else "false" }, (data)->
+ $scope.invoice.VAT.active = result.active
+ if result.active
+ growl.success(_t('VAT_successfully_activated'))
+ else
+ growl.success(_t('VAT_successfully_disabled'))
+ , (error)->
+ growl.error(_t('an_error_occurred_while_activating_the_VAT'))
+ console.error(error)
+
+
+
+ ##
+ # Callback to save the value of the text zone when editing is done
+ ##
+ $scope.textEditEnd = (event) ->
+ parsed = parseHtml($scope.invoice.text.content)
+ Setting.update { name: 'invoice_text' }, { value: parsed }, (data)->
+ $scope.invoice.text.content = parsed
+ growl.success(_t('text_successfully_saved'))
+ , (error)->
+ growl.error(_t('an_error_occurred_while_saving_the_text'))
+ console.error(error)
+
+
+
+ ##
+ # Callback to save the value of the legal informations zone when editing is done
+ ##
+ $scope.legalsEditEnd = (event) ->
+ parsed = parseHtml($scope.invoice.legals.content)
+ Setting.update { name: 'invoice_legals' }, { value: parsed }, (data)->
+ $scope.invoice.legals.content = parsed
+ growl.success(_t('address_and_legal_information_successfully_saved'))
+ , (error)->
+ growl.error(_t('an_error_occurred_while_saving_the_address_and_the_legal_information'))
+ console.error(error)
+
+
+
+ ### PRIVATE SCOPE ###
+
+ ##
+ # Kind of constructor: these actions will be realized first when the controller is loaded
+ ##
+ initialize = ->
+ # retrieve settings from the DB through the API
+ $scope.invoice.legals.content = settings['invoice_legals']
+ $scope.invoice.text.content = settings['invoice_text']
+ $scope.invoice.VAT.rate = parseFloat(settings['invoice_VAT-rate'])
+ $scope.invoice.VAT.active = (settings['invoice_VAT-active'] == "true")
+ $scope.invoice.number.model = settings['invoice_order-nb']
+ $scope.invoice.code.model = settings['invoice_code-value']
+ $scope.invoice.code.active = (settings['invoice_code-active'] == "true")
+ $scope.invoice.reference.model = settings['invoice_reference']
+ $scope.invoice.logo =
+ filetype: 'image/png'
+ filename: 'logo.png'
+ base64: settings['invoice_logo']
+
+ # Watch the logo, when a change occurs, save it
+ $scope.$watch 'invoice.logo', ->
+ if $scope.invoice.logo and $scope.invoice.logo.filesize
+ Setting.update { name: 'invoice_logo' }, { value: $scope.invoice.logo.base64 }, (data)->
+ growl.success(_t('logo_successfully_saved'))
+ , (error)->
+ growl.error(_t('an_error_occurred_while_saving_the_logo'))
+ console.error(error)
+
+
+
+ ##
+ # Output the given integer with leading zeros. If the given value is longer than the given
+ # length, it will be truncated.
+ # @param value {number} the integer to pad
+ # @param length {number} the length of the resulting string.
+ ##
+ padWithZeros = (value, length) ->
+ (1e15+value+"").slice(-length)
+
+
+
+ ##
+ # Remove every unsupported html tag from the given html text (like , , ...).
+ # The supported tags are , , and .
+ # @param html {string} single line html text
+ # @return {string} multi line simplified html text
+ ##
+ parseHtml = (html) ->
+ html = html.replace(/<\/?(\w+)((\s+\w+(\s*=\s*(?:".*?"|'.*?'|[^'">\s]+))?)+\s*|\s*)\/?>/g, (match, p1, offset, string) ->
+ if p1 in ['b', 'u', 'i', 'br']
+ match
+ else
+ ''
+ )
+
+
+
+ ## !!! MUST BE CALLED AT THE END of the controller
+ initialize()
+]
+
+
+
+##
+# Controller used in the invoice refunding modal window
+##
+Application.Controllers.controller 'AvoirModalController', ["$scope", "$uibModalInstance", '$locale', "invoice", "Invoice", "growl", '_t'
+, ($scope, $uibModalInstance, $locale, invoice, Invoice, growl, _t) ->
+
+
+
+ ### PUBLIC SCOPE ###
+
+ ## invoice linked to the current refund
+ $scope.invoice = invoice
+
+ ## Associative array containing invoice_item ids associated with boolean values
+ $scope.partial = {}
+
+ ## Default refund parameters
+ $scope.avoir =
+ invoice_id: invoice.id
+ subscription_to_expire: false
+ invoice_items_ids: []
+
+ ## Possible refunding methods
+ $scope.avoirModes = [
+ {name: _t('none'), value: 'none'}
+ {name: _t('by_cash'), value: 'cash'}
+ {name: _t('by_cheque'), value: 'cheque'}
+ {name: _t('by_transfer'), value: 'transfer'}
+ ]
+
+ ## If a subscription was took with the current invoice, should it be canceled or not
+ $scope.subscriptionExpireOptions = {}
+ $scope.subscriptionExpireOptions[_t('yes')] = true
+ $scope.subscriptionExpireOptions[_t('no')] = false
+
+ ## AngularUI-Bootstrap datepicker parameters to define when to refund
+ $scope.datePicker =
+ format: $locale.DATETIME_FORMATS.shortDate
+ opened: false # default: datePicker is not shown
+ options:
+ startingDay: Fablab.weekStartingDay
+
+
+
+ ##
+ # Callback to open the datepicker
+ ##
+ $scope.openDatePicker = ($event) ->
+ $event.preventDefault()
+ $event.stopPropagation()
+ $scope.datePicker.opened = true
+
+
+
+ ##
+ # Validate the refunding and generate a refund invoice
+ ##
+ $scope.ok = ->
+ # check that at least 1 element of the invoice is refunded
+ $scope.avoir.invoice_items_ids = []
+ for itemId, refundItem of $scope.partial
+ $scope.avoir.invoice_items_ids.push(parseInt(itemId)) if refundItem
+
+ if $scope.avoir.invoice_items_ids.length is 0
+ growl.error(_t('you_must_select_at_least_one_element_to_create_a_refund'))
+ else
+ Invoice.save {avoir: $scope.avoir}, (avoir) ->
+ # success
+ $uibModalInstance.close({avoir:avoir, invoice:$scope.invoice})
+ , (err) ->
+ # failed
+ growl.error(_t('unable_to_create_the_refund'))
+
+
+
+ ##
+ # Cancel the refund, dismiss the modal window
+ ##
+ $scope.cancel = ->
+ $uibModalInstance.dismiss('cancel')
+
+
+
+ ### PRIVATE SCOPE ###
+
+ ##
+ # Kind of constructor: these actions will be realized first when the controller is loaded
+ ##
+ initialize = ->
+ ## if the invoice was payed with stripe, allow to refund through stripe
+ Invoice.get {id: invoice.id}, (data) ->
+ $scope.invoice = data
+ # default : all elements of the invoice are refund
+ for item in data.items
+ $scope.partial[item.id] = (typeof item.avoir_item_id isnt 'number')
+
+ if invoice.stripe
+ $scope.avoirModes.push {name: _t('online_payment'), value: 'stripe'}
+
+
+ ## !!! MUST BE CALLED AT THE END of the controller
+ initialize()
+]
diff --git a/app/assets/javascripts/controllers/admin/members.coffee b/app/assets/javascripts/controllers/admin/members.coffee
deleted file mode 100644
index a9117e411..000000000
--- a/app/assets/javascripts/controllers/admin/members.coffee
+++ /dev/null
@@ -1,153 +0,0 @@
-'use strict'
-
-### COMMON CODE ###
-
-##
-# Provides a set of common properties and methods to the $scope parameter. They are used
-# in the various members' admin controllers.
-#
-# Provides :
-# - $scope.groups = [{Group}]
-# - $scope.datePicker = {}
-# - $scope.submited(content)
-# - $scope.cancel()
-# - $scope.fileinputClass(v)
-# - $scope.openDatePicker($event)
-#
-# Requires :
-# - $state (Ui-Router) [ 'app.admin.members' ]
-##
-class MembersController
- constructor: ($scope, $state, Group) ->
-
- ## Retrieve the profiles groups (eg. students ...)
- Group.query (groups) ->
- $scope.groups = groups
- $scope.user.group_id = $scope.groups[0].id
-
- ## Default parameters for AngularUI-Bootstrap datepicker
- $scope.datePicker =
- format: 'dd/MM/yyyy'
- opened: false # default: datePicker is not shown
- options:
- startingDay: 1 # France: the week starts on monday
-
-
- ##
- # Shows the birth day datepicker
- # @param $event {Object} jQuery event object
- ##
- $scope.openDatePicker = ($event) ->
- $event.preventDefault()
- $event.stopPropagation()
- $scope.datePicker.opened = true
-
-
-
- ##
- # For use with ngUpload (https://github.com/twilson63/ngUpload).
- # Intended to be the callback when an upload is done: any raised error will be stacked in the
- # $scope.alerts array. If everything goes fine, the user is redirected to the members listing page.
- # @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.admin.members')
-
-
-
- ##
- # Changes the admin's view to the members list page
- ##
- $scope.cancel = ->
- $state.go('app.admin.members')
-
-
-
- ##
- # 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'
-
-
-
-##
-# Controller used in the member edition page
-##
-Application.Controllers.controller "editMemberController", ["$scope", "$state", "$stateParams", "Member", 'dialogs', 'growl', 'Group', 'CSRF', ($scope, $state, $stateParams, Member, dialogs, growl, Group, CSRF) ->
- CSRF.setMetaTags()
-
-
- ### PUBLIC SCOPE ###
-
- ## API URL where the form will be posted
- $scope.actionUrl = "/api/members/" + $stateParams.id
-
- ## Form action on the above URL
- $scope.method = 'patch'
-
- ## The user to edit
- $scope.user = {}
-
- ## Profiles types (student/standard/...)
- $scope.groups = []
-
-
-
- ### PRIVATE SCOPE ###
-
- ##
- # Kind of constructor: these actions will be realized first when the controller is loaded
- ##
- initialize = ->
- ## Retrieve the member's profile details
- Member.get {id: $stateParams.id}, (resp)->
- $scope.user = resp
-
- ## Using the MembersController
- new MembersController($scope, $state, Group)
-
-
-
- ## !!! MUST BE CALLED AT THE END of the controller
- initialize()
-]
-
-
-
-##
-# Controller used in the member's creation page (admin view)
-##
-Application.Controllers.controller "newMemberController", ["$scope", "$state", "$stateParams", "Member", 'Group', 'CSRF', ($scope, $state, $stateParams, Member, Group, CSRF) ->
- CSRF.setMetaTags()
-
- ### PUBLIC SCOPE ###
-
- ## API URL where the form will be posted
- $scope.actionUrl = "/api/members"
-
- ## Form action on the above URL
- $scope.method = 'post'
-
- ## Default member's profile parameters
- $scope.user =
- plan_interval: ''
-
-
-
- ## Using the MembersController
- new MembersController($scope, $state, Group)
-]
diff --git a/app/assets/javascripts/controllers/admin/members.coffee.erb b/app/assets/javascripts/controllers/admin/members.coffee.erb
new file mode 100644
index 000000000..7e135ce54
--- /dev/null
+++ b/app/assets/javascripts/controllers/admin/members.coffee.erb
@@ -0,0 +1,439 @@
+'use strict'
+
+### COMMON CODE ###
+
+##
+# Provides a set of common properties and methods to the $scope parameter. They are used
+# in the various members' admin controllers.
+#
+# Provides :
+# - $scope.groups = [{Group}]
+# - $scope.trainings = [{Training}]
+# - $scope.plans = []
+# - $scope.datePicker = {}
+# - $scope.submited(content)
+# - $scope.cancel()
+# - $scope.fileinputClass(v)
+# - $scope.openDatePicker($event)
+# - $scope.openSubscriptionDatePicker($event)
+#
+# Requires :
+# - $state (Ui-Router) [ 'app.admin.members' ]
+##
+class MembersController
+ constructor: ($scope, $state, $locale, Group, Training) ->
+
+ ## Retrieve the profiles groups (eg. students ...)
+ Group.query (groups) ->
+ $scope.groups = groups
+
+ ## Retrieve the list the available trainings
+ Training.query().$promise.then (data)->
+ $scope.trainings = data.map (d) ->
+ id: d.id
+ name: d.name
+
+ ## Default parameters for AngularUI-Bootstrap datepicker
+ $scope.datePicker =
+ format: $locale.DATETIME_FORMATS.shortDate
+ opened: false # default: datePicker is not shown
+ subscription_date_opened: false
+ options:
+ startingDay: Fablab.weekStartingDay
+
+ ##
+ # Shows the birth day datepicker
+ # @param $event {Object} jQuery event object
+ ##
+ $scope.openDatePicker = ($event) ->
+ $event.preventDefault()
+ $event.stopPropagation()
+ $scope.datePicker.opened = true
+
+
+
+ ##
+ # Shows the end of subscription datepicker
+ # @param $event {Object} jQuery event object
+ ##
+ $scope.openSubscriptionDatePicker = ($event) ->
+ $event.preventDefault()
+ $event.stopPropagation()
+ $scope.datePicker.subscription_date_opened = true
+
+
+
+ ##
+ # For use with ngUpload (https://github.com/twilson63/ngUpload).
+ # Intended to be the callback when an upload is done: any raised error will be stacked in the
+ # $scope.alerts array. If everything goes fine, the user is redirected to the members listing page.
+ # @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.admin.members')
+
+
+
+ ##
+ # Changes the admin's view to the members list page
+ ##
+ $scope.cancel = ->
+ $state.go('app.admin.members')
+
+
+
+ ##
+ # 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'
+
+
+##
+# Controller used in the members/groups management page
+##
+Application.Controllers.controller "AdminMembersController", ["$scope", 'membersPromise', 'adminsPromise', 'growl', 'Admin', 'dialogs', '_t'
+, ($scope, membersPromise, adminsPromise, growl, Admin, dialogs, _t) ->
+
+
+
+ ### PUBLIC SCOPE ###
+
+ ## members list
+ $scope.members = membersPromise
+
+ ## admins list
+ $scope.admins = adminsPromise.admins
+
+ ## Members ordering/sorting. Default: not sorted
+ $scope.orderMember = null
+
+ ## Admins ordering/sorting. Default: not sorted
+ $scope.orderAdmin = null
+
+
+
+ ##
+ # Change the members ordering criterion to the one provided
+ # @param orderBy {string} ordering criterion
+ ##
+ $scope.setOrderMember = (orderBy)->
+ if $scope.orderMember == orderBy
+ $scope.orderMember = '-'+orderBy
+ else
+ $scope.orderMember = orderBy
+
+
+
+ ##
+ # Change the admins ordering criterion to the one provided
+ # @param orderBy {string} ordering criterion
+ ##
+ $scope.setOrderAdmin = (orderAdmin)->
+ if $scope.orderAdmin == orderAdmin
+ $scope.orderAdmin = '-'+orderAdmin
+ else
+ $scope.orderAdmin = orderAdmin
+
+
+
+ ##
+ # Ask for confirmation then delete the specified administrator
+ # @param admins {Array} full list of administrators
+ # @param admin {Object} administrator to delete
+ ##
+ $scope.destroyAdmin = (admins, admin)->
+ dialogs.confirm
+ resolve:
+ object: ->
+ title: _t('confirmation_required')
+ msg: _t('do_you_really_want_to_delete_this_administrator_this_cannot_be_undone')
+ , -> # cancel confirmed
+ Admin.delete id: admin.id, ->
+ admins.splice(findAdminIdxById(admins, admin.id), 1)
+ growl.success(_t('administrator_successfully_deleted'))
+ , (error)->
+ growl.error(_t('unable_to_delete_the_administrator'))
+
+
+
+ ### PRIVATE SCOPE ###
+
+ ##
+ # Iterate through the provided array and return the index of the requested admin
+ # @param admins {Array} full list of users with role 'admin'
+ # @param id {Number} user id of the admin to retrieve in the list
+ # @returns {Number} index of the requested admin, in the provided array
+ ##
+ findAdminIdxById = (admins, id)->
+ (admins.map (admin)->
+ admin.id
+ ).indexOf(id)
+]
+
+
+##
+# Controller used in the member edition page
+##
+Application.Controllers.controller "EditMemberController", ["$scope", "$state", "$stateParams", '$locale', "Member", 'Training', 'dialogs', 'growl', 'Group', 'Subscription', 'CSRF', 'memberPromise', 'tagsPromise', '$uibModal', 'Plan', '$filter', '_t'
+, ($scope, $state, $stateParams, $locale, Member, Training, dialogs, growl, Group, Subscription, CSRF, memberPromise, tagsPromise, $uibModal, Plan, $filter, _t) ->
+
+
+
+ ### PUBLIC SCOPE ###
+
+ ## API URL where the form will be posted
+ $scope.actionUrl = "/api/members/" + $stateParams.id
+
+ ## Form action on the above URL
+ $scope.method = 'patch'
+
+ ## List of tags associables with user
+ $scope.tags = tagsPromise
+
+ ## The user to edit
+ $scope.user = memberPromise
+
+ ## the user subscription
+ if $scope.user.subscribed_plan? and $scope.user.subscription?
+ $scope.subscription = $scope.user.subscription
+ $scope.subscription.expired_at = $scope.subscription.expired_at
+ else
+ Plan.query group_id: $scope.user.group_id, (plans)->
+ $scope.plans = plans
+ for plan in $scope.plans
+ plan.nameToDisplay = $filter('humanReadablePlanName')(plan)
+
+
+ ## Available trainings list
+ $scope.trainings = []
+
+ ## Profiles types (student/standard/...)
+ $scope.groups = []
+
+
+
+ ##
+ # Open a modal dialog, allowing the admin to extend the current user's subscription (freely or not)
+ # @param subscription {Object} User's subscription object
+ # @param free {boolean} True if the extent is offered, false otherwise
+ ##
+ $scope.updateSubscriptionModal = (subscription, free)->
+ modalInstance = $uibModal.open
+ animation: true,
+ templateUrl: '<%= asset_path "admin/subscriptions/expired_at_modal.html" %>'
+ size: 'lg',
+ controller: ['$scope', '$uibModalInstance', 'Subscription', ($scope, $uibModalInstance, Subscription) ->
+ $scope.new_expired_at = angular.copy(subscription.expired_at)
+ $scope.free = free
+ $scope.datePicker =
+ opened: false
+ format: $locale.DATETIME_FORMATS.shortDate
+ options:
+ startingDay: Fablab.weekStartingDay
+ minDate: new Date
+
+ $scope.openDatePicker = (ev)->
+ ev.preventDefault();
+ ev.stopPropagation();
+ $scope.datePicker.opened = true
+
+
+ $scope.ok = ->
+ Subscription.update { id: subscription.id }, { subscription: { expired_at: $scope.new_expired_at, free: free } }, (_subscription)->
+ growl.success(_t('you_successfully_changed_the_expiration_date_of_the_user_s_subscription'))
+ $uibModalInstance.close(_subscription)
+ , (error)->
+ growl.error(_t('a_problem_occurred_while_saving_the_date'))
+ $scope.cancel = ->
+ $uibModalInstance.dismiss('cancel')
+ ]
+ # once the form was validated succesfully ...
+ modalInstance.result.then (subscription) ->
+ $scope.subscription.expired_at = subscription.expired_at
+
+
+
+ ##
+ # Open a modal dialog allowing the admin to set a subscription for the given user.
+ # @param user {Object} User object, user currently reviewed, as recovered from GET /api/members/:id
+ # @param plans {Array} List of plans, availables for the currently reviewed user, as recovered from GET /api/plans
+ ##
+ $scope.createSubscriptionModal = (user, plans)->
+ modalInstance = $uibModal.open
+ animation: true,
+ templateUrl: '<%= asset_path "admin/subscriptions/create_modal.html" %>'
+ size: 'lg',
+ controller: ['$scope', '$uibModalInstance', 'Subscription', 'Group', ($scope, $uibModalInstance, Subscription, Group) ->
+
+ ## selected user
+ $scope.user = user
+
+ ## available plans for the selected user
+ $scope.plans = plans
+
+ ##
+ # Generate a string identifying the given plan by literal humain-readable name
+ # @param plan {Object} Plan object, as recovered from GET /api/plan/:id
+ # @param groups {Array} List of Groups objects, as recovered from GET /api/groups
+ # @param short {boolean} If true, the generated name will contains the group slug, otherwise the group full name
+ # will be included.
+ # @returns {String}
+ ##
+ $scope.humanReadablePlanName = (plan, groups, short)->
+ "#{$filter('humanReadablePlanName')(plan, groups, short)}"
+
+ ##
+ # Modal dialog validation callback
+ ##
+ $scope.ok = ->
+ $scope.subscription.user_id = user.id
+ Subscription.save { }, { subscription: $scope.subscription }, (_subscription)->
+
+ growl.success(_t('subscription_successfully_purchased'))
+ $uibModalInstance.close(_subscription)
+ $state.reload()
+ , (error)->
+ growl.error(_t('a_problem_occurred_while_taking_the_subscription'))
+
+ ##
+ # Modal dialog cancellation callback
+ ##
+ $scope.cancel = ->
+ $uibModalInstance.dismiss('cancel')
+ ]
+ # once the form was validated succesfully ...
+ modalInstance.result.then (subscription) ->
+ $scope.subscription = subscription
+
+
+
+ ### PRIVATE SCOPE ###
+
+
+
+ ##
+ # Kind of constructor: these actions will be realized first when the controller is loaded
+ ##
+ initialize = ->
+ CSRF.setMetaTags()
+
+ # init the birth date to JS object
+ $scope.user.profile.birthday = moment($scope.user.profile.birthday).toDate()
+
+ ## the user subscription
+ if $scope.user.subscribed_plan? and $scope.user.subscription?
+ $scope.subscription = $scope.user.subscription
+ $scope.subscription.expired_at = $scope.subscription.expired_at
+ else
+ Plan.query group_id: $scope.user.group_id, (plans)->
+ $scope.plans = plans
+ for plan in $scope.plans
+ plan.nameToDisplay = "#{plan.base_name} - #{plan.interval}"
+
+ # Using the MembersController
+ new MembersController($scope, $state, $locale, Group, Training)
+
+
+
+ ## !!! MUST BE CALLED AT THE END of the controller
+ initialize()
+]
+
+
+
+##
+# Controller used in the member's creation page (admin view)
+##
+Application.Controllers.controller "NewMemberController", ["$scope", "$state", "$locale", "$stateParams", "Member", 'Training', 'Group', 'CSRF'
+, ($scope, $state, $locale, $stateParams, Member, Training, Group, CSRF) ->
+
+ CSRF.setMetaTags()
+
+ ### PUBLIC SCOPE ###
+
+ ## API URL where the form will be posted
+ $scope.actionUrl = "/api/members"
+
+ ## Form action on the above URL
+ $scope.method = 'post'
+
+ ## Default member's profile parameters
+ $scope.user =
+ plan_interval: ''
+
+
+
+ ## Using the MembersController
+ new MembersController($scope, $state, $locale, Group, Training)
+]
+
+
+
+##
+# Controller used in the admin's creation page (admin view)
+##
+Application.Controllers.controller 'NewAdminController', ['$state', '$scope', '$locale', 'Admin', 'growl', ($state, $scope, $locale, Admin, growl)->
+
+ ## default admin profile
+ $scope.admin =
+ profile_attributes:
+ gender: true
+
+ ## Default parameters for AngularUI-Bootstrap datepicker
+ $scope.datePicker =
+ format: $locale.DATETIME_FORMATS.shortDate
+ opened: false
+ options:
+ startingDay: Fablab.weekStartingDay
+
+
+
+ ##
+ # Shows the birth day datepicker
+ # @param $event {Object} jQuery event object
+ ##
+ $scope.openDatePicker = ($event)->
+ $scope.datePicker.opened = true
+
+
+
+ ##
+ # Send the new admin, currently stored in $scope.admin, to the server for database saving
+ ##
+ $scope.saveAdmin = ->
+ Admin.save {}, { admin: $scope.admin }, ->
+ growl.success(_t('administrator_successfully_created_he_will_receive_his_connection_directives_by_email', {GENDER:getGender($scope.admin)}, "messageformat"))
+ $state.go('app.admin.members')
+ , (error)->
+ console.log(error)
+
+
+
+ ### PRIVATE SCOPE ###
+
+ ##
+ # Return an enumerable meaninful string for the gender of the provider user
+ # @param user {Object} Database user record
+ # @return {string} 'male' or 'female'
+ ##
+ getGender = (user) ->
+ if user.profile_attributes
+ if user.profile_attributes.gender then 'male' else 'female'
+ else 'other'
+
+
+]
diff --git a/app/assets/javascripts/controllers/admin/plans.coffee.erb b/app/assets/javascripts/controllers/admin/plans.coffee.erb
new file mode 100644
index 000000000..c5bf1151b
--- /dev/null
+++ b/app/assets/javascripts/controllers/admin/plans.coffee.erb
@@ -0,0 +1,260 @@
+'use strict'
+
+### COMMON CODE ###
+
+
+
+class PlanController
+
+ constructor: ($scope, groups, plans, machines, prices, partners, CSRF) ->
+ # protection against request forgery
+ CSRF.setMetaTags()
+
+
+
+ ## groups list
+ $scope.groups = groups
+
+ ## plans list
+ $scope.plans = plans
+
+ ## machines list
+ $scope.machines = machines
+
+ ## users with role 'partner', notifiables for a partner plan
+ $scope.partners = partners.users
+
+ ## Subscriptions prices, machines prices and training prices, per groups
+ $scope.group_pricing = prices
+
+ ##
+ # 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'
+
+ ##
+ # Mark the provided file for deletion
+ # @param file {Object}
+ ##
+ $scope.deleteFile = (file) ->
+ if file? and file.id?
+ file._destroy = true
+
+
+
+ ##
+ # 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
+
+
+
+
+
+
+##
+# Controller used in the plan creation form
+##
+Application.Controllers.controller 'NewPlanController', ['$scope', '$uibModal', 'groups', 'plans', 'machines', 'prices', 'partners', 'CSRF', '$state', 'growl', '_t', '$locale'
+, ($scope, $uibModal, groups, plans, machines, prices, partners, CSRF, $state, growl, _t, $locale) ->
+
+
+
+ ### PRIVATE STATIC CONSTANTS ###
+
+ ## when creating a new contact for a partner plan, this ID will be sent to the server
+ NEW_PARTNER_ID: null
+
+
+
+ ### PUBLIC SCOPE ###
+
+ ## current form is used to create a new plan
+ $scope.mode = 'creation'
+
+ ## prices bindings
+ $scope.prices =
+ training: {}
+ machine: {}
+
+ ## form inputs bindings
+ $scope.plan =
+ type: null
+ group_id: null
+ interval: null
+ intervalCount: 0
+ amount: null
+ isRolling: false
+ partnerId: null
+ partnerContact: null
+ ui_weight: 0
+
+ ## API URL where the form will be posted
+ $scope.actionUrl = "/api/plans/"
+
+ ## HTTP method for the rest API
+ $scope.method = 'POST'
+
+
+ ## currency symbol for the current locale (cf. angular-i18n)
+ $scope.currencySymbol = $locale.NUMBER_FORMATS.CURRENCY_SYM;
+
+
+
+ ##
+ # Checks if the partner contact is a valid data. Used in the form validation process
+ # @returns {boolean}
+ ##
+ $scope.partnerIsValid = ->
+ ($scope.plan.type == "Plan") or ($scope.plan.partnerId or ($scope.plan.partnerContact and $scope.plan.partnerContact.email))
+
+
+
+ ##
+ # Open a modal dialog allowing the admin to create a new partner user
+ ##
+ $scope.openPartnerNewModal = (subscription)->
+ modalInstance = $uibModal.open
+ animation: true,
+ templateUrl: '<%= asset_path "shared/_partner_new_modal.html" %>'
+ size: 'lg',
+ controller: ['$scope', '$uibModalInstance', 'User', ($scope, $uibModalInstance, User) ->
+ $scope.partner = {}
+
+ $scope.ok = ->
+ User.save {}, { user: $scope.partner }, (user)->
+ $scope.partner.id = user.id
+ $scope.partner.name = "#{user.first_name} #{user.last_name}"
+ $uibModalInstance.close($scope.partner)
+ , (error)->
+ growl.error(_t('unable_to_save_this_user_check_that_there_isnt_an_already_a_user_with_the_same_name'))
+ $scope.cancel = ->
+ $uibModalInstance.dismiss('cancel')
+ ]
+ # once the form was validated succesfully ...
+ modalInstance.result.then (partner) ->
+ $scope.partners.push(partner)
+ $scope.plan.partnerId = partner.id
+
+
+
+ ##
+ # Display some messages and redirect the user, once the form was submitted, depending on the result status
+ # (failed/succeeded).
+ # @param content {Object}
+ ##
+ $scope.afterSubmit = (content) ->
+ if !content.id? and !content.plan_ids?
+ growl.error(_t('unable_to_create_the_subscription_please_try_again'))
+ else
+ growl.success(_t('successfully_created_subscription(s)_dont_forget_to_redefine_prices'))
+ if content.plan_ids?
+ $state.go('app.admin.pricing')
+ else
+ if content.id?
+ $state.go('app.admin.plans.edit', {id: content.id})
+
+
+
+ new PlanController($scope, groups, plans, machines, prices, partners, CSRF)
+]
+
+
+
+##
+# Controller used in the plan edition form
+##
+Application.Controllers.controller 'EditPlanController', ['$scope', 'groups', 'plans', 'planPromise', 'machines', 'prices', 'partners', 'CSRF', '$state', '$stateParams', 'growl', '$filter', '_t', '$locale'
+, ($scope, groups, plans, planPromise, machines, prices, partners, CSRF, $state, $stateParams, growl, $filter, _t, $locale) ->
+
+
+
+ ### PUBLIC SCOPE ###
+ $scope.groups = groups
+ ## current form is used for edition mode
+ $scope.mode = 'edition'
+
+ ## edited plan data
+ $scope.plan = planPromise
+ $scope.plan.type = "Plan" if $scope.plan.type == null
+
+ ## API URL where the form will be posted
+ $scope.actionUrl = "/api/plans/" + $stateParams.id
+
+ ## HTTP method for the rest API
+ $scope.method = 'PATCH'
+
+
+ ## currency symbol for the current locale (cf. angular-i18n)
+ $scope.currencySymbol = $locale.NUMBER_FORMATS.CURRENCY_SYM;
+
+
+
+ ##
+ # If a parent plan was set ($scope.plan.parent), the prices will be copied from this parent plan into
+ # the current plan prices list. Otherwise, the current plan prices will be erased.
+ ##
+ $scope.copyPricesFromPlan = ->
+ if $scope.plan.parent
+ parentPlan = $scope.getPlanFromId($scope.plan.parent)
+ for parentPrice in parentPlan.prices
+ for childKey, childPrice of $scope.plan.prices
+ if childPrice.priceable_type == parentPrice.priceable_type and childPrice.priceable_id == parentPrice.priceable_id
+ $scope.plan.prices[childKey].amount = parentPrice.amount
+ break
+ # if no plan were selected, unset every prices
+ else
+ for key, price of $scope.plan.prices
+ $scope.plan.prices[key].amount = 0
+
+
+
+ ##
+ # Display some messages once the form was submitted, depending on the result status (failed/succeeded)
+ # @param content {Object}
+ ##
+ $scope.afterSubmit = (content) ->
+ if !content.id? and !content.plan_ids?
+ growl.error(_t('unable_to_save_subscription_changes_please_try_again'))
+ else
+ growl.success(_t('subscription_successfully_changed'))
+ $state.go('app.admin.pricing')
+
+
+
+ ##
+ # Generate a string identifying the given plan by literal humain-readable name
+ # @param plan {Object} Plan object, as recovered from GET /api/plan/:id
+ # @param groups {Array} List of Groups objects, as recovered from GET /api/groups
+ # @param short {boolean} If true, the generated name will contains the group slug, otherwise the group full name
+ # will be included.
+ # @returns {String}
+ ##
+ $scope.humanReadablePlanName = (plan, groups, short)->
+ "#{$filter('humanReadablePlanName')(plan, groups, short)}"
+
+
+
+ ### PRIVATE SCOPE ###
+
+ ##
+ # Kind of constructor: these actions will be realized first when the controller is loaded
+ ##
+ initialize = ->
+ # Using the PlansController
+ new PlanController($scope, groups, plans, machines, prices, partners, CSRF)
+
+ ## !!! MUST BE CALLED AT THE END of the controller
+ initialize()
+]
\ No newline at end of file
diff --git a/app/assets/javascripts/controllers/admin/pricing.coffee.erb b/app/assets/javascripts/controllers/admin/pricing.coffee.erb
new file mode 100644
index 000000000..7a60717df
--- /dev/null
+++ b/app/assets/javascripts/controllers/admin/pricing.coffee.erb
@@ -0,0 +1,383 @@
+'use strict'
+
+##
+# Controller used in the prices edition page
+##
+Application.Controllers.controller "EditPricingController", ["$scope", "$state", '$uibModal', 'Training', 'TrainingsPricing', 'Machine', '$filter', 'Credit', 'Pricing', 'Plan', 'plans', 'groups', 'growl', 'machinesPricesPromise', 'Price', 'dialogs', 'trainingsPricingsPromise', '_t'
+, ($scope, $state, $uibModal, Training, TrainingsPricing, Machine, $filter, Credit, Pricing, Plan, plans, groups, growl, machinesPricesPromise, Price, dialogs, trainingsPricingsPromise, _t) ->
+
+ ### PUBLIC SCOPE ###
+ ## List of machines prices (not considering any plan)
+ $scope.machinesPrices = machinesPricesPromise.prices
+
+ ## List of trainings pricing
+ $scope.trainingsPricings = trainingsPricingsPromise
+
+ ## List of available subscriptions plans (eg. student/month, PME/year ...)
+ $scope.plans = plans
+
+ ## List of groups (eg. normal, student ...)
+ $scope.groups = groups
+
+ ## Associate free machine hours with subscriptions
+ $scope.machineCredits = []
+
+ ## Array of associations (plan <-> training)
+ $scope.trainingCredits = []
+
+ ## Associate a plan with all its trainings ids
+ $scope.trainingCreditsGroups = {}
+
+ ## List of trainings
+ $scope.trainings = []
+
+ ## List of machines
+ $scope.machines = []
+
+ ## The plans list ordering. Default: by group
+ $scope.orderPlans = 'group_id'
+
+ ## Status of the drop-down menu in Credits tab
+ $scope.status =
+ isopen: false
+
+
+
+ $scope.findTrainingsPricing = (trainingsPricings, trainingId, groupId)->
+ for trainingsPricing in trainingsPricings
+ if trainingsPricing.training_id == trainingId and trainingsPricing.group_id == groupId
+ return trainingsPricing
+
+
+ $scope.updateTrainingsPricing = (data, trainingsPricing)->
+ if data?
+ TrainingsPricing.update({ id: trainingsPricing.id }, { trainings_pricing: { amount: data } }).$promise
+ else
+ _t('please_specify_a_number')
+
+ ##
+ # Retrieve a plan from its given identifier and returns it
+ # @param id {number} plan ID
+ # @returns {Object} Plan, inherits from $resource
+ ##
+ $scope.getPlanFromId = (id) ->
+ for plan in $scope.plans
+ if plan.id == parseInt(id)
+ return plan
+
+
+
+ ##
+ # Retrieve a group from its given identifier and returns it
+ # @param id {number} group ID
+ # @returns {Object} Group, inherits from $resource
+ ##
+ $scope.getGroupFromId = (groups, id) ->
+ for group in groups
+ if group.id == parseInt(id)
+ return group
+
+
+ ##
+ # Returns a human readable string of named trainings, according to the provided array.
+ # $scope.trainings may contains the full list of training. The returned string will only contains the trainings
+ # whom ID are given in the provided parameter
+ # @param trainings {Array} trainings IDs array
+ ##
+ $scope.showTrainings = (trainings) ->
+ unless angular.isArray(trainings) and trainings.length > 0
+ return _t('none')
+
+ selected = []
+ angular.forEach $scope.trainings, (t) ->
+ if trainings.indexOf(t.id) >= 0
+ selected.push t.name
+ return if selected.length then selected.join(' | ') else _t('none')
+
+
+
+ ##
+ # Validation callback when editing training's credits. Save the changes.
+ # @param newdata {Object} training and associated plans
+ # @param planId {number|string} plan id
+ ##
+ $scope.saveTrainingCredits = (newdata, planId) ->
+ # save the number of credits
+ Plan.update {id: planId},
+ training_credit_nb: newdata.training_credits
+ , angular.noop() # do nothing in case of success
+ , (error) ->
+ growl.error(_t('an_error_occurred_while_saving_the_number_of_credits'))
+
+ # save the associated trainings
+ angular.forEach $scope.trainingCreditsGroups, (original, key) ->
+ if parseInt(key) == parseInt(planId) # we've got the original data
+ if original.join('_') != newdata.training_ids.join('_') # if any changes
+ # iterate through the previous credits to remove
+ angular.forEach original, (oldTrainingId) ->
+ if newdata.training_ids.indexOf(oldTrainingId) == -1
+ tc = findTrainingCredit(oldTrainingId, planId)
+ if tc
+ tc.$delete {}
+ , ->
+ $scope.trainingCredits.splice($scope.trainingCredits.indexOf(tc), 1)
+ $scope.trainingCreditsGroups[planId].splice($scope.trainingCreditsGroups[planId].indexOf(tc.id), 1)
+ , (error) ->
+ growl.error(_t('an_error_occurred_while_deleting_credit_with_the_TRAINING', {TRAINING:tc.creditable.name}))
+ else
+ growl.error(_t('an_error_occurred_unable_to_find_the_credit_to_revoke'))
+
+ # iterate through the new credits to add
+ angular.forEach newdata.training_ids, (newTrainingId) ->
+ if original.indexOf(newTrainingId) == -1
+ Credit.save
+ credit:
+ creditable_id: newTrainingId
+ creditable_type: 'Training'
+ plan_id: planId
+ , (newTc) -> # success
+ $scope.trainingCredits.push(newTc)
+ $scope.trainingCreditsGroups[newTc.plan_id].push(newTc.creditable_id)
+ , (error) -> # failed
+ training = getTrainingFromId(newTrainingId)
+ growl.error(_t('an_error_occurred_while_creating_credit_with_the_TRAINING', {TRAINING: training.name}))
+ console.error(error)
+
+
+
+
+ ##
+ # Cancel the current training credit modification
+ # @param rowform {Object} see http://vitalets.github.io/angular-xeditable/
+ ##
+ $scope.cancelTrainingCredit = (rowform) ->
+ rowform.$cancel()
+
+
+ ##
+ # Create a new empty entry in the $scope.machineCredits array
+ # @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
+ ##
+ $scope.addMachineCredit = (e)->
+ e.preventDefault()
+ e.stopPropagation()
+ $scope.inserted =
+ creditable_type: 'Machine'
+ $scope.machineCredits.push($scope.inserted)
+ $scope.status.isopen = !$scope.status.isopen
+
+
+
+ ##
+ # In the Credits tab, while editing a machine credit row, select the current machine from the
+ # drop-down list of machines as the current item.
+ # @param credit {Object} credit object, inherited from $resource
+ ##
+ $scope.showCreditableName = (credit) ->
+ selected = _t('not_set')
+ if credit and credit.creditable_id
+ angular.forEach $scope.machines, (m)->
+ if m.id == credit.creditable_id
+ selected = m.name+' ( id. '+m.id+' )'
+ return selected
+
+
+
+ ##
+ # Validation callback when editing machine's credits. Save the changes.
+ # This will prevent the creation of two credits associating the same machine and plan.
+ # @param data {Object} machine, associated plan and number of credit hours.
+ # @param [id] {number} credit id for edition, create a new credit object if not provided
+ ##
+ $scope.saveMachineCredit = (data, id) ->
+ for mc in $scope.machineCredits
+ if mc.plan_id == data.plan_id and mc.creditable_id == data.creditable_id and (id == null or mc.id != id)
+ growl.error(_t('error_a_credit_linking_this_machine_with_that_subscription_already_exists'))
+ unless id
+ $scope.machineCredits.pop()
+ return false
+
+ if id?
+ Credit.update {id: id}, credit: data, ->
+ growl.success(_t('changes_have_been_successfully_saved'))
+ else
+ data.creditable_type = 'Machine'
+ Credit.save
+ credit: data
+ , (resp) ->
+ $scope.machineCredits[$scope.machineCredits.length-1].id = resp.id
+ growl.success(_t('credit_was_successfully_saved'))
+
+
+ ##
+ # Removes the newly inserted but not saved machine credit / Cancel the current machine credit modification
+ # @param rowform {Object} see http://vitalets.github.io/angular-xeditable/
+ # @param index {number} theme index in the $scope.machineCredits array
+ ##
+ $scope.cancelMachineCredit = (rowform, index) ->
+ if $scope.machineCredits[index].id?
+ rowform.$cancel()
+ else
+ $scope.machineCredits.splice(index, 1)
+
+
+
+ ##
+ # Deletes the machine credit at the specified index
+ # @param index {number} machine credit index in the $scope.machineCredits array
+ ##
+ $scope.removeMachineCredit = (index) ->
+ Credit.delete $scope.machineCredits[index]
+ $scope.machineCredits.splice(index, 1)
+
+
+
+ ##
+ # If the plan does not have a type, return a default value for display purposes
+ # @param type {string|undefined|null} plan's type (eg. 'partner')
+ # @returns {string}
+ ##
+ $scope.getPlanType = (type) ->
+ if type == 'PartnerPlan'
+ return _t('partner')
+ else return _t('standard')
+
+ ##
+ # Change the plans ordering criterion to the one provided
+ # @param orderBy {string} ordering criterion
+ ##
+ $scope.setOrderPlans = (orderBy) ->
+ if $scope.orderPlans == orderBy
+ $scope.orderPlans = '-'+orderBy
+ else
+ $scope.orderPlans = orderBy
+
+ ##
+ # Retrieve a price from prices array by a machineId and a groupId
+ ##
+ $scope.findPriceBy = (prices, machineId, groupId)->
+ for price in prices
+ if price.priceable_id == machineId and price.group_id == groupId
+ return price
+
+ ##
+ # update a price for a machine and a group, not considering any plan
+ ##
+ $scope.updatePrice = (data, price)->
+ if data?
+ Price.update({ id: price.id }, { price: { amount: data } }).$promise
+ else
+ _t('please_specify_a_number')
+
+ ##
+ # Delete the specified subcription plan
+ # @param id {number} plan id
+ ##
+ $scope.deletePlan = (plans, id) ->
+ if typeof id != 'number'
+ console.error('[editPricingController::deletePlan] Error: invalid id parameter')
+ else
+ # open a confirmation dialog
+ dialogs.confirm
+ resolve:
+ object: ->
+ title: _t('confirmation_required')
+ msg: _t('do_you_really_want_to_delete_this_subscription_plan')
+ , ->
+ # the admin has confirmed, delete the plan
+ Plan.delete {id: id}, (res) ->
+ growl.success(_t('subscription_plan_was_successfully_deleted'))
+ $scope.plans.splice(findPlanIdxById(plans, id), 1)
+
+ , (error) ->
+ console.error('[editPricingController::deletePlan] Error: '+error.statusText) if error.statusText
+ growl.error(_t('unable_to_delete_the_specified_subscription_an_error_occurred'))
+
+
+
+ ##
+ # Generate a string identifying the given plan by literal humain-readable name
+ # @param plan {Object} Plan object, as recovered from GET /api/plan/:id
+ # @param groups {Array} List of Groups objects, as recovered from GET /api/groups
+ # @param short {boolean} If true, the generated name will contains the group slug, otherwise the group full name
+ # will be included.
+ # @returns {String}
+ ##
+ $scope.humanReadablePlanName = (plan, groups, short)->
+ "#{$filter('humanReadablePlanName')(plan, groups, short)}"
+
+
+
+ ### PRIVATE SCOPE ###
+
+ findPlanIdxById = (plans, id)->
+ (plans.map (plan)->
+ plan.id
+ ).indexOf(id)
+
+ ##
+ # Kind of constructor: these actions will be realized first when the controller is loaded
+ ##
+ initialize = ->
+
+ Credit.query({creditable_type: 'Training'}).$promise.then (data)->
+ $scope.trainingCredits = data
+ $scope.trainingCreditsGroups = groupCreditsByPlan(data)
+
+ ## adds empty array for plan which hasn't any credits yet
+ for plan in $scope.plans
+ unless $scope.trainingCreditsGroups[plan.id]?
+ $scope.trainingCreditsGroups[plan.id] = []
+
+ Credit.query({creditable_type: 'Machine'}).$promise.then (data)->
+ $scope.machineCredits = data
+
+ Training.query().$promise.then (data)->
+ $scope.trainings = data
+
+ Machine.query().$promise.then (data)->
+ $scope.machines = data
+
+
+ ##
+ # Group the given credits array into a map associating the plan ID with its associated trainings/machines
+ # @return {Object} the association map
+ ##
+ groupCreditsByPlan = (credits) ->
+ creditsMap = {}
+ angular.forEach credits, (c) ->
+ unless creditsMap[c.plan_id]
+ creditsMap[c.plan_id] = []
+ creditsMap[c.plan_id].push(c.creditable_id)
+ creditsMap
+
+
+
+ ##
+ # Iterate through $scope.traininfCredits to find the credit matching the given criterion
+ # @param trainingId {number|string} training ID
+ # @param planId {number|string} plan ID
+ ##
+ findTrainingCredit = (trainingId, planId) ->
+ trainingId = parseInt(trainingId)
+ planId = parseInt(planId)
+
+ for credit in $scope.trainingCredits
+ if credit.plan_id == planId and credit.creditable_id == trainingId
+ return credit
+
+
+ ##
+ # Retrieve a training from its given identifier and returns it
+ # @param id {number} training ID
+ # @returns {Object} Training inherited from $resource
+ ##
+ getTrainingFromId = (id) ->
+ for training in $scope.trainings
+ if training.id == parseInt(id)
+ return training
+
+
+ ## !!! MUST BE CALLED AT THE END of the controller
+ initialize()
+]
diff --git a/app/assets/javascripts/controllers/admin/project_elements.coffee b/app/assets/javascripts/controllers/admin/project_elements.coffee
index e5f9e0460..67b7c25c8 100644
--- a/app/assets/javascripts/controllers/admin/project_elements.coffee
+++ b/app/assets/javascripts/controllers/admin/project_elements.coffee
@@ -1,17 +1,16 @@
'use strict'
-Application.Controllers.controller "projectElementsController", ["$scope", "$state", 'Component', 'Licence', 'Theme', ($scope, $state, Component, Licence, Theme) ->
+Application.Controllers.controller "ProjectElementsController", ["$scope", "$state", 'Component', 'Licence', 'Theme', 'componentsPromise', 'licencesPromise', 'themesPromise'
+, ($scope, $state, Component, Licence, Theme, componentsPromise, licencesPromise, themesPromise) ->
## Materials list (plastic, wood ...)
- $scope.components = Component.query()
+ $scope.components = componentsPromise
## Licences list (Creative Common ...)
- $scope.licences = Licence.query()
+ $scope.licences = licencesPromise
## Themes list (cooking, sport ...)
- $scope.themes = Theme.query()
-
-
+ $scope.themes = themesPromise
##
# Saves a new component / Update an existing material to the server (form validation callback)
@@ -153,5 +152,3 @@ Application.Controllers.controller "projectElementsController", ["$scope", "$sta
else
$scope.licences.splice(index, 1)
]
-
-
diff --git a/app/assets/javascripts/controllers/admin/settings.coffee b/app/assets/javascripts/controllers/admin/settings.coffee
new file mode 100644
index 000000000..ff84c7170
--- /dev/null
+++ b/app/assets/javascripts/controllers/admin/settings.coffee
@@ -0,0 +1,196 @@
+'use strict'
+
+Application.Controllers.controller "SettingsController", ["$scope", 'Setting', 'growl', 'settingsPromise', 'cgvFile', 'cguFile', 'logoFile', 'logoBlackFile', 'faviconFile', 'CSRF', '_t'
+ ($scope, Setting, growl, settingsPromise, cgvFile, cguFile, logoFile, logoBlackFile, faviconFile, CSRF, _t) ->
+
+
+
+ ### PUBLIC SCOPE ###
+
+ ## timepickers steps configuration
+ $scope.timepicker =
+ hstep: 1
+ mstep: 15
+
+ ## API URL where the upload forms will be posted
+ $scope.actionUrl =
+ cgu: "/api/custom_assets"
+ cgv: "/api/custom_assets"
+ logo: "/api/custom_assets"
+ logoBlack: "/api/custom_assets"
+ favicon: "/api/custom_assets"
+
+ ## Form actions on the above URL
+ $scope.methods =
+ cgu: "post"
+ cgv: "post"
+ logo: "post"
+ logoBlack: "post"
+ favicon: "post"
+
+ ## Are we uploading the files currently (if so, display the loader)
+ $scope.loader =
+ cgu: false
+ cgv: false
+
+ ## various parametrable settings
+ $scope.twitterSetting = { name: 'twitter_name', value: settingsPromise.twitter_name }
+ $scope.aboutTitleSetting = { name: 'about_title', value: settingsPromise.about_title }
+ $scope.aboutBodySetting = { name: 'about_body', value: settingsPromise.about_body }
+ $scope.aboutContactsSetting = { name: 'about_contacts', value: settingsPromise.about_contacts }
+ $scope.homeBlogpostSetting = { name: 'home_blogpost', value: settingsPromise.home_blogpost }
+ $scope.machineExplicationsAlert = { name: 'machine_explications_alert', value: settingsPromise.machine_explications_alert }
+ $scope.trainingExplicationsAlert = { name: 'training_explications_alert', value: settingsPromise.training_explications_alert }
+ $scope.trainingInformationMessage = { name: 'training_information_message', value: settingsPromise.training_information_message}
+ $scope.subscriptionExplicationsAlert = { name: 'subscription_explications_alert', value: settingsPromise.subscription_explications_alert }
+ $scope.eventReducedAmountAlert = { name: 'event_reduced_amount_alert', value: settingsPromise.event_reduced_amount_alert }
+ $scope.windowStart = { name: 'booking_window_start', value: settingsPromise.booking_window_start }
+ $scope.windowEnd = { name: 'booking_window_end', value: settingsPromise.booking_window_end }
+ $scope.mainColorSetting = { name: 'main_color', value: settingsPromise.main_color }
+ $scope.secondColorSetting = { name: 'secondary_color', value: settingsPromise.secondary_color }
+ $scope.fablabName = { name: 'fablab_name', value: settingsPromise.fablab_name }
+ $scope.nameGenre = { name: 'name_genre', value: settingsPromise.name_genre }
+ $scope.cguFile = cguFile.custom_asset
+ $scope.cgvFile = cgvFile.custom_asset
+ $scope.customLogo = logoFile.custom_asset
+ $scope.customLogoBlack = logoBlackFile.custom_asset
+ $scope.customFavicon = faviconFile.custom_asset
+
+ $scope.enableMove =
+ name: 'booking_move_enable'
+ value: (settingsPromise.booking_move_enable == "true")
+
+ $scope.moveDelay =
+ name: 'booking_move_delay'
+ value: parseInt(settingsPromise.booking_move_delay)
+
+ $scope.enableCancel =
+ name: 'booking_cancel_enable'
+ value: (settingsPromise.booking_cancel_enable == "true")
+
+ $scope.cancelDelay =
+ name: 'booking_cancel_delay'
+ value: parseInt(settingsPromise.booking_cancel_delay)
+
+
+
+ ##
+ # 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'
+
+
+
+ ##
+ # Callback to save the setting value to the database
+ # @param setting {{value:*, name:string}} note that the value will be stringified
+ ##
+ $scope.save = (setting)->
+ # trim empty html
+ if setting.value == " " or setting.value == "
"
+ setting.value = ""
+ # convert dates to ISO format
+ if setting.value instanceof Date
+ setting.value = setting.value.toISOString()
+
+ if setting.value isnt null
+ value = setting.value.toString()
+ else
+ value = setting.value
+
+ Setting.update { name: setting.name }, { value: value }, (data)->
+ growl.success(_t('customization_of_SETTING_successfully_saved', {SETTING:setting.name}))
+ , (error)->
+ console.log(error)
+
+
+
+ ##
+ # For use with ngUpload (https://github.com/twilson63/ngUpload).
+ # Intended to be the callback when the upload is done: Any raised error will be displayed in a growl
+ # message. If everything goes fine, a growl success message is shown.
+ # @param content {Object} JSON - The upload's result
+ ##
+ $scope.submited = (content) ->
+ if !content.custom_asset?
+ $scope.alerts = []
+ angular.forEach content, (v, k)->
+ angular.forEach v, (err)->
+ growl.error(err)
+ else
+ growl.success(_t('file_successfully_updated'))
+ if content.custom_asset.name is 'cgu-file'
+ $scope.cguFile = content.custom_asset
+ $scope.methods.cgu = 'put'
+ $scope.actionUrl.cgu += '/cgu-file' unless $scope.actionUrl.cgu.indexOf('/cgu-file') > 0
+ $scope.loader.cgu = false
+ else if content.custom_asset.name is 'cgv-file'
+ $scope.cgvFile = content.custom_asset
+ $scope.methods.cgv = 'put'
+ $scope.actionUrl.cgv += '/cgv-file' unless $scope.actionUrl.cgv.indexOf('/cgv-file') > 0
+ $scope.loader.cgv = false
+ else if content.custom_asset.name is 'logo-file'
+ $scope.logoFile = content.custom_asset
+ $scope.methods.logo = 'put'
+ $scope.actionUrl.logo += '/logo-file' unless $scope.actionUrl.logo.indexOf('/logo-file') > 0
+ else if content.custom_asset.name is 'logo-black-file'
+ $scope.logoBlackFile = content.custom_asset
+ $scope.methods.logoBlack = 'put'
+ $scope.actionUrl.logoBlack += '/logo-black-file' unless $scope.actionUrl.logoBlack.indexOf('/logo-black-file') > 0
+ else if content.custom_asset.name is 'favicon-file'
+ $scope.faviconFile = content.custom_asset
+ $scope.methods.favicon = 'put'
+ $scope.actionUrl.favicon += '/favicon-file' unless $scope.actionUrl.favicon.indexOf('/favicon-file') > 0
+
+
+
+ ##
+ # @param target {String} 'cgu' | 'cgv'
+ ##
+ $scope.addLoader = (target) ->
+ $scope.loader[target] = true
+
+
+
+ ### PRIVATE SCOPE ###
+
+ ##
+ # Kind of constructor: these actions will be realized first when the controller is loaded
+ ##
+ initialize = ->
+ # set the authenticity tokens in the forms
+ CSRF.setMetaTags()
+
+ # we prevent the admin from setting the closing time before the opening time
+ $scope.$watch 'windowEnd.value', (newValue, oldValue, scope) ->
+ if $scope.windowStart and moment($scope.windowStart.value).isAfter(newValue)
+ $scope.windowEnd.value = oldValue
+
+ # change form methods to PUT if items already exists
+ if cguFile.custom_asset
+ $scope.methods.cgu = 'put'
+ $scope.actionUrl.cgu += '/cgu-file'
+ if cgvFile.custom_asset
+ $scope.methods.cgv = 'put'
+ $scope.actionUrl.cgv += '/cgv-file'
+ if logoFile.custom_asset
+ $scope.methods.logo = 'put'
+ $scope.actionUrl.logo += '/logo-file'
+ if logoBlackFile.custom_asset
+ $scope.methods.logoBlack = 'put'
+ $scope.actionUrl.logoBlack += '/logo-black-file'
+ if faviconFile.custom_asset
+ $scope.methods.favicon = 'put'
+ $scope.actionUrl.favicon += '/favicon-file'
+
+
+ # init the controller (call at the end !)
+ initialize()
+
+]
diff --git a/app/assets/javascripts/controllers/admin/statistics.coffee b/app/assets/javascripts/controllers/admin/statistics.coffee
new file mode 100644
index 000000000..db22fc55e
--- /dev/null
+++ b/app/assets/javascripts/controllers/admin/statistics.coffee
@@ -0,0 +1,453 @@
+'use strict'
+
+Application.Controllers.controller "StatisticsController", ["$scope", "$state", "$rootScope", "$locale", "Statistics", "es", "Member", '_t'
+, ($scope, $state, $rootScope, $locale, Statistics, es, Member, _t) ->
+
+
+
+ ### PUBLIC SCOPE ###
+
+ ## ui-view transitions optimization: if true, the stats will never be refreshed
+ $scope.preventRefresh = false
+
+ ## statistics structure in elasticSearch
+ $scope.statistics = []
+
+ ## fablab users list
+ $scope.members = []
+
+ ## statistics data recovered from elasticSearch
+ $scope.data = null
+
+ ## configuration of the widget allowing to pick the ages range
+ $scope.agePicker =
+ show: false
+ start: null
+ end: null
+
+ ## total CA for the current view
+ $scope.sumCA = 0
+
+ ## average users' age for the current view
+ $scope.averageAge = 0
+
+ ## total of the stat field for non simple types
+ $scope.sumStat = 0
+
+ ## default: results are not sorted
+ $scope.sorting =
+ ca: 'none'
+
+ ## active tab will be set here
+ $scope.selectedIndex = null
+
+ ## type filter binding
+ $scope.type =
+ selected: null
+ active: null
+
+ ## selected custom filter
+ $scope.customFilter =
+ show: false
+ criterion: {}
+ value : null
+ exclude: false
+ datePicker:
+ format: $locale.DATETIME_FORMATS.shortDate
+ opened: false # default: datePicker is not shown
+ minDate: null
+ maxDate: moment().toDate()
+ options:
+ startingDay: 1 # France: the week starts on monday
+
+ ## available custom filters
+ $scope.filters = []
+
+ ## default: we do not open the datepicker menu
+ $scope.datePicker =
+ show: false
+
+ ## datePicker parameters for interval beginning
+ $scope.datePickerStart =
+ format: $locale.DATETIME_FORMATS.shortDate
+ opened: false # default: datePicker is not shown
+ minDate: null
+ maxDate: moment().subtract(1, 'day').toDate()
+ selected: moment().utc().subtract(1, 'months').subtract(1, 'day').startOf('day').toDate()
+ options:
+ startingDay: Fablab.weekStartingDay
+
+ ## datePicker parameters for interval ending
+ $scope.datePickerEnd =
+ format: $locale.DATETIME_FORMATS.shortDate
+ opened: false # default: datePicker is not shown
+ minDate: null
+ maxDate: moment().subtract(1, 'day').toDate()
+ selected: moment().subtract(1, 'day').endOf('day').toDate()
+ options:
+ startingDay: Fablab.weekStartingDay
+
+
+
+ ##
+ # Callback to open the datepicker (interval start)
+ # @param $event {Object} jQuery event object
+ ##
+ $scope.toggleStartDatePicker = ($event) ->
+ toggleDatePicker($event, $scope.datePickerStart)
+
+
+
+ ##
+ # Callback to open the datepicker (interval end)
+ # @param $event {Object} jQuery event object
+ ##
+ $scope.toggleEndDatePicker = ($event) ->
+ toggleDatePicker($event, $scope.datePickerEnd)
+
+
+
+ ##
+ # Callback to open the datepicker (custom filter)
+ # @param $event {Object} jQuery event object
+ ##
+ $scope.toggleCustomDatePicker = ($event) ->
+ toggleDatePicker($event, $scope.customFilter.datePicker)
+
+
+
+ ##
+ # Callback called when the active tab is changed.
+ # recover the current tab and store its value in $scope.selectedIndex
+ # @param tab {Object} elasticsearch statistic structure
+ ##
+ $scope.setActiveTab = (tab) ->
+ $scope.selectedIndex = tab
+ $scope.type.selected = tab.types[0]
+ $scope.type.active = $scope.type.selected
+ $scope.customFilter.criterion = {}
+ $scope.customFilter.value = null
+ $scope.customFilter.exclude = false
+ $scope.sorting.ca = 'none'
+ buildCustomFiltersList()
+ refreshStats()
+
+
+
+ ##
+ # Callback to validate the filters and send a new request to elastic
+ ##
+ $scope.validateFilterChange = ->
+ $scope.agePicker.show = false
+ $scope.customFilter.show = false
+ $scope.type.active = $scope.type.selected
+ buildCustomFiltersList()
+ refreshStats()
+
+
+
+ ##
+ # Callback to validate the dates range and refresh the data from elastic
+ ##
+ $scope.validateDateChange = ->
+ $scope.datePicker.show = false
+ refreshStats()
+
+
+
+ ##
+ # Parse the given date and return a user-friendly string
+ # @param date {Date} JS date or ant moment.js compatible date string
+ ##
+ $scope.formatDate = (date) ->
+ moment(date).format("LL")
+
+
+
+ ##
+ # Parse the sex and return a user-friendly string
+ # @param sex {string} 'male' | 'female'
+ ##
+ $scope.formatSex = (sex) ->
+ if sex == 'male'
+ return _t('man')
+ if sex == 'female'
+ return t('woman')
+
+
+
+ ##
+ # Retrieve the label for the given subtype in the current type
+ # @param key {string} statistic subtype key
+ ##
+ $scope.formatSubtype = (key) ->
+ label = ""
+ angular.forEach $scope.type.active.subtypes, (subtype) ->
+ if subtype.key == key
+ label = subtype.label
+ label
+
+
+
+ ##
+ # Helper usable in ng-switch to determine the input type to display for custom filter value
+ # @param filter {Object} custom filter criterion
+ ##
+ $scope.getCustomValueInputType = (filter) ->
+ if filter and filter.values
+ if typeof(filter.values[0]) == 'string'
+ return filter.values[0]
+ else if typeof(filter.values[0] == 'object')
+ return 'input_select'
+ else
+ 'input_text'
+
+
+
+ ##
+ # Change the sorting order and refresh the results to match the new order
+ # @param filter {Object} any filter
+ ##
+ $scope.toggleSorting = (filter) ->
+ switch $scope.sorting[filter]
+ when 'none' then $scope.sorting[filter] = 'asc'
+ when 'asc' then $scope.sorting[filter] = 'desc'
+ when 'desc' then $scope.sorting[filter] = 'none'
+ refreshStats()
+
+
+
+ ##
+ # Return the user's name from his given ID
+ # @param id {number} user ID
+ ##
+ $scope.getUserNameFromId = (id) ->
+ if $scope.members.length == 0
+ return "ID "+id
+ else
+ for member in $scope.members
+ if member.id == id
+ return member.name
+ return "ID "+id
+
+
+
+ ### PRIVATE SCOPE ###
+
+ ##
+ # Kind of constructor: these actions will be realized first when the controller is loaded
+ ##
+ initialize = ->
+ Statistics.query (stats) ->
+ $scope.statistics = stats
+
+ Member.query (members) ->
+ $scope.members = members
+
+ # workaround for angular-bootstrap::tabs behavior: on tab deletion, another tab will be selected
+ # which will cause every tabs to reload, one by one, when the view is closed
+ $rootScope.$on '$stateChangeStart', (event, toState, toParams, fromState, fromParams) ->
+ if fromState.name == 'app.admin.statistics' and Object.keys(fromParams).length == 0
+ $scope.preventRefresh = true
+
+
+
+ ##
+ # Generic function to toggle a bootstrap datePicker
+ # @param $event {Object} jQuery event object
+ # @param datePicker {Object} settings object of the concerned datepicker. Must have an 'opened' property
+ ##
+ toggleDatePicker = ($event, datePicker) ->
+ $event.preventDefault()
+ $event.stopPropagation()
+ datePicker.opened = !datePicker.opened
+
+
+
+ ##
+ # Force update the statistics table, querying elasticSearch according to the current config values
+ ##
+ refreshStats = ->
+ if $scope.selectedIndex and !$scope.preventRefresh
+ $scope.data = []
+ $scope.sumCA = 0
+ $scope.averageAge = 0
+ $scope.sumStat = 0
+ custom = null
+ if $scope.customFilter.criterion and $scope.customFilter.criterion.key and $scope.customFilter.value
+ custom = {}
+ custom.key = $scope.customFilter.criterion.key
+ custom.value = $scope.customFilter.value
+ custom.exclude = $scope.customFilter.exclude
+ queryElasticStats $scope.selectedIndex.es_type_key, $scope.type.active.key, custom, (res, err)->
+ if (err)
+ console.error("[statisticsController::refreshStats] Unable to refresh due to "+err)
+ else
+ $scope.data = res.hits
+ sumCA = 0
+ sumAge = 0
+ sumStat = 0
+ if $scope.data.length > 0
+ angular.forEach $scope.data, (datum) ->
+ if datum._source.ca
+ sumCA += parseInt(datum._source.ca)
+ if datum._source.age
+ sumAge += parseInt(datum._source.age)
+ if datum._source.stat
+ sumStat += parseInt(datum._source.stat)
+ sumAge /= $scope.data.length
+ $scope.sumCA = sumCA
+ $scope.averageAge = Math.round(sumAge*100)/100
+ $scope.sumStat = sumStat
+
+
+
+ ##
+ # Run the elasticSearch query to retreive the /stats/type aggregations
+ # @param index {String} elasticSearch document type (account|event|machine|project|subscription|training)
+ # @param type {String} statistics type (month|year|booking|hour|user|project)
+ # @param custom {{key:{string}, value:{string}}|null} custom filter property or null to disable this filter
+ # @param callback {function} function be to run after results were retrieved, it will receive
+ # two parameters : results {Array}, error {String} (if any)
+ ##
+ queryElasticStats = (index, type, custom, callback) ->
+ # handle invalid callback
+ if typeof(callback) != "function"
+ console.error('[statisticsController::queryElasticStats] Error: invalid callback provided')
+ return
+
+ # run query
+ es.search
+ "index": "stats"
+ "type": index
+ "size": 1000000000
+ "body": buildElasticDataQuery(type, custom, $scope.agePicker.start, $scope.agePicker.end, moment($scope.datePickerStart.selected), moment($scope.datePickerEnd.selected), $scope.sorting)
+ , (error, response) ->
+ if (error)
+ callback([], "Error: something unexpected occurred during elasticSearch query: "+error)
+ else
+ callback(response.hits)
+
+
+
+ ##
+ # Build an object representing the content of the REST-JSON query to elasticSearch,
+ # based on the provided parameters for row data recovering.
+ # @param type {String} statistics type (month|year|booking|hour|user|project)
+ # @param custom {{key:{string}, value:{string}}|null} custom filter property or null to disable this filter
+ # @param ageMin {Number|null} filter by age: range lower value OR null to do not filter
+ # @param ageMax {Number|null} filter by age: range higher value OR null to do not filter
+ # @param intervalBegin {moment} statitics interval beginning (moment.js type)
+ # @param intervalEnd {moment} statitics interval ending (moment.js type)
+ # @param sortings {Array|null} elasticSearch criteria for sorting the results
+ ##
+ buildElasticDataQuery = (type, custom, ageMin, ageMax, intervalBegin, intervalEnd, sortings) ->
+ q =
+ "query":
+ "bool":
+ "must": [
+ {
+ "term":
+ "type": type
+ }
+ {
+ "range":
+ "date":
+ "gte": intervalBegin.format()
+ "lte": intervalEnd.format()
+ }
+ ]
+ # optional date range
+ if ageMin && ageMax
+ q.query.bool.must.push
+ "range":
+ "age":
+ "gte": ageMin
+ "lte": ageMax
+ # optional criterion
+ if custom
+ criterion = {
+ "match" : {}
+ }
+ switch $scope.getCustomValueInputType($scope.customFilter.criterion)
+ when 'input_date' then criterion.match[custom.key] = moment(custom.value).format('YYYY-MM-DD')
+ when 'input_select' then criterion.match[custom.key] = custom.value.key
+ when 'input_list' then criterion.match[custom.key+".name"] = custom.value
+ else criterion.match[custom.key] = custom.value
+
+ if (custom.exclude)
+ q = "query": {
+ "filtered": {
+ "query": q.query,
+ "filter": {
+ "not": {
+ "term": criterion.match
+ }
+ }
+ }
+ }
+ else
+ q.query.bool.must.push(criterion)
+
+
+ if sortings
+ q["sort"] = buildElasticSortCriteria(sortings)
+ q
+
+
+
+ ##
+ # Parse the provided criteria array and return the corresponding elasticSearch syntax
+ # @param criteria {Array} array of {key_to_sort:order}
+ ##
+ buildElasticSortCriteria = (criteria) ->
+ crits = []
+ angular.forEach criteria, (value, key) ->
+ if typeof value != 'undefined' and value != null and value != 'none'
+ c = {}
+ c[key] = {'order': value}
+ crits.push(c)
+ crits
+
+
+
+ ##
+ # Fullfil the list of available options in the custom filter panel. The list will be based on common
+ # properties and on index-specific properties (additional_fields)
+ ##
+ buildCustomFiltersList = ->
+ $scope.filters = []
+
+ filters = [
+ {key: 'date', label: _t('date'), values: ['input_date']},
+ {key: 'userId', label: _t('user_id'), values: ['input_number']},
+ {key: 'gender', label: _t('gender'), values: [{key:'male', label:_t('man')}, {key:'female', label:_t('woman')}]},
+ {key: 'age', label: _t('age'), values: ['input_number']},
+ {key: 'subType', label: _t('type'), values: $scope.type.active.subtypes},
+ {key: 'ca', label: _t('revenue'), values: ['input_number']}
+ ]
+
+ $scope.filters = filters
+
+ if !$scope.type.active.simple
+ f = {key: 'stat', label: $scope.type.active.label, values: ['input_number']}
+ $scope.filters.push(f)
+
+ angular.forEach $scope.selectedIndex.additional_fields, (field) ->
+ filter = {key: field.key, label: field.label, values:[]}
+ switch field.data_type
+ when 'index' then filter.values.push('input_number')
+ when 'number' then filter.values.push('input_number')
+ when 'date' then filter.values.push('input_date')
+ when 'list' then filter.values.push('input_list')
+ else filter.values.push('input_text')
+
+ $scope.filters.push(filter)
+
+
+
+
+ # init the controller (call at the end !)
+ initialize()
+
+]
diff --git a/app/assets/javascripts/controllers/admin/tags.coffee.erb b/app/assets/javascripts/controllers/admin/tags.coffee.erb
new file mode 100644
index 000000000..5402a7871
--- /dev/null
+++ b/app/assets/javascripts/controllers/admin/tags.coffee.erb
@@ -0,0 +1,65 @@
+Application.Controllers.controller "TagsController", ["$scope", 'tagsPromise', 'Tag', 'growl', '_t', ($scope, tagsPromise, Tag, growl, _t) ->
+
+ ## List of users's tags
+ $scope.tags = tagsPromise
+
+
+
+ ##
+ # Removes the newly inserted but not saved tag / Cancel the current tag modification
+ # @param rowform {Object} see http://vitalets.github.io/angular-xeditable/
+ # @param index {number} tag index in the $scope.tags array
+ ##
+ $scope.cancelTag = (rowform, index) ->
+ if $scope.tags[index].id?
+ rowform.$cancel()
+ else
+ $scope.tags.splice(index, 1)
+
+
+
+ ##
+ # Creates a new empty entry in the $scope.tags array
+ ##
+ $scope.addTag = ->
+ $scope.inserted =
+ name: ''
+ $scope.tags.push($scope.inserted)
+
+
+
+ ##
+ # Saves a new tag / Update an existing tag to the server (form validation callback)
+ # @param data {Object} tag name
+ # @param [data] {number} tag id, in case of update
+ ##
+ $scope.saveTag = (data, id) ->
+ if id?
+ Tag.update {id: id}, { tag: data }, (response) ->
+ growl.success(_t('changes_successfully_saved'))
+ , (error) ->
+ growl.error(_t('an_error_occurred_while_saving_changes'))
+ else
+ Tag.save { tag: data }, (resp)->
+ growl.success(_t('new_tag_successfully_saved'))
+ $scope.tags[$scope.tags.length-1].id = resp.id
+ , (error) ->
+ growl.error(_t('an_error_occurred_while_saving_the_new_tag'))
+ $scope.tags.splice($scope.tags.length-1, 1)
+
+
+
+ ##
+ # Deletes the tag at the specified index
+ # @param index {number} tag index in the $scope.tags array
+ ##
+ $scope.removeTag = (index) ->
+ # TODO add confirmation : les utilisateurs seront déasociés
+ Tag.delete { id: $scope.tags[index].id }, (resp) ->
+ growl.success(_t('tag_successfully_deleted'))
+ $scope.tags.splice(index, 1)
+ , (error) ->
+ growl.error(_t('an_error_occurred_and_the_tag_deletion_failed'))
+
+
+]
diff --git a/app/assets/javascripts/controllers/admin/trainings.coffee.erb b/app/assets/javascripts/controllers/admin/trainings.coffee.erb
new file mode 100644
index 000000000..44051492f
--- /dev/null
+++ b/app/assets/javascripts/controllers/admin/trainings.coffee.erb
@@ -0,0 +1,230 @@
+'use strict'
+
+Application.Controllers.controller "TrainingsController", ["$scope", "$state", "$uibModal", 'Training', 'trainingsPromise', 'machinesPromise', '_t', 'growl'
+, ($scope, $state, $uibModal, Training, trainingsPromise, machinesPromise, _t, growl) ->
+
+
+
+ ### PUBLIC SCOPE ###
+
+ ## list of trainings
+ $scope.trainings = trainingsPromise
+
+ ## simplified list of machines
+ $scope.machines = machinesPromise
+
+ ## list of training availabilies, grouped by date
+ $scope.groupedAvailabilities = {}
+
+ ## default: accordions are not open
+ $scope.accordions = {}
+
+ ## Binding for the parseInt function
+ $scope.parseInt = parseInt
+
+ ##
+ # In the trainings listing tab, return the stringified list of machines associated with the provided training
+ # @param training {Object} Training object, inherited from $resource
+ # @returns {string}
+ ##
+ $scope.showMachines = (training) ->
+ selected = []
+ angular.forEach $scope.machines, (m) ->
+ if (training.machine_ids.indexOf(m.id) >= 0)
+ selected.push(m.name)
+ return if selected.length then selected.join(', ') else _t('none')
+
+
+
+ ##
+ # Create a new empty training object and append it to the $scope.trainings list
+ ##
+ $scope.addTraining = ->
+ $scope.inserted =
+ name: ''
+ machine_ids: []
+ $scope.trainings.push($scope.inserted)
+
+
+
+ ##
+ # Saves a new training / Update an existing training to the server (form validation callback)
+ # @param data {Object} training name, associated machine(s) and default places number
+ # @param id {number} training id, in case of update
+ ##
+ $scope.saveTraining = (data, id) ->
+ if id?
+ Training.update {id: id},
+ training: data
+ else
+ Training.save
+ training: data
+ , (resp) ->
+ $scope.trainings[$scope.trainings.length-1] = resp
+ console.log(resp)
+
+
+
+ ##
+ # Removes the newly inserted but not saved training / Cancel the current training modification
+ # @param rowform {Object} see http://vitalets.github.io/angular-xeditable/
+ # @param index {number} training index in the $scope.trainings array
+ ##
+ $scope.cancelTraining = (rowform, index) ->
+ if $scope.trainings[index].id?
+ rowform.$cancel()
+ else
+ $scope.trainings.splice(index, 1)
+
+
+
+ ##
+ # In the trainings monitoring tab, callback to open a modal window displaying the current bookings for the
+ # provided training slot. The admin will be then able to validate the training for the users that followed
+ # the training.
+ # @param training {Object} Training object, inherited from $resource
+ # @param availability {Object} time slot when the training occurs
+ ##
+ $scope.showReservations = (training, availability) ->
+ $uibModal.open
+ templateUrl: '<%= asset_path "admin/trainings/validTrainingModal.html" %>'
+ controller: ['$scope', '$uibModalInstance', ($scope, $uibModalInstance) ->
+ $scope.availability = availability
+
+ $scope.usersToValid = []
+
+ ##
+ # Mark/unmark the provided user for training validation
+ # @param user {Object} from the availability.reservation_users list
+ ##
+ $scope.toggleSelection = (user) ->
+ index = $scope.usersToValid.indexOf(user)
+ if index > -1
+ $scope.usersToValid.splice(index, 1)
+ else
+ $scope.usersToValid.push user
+
+ ##
+ # Validates the modifications (training validations) and save them to the server
+ ##
+ $scope.ok = ->
+ users = $scope.usersToValid.map (u) ->
+ u.id
+ Training.update {id: training.id},
+ training:
+ users: users
+ , -> # success
+ angular.forEach $scope.usersToValid, (u) ->
+ u.is_valid = true
+ $scope.usersToValid = []
+ $uibModalInstance.close(training)
+
+ ##
+ # Cancel the modifications and close the modal window
+ ##
+ $scope.cancel = ->
+ $uibModalInstance.dismiss('cancel')
+ ]
+
+
+
+ ##
+ # Delete the provided training and, in case of sucess, remove it from the trainings list afterwards
+ # @param index {number} index of the provided training in $scope.trainings
+ # @param training {Object} training to delete
+ ##
+ $scope.removeTraining = (index, training)->
+ training.$delete ->
+ $scope.trainings.splice(index, 1)
+ growl.info(_t('training_successfully_deleted'))
+ , (error)->
+ growl.warning(_t('unable_to_delete_the_training_because_some_users_alredy_booked_it'))
+
+
+
+ ##
+ # Open the modal to edit description of the training
+ # @param training {Object} Training to edit description
+ ##
+ $scope.openModalToSetDescription = (training)->
+ $uibModal.open(
+ templateUrl: "<%= asset_path 'admin/trainings/modal_edit.html' %>"
+ controller: ['$scope', '$uibModalInstance', 'Training', 'growl', ($scope, $uibModalInstance, Training, growl)->
+ $scope.training = training
+ $scope.save = ->
+ Training.update id: training.id, { training: { description: $scope.training.description } }, (training)->
+ $uibModalInstance.close()
+ growl.success(_t('description_was_successfully_saved'))
+ return
+ ]
+ )
+
+
+
+ ##
+ # Takes a month number and return its localized literal name
+ # @param {Number} from 0 to 11
+ # @returns {String} eg. 'janvier'
+ ##
+ $scope.formatMonth = (number) ->
+ number = parseInt(number)
+ moment().month(number).format('MMMM')
+
+
+
+ ##
+ # Given a day, month and year, return a localized literal name for the day
+ # @param day {Number} from 1 to 31
+ # @param month {Number} from 0 to 11
+ # @param year {Number} Gregorian's year number
+ # @returns {String} eg. 'mercredi 12'
+ ##
+ $scope.formatDay = (day, month, year) ->
+ day = parseInt(day)
+ month = parseInt(month)
+ year = parseInt(year)
+
+ moment({year: year, month:month, day:day}).format('dddd D')
+
+
+
+ ### PRIVATE SCOPE ###
+
+ ##
+ # Kind of constructor: these actions will be realized first when the controller is loaded
+ ##
+ initialize = ->
+ $scope.groupedAvailabilities = groupAvailabilities($scope.trainings)
+
+
+
+ ##
+ # Group the trainings availabilites by trainings and by dates and return the resulting tree
+ # @param trainings {Array} $scope.trainings is expected here
+ # @returns {Object} Tree constructed as /training_name/year/month/day/[availabilities]
+ ##
+ groupAvailabilities = (trainings) ->
+ tree = {}
+ for training in trainings
+ tree[training.name] = {}
+ tree[training.name].training = training
+ for availability in training.availabilities
+ start = moment(availability.start_at)
+
+ # init the tree structure
+ if typeof tree[training.name][start.year()] == 'undefined'
+ tree[training.name][start.year()] = {}
+ if typeof tree[training.name][start.year()][start.month()] == 'undefined'
+ tree[training.name][start.year()][start.month()] = {}
+ if typeof tree[training.name][start.year()][start.month()][start.date()] == 'undefined'
+ tree[training.name][start.year()][start.month()][start.date()] = []
+
+ # add the availability at its right place
+ tree[training.name][start.year()][start.month()][start.date()].push( availability )
+ tree
+
+
+
+ # init the controller (call at the end !)
+ initialize()
+]
diff --git a/app/assets/javascripts/controllers/application.coffee.erb b/app/assets/javascripts/controllers/application.coffee.erb
index 49cefa6db..604310e0c 100644
--- a/app/assets/javascripts/controllers/application.coffee.erb
+++ b/app/assets/javascripts/controllers/application.coffee.erb
@@ -1,6 +1,7 @@
'use strict'
-Application.Controllers.controller 'ApplicationController', ["$rootScope", "$scope", "Session", "AuthService", "Auth", "$modal", "$state", 'growl', 'Notification', '$interval', ($rootScope, $scope, Session, AuthService, Auth, $modal, $state, growl, Notification, $interval) ->
+Application.Controllers.controller 'ApplicationController', ["$rootScope", "$scope", "$window", "Session", "AuthService", "Auth", "$uibModal", "$state", 'growl', 'Notification', '$interval', "Setting", '$locale', '_t'
+, ($rootScope, $scope, $window, Session, AuthService, Auth, $uibModal, $state, growl, Notification, $interval, Setting, $locale, _t) ->
@@ -18,14 +19,14 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
# @param user {Object} Rails/Devise user
##
$scope.setCurrentUser = (user) ->
- $scope.currentUser = user
+ $rootScope.currentUser = user
Session.create(user);
getNotifications()
##
# Login callback
- # @param e {Object} jQuery event
+ # @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
# @param callback {function}
##
$scope.login = (e, callback) ->
@@ -36,14 +37,14 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
##
# Logout callback
- # @param e {Object} jQuery event
+ # @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
##
$scope.logout = (e) ->
e.preventDefault()
Auth.logout().then (oldUser) ->
# console.log(oldUser.name + " you're signed out now.");
Session.destroy()
- $scope.currentUser = null
+ $rootScope.currentUser = null
$rootScope.toCheckNotifications = false
$scope.notifications = []
$state.go('app.public.home')
@@ -54,21 +55,21 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
##
# Open the modal window allowing the user to create an account.
- # @param e {Object} jQuery event
+ # @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
##
$scope.signup = (e) ->
e.preventDefault() if e
- $modal.open
+ $uibModal.open
templateUrl: '<%= asset_path "shared/signupModal.html" %>'
size: 'md'
- controller: ['$scope', '$modalInstance', 'Group', ($scope, $modalInstance, Group) ->
+ controller: ['$scope', '$uibModalInstance', 'Group', 'CustomAsset', ($scope, $uibModalInstance, Group, CustomAsset) ->
# default parameters for the date picker in the account creation modal
$scope.datePicker =
- format: 'dd/MM/yyyy'
+ format: $locale.DATETIME_FORMATS.shortDate
opened: false
options:
- startingDay: 1
+ startingDay: Fablab.weekStartingDay
# callback to open the date picker (account creation modal)
$scope.openDatePicker = ($event) ->
@@ -80,6 +81,10 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
Group.query (groups) ->
$scope.groups = groups
+ # retrieve the CGU
+ CustomAsset.get {name: 'cgu-file'}, (cgu) ->
+ $scope.cgu = cgu.custom_asset
+
# default user's parameters
$scope.user =
is_allow_contact: true
@@ -95,7 +100,7 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
$scope.alerts = []
Auth.register($scope.user).then (user) ->
# creation successful
- $modalInstance.close(user)
+ $uibModalInstance.close(user)
, (error) ->
# creation failed...
angular.forEach error.data.errors, (v, k) ->
@@ -114,10 +119,10 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
# @param token {string} security token for password changing. The user should have recieved it by mail
##
$scope.editPassword = (token) ->
- $modal.open
+ $uibModal.open
templateUrl: '<%= asset_path "shared/passwordEditModal.html" %>'
size: 'md'
- controller: ['$scope', '$modalInstance', '$http', ($scope, $modalInstance, $http) ->
+ controller: ['$scope', '$uibModalInstance', '$http', '_t', ($scope, $uibModalInstance, $http, _t) ->
$scope.user =
reset_password_token: token
$scope.alerts = []
@@ -127,7 +132,7 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
$scope.changePassword = ->
$scope.alerts = []
$http.put('/users/password.json', {user: $scope.user}).success (data) ->
- $modalInstance.close()
+ $uibModalInstance.close()
.error (data) ->
angular.forEach data.errors, (v, k) ->
angular.forEach v, (err) ->
@@ -136,20 +141,20 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
type: 'danger'
]
.result['finally'](null).then (user) ->
- growl.addInfoMessage('Votre mot de passe a bien été modifié.')
+ growl.success(_t('your_password_was_successfully_changed'))
Auth.login().then (user) ->
$scope.setCurrentUser(user)
, (error) ->
# Authentication failed...
-
+
##
# Compact/Expend the width of the left navigation bar
- # @param e {Object} jQuery event object
+ # @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
##
$scope.toggleNavSize = (event) ->
if typeof event == 'undefined'
- console.error '[applicationController::toggleNavSize] Missing event parameter'
+ console.error '[ApplicationController::toggleNavSize] Missing event parameter'
return
toggler = $(event.target)
@@ -184,14 +189,16 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
### PRIVATE SCOPE ###
-
##
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
+
# try to retrieve any currently logged user
Auth.login().then (user) ->
$scope.setCurrentUser(user)
+ if user.need_completion
+ $state.transitionTo('app.logged.profileCompletion')
, (error) ->
# Authentication failed...
$rootScope.toCheckNotifications = false
@@ -205,11 +212,17 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
event.preventDefault()
if AuthService.isAuthenticated()
# user is not allowed
- console.log('user is not allowed')
+ console.error('[ApplicationController::initialize] user is not allowed')
else
# user is not logged in
openLoginModal(toState, toParams)
+ Setting.get { name: 'fablab_name' }, (data)->
+ $scope.fablabName = data.setting.value
+ Setting.get { name: 'name_genre' }, (data)->
+ $scope.nameGenre = data.setting.value
+
+
# shorthands
$scope.isAuthenticated = Auth.isAuthenticated;
@@ -223,7 +236,7 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
##
getNotifications = ->
$rootScope.toCheckNotifications = true
- unless $rootScope.checkNotificationsIsInit or !$scope.currentUser
+ unless $rootScope.checkNotificationsIsInit or !$rootScope.currentUser
$scope.notifications = Notification.query {is_read: false}
$scope.$watch 'notifications', (newValue, oldValue) ->
diff = []
@@ -239,7 +252,7 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
angular.forEach diff, (notification, key) ->
- growl.addInfoMessage(notification.message.description)
+ growl.info(notification.message.description)
, true
@@ -257,35 +270,39 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
# Open the modal window allowing the user to log in.
##
openLoginModal = (toState, toParams, callback) ->
- $modal.open
+ <% active_provider = AuthProvider.active %>
+ <% if active_provider.providable_type != DatabaseProvider.name %>
+ $window.location.href = '<%=user_omniauth_authorize_path(AuthProvider.active.strategy_name.to_sym)%>'
+ <% else %>
+ $uibModal.open
templateUrl: '<%= asset_path "shared/deviseModal.html" %>'
size: 'sm'
- controller: ['$scope', '$modalInstance', ($scope, $modalInstance) ->
+ controller: ['$scope', '$uibModalInstance', '_t', ($scope, $uibModalInstance, _t) ->
user = $scope.user = {}
$scope.login = () ->
Auth.login(user).then (user) ->
# Authentification succeeded ...
- $modalInstance.close(user)
+ $uibModalInstance.close(user)
if callback and typeof callback is "function"
callback(user)
, (error) ->
# Authentication failed...
$scope.alerts = []
$scope.alerts.push
- msg: 'E-mail ou mot de passe incorrect.'
+ msg: _t('wrong_email_or_password')
type: 'danger'
# handle modal behaviors. The provided reason will be used to define the following actions
$scope.dismiss = ->
- $modalInstance.dismiss('cancel')
+ $uibModalInstance.dismiss('cancel')
$scope.openSignup = (e) ->
e.preventDefault()
- $modalInstance.dismiss('signup')
+ $uibModalInstance.dismiss('signup')
$scope.openResetPassword = (e) ->
e.preventDefault()
- $modalInstance.dismiss('resetPassword')
+ $uibModalInstance.dismiss('resetPassword')
]
# what to do when the modal is closed
@@ -303,25 +320,26 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
$scope.signup()
else if reason is 'resetPassword'
# open the 'reset password' modal
- $modal.open
+ $uibModal.open
templateUrl: '<%= asset_path "shared/passwordNewModal.html" %>'
size: 'sm'
- controller: ['$scope', '$modalInstance', '$http', ($scope, $modalInstance, $http) ->
+ controller: ['$scope', '$uibModalInstance', '$http', ($scope, $uibModalInstance, $http) ->
$scope.user = {email: ''}
$scope.sendReset = () ->
$scope.alerts = []
$http.post('/users/password.json', {user: $scope.user}).success ->
- $modalInstance.close()
+ $uibModalInstance.close()
.error ->
$scope.alerts.push
- msg: "Votre adresse email n'existe pas."
+ msg: _t('your_email_address_is_unknown')
type: 'danger'
]
.result['finally'](null).then ->
- growl.addInfoMessage('Vous allez recevoir sous quelques minutes un e-mail vous indiquant comment réinitialiser votre mot de passe.')
+ growl.info(_t('you_will_receive_in_a_moment_an_email_with_instructions_to_reset_your_password'))
# otherwise the user just closed the modal
+ <% end %>
diff --git a/app/assets/javascripts/controllers/dashboard.coffee b/app/assets/javascripts/controllers/dashboard.coffee
index e01bde51e..2e878fa46 100644
--- a/app/assets/javascripts/controllers/dashboard.coffee
+++ b/app/assets/javascripts/controllers/dashboard.coffee
@@ -1,43 +1,7 @@
'use strict'
-##
-# Controller used on the private projects listing page (my dashboard/projects)
-##
-Application.Controllers.controller "dashboardProjectsController", ["$scope", 'Member', ($scope, Member) ->
+Application.Controllers.controller "DashboardController", ["$scope", 'memberPromise', ($scope, memberPromise) ->
-## Current user's profile
- $scope.user = Member.get {id: $scope.currentUser.id}
-]
-
-
-
-##
-# Controller used on the personal trainings page (my dashboard/trainings)
-##
-Application.Controllers.controller "dashboardTrainingsController", ["$scope", 'Member', ($scope, Member) ->
-
-## Current user's profile
- $scope.user = Member.get {id: $scope.currentUser.id}
-]
-
-
-
-##
-# Controller used on the private events page (my dashboard/events)
-##
-Application.Controllers.controller "dashboardEventsController", ["$scope", 'Member', ($scope, Member) ->
-
-## Current user's profile
- $scope.user = Member.get {id: $scope.currentUser.id}
-]
-
-
-
-##
-# Controller used on the personal invoices listing page (my dashboard/invoices)
-##
-Application.Controllers.controller "dashboardInvoicesController", ["$scope", 'Member', ($scope, Member) ->
-
-## Current user's profile
- $scope.user = Member.get {id: $scope.currentUser.id}
+ ## Current user's profile
+ $scope.user = memberPromise
]
diff --git a/app/assets/javascripts/controllers/events.coffee.erb b/app/assets/javascripts/controllers/events.coffee.erb
index 21eab3d96..0ef4e2939 100644
--- a/app/assets/javascripts/controllers/events.coffee.erb
+++ b/app/assets/javascripts/controllers/events.coffee.erb
@@ -1,71 +1,72 @@
'use strict'
-Application.Controllers.controller "eventsController", ["$scope", "$state", 'Event', ($scope, $state, Event) ->
+Application.Controllers.controller "EventsController", ["$scope", "$state", 'Event', ($scope, $state, Event) ->
- ### PRIVATE STATIC CONSTANTS ###
+ ### PRIVATE STATIC CONSTANTS ###
- # Number of events added to the page when the user clicks on 'load next events'
- EVENTS_PER_PAGE = 12
+ # Number of events added to the page when the user clicks on 'load next events'
+ EVENTS_PER_PAGE = 12
- ### PUBLIC SCOPE ###
+ ### PUBLIC SCOPE ###
- ## The events displayed on the page
- $scope.events = []
+ ## The events displayed on the page
+ $scope.events = []
- ## By default, the pagination mode is activated to limit the page size
- $scope.paginateActive = true
+ ## By default, the pagination mode is activated to limit the page size
+ $scope.paginateActive = true
- ## The currently displayed page number
- $scope.page = 1
+ ## The currently displayed page number
+ $scope.page = 1
- ##
- # Adds EVENTS_PER_PAGE events to the bottom of the page, grouped by month
- ##
- $scope.loadMoreEvents = ->
- Event.query {page: $scope.page}, (data) ->
- $scope.events = $scope.events.concat data
- if data.length > 0
- $scope.paginateActive = false if ($scope.page-2)*EVENTS_PER_PAGE+data.length >= data[0].nb_total_events
+ ##
+ # Adds EVENTS_PER_PAGE events to the bottom of the page, grouped by month
+ ##
+ $scope.loadMoreEvents = ->
+ Event.query {page: $scope.page}, (data) ->
+ $scope.events = $scope.events.concat data
+ if data.length > 0
+ $scope.paginateActive = false if ($scope.page-2)*EVENTS_PER_PAGE+data.length >= data[0].nb_total_events
- $scope.eventsGroupByMonth = _.groupBy($scope.events, (obj) ->
- _.map ['month', 'year'], (key, value) -> obj[key]
- )
- $scope.monthOrder = _.sortBy _.keys($scope.eventsGroupByMonth), (k)->
- monthYearArray = k.split(',')
- date = new Date()
- date.setMonth(monthYearArray[0])
- date.setYear(monthYearArray[1])
- return -date.getTime()
- else
- $scope.paginateActive = false
- $scope.page += 1
+ $scope.eventsGroupByMonth = _.groupBy($scope.events, (obj) ->
+ _.map ['month', 'year'], (key, value) -> obj[key]
+ )
+ $scope.monthOrder = _.sortBy _.keys($scope.eventsGroupByMonth), (k)->
+ monthYearArray = k.split(',')
+ date = new Date()
+ date.setMonth(monthYearArray[0])
+ date.setYear(monthYearArray[1])
+ return -date.getTime()
+ else
+ $scope.paginateActive = false
+ $scope.page += 1
- ##
- # Callback to redirect the user to the specified event page
- # @param event {{id:number}}
- ##
- $scope.showEvent = (event) ->
- $state.go('app.public.events_show', {id: event.id})
+ ##
+ # Callback to redirect the user to the specified event page
+ # @param event {{id:number}}
+ ##
+ $scope.showEvent = (event) ->
+ $state.go('app.public.events_show', {id: event.id})
- ### PRIVATE SCOPE ###
+ ### PRIVATE SCOPE ###
- ##
- # Kind of constructor: these actions will be realized first when the controller is loaded
- ##
- initialize = ->
- $scope.loadMoreEvents()
+ ##
+ # Kind of constructor: these actions will be realized first when the controller is loaded
+ ##
+ initialize = ->
+ $scope.loadMoreEvents()
- ## !!! MUST BE CALLED AT THE END of the controller
- initialize()
+
+ ## !!! MUST BE CALLED AT THE END of the controller
+ initialize()
]
@@ -74,47 +75,440 @@ Application.Controllers.controller "eventsController", ["$scope", "$state", 'Eve
-Application.Controllers.controller "showEventController", ["$scope", "$state", "$stateParams", "Event", '$modal', 'Member', ($scope, $state, $stateParams, Event, $modal, Member) ->
+Application.Controllers.controller "ShowEventController", ["$scope", "$state", "$stateParams", "Event", '$uibModal', 'Member', 'Reservation', 'Price', 'CustomAsset', 'eventPromise', 'reducedAmountAlert', 'growl', '_t'
+($scope, $state, $stateParams, Event, $uibModal, Member, Reservation, Price, CustomAsset, eventPromise, reducedAmountAlert, growl, _t) ->
- ### PUBLIC SCOPE ###
+ ### PUBLIC SCOPE ###
+ $scope.reducedAmountAlert = reducedAmountAlert.setting.value
- ## current event details
- $scope.event = {}
+ ## reservations for the currently shown event
+ $scope.reservations = []
+
+ ## user to deal with
+ $scope.ctrl =
+ member: {}
+
+ ## parameters for a new reservation
+ $scope.reserve =
+ nbPlaces: []
+ nbReducedPlaces: []
+ nbReservePlaces: 0
+ nbReserveReducedPlaces: 0
+ toReserve: false
+ amountTotal : 0
- ##
- # Callback to delete the provided event (admins only)
- # @param event {$resource} angular's Event $resource
- ##
- $scope.deleteEvent = (event) ->
- event.$delete ->
- $state.go('app.public.events_list')
+ # get the details for the current event (event's id is recovered from the current URL)
+ $scope.event = eventPromise
- ### PRIVATE SCOPE ###
-
- ##
- # Kind of constructor: these actions will be realized first when the controller is loaded
- ##
- initialize = ->
-
- # get the details for the current event (event's id is recovered from the current URL)
- Event.get {id: $stateParams.id}
- , (data) ->
- $scope.event = data
- if !$scope.event.reduced_amount
- $scope.event.reduced_amount = 0
- return
- , ->
- $state.go('app.public.events_list')
+ ##
+ # Callback to delete the provided event (admins only)
+ # @param event {$resource} angular's Event $resource
+ ##
+ $scope.deleteEvent = (event) ->
+ event.$delete ->
+ $state.go('app.public.events_list')
- ## !!! MUST BE CALLED AT THE END of the controller
- initialize()
+ ##
+ # Callback to call when the number of places change in the current booking
+ ##
+ $scope.changeNbPlaces = ->
+ reste = $scope.event.nb_free_places - $scope.reserve.nbReservePlaces
+ $scope.reserve.nbReducedPlaces = [0..reste]
+ $scope.computeEventAmount()
+
+
+
+ ##
+ # Callback to call when the number of discounted places change in the current booking
+ ##
+ $scope.changeNbReducedPlaces = ->
+ reste = $scope.event.nb_free_places - $scope.reserve.nbReserveReducedPlaces
+ $scope.reserve.nbPlaces = [0..reste]
+ $scope.computeEventAmount()
+
+
+
+ ##
+ # Callback to reset the current reservation parameters
+ # @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
+ ##
+ $scope.cancelReserve = (e)->
+ e.preventDefault()
+ resetEventReserve()
+
+
+
+ ##
+ # Callback to allow the user to set the details for his reservation
+ ##
+ $scope.reserveEvent = ->
+ if $scope.event.nb_total_places > 0
+ $scope.reserveSuccess = false
+ if !$scope.isAuthenticated()
+ $scope.login null, (user)->
+ $scope.reserve.toReserve = !$scope.reserve.toReserve
+ if user.role isnt 'admin'
+ $scope.ctrl.member = user
+ else
+ Member.query (members) ->
+ $scope.members = members
+ else
+ $scope.reserve.toReserve = !$scope.reserve.toReserve
+
+
+
+ ##
+ # 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 = ->
+ resetEventReserve()
+ $scope.reserveSuccess = false
+ if $scope.ctrl.member
+ getReservations($scope.event.id, 'Event', $scope.ctrl.member.id)
+
+
+
+ ##
+ # Callback to trigger the payment process of the current reservation
+ ##
+ $scope.payEvent = ->
+
+ # first, we check that a user was selected
+ if Object.keys($scope.ctrl.member).length > 0
+ reservation = mkReservation($scope.ctrl.member, $scope.reserve, $scope.event)
+
+ if $scope.currentUser.role isnt 'admin' and $scope.reserve.amountTotal > 0
+ payByStripe(reservation)
+ else
+ if $scope.currentUser.role is 'admin' or $scope.reserve.amountTotal 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'))
+
+
+
+ ##
+ # Callback to validate the booking of a free event
+ ##
+ $scope.validReserveEvent = ->
+ reservation =
+ user_id: $scope.ctrl.member.id
+ reservable_id: $scope.event.id
+ reservable_type: 'Event'
+ slots_attributes: []
+ nb_reserve_places: $scope.reserve.nbReservePlaces
+ nb_reserve_reduced_places: $scope.reserve.nbReserveReducedPlaces
+ reservation.slots_attributes.push
+ start_at: $scope.event.start_date
+ end_at: $scope.event.end_date
+ availability_id: $scope.event.availability.id
+ $scope.attempting = true
+ Reservation.save reservation: reservation, (reservation) ->
+ afterPayment(reservation)
+ $scope.attempting = false
+ , (response)->
+ $scope.alerts = []
+ $scope.alerts.push
+ msg: response.data.card[0]
+ type: 'danger'
+ $scope.attempting = false
+
+
+
+ ##
+ # Callback to alter an already booked reservation date. A modal window will be opened to allow the user to choose
+ # a new date for his reservation (if any available)
+ # @param reservation {{id:number, reservable_id:number, nb_reserve_places:number, nb_reserve_reduced_places:number}}
+ # @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
+ ##
+ $scope.modifyReservation = (reservation, e)->
+ e.preventDefault()
+ e.stopPropagation()
+
+ index = $scope.reservations.indexOf(reservation)
+ $uibModal.open
+ templateUrl: '<%= asset_path "events/modify_event_reservation_modal.html" %>'
+ resolve:
+ event: -> $scope.event
+ reservation: -> reservation
+ controller: ['$scope', '$uibModalInstance', 'event', 'reservation', 'Reservation', ($scope, $uibModalInstance, event, reservation, Reservation) ->
+ # we copy the controller's resolved parameters into the scope
+ $scope.event = event
+ $scope.reservation = angular.copy reservation
+
+ # set the reservable_id to the first available event
+ for e in event.recurrence_events
+ if e.nb_free_places > (reservation.nb_reserve_places + reservation.nb_reserve_reduced_places)
+ $scope.reservation.reservable_id = e.id
+ break
+
+ # Callback to validate the new reservation's date
+ $scope.ok = ->
+ eventToPlace = null
+ angular.forEach event.recurrence_events, (e)->
+ if e.id is parseInt($scope.reservation.reservable_id, 10)
+ eventToPlace = e
+ $scope.reservation.slots[0].start_at = eventToPlace.start_date
+ $scope.reservation.slots[0].end_at = eventToPlace.end_date
+ $scope.reservation.slots[0].availability_id = eventToPlace.availability_id
+ $scope.reservation.slots_attributes = $scope.reservation.slots
+ $scope.attempting = true
+ Reservation.update {id: reservation.id}, {reservation: $scope.reservation}, (reservation) ->
+ $uibModalInstance.close(reservation)
+ $scope.attempting = true
+ , (response)->
+ $scope.alerts = []
+ angular.forEach response, (v, k)->
+ angular.forEach v, (err)->
+ $scope.alerts.push({msg: k+': '+err, type: 'danger'})
+ $scope.attempting = false
+
+ # Callback to cancel the modification
+ $scope.cancel = ->
+ $uibModalInstance.dismiss('cancel')
+ ]
+ .result['finally'](null).then (reservation)->
+ $scope.reservations.splice(index, 1)
+ $scope.event.nb_free_places = $scope.event.nb_free_places + reservation.nb_reserve_places + reservation.nb_reserve_reduced_places
+ angular.forEach $scope.event.recurrence_events, (e)->
+ if e.id is parseInt(reservation.reservable_id, 10)
+ e.nb_free_places = e.nb_free_places - reservation.nb_reserve_places - reservation.nb_reserve_reduced_places
+
+
+
+ ##
+ # Checks if the provided reservation is able to be modified
+ # @param reservation {{nb_reserve_places:number, nb_reserve_reduced_places:number}}
+ ##
+ $scope.reservationCanModify = (reservation)->
+ isAble = false
+ angular.forEach $scope.event.recurrence_events, (e)->
+ isAble = true if e.nb_free_places > (reservation.nb_reserve_places + reservation.nb_reserve_reduced_places)
+ isAble
+
+
+
+ ##
+ # Compute the total amount for the current reservation according to the previously set parameters
+ # and assign the result in $scope.reserve.amountTotal
+ ##
+ $scope.computeEventAmount = ->
+ # first we check that a user was selected
+ if Object.keys($scope.ctrl.member).length > 0
+ r = mkReservation($scope.ctrl.member, $scope.reserve, $scope.event)
+ Price.compute {reservation: r}, (res) ->
+ $scope.reserve.amountTotal = res.price
+ else
+ $scope.reserve.amountTotal = null
+
+
+
+ ### PRIVATE SCOPE ###
+
+ ##
+ # Kind of constructor: these actions will be realized first when the controller is loaded
+ ##
+ initialize = ->
+ # gather the current user or the list of users if the current user is an admin
+ if $scope.currentUser
+ if $scope.currentUser.role isnt 'admin'
+ $scope.ctrl.member = $scope.currentUser
+ else
+ Member.query (members) ->
+ $scope.members = members
+
+ # check that the event's reduced rate is initialized
+ if !$scope.event.reduced_amount
+ $scope.event.reduced_amount = 0
+
+ # initialize the "reserve" object with the event's data
+ $scope.reserve.nbPlaces = [0..$scope.event.nb_free_places]
+ $scope.reserve.nbReducedPlaces = [0..$scope.event.nb_free_places]
+
+ # if non-admin, get the current user's reservations into $scope.reservations
+ if $scope.currentUser
+ getReservations($scope.event.id, 'Event', $scope.currentUser.id)
+
+
+
+ ##
+ # Retrieve the reservations for the couple event / user
+ # @param reservable_id {number} the current event id
+ # @param reservable_type {string} 'Event'
+ # @param user_id {number} the user's id (current or managed)
+ ##
+ getReservations = (reservable_id, reservable_type, user_id)->
+ Reservation.query(reservable_id: reservable_id, reservable_type: reservable_type, user_id: user_id).$promise.then (reservations)->
+ $scope.reservations = reservations
+
+
+
+ ##
+ # 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 reserve {Object} Reservation parameters (places...)
+ # @param event {Object} Current event (Atelier/Stage)
+ # @return {{user_id:Number, reservable_id:Number, reservable_type:String, slots_attributes:Array, nb_reserve_places:Number, nb_reserve_reduced_places:Number}}
+ ##
+ mkReservation = (member, reserve, event) ->
+ reservation =
+ user_id: member.id
+ reservable_id: event.id
+ reservable_type: 'Event'
+ slots_attributes: []
+ nb_reserve_places: reserve.nbReservePlaces
+ nb_reserve_reduced_places: reserve.nbReserveReducedPlaces
+
+ reservation.slots_attributes.push
+ start_at: event.start_date
+ end_at: event.end_date
+ availability_id: event.availability.id
+ offered: event.offered || false
+
+ reservation
+
+
+
+ ##
+ # Set the current reservation to the default values. This implies to reservation form to be hidden.
+ ##
+ resetEventReserve = ->
+ if $scope.event
+ $scope.reserve =
+ nbPlaces: [0..$scope.event.nb_free_places]
+ nbReducedPlaces: [0..$scope.event.nb_free_places]
+ nbReservePlaces: 0
+ nbReserveReducedPlaces: 0
+ toReserve: false
+ amountTotal : 0
+ $scope.event.offered = false
+
+
+
+ ##
+ # Open a modal window which trigger the stripe payment process
+ # @param reservation {Object} to book
+ ##
+ payByStripe = (reservation) ->
+ $uibModal.open
+ templateUrl: '<%= asset_path "stripe/payment_modal.html" %>'
+ size: 'md'
+ resolve:
+ reservation: ->
+ reservation
+ price: ->
+ Price.compute({reservation: reservation}).$promise
+ cgv: ->
+ CustomAsset.get({name: 'cgv-file'}).$promise
+ objectToPay: ->
+ eventToReserve: $scope.event
+ reserve: $scope.reserve
+ member: $scope.ctrl.member
+ controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'cgv', 'Auth', 'Reservation', 'growl', ($scope, $uibModalInstance, $state, reservation, price, cgv, Auth, Reservation, growl) ->
+ # Price
+ $scope.amount = price.price
+
+ # CGV
+ $scope.cgv = cgv.custom_asset
+
+ # Reservation
+ $scope.reservation = reservation
+
+ # Callback for the stripe payment authorization
+ $scope.payment = (status, response) ->
+ if response.error
+ growl.error(response.error.message)
+ else
+ $scope.attempting = true
+ $scope.reservation.card_token = response.id
+ Reservation.save reservation: $scope.reservation, (reservation) ->
+ $uibModalInstance.close(reservation)
+ , (response)->
+ $scope.alerts = []
+ $scope.alerts.push
+ msg: response.data.card[0]
+ type: 'danger'
+ $scope.attempting = false
+ ]
+ .result['finally'](null).then (reservation)->
+ afterPayment(reservation)
+
+
+
+ ##
+ # Open a modal window which trigger the local payment process
+ # @param reservation {Object} to book
+ ##
+ payOnSite = (reservation) ->
+ $uibModal.open
+ templateUrl: '<%= asset_path "shared/valid_reservation_modal.html" %>'
+ size: 'sm'
+ resolve:
+ reservation: ->
+ reservation
+ price: ->
+ Price.compute({reservation: reservation}).$promise
+ controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'Auth', 'Reservation', ($scope, $uibModalInstance, $state, reservation, price, Auth, Reservation) ->
+ # Price
+ $scope.amount = price.price
+
+ # Reservation
+ $scope.reservation = reservation
+
+ # Button label
+ if $scope.amount > 0
+ $scope.validButtonName = _t('confirm_(payment_on_site)')
+ else
+ $scope.validButtonName = _t('confirm')
+
+ # Callback to validate the payment
+ $scope.ok = ->
+ $scope.attempting = true
+ Reservation.save reservation: $scope.reservation, (reservation) ->
+ $uibModalInstance.close(reservation)
+ $scope.attempting = true
+ , (response)->
+ $scope.alerts = []
+ angular.forEach response, (v, k)->
+ angular.forEach v, (err)->
+ $scope.alerts.push
+ msg: k+': '+err
+ type: 'danger'
+ $scope.attempting = false
+
+ # Callback to cancel the payment
+ $scope.cancel = ->
+ $uibModalInstance.dismiss('cancel')
+ ]
+ .result['finally'](null).then (reservation)->
+ afterPayment(reservation)
+
+
+
+ ##
+ # What to do after the payment was successful
+ # @param resveration {Object} booked reservation
+ ##
+ afterPayment = (reservation)->
+ $scope.event.nb_free_places = $scope.event.nb_free_places - reservation.nb_reserve_places - reservation.nb_reserve_reduced_places
+ resetEventReserve()
+ $scope.reserveSuccess = true
+ $scope.reservations.push reservation
+ if $scope.currentUser.role == 'admin'
+ $scope.ctrl.member = null
+
+
+
+ ## !!! MUST BE CALLED AT THE END of the controller
+ initialize()
]
-
diff --git a/app/assets/javascripts/controllers/home.coffee b/app/assets/javascripts/controllers/home.coffee
index 5215748e4..a3a6e2085 100644
--- a/app/assets/javascripts/controllers/home.coffee
+++ b/app/assets/javascripts/controllers/home.coffee
@@ -1,35 +1,34 @@
'use strict'
-Application.Controllers.controller "homeController", ['$scope', '$stateParams', 'Member', 'Twitter', 'Project', 'Event', ($scope, $stateParams, Member, Twitter, Project, Event) ->
-
-
-
- ### PRIVATE STATIC CONSTANTS ###
-
- # The 4 last users will be displayed on the home page
- LAST_MEMBERS_LIMIT = 4
-
- # Only the last tweet is shown
- LAST_TWEETS_LIMIT = 1
-
- # The 3 closest events are shown
- LAST_EVENTS_LIMIT = 3
-
-
+Application.Controllers.controller "HomeController", ['$scope', '$stateParams', 'Twitter', 'lastMembersPromise', 'lastProjectsPromise', 'upcomingEventsPromise', 'homeBlogpostPromise', 'twitterNamePromise', ($scope, $stateParams, Twitter, lastMembersPromise, lastProjectsPromise, upcomingEventsPromise, homeBlogpostPromise, twitterNamePromise)->
### PUBLIC SCOPE ###
## The last registered members who confirmed their addresses
- $scope.last_members = []
+ $scope.lastMembers = lastMembersPromise
## The last tweets from the Fablab official twitter account
- $scope.last_tweets = []
+ $scope.lastTweets = []
## The last projects published/documented on the plateform
- $scope.last_projects = []
+ $scope.lastProjects = lastProjectsPromise
## The closest upcoming events
- $scope.upcoming_events = []
+ $scope.upcomingEvents = upcomingEventsPromise
+
+ ## The admin blogpost
+ $scope.homeBlogpost = homeBlogpostPromise.setting.value
+
+ ## Twitter username
+ $scope.twitterName = twitterNamePromise.setting.value
+
+ ##
+ # Test if the provided event run on a single day or not
+ # @param event {Object} single event from the $scope.upcomingEvents array
+ # @returns {boolean} false if the event runs on more that 1 day
+ ##
+ $scope.isOneDayEvent = (event) ->
+ moment(event.start_date).isSame(event.end_date, 'day')
@@ -39,20 +38,15 @@ Application.Controllers.controller "homeController", ['$scope', '$stateParams',
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
- # display the reset password dialog if the parameter was provided
+ # we retrieve tweets from here instead of ui-router's promise because, if adblock stop the API request,
+ # this prevent the whole home page to be blocked
+ $scope.lastTweets = Twitter.query(limit: 1)
+
+ # if we recieve a token to reset the password as GET parameter, trigger the
+ # changePassword modal from the parent controller
if $stateParams.reset_password_token
$scope.$parent.editPassword($stateParams.reset_password_token)
- # initialize the homepage data
- Member.lastSubscribed {limit: LAST_MEMBERS_LIMIT}, (members) ->
- $scope.last_members = members
- Twitter.query {limit: LAST_TWEETS_LIMIT}, (tweets) ->
- $scope.last_tweets = tweets
- Project.lastPublished (projects) ->
- $scope.last_projects = projects
- Event.upcoming {limit: LAST_EVENTS_LIMIT}, (events) ->
- $scope.upcoming_events = events
-
## !!! MUST BE CALLED AT THE END of the controller
diff --git a/app/assets/javascripts/controllers/machines.coffee.erb b/app/assets/javascripts/controllers/machines.coffee.erb
index f5bfa0ec1..de4587a94 100644
--- a/app/assets/javascripts/controllers/machines.coffee.erb
+++ b/app/assets/javascripts/controllers/machines.coffee.erb
@@ -74,19 +74,96 @@ class MachinesController
+##
+# Manages the transition when a user clicks on the reservation button.
+# According to the status of user currently logged into the system, redirect him to the reservation page,
+# or display a modal window asking him to complete a training before he can book a machine reservation.
+# @param machine {{id:number}} An object containg the id of the machine to book,
+# the object will be completed before the fonction returns.
+# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
+##
+_reserveMachine = (machine, e) ->
+ _this = this
+ e.preventDefault()
+ e.stopPropagation()
+
+ # retrieve the full machine object
+ machine = _this.Machine.get {id: machine.id}, ->
+
+ # if the currently logged'in user has completed the training for this machine, or this machine does not require
+ # a prior training, just redirect him to the machine's booking page
+ if machine.current_user_is_training or machine.trainings.length == 0
+ _this.$state.go('app.logged.machines_reserve', {id: machine.id})
+ else
+ # otherwise, if a user is authenticated ...
+ if _this.$scope.isAuthenticated()
+ # ... and have booked a training for this machine, tell him that he must wait for an admin to validate
+ # the training before he can book the reservation
+ if machine.current_user_training_reservation
+ _this.$uibModal.open
+ templateUrl: '<%= asset_path "machines/training_reservation_modal.html" %>'
+ controller: ['$scope', '$uibModalInstance', '$state', ($scope, $uibModalInstance, $state) ->
+ $scope.machine = machine
+ $scope.cancel = ->
+ $uibModalInstance.dismiss('cancel')
+ ]
+ # ... but does not have booked the training, tell him to register for a training session first
+ else
+ _this.$uibModal.open
+ templateUrl: '<%= asset_path "machines/request_training_modal.html" %>'
+ controller: ['$scope', '$uibModalInstance', '$state', ($scope, $uibModalInstance, $state) ->
+ $scope.machine = machine
+ $scope.member = _this.$scope.currentUser
+
+ # transform the name of the trainings associated with the machine to integrate them in a sentence
+ $scope.humanizeTrainings = ->
+ text = ''
+ angular.forEach $scope.machine.trainings, (training) ->
+ if text.length > 0
+ text += _this._t('_or_the_')
+ text += training.name.substr(0,1).toLowerCase() + training.name.substr(1)
+ text
+
+ # modal is close with validation
+ $scope.ok = ->
+ $state.go('app.logged.trainings_reserve')
+ $uibModalInstance.close(machine)
+
+ # modal is closed with escaping
+ $scope.cancel = (e)->
+ e.preventDefault()
+ $uibModalInstance.dismiss('cancel')
+ ]
+ # if the user is not logged, open the login modal window
+ else
+ _this.$scope.login()
+
+
+
+
##
# Controller used in the public listing page, allowing everyone to see the list of machines
##
-Application.Controllers.controller "machinesController", ["$scope", "$state", 'Machine', '$modal', ($scope, $state, Machine, $modal) ->
+Application.Controllers.controller "MachinesController", ["$scope", "$state", '_t', 'Machine', '$uibModal', 'machinesPromise', ($scope, $state, _t, Machine, $uibModal, machinesPromise) ->
- ## Retrieve the list of machines
- $scope.machines = Machine.query()
+## Retrieve the list of machines
+ $scope.machines = machinesPromise
##
# Redirect the user to the machine details page
##
$scope.showMachine = (machine) ->
$state.go('app.public.machines_show', {id: machine.slug})
+
+ ##
+ # Callback to book a reservation for the current machine
+ ##
+ $scope.reserveMachine = _reserveMachine.bind
+ $scope: $scope
+ $state: $state
+ _t: _t
+ $uibModal: $uibModal
+ Machine: Machine
]
@@ -94,7 +171,7 @@ Application.Controllers.controller "machinesController", ["$scope", "$state", 'M
##
# Controller used in the machine creation page (admin)
##
-Application.Controllers.controller "newMachineController", ["$scope", "$state", 'CSRF', ($scope, $state, CSRF) ->
+Application.Controllers.controller "NewMachineController", ["$scope", "$state", 'CSRF',($scope, $state, CSRF) ->
CSRF.setMetaTags()
## API URL where the form will be posted
@@ -116,8 +193,11 @@ Application.Controllers.controller "newMachineController", ["$scope", "$state",
##
# Controller used in the machine edition page (admin)
##
-Application.Controllers.controller "editMachineController", ["$scope", "$state", '$stateParams', 'Machine', 'CSRF', ($scope, $state, $stateParams, Machine, CSRF) ->
- CSRF.setMetaTags()
+Application.Controllers.controller "EditMachineController", ["$scope", '$state', '$stateParams', 'machinePromise', 'CSRF', ($scope, $state, $stateParams, machinePromise, CSRF) ->
+
+
+
+ ### PUBLIC SCOPE ###
## API URL where the form will be posted
$scope.actionUrl = "/api/machines/" + $stateParams.id
@@ -126,14 +206,24 @@ Application.Controllers.controller "editMachineController", ["$scope", "$state",
$scope.method = "put"
## Retrieve the details for the machine id in the URL, if an error occurs redirect the user to the machines list
- $scope.machine = Machine.get {id: $stateParams.id}
- , ->
- return
- , ->
- $state.go('app.public.machines_list')
+ $scope.machine = machinePromise
- ## Using the MachinesController
- new MachinesController($scope, $state)
+
+
+ ### PRIVATE SCOPE ###
+
+ ##
+ # Kind of constructor: these actions will be realized first when the controller is loaded
+ ##
+ initialize = ->
+ CSRF.setMetaTags()
+
+ ## Using the MachinesController
+ new MachinesController($scope, $state)
+
+
+ ## !!! MUST BE CALLED AT THE END of the controller
+ initialize()
]
@@ -141,14 +231,11 @@ Application.Controllers.controller "editMachineController", ["$scope", "$state",
##
# Controller used in the machine details page (public)
##
-Application.Controllers.controller "showMachineController", ['$scope', '$state', '$modal', '$stateParams', 'Machine', ($scope, $state, $modal, $stateParams, Machine) ->
+Application.Controllers.controller "ShowMachineController", ['$scope', '$state', '$uibModal', '$stateParams', '_t', 'Machine', 'growl', 'machinePromise'
+, ($scope, $state, $uibModal, $stateParams, _t, Machine, growl, machinePromise) ->
- ## Retrieve the details for the machine id in the URL, if an error occurs redirect the user to the machines list
- $scope.machine = Machine.get {id: $stateParams.id}
- , ->
- return
- , ->
- $state.go('app.public.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
##
# Callback to delete the current machine (admins only)
@@ -156,9 +243,730 @@ Application.Controllers.controller "showMachineController", ['$scope', '$state',
$scope.delete = (machine) ->
# check the permissions
if $scope.currentUser.role isnt 'admin'
- console.error 'Unauthorized operation'
+ console.error _t('unauthorized_operation')
else
# delete the machine then redirect to the machines listing
machine.$delete ->
$state.go('app.public.machines_list')
+ , (error)->
+ growl.warning(_t('the_machine_cant_be_deleted_because_it_is_already_reserved_by_some_users'))
+ ##
+ # Callback to book a reservation for the current machine
+ ##
+ $scope.reserveMachine = _reserveMachine.bind
+ $scope: $scope
+ $state: $state
+ _t: _t
+ $uibModal: $uibModal
+ Machine: Machine
+]
+
+
+
+##
+# Controller used in the machine reservation page (for logged users who have completed the training and admins).
+# 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', 'settingsPromise',
+($scope, $state, $stateParams, $uibModal, _t, moment, Machine, Auth, dialogs, $timeout, Price, Member, Availability, Slot, Setting, CustomAsset, plansPromise, groupsPromise, growl, settingsPromise) ->
+
+
+
+ ### PRIVATE STATIC CONSTANTS ###
+
+ # The calendar is divided in slots of 60 minutes
+ BASE_SLOT = '01:00:00'
+
+ # The calendar will be initialized positioned under 9:00 AM
+ DEFAULT_CALENDAR_POSITION = '09:00:00'
+
+ # The user is unable to modify his already booked reservation 1 day before it occurs
+ PREVENT_BOOKING_MODIFICATION_DELAY = 1
+
+ # Slot already booked by the current user
+ FREE_SLOT_BORDER_COLOR = '#e4cd78'
+
+ # Slot already booked by another user
+ UNAVAILABLE_SLOT_BORDER_COLOR = '#1d98ec'
+
+ # Slot free to be booked
+ BOOKED_SLOT_BORDER_COLOR = '#b2e774'
+
+
+
+ ### PUBLIC SCOPE ###
+
+ ## after fullCalendar loads, provides access to its methods through $scope.calendar.fullCalendar()
+ $scope.calendar = null
+
+ ## bind the machine availabilities with full-Calendar events
+ $scope.eventSources = []
+
+ ## fullCalendar event. The last selected slot that the user want to book
+ $scope.slotToPlace = null
+
+ ## fullCalendar event. An already booked slot that the user want to modify
+ $scope.slotToModify = null
+
+ ## indicates the state of the current view : calendar or plans informations
+ $scope.plansAreShown = false
+
+ ## will store the user's plan if he choosed to buy one
+ $scope.selectedPlan = null
+
+ ## array of fullCalendar events. Slots where the user want to book
+ $scope.eventsReserved = []
+
+ ## total amount of the bill to pay
+ $scope.amountTotal = 0
+
+ ## is the user allowed to change the date of his booking
+ $scope.enableBookingMove = true
+
+ ## how many hours before the reservation, the user is still allowed to change his booking
+ $scope.moveBookingDelay = 24
+
+ ## 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)
+
+ ## the user to deal with, ie. the current user for non-admins
+ $scope.ctrl =
+ member: {}
+
+ ## fablab users list
+ $scope.members = []
+
+ ## current machine to reserve
+ $scope.machine = {}
+
+ ## fullCalendar (v2) configuration
+ $scope.calendarConfig =
+ timezone: Fablab.timezone
+ lang: Fablab.fullcalendar_locale
+ header:
+ left: 'month agendaWeek'
+ center: 'title'
+ right: 'today prev,next'
+ firstDay: 1 # Week start on monday (France)
+ scrollTime: DEFAULT_CALENDAR_POSITION
+ slotDuration: BASE_SLOT
+ allDayDefault: false
+ minTime: '00:00:00'
+ maxTime: '24:00:00'
+ height: 'auto'
+ buttonIcons:
+ prev: 'left-single-arrow'
+ next: 'right-single-arrow'
+ timeFormat:
+ agenda:'H:mm'
+ month: 'H(:mm)'
+ axisFormat: 'H:mm'
+
+ allDaySlot: false
+ defaultView: 'agendaWeek'
+ editable: false
+ eventClick: (event, jsEvent, view) ->
+ calendarEventClickCb(event, jsEvent, view)
+ eventRender: (event, element, view) ->
+ eventRenderCb(event, element)
+
+ ## Global config: message to the end user concerning the subscriptions rules
+ $scope.subscriptionExplicationsAlert = settingsPromise.subscription_explications_alert
+
+ ## Gloabl config: message to the end user concerning the machine bookings
+ $scope.machineExplicationsAlert = settingsPromise.machine_explications_alert
+
+ ## Global config: is the user authorized to change his bookings slots?
+ $scope.enableBookingMove = (settingsPromise.booking_move_enable == "true")
+
+ ## Global config: delay in hours before a booking while changing the booking slot is forbidden
+ $scope.moveBookingDelay = parseInt(settingsPromise.booking_move_delay)
+
+ ## Global config: is the user authorized to cancel his bookings?
+ $scope.enableBookingCancel = (settingsPromise.booking_cancel_enable == "true")
+
+ ## Global config: delay in hours before a booking while the cancellation is forbidden
+ $scope.cancelBookingDelay = parseInt(settingsPromise.booking_cancel_delay)
+
+ ## Global config: calendar window in the morning
+ $scope.calendarConfig.minTime = moment.duration(moment(settingsPromise.booking_window_start).format('HH:mm:ss'))
+
+ ## Global config: calendar window in the evening
+ $scope.calendarConfig.maxTime = moment.duration(moment(settingsPromise.booking_window_end).format('HH:mm:ss'))
+
+
+
+ ##
+ # 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 = null
+ $scope.slotToModify.title = if $scope.currentUser.role isnt 'admin' then _t('i_ve_reserved') else _t('not_available')
+ $scope.slotToModify.backgroundColor = 'white'
+ $scope.slotToModify = null
+ $scope.calendar.fullCalendar 'rerenderEvents'
+
+
+
+ ##
+ # When modifying an already booked reservation, cancel the choice of the new slot
+ # @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
+ ##
+ $scope.removeSlotToPlace = (e)->
+ e.preventDefault()
+ $scope.slotToPlace.backgroundColor = 'white'
+ $scope.slotToPlace.title = ''
+ $scope.slotToPlace = null
+ $scope.calendar.fullCalendar 'rerenderEvents'
+
+
+
+ ##
+ # When modifying an already booked reservation, confirm the modification.
+ ##
+ $scope.modifyMachineSlot = ->
+ Slot.update {id: $scope.slotToModify.id},
+ slot:
+ start_at: $scope.slotToPlace.start
+ end_at: $scope.slotToPlace.end
+ availability_id: $scope.slotToPlace.availability_id
+ , -> # success
+ $scope.modifiedSlots =
+ newReservedSlot: $scope.slotToPlace
+ oldReservedSlot: $scope.slotToModify
+ $scope.slotToPlace.title = if $scope.currentUser.role isnt 'admin' then _t('i_ve_reserved') else _t('not_available')
+ $scope.slotToPlace.backgroundColor = 'white'
+ $scope.slotToPlace.borderColor = $scope.slotToModify.borderColor
+ $scope.slotToPlace.id = $scope.slotToModify.id
+ $scope.slotToPlace.is_reserved = true
+ $scope.slotToPlace.can_modify = true
+ $scope.slotToPlace = null
+
+ $scope.slotToModify.backgroundColor = 'white'
+ $scope.slotToModify.title = ''
+ $scope.slotToModify.borderColor = FREE_SLOT_BORDER_COLOR
+ $scope.slotToModify.id = null
+ $scope.slotToModify.is_reserved = false
+ $scope.slotToModify.can_modify = false
+ $scope.slotToModify = null
+
+ $scope.calendar.fullCalendar 'rerenderEvents'
+ , (err) -> # failure
+ growl.error(_t('unable_to_change_the_reservation'))
+ console.error(err)
+
+
+
+ ##
+ # Cancel the current booking modification, reseting the whole process
+ ##
+ $scope.cancelModifyMachineSlot = ->
+ $scope.slotToPlace.backgroundColor = 'white'
+ $scope.slotToPlace.title = ''
+ $scope.slotToPlace = null
+ $scope.slotToModify.title = if $scope.currentUser.role isnt 'admin' then _t('i_ve_reserved') else _t('not_available')
+ $scope.slotToModify.backgroundColor = 'white'
+ $scope.slotToModify = null
+
+ $scope.calendar.fullCalendar 'rerenderEvents'
+
+
+
+ ##
+ # 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 = ->
+ $scope.paidMachineSlots = null
+ $scope.plansAreShown = false
+ $scope.selectedPlan = null
+ 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 ->
+ $scope.calendar.fullCalendar 'refetchEvents'
+ $scope.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
+
+
+
+ ##
+ # 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.selectPlan($scope.selectedPlan)
+
+
+
+ ##
+ # 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)
+
+ if $scope.currentUser.role isnt 'admin' and $scope.amountTotal > 0
+ payByStripe(reservation)
+ else
+ if $scope.currentUser.role is 'admin' or $scope.amountTotal 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
+ ##
+ $scope.showPlans = ->
+ $scope.plansAreShown = true
+
+
+
+ ##
+ # Add the provided plan to the current shopping cart
+ # @param plan {Object} the plan to subscribe
+ ##
+ $scope.selectPlan = (plan) ->
+ if $scope.isAuthenticated()
+ angular.forEach $scope.eventsReserved, (machineSlot)->
+ angular.forEach $scope.ctrl.member.machine_credits, (credit)->
+ if credit.machine_id = machineSlot.machine.id
+ credit.hours_used = 0
+ machineSlot.machine.is_reduced_amount = false
+
+ if $scope.selectedPlan != plan
+ $scope.selectedPlan = plan
+ else
+ $scope.selectedPlan = null
+ updateCartPrice()
+ else
+ $scope.login null, ->
+ $scope.selectedPlan = plan
+ updateCartPrice()
+
+
+
+ ##
+ # Checks if $scope.slotToModify and $scope.slotToPlace have tag incompatibilities
+ # @returns {boolean} true in case of incompatibility
+ ##
+ $scope.tagMissmatch = ->
+ for tag in $scope.slotToModify.tags
+ if tag.id not in $scope.slotToPlace.tag_ids
+ return true
+ false
+
+
+
+ ### PRIVATE SCOPE ###
+
+ ##
+ # Kind of constructor: these actions will be realized first when the controller is loaded
+ ##
+ initialize = ->
+ Availability.machine {machineId: $stateParams.id}, (availabilities) ->
+ $scope.eventSources.push
+ events: availabilities
+ textColor: 'black'
+
+ if $scope.currentUser.role isnt 'admin'
+ $scope.ctrl.member = $scope.currentUser
+ else
+ Member.query {requested_attributes:'[subscription,credits]'}, (members) ->
+ $scope.members = members
+
+ $scope.machine = Machine.get {id: $stateParams.id}
+ , ->
+ return
+ , ->
+ $state.go('app.public.machines_list')
+
+
+
+ ##
+ # Create an hash map implementing the Reservation specs
+ # @param member {Object} User as retreived from the API: current user / selected user if current is admin
+ # @param slots {Array} Array of fullCalendar events: slots selected on the calendar
+ # @param [plan] {Object} Plan as retrived from the API: plan to buy with the current reservation
+ # @return {{user_id:Number, reservable_id:Number, reservable_type:String, slots_attributes:Array, plan_id:Number|null}}
+ ##
+ mkReservation = (member, slots, plan = null) ->
+ reservation =
+ user_id: member.id
+ reservable_id: (slots[0].machine.id if slots.length > 0)
+ reservable_type: 'Machine'
+ slots_attributes: []
+ plan_id: (plan.id if plan)
+ angular.forEach slots, (slot, key) ->
+ reservation.slots_attributes.push
+ start_at: slot.start
+ end_at: slot.end
+ availability_id: slot.availability_id
+ offered: slot.offered || false
+
+ reservation
+
+
+
+ ##
+ # 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 {reservation: r}, (res) ->
+ $scope.amountTotal = res.price
+ 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
+
+
+ ##
+ # Triggered when the user click 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).
+ ##
+ calendarEventClickCb = (event, jsEvent, view) ->
+
+ if !event.is_reserved && !$scope.slotToModify
+ index = $scope.eventsReserved.indexOf(event)
+ if index == -1
+ event.backgroundColor = FREE_SLOT_BORDER_COLOR
+ event.title = _t('i_reserve')
+ $scope.eventsReserved.push event
+ else
+ $scope.removeMachineSlot(event)
+ $scope.paidMachineSlots = null
+ $scope.selectedPlan = null
+ $scope.modifiedSlots = null
+ else if !event.is_reserved && $scope.slotToModify
+ if $scope.slotToPlace
+ $scope.slotToPlace.backgroundColor = 'white'
+ $scope.slotToPlace.title = ''
+ $scope.slotToPlace = event
+ event.backgroundColor = '#bbb'
+ event.title = _t('i_shift')
+ else if event.is_reserved and (slotCanBeModified(event) or slotCanBeCanceled(event)) and !$scope.slotToModify and $scope.eventsReserved.length == 0
+ event.movable = slotCanBeModified(event)
+ event.cancelable = slotCanBeCanceled(event)
+ dialogs.confirm
+ templateUrl: '<%= asset_path "shared/confirm_modify_slot_modal.html" %>'
+ resolve:
+ object: -> event
+ , (type) ->
+ if type == 'move'
+ $scope.modifiedSlots = null
+ $scope.slotToModify = event
+ event.backgroundColor = '#eee'
+ event.title = _t('i_change')
+ $scope.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
+ $scope.calendar.fullCalendar 'rerenderEvents'
+ , -> # error while canceling
+ growl.error _t('cancellation_failed')
+ , ->
+ $scope.paidMachineSlots = null
+ $scope.selectedPlan = null
+ $scope.modifiedSlots = null
+
+ $scope.calendar.fullCalendar 'rerenderEvents'
+
+ updateCartPrice()
+
+
+
+
+ ##
+ # 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) ->
+ if $scope.currentUser.role is 'admin' and event.tags.length > 0
+ html = ''
+ for tag in event.tags
+ html += "#{tag.name} "
+ element.find('.fc-time').append(html)
+
+
+
+ ##
+ # 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({reservation: reservation}).$promise
+ cgv: ->
+ CustomAsset.get({name: 'cgv-file'}).$promise
+ controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'cgv', 'Auth', 'Reservation', ($scope, $uibModalInstance, $state, reservation, price, cgv, Auth, Reservation) ->
+ # Price
+ $scope.amount = price.price
+
+ # CGV
+ $scope.cgv = cgv.custom_asset
+
+ # Reservation
+ $scope.reservation = reservation
+
+ ##
+ # 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 reservation: $scope.reservation, (reservation) ->
+ $uibModalInstance.close(reservation)
+ , (response)->
+ $scope.alerts = []
+ $scope.alerts.push
+ msg: response.data.card[0]
+ 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({reservation: reservation}).$promise
+ controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'Auth', 'Reservation', ($scope, $uibModalInstance, $state, reservation, price, Auth, Reservation) ->
+
+ # Price
+ $scope.amount = price.price
+
+ # Reservation
+ $scope.reservation = reservation
+
+ # Button label
+ if $scope.amount > 0
+ $scope.validButtonName = _t('confirm_(payment_on_site)')
+ else
+ $scope.validButtonName = _t('confirm')
+
+ ##
+ # Callback to process the local payment, triggered on button click
+ ##
+ $scope.ok = ->
+ $scope.attempting = true
+ Reservation.save reservation: $scope.reservation, (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 = []
+
+ if $scope.selectedPlan
+ $scope.ctrl.member.subscribed_plan = angular.copy($scope.selectedPlan)
+ Auth._currentUser.subscribed_plan = angular.copy($scope.selectedPlan)
+ $scope.plansAreShown = false
+
+ $scope.calendar.fullCalendar 'refetchEvents'
+ $scope.calendar.fullCalendar 'rerenderEvents'
+
+
+
+ ##
+ # After payment, update the id of the newly reserved slot with the id returned by the server.
+ # This will allow the user to modify the reservation he just booked. The associated user will also be registered
+ # with the slot.
+ # @param slot {Object}
+ # @param reservation {Object}
+ # @param user {Object} user associated with the slot
+ ##
+ updateMachineSlot = (slot, reservation, user)->
+ angular.forEach reservation.slots, (s)->
+ if slot.start.isSame(s.start_at)
+ slot.id = s.id
+ slot.user = user
+
+
+
+ ##
+ # Search for the requested plan in the provided array and return its price.
+ # @param plansArray {Array} full list of plans
+ # @param planId {Number} plan identifier
+ # @returns {Number|null} price of the given plan or null if not found
+ ##
+ findAmountByPlanId = (plansArray, planId)->
+ for plan in plansArray
+ return plan.amount if plan.plan_id == planId
+ return null
+
+
+ ## !!! MUST BE CALLED AT THE END of the controller
+ initialize()
]
diff --git a/app/assets/javascripts/controllers/main_nav.coffee.erb b/app/assets/javascripts/controllers/main_nav.coffee.erb
index 0f05d63c9..96b1c5775 100644
--- a/app/assets/javascripts/controllers/main_nav.coffee.erb
+++ b/app/assets/javascripts/controllers/main_nav.coffee.erb
@@ -3,55 +3,97 @@
##
# Navigation controller. List the links availables in the left navigation pane and their icon.
##
-Application.Controllers.controller "mainNavController", ["$scope", "$location", "$cookies", ($scope, $location, $cookies) ->
+Application.Controllers.controller "MainNavController", ["$scope", "$location", "$cookies", ($scope, $location, $cookies) ->
-## Common links (public application)
+ ## Common links (public application)
$scope.navLinks = [
{
state: 'app.public.home'
- linkText: 'Accueil'
+ linkText: 'home'
linkIcon: 'home'
}
{
state: 'app.public.machines_list'
- linkText: 'Liste des machines'
- linkIcon: 'gears'
+ linkText: 'reserve_a_machine'
+ linkIcon: 'calendar'
+ }
+ {
+ state: 'app.logged.trainings_reserve'
+ linkText: 'trainings_registrations'
+ linkIcon: 'graduation-cap'
}
{
state: 'app.public.events_list'
- linkText: 'Liste des stages et ateliers'
+ linkText: 'courses_and_workshops_registrations'
linkIcon: 'tags'
}
{
state: 'app.public.projects_list'
- linkText: 'Galerie de projets'
+ linkText: 'projects_gallery'
linkIcon: 'th'
}
+
]
- ## Admin links (backoffice application)
+ unless Fablab.withoutPlans
+ $scope.navLinks.push({
+ state: 'app.public.plans'
+ linkText: 'subscriptions'
+ linkIcon: 'credit-card'
+ })
+
+
$scope.adminNavLinks = [
+ {
+ state: 'app.admin.trainings'
+ linkText: 'trainings_monitoring'
+ linkIcon: 'graduation-cap'
+ }
+ {
+ state: 'app.admin.calendar'
+ linkText: 'manage_the_calendar'
+ linkIcon: 'calendar'
+ }
{
state: 'app.admin.members'
- linkText: 'Suivi utilisateurs'
+ linkText: 'manage_the_users'
linkIcon: 'users'
}
+ {
+ state: 'app.admin.invoices'
+ linkText: 'manage_the_invoices'
+ linkIcon: 'file-pdf-o'
+ }
+ {
+ state: 'app.admin.pricing'
+ linkText: 'subscriptions_and_prices'
+ linkIcon: 'money'
+ }
{
state: 'app.admin.events'
- linkText: 'Suivi stages et ateliers'
+ linkText: 'courses_and_workshops_monitoring'
linkIcon: 'tags'
}
{
state: 'app.public.machines_list'
- linkText: 'Gérer les machines'
+ linkText: 'manage_the_machines'
linkIcon: 'cogs'
}
{
state: 'app.admin.project_elements'
- linkText: 'Gérer les éléments Projets'
+ linkText: 'manage_the_projects_elements'
linkIcon: 'tasks'
}
+ {
+ state: 'app.admin.statistics'
+ linkText: 'statistics'
+ linkIcon: 'bar-chart-o'
+ }
+ {
+ state: 'app.admin.settings'
+ linkText: 'customization'
+ linkIcon: 'gear'
+ }
]
-
]
diff --git a/app/assets/javascripts/controllers/members.coffee b/app/assets/javascripts/controllers/members.coffee
index 594996380..bef2e9bed 100644
--- a/app/assets/javascripts/controllers/members.coffee
+++ b/app/assets/javascripts/controllers/members.coffee
@@ -3,23 +3,11 @@
##
# Controller used in the members listing page
##
-Application.Controllers.controller "membersController", ["$scope", "$state", 'Member', ($scope, $state, Member) ->
+Application.Controllers.controller "MembersController", ["$scope", 'membersPromise', ($scope, membersPromise) ->
## members list
- $scope.members = Member.query()
+ $scope.members = membersPromise
- ## Merbers ordering/sorting. Default: not sorted
- $scope.orderMember = null
-
- ##
- # Change the members ordering criterion to the one provided
- # @param orderBy {string} ordering criterion
- ##
- $scope.setOrderMember = (orderBy)->
- if $scope.orderMember == orderBy
- $scope.orderMember = '-'+orderBy
- else
- $scope.orderMember = orderBy
]
@@ -27,24 +15,71 @@ Application.Controllers.controller "membersController", ["$scope", "$state", 'Me
##
# Controller used when editing the current user's profile
##
-Application.Controllers.controller "editProfileController", ["$scope", "$state", "Member", "Auth", 'growl', 'dialogs', 'CSRF', ($scope, $state, Member, Auth, growl, dialogs, CSRF) ->
- CSRF.setMetaTags()
+Application.Controllers.controller "EditProfileController", ["$scope", "$rootScope", "$state", "$window", '$locale', "Member", "Auth", "Session", "activeProviderPromise", 'growl', 'dialogs', 'CSRF', 'memberPromise', 'groups', '_t'
+, ($scope, $rootScope, $state, $window, $locale, Member, Auth, Session, activeProviderPromise, growl, dialogs, CSRF, memberPromise, groups, _t) ->
+
+
+
+ ### PUBLIC SCOPE ###
## API URL where the form will be posted
$scope.actionUrl = "/api/members/" + $scope.currentUser.id
+ ## list of groups
+ $scope.groups = groups
+
## Form action on the above URL
$scope.method = 'patch'
## Current user's profile
- $scope.user = Member.get {id: $scope.currentUser.id}
+ $scope.user = memberPromise
+
+ ## default : do not show the group changing form
+ $scope.group =
+ change: false
+
+ ## group ID of the current/selected user
+ $scope.userGroup = memberPromise.group_id
+
+ ## active authentication provider parameters
+ $scope.activeProvider = activeProviderPromise
+
+ ## allow the user to change his password except if he connect from an SSO
+ $scope.preventPassword = false
+
+ ## mapping of fields to disable
+ $scope.preventField = {}
## Angular-Bootstrap datepicker configuration for birthday
$scope.datePicker =
- format: 'dd/MM/yyyy'
+ format: $locale.DATETIME_FORMATS.shortDate
opened: false # default: datePicker is not shown
options:
- startingDay: 1 # France: the week starts on monday
+ startingDay: Fablab.weekStartingDay
+
+
+
+ ##
+ # Return the group object, identified by the ID set in $scope.userGroup
+ ##
+ $scope.getUserGroup = ->
+ for group in $scope.groups
+ if group.id == $scope.userGroup
+ return group
+
+
+
+ ##
+ # Change the group of the current user to the one set in $scope.userGroup
+ ##
+ $scope.selectGroup = ->
+ Member.update {id: $scope.user.id}, {user: {group_id: $scope.userGroup}}, (user) ->
+ $scope.user = user
+ $scope.group.change = false
+ growl.success(_t('your_group_has_been_successfully_changed'))
+ , (err) ->
+ growl.error(_t('an_unexpected_error_prevented_your_group_from_being_changed'))
+ console.error(err)
@@ -81,10 +116,32 @@ Application.Controllers.controller "editProfileController", ["$scope", "$state",
Auth._currentUser.name = content.name
$scope.currentUser = content
Auth._currentUser = content
+ $rootScope.currentUser = content
$state.go('app.public.home')
+ ##
+ # Ask for confirmation then delete the current user's account
+ # @param user {Object} the current user (to delete)
+ ##
+ $scope.deleteUser = (user)->
+ dialogs.confirm
+ resolve:
+ object: ->
+ title: _t('confirmation_required')
+ msg: _t('do_you_really_want_to_delete_your_account')+' '+_t('all_data_relative_to_your_projects_will_be_lost')
+ , -> # cancel confirmed
+ Member.remove { id: user.id }, ->
+ Auth.logout().then ->
+ $state.go('app.public.home')
+ growl.success(_t('your_user_account_has_been_successfully_deleted_goodbye'))
+ , (error)->
+ console.log(error)
+ growl.error(_t('an_error_occured_preventing_your_account_from_being_deleted'))
+
+
+
##
# 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.
@@ -95,6 +152,52 @@ Application.Controllers.controller "editProfileController", ["$scope", "$state",
'fileinput-exists'
else
'fileinput-new'
+
+
+ ##
+ # Check if the of the properties editable by the user are linked to the SSO
+ # @return {boolean} true if some editable fields are mapped with the SSO, false otherwise
+ ##
+ $scope.hasSsoFields = ->
+ # if check if keys > 1 because there's a minimum of 1 mapping (id <-> provider-uid)
+ # so the user may want to edit his profile on the SSO if at least 2 mappings exists
+ Object.keys($scope.preventField).length > 1
+
+
+ ##
+ # Disconnect and re-connect the user to the SSO to force the synchronisation of the profile's data
+ ##
+ $scope.syncProfile = ->
+ Auth.logout().then (oldUser) ->
+ Session.destroy()
+ $rootScope.currentUser = null
+ $rootScope.toCheckNotifications = false
+ $scope.notifications = []
+ $window.location.href = $scope.activeProvider.link_to_sso_connect
+
+
+ ### PRIVATE SCOPE ###
+
+ ##
+ # Kind of constructor: these actions will be realized first when the controller is loaded
+ ##
+ initialize = ->
+ CSRF.setMetaTags()
+
+ # init the birth date to JS object
+ $scope.user.profile.birthday = moment($scope.user.profile.birthday).toDate()
+
+ if $scope.activeProvider.providable_type != 'DatabaseProvider'
+ $scope.preventPassword = true
+ # bind fields protection with sso fields
+ angular.forEach activeProviderPromise.mapping, (map) ->
+ $scope.preventField[map] = true
+
+
+
+
+ ## !!! MUST BE CALLED AT THE END of the controller
+ initialize()
]
@@ -102,8 +205,8 @@ Application.Controllers.controller "editProfileController", ["$scope", "$state",
##
# Controller used on the public user's profile page (seeing another user's profile)
##
-Application.Controllers.controller "showProfileController", ["$scope", "$stateParams", 'Member', ($scope, $stateParams, Member) ->
+Application.Controllers.controller "ShowProfileController", ["$scope", "$stateParams", 'Member', 'memberPromise', ($scope, $stateParams, Member, memberPromise) ->
## Selected user's profile (id from the current URL)
- $scope.user = Member.get {id: $stateParams.id}
+ $scope.user = memberPromise
]
diff --git a/app/assets/javascripts/controllers/notifications.coffee b/app/assets/javascripts/controllers/notifications.coffee
index 05c7457f6..3426aa1d0 100644
--- a/app/assets/javascripts/controllers/notifications.coffee
+++ b/app/assets/javascripts/controllers/notifications.coffee
@@ -4,7 +4,7 @@
# Controller used in notifications page
# inherits $scope.$parent.notifications (unread notifications) from ApplicationController
##
-Application.Controllers.controller "notificationsController", ["$scope", 'Notification', ($scope, Notification) ->
+Application.Controllers.controller "NotificationsController", ["$scope", 'Notification', ($scope, Notification) ->
@@ -32,7 +32,7 @@ Application.Controllers.controller "notificationsController", ["$scope", 'Notifi
# Mark the provided notification as read, updating its status on the server and moving it
# to the already read notifications list.
# @param notification {{id:number}} the notification to mark as read
- # @param e {Object} jQuery event object
+ # @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
##
$scope.markAsRead = (notification, e) ->
e.preventDefault()
diff --git a/app/assets/javascripts/controllers/plans.coffee.erb b/app/assets/javascripts/controllers/plans.coffee.erb
new file mode 100644
index 000000000..0e0d3102b
--- /dev/null
+++ b/app/assets/javascripts/controllers/plans.coffee.erb
@@ -0,0 +1,232 @@
+'use strict'
+
+Application.Controllers.controller "PlansIndexController", ["$scope", "$state", '$uibModal', 'Auth', 'dialogs', 'growl', 'plansPromise', 'groupsPromise', 'Subscription', 'Member', 'subscriptionExplicationsPromise', '_t'
+, ($scope, $state, $uibModal, Auth, dialogs, growl, plansPromise, groupsPromise, Subscription, Member, subscriptionExplicationsPromise, _t) ->
+
+
+
+ ### PUBLIC SCOPE ###
+
+ ## list of groups
+ $scope.groups = groupsPromise
+
+ ## default : do not show the group changing form
+ $scope.changeGroup = false
+
+ ## group ID of the current/selected user
+ $scope.userGroup = null
+
+ ## 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)
+
+ ## user to deal with
+ $scope.ctrl =
+ member: null
+ member_id: null
+
+ ## already subscribed plan of the current user
+ $scope.paidPlan = null
+
+ ## plan to subscribe (shopping cart)
+ $scope.selectedPlan = null
+
+ ##
+ $scope.subscriptionExplicationsAlert = subscriptionExplicationsPromise.setting.value
+
+ ##
+ # Callback to deal with the subscription of the user selected in the dropdown list instead of the current user's
+ # subscription. (admins only)
+ ##
+ $scope.updateMember = ->
+ $scope.selectedPlan = null
+ $scope.paidPlan = null
+ $scope.userGroup = $scope.ctrl.member.group_id
+ $scope.changeGroup = false
+
+
+
+ ##
+ # Add the provided plan to the shopping basket
+ # @param plan {Object} The plan to subscribe to
+ ##
+ $scope.selectPlan = (plan) ->
+ if $scope.isAuthenticated()
+ if $scope.selectedPlan != plan
+ $scope.selectedPlan = plan
+ else
+ $scope.selectedPlan = null
+ else
+ $scope.login()
+
+
+
+ ##
+ # Callback to trigger the payment process of the subscription
+ ##
+ $scope.openSubscribePlanModal = ->
+ if $scope.currentUser.role isnt 'admin'
+ payByStripe()
+ else
+ payOnSite()
+
+
+
+ ##
+ # Return the group object, identified by the ID set in $scope.userGroup
+ ##
+ $scope.getUserGroup = ->
+ for group in $scope.groups
+ if group.id == $scope.userGroup
+ return group
+
+
+
+ ##
+ # Change the group of the current/selected user to the one set in $scope.userGroup
+ ##
+ $scope.selectGroup = ->
+ Member.update {id: $scope.ctrl.member.id}, {user: {group_id: $scope.userGroup}}, (user) ->
+ $scope.ctrl.member = user
+ $scope.changeGroup = false
+ if $scope.currentUser.role isnt 'admin'
+ $scope.currentUser = user
+ growl.success(_t('your_group_was_successfully_changed'))
+ else
+ growl.success(_t('the_user_s_group_was_successfully_changed'))
+ , (err) ->
+ if $scope.currentUser.role isnt 'admin'
+ growl.error(_t('an_error_prevented_your_group_from_being_changed'))
+ else
+ growl.error(_t('an_error_prevented_to_change_the_user_s_group'))
+ console.error(err)
+
+
+ ##
+ # Return an enumerable meaninful string for the gender of the provider user
+ # @param user {Object} Database user record
+ # @return {string} 'male' or 'female'
+ ##
+ $scope.getGender = (user) ->
+ if user.profile
+ if user.profile.gender == "true" then 'male' else 'female'
+ else 'other'
+
+
+
+ ### PRIVATE SCOPE ###
+
+ ##
+ # Kind of constructor: these actions will be realized first when the controller is loaded
+ ##
+ initialize = ->
+ if $scope.currentUser
+ if $scope.currentUser.role isnt 'admin'
+ $scope.ctrl.member = $scope.currentUser
+ $scope.paidPlan = $scope.currentUser.subscribed_plan
+ $scope.userGroup = $scope.currentUser.group_id
+ else
+ Member.query {requested_attributes:'[subscription]'}, (members) ->
+ membersNoPlan = []
+ angular.forEach members, (v)->
+ membersNoPlan.push v unless v.subscribed_plan
+ $scope.members = membersNoPlan
+
+ $scope.$on 'devise:new-session', (event, user)->
+ $scope.ctrl.member = user
+
+
+ $scope.isInFuture = (dateTime)->
+ if moment().diff(moment(dateTime)) < 0
+ true
+ else
+ false
+
+ ##
+ # Open a modal window which trigger the stripe payment process
+ ##
+ payByStripe = ->
+ $uibModal.open
+ templateUrl: '<%= asset_path "stripe/payment_modal.html" %>'
+ size: 'md'
+ resolve:
+ selectedPlan: -> $scope.selectedPlan
+ member: -> $scope.ctrl.member
+ controller: ['$scope', '$uibModalInstance', '$state', 'selectedPlan', 'member', 'Subscription', 'CustomAsset', ($scope, $uibModalInstance, $state, selectedPlan, member, Subscription, CustomAsset) ->
+ $scope.amount = selectedPlan.amount
+ $scope.selectedPlan = selectedPlan
+ # retrieve the CGV
+ CustomAsset.get {name: 'cgv-file'}, (cgv) ->
+ $scope.cgv = cgv.custom_asset
+ $scope.payment = (status, response) ->
+ if response.error
+ growl.error(response.error.message)
+ else
+ $scope.attempting = true
+ Subscription.save
+ subscription:
+ plan_id: selectedPlan.id
+ user_id: member.id
+ card_token: response.id
+ , (data, status) -> # success
+ $uibModalInstance.close(data)
+ , (data, status) -> # failed
+ $scope.alerts = []
+ $scope.alerts.push({msg: _t('an_error_occured_during_the_payment_process_please_try_again_later'), type: 'danger' })
+ $scope.attempting = false
+ ]
+ .result['finally'](null).then (subscription)->
+ $scope.ctrl.member.subscribed_plan = angular.copy($scope.selectedPlan)
+ Auth._currentUser.subscribed_plan = angular.copy($scope.selectedPlan)
+ $scope.paidPlan = angular.copy($scope.selectedPlan)
+ $scope.selectedPlan = null
+
+
+
+ ##
+ # Open a modal window which trigger the local payment process
+ ##
+ payOnSite = ->
+ $uibModal.open
+ templateUrl: '<%= asset_path "plans/payment_modal.html" %>'
+ size: 'sm'
+ resolve:
+ selectedPlan: -> $scope.selectedPlan
+ member: -> $scope.ctrl.member
+ controller: ['$scope', '$uibModalInstance', '$state', 'selectedPlan', 'member', 'Subscription', ($scope, $uibModalInstance, $state, selectedPlan, member, Subscription) ->
+ $scope.plan = selectedPlan
+ $scope.member = member
+ $scope.ok = ->
+ $scope.attempting = true
+ Subscription.save
+ subscription:
+ plan_id: selectedPlan.id
+ user_id: member.id
+ , (data, status) -> # success
+ $uibModalInstance.close(data)
+ , (data, status) -> # failed
+ $scope.alerts = []
+ $scope.alerts.push({msg: _t('an_error_occured_during_the_payment_process_please_try_again_later'), type: 'danger' })
+ $scope.attempting = false
+
+ $scope.cancel = ->
+ $uibModalInstance.dismiss('cancel')
+ ]
+ .result['finally'](null).then (reservation)->
+ $scope.ctrl.member.subscribed_plan = angular.copy($scope.selectedPlan)
+ Auth._currentUser.subscribed_plan = angular.copy($scope.selectedPlan)
+ index = $scope.members.indexOf($scope.ctrl.member)
+ $scope.members.splice(index, 1)
+ $scope.ctrl.member = null
+ $scope.paidPlan = angular.copy($scope.selectedPlan)
+ $scope.selectedPlan = null
+
+
+
+ ## !!! MUST BE CALLED AT THE END of the controller
+ initialize()
+]
diff --git a/app/assets/javascripts/controllers/profile.coffee b/app/assets/javascripts/controllers/profile.coffee
new file mode 100644
index 000000000..9162f4d28
--- /dev/null
+++ b/app/assets/javascripts/controllers/profile.coffee
@@ -0,0 +1,152 @@
+
+'use strict'
+
+Application.Controllers.controller "CompleteProfileController", ["$scope", "$rootScope", "$state", "_t", "$locale", "growl", "CSRF", "Auth", "Member", "settingsPromise", "activeProviderPromise", "groupsPromise", "cguFile", "memberPromise"
+, ($scope, $rootScope, $state, _t, $locale, growl, CSRF, Auth, Member, settingsPromise, activeProviderPromise, groupsPromise, cguFile, memberPromise) ->
+
+
+
+ ### PUBLIC SCOPE ###
+
+ ## API URL where the form will be posted
+ $scope.actionUrl = "/api/members/" + memberPromise.id
+
+ ## Form action on the above URL
+ $scope.method = 'patch'
+
+ ## genre of the application name (eg. "_le_ Fablab" or "_la_ Fabrique")
+ $scope.nameGenre = settingsPromise.name_genre
+
+ ## name of the current fablab application (eg. "Fablab de la Casemate")
+ $scope.fablabName = settingsPromise.fablab_name
+
+ ## informations from the current SSO provider
+ $scope.activeProvider = activeProviderPromise
+
+ ## list of user's groups (student/standard/...)
+ $scope.groups = groupsPromise
+
+ ## current user, contains informations retrieved from the SSO
+ $scope.user = memberPromise
+
+ ## disallow the user to change his password as he connect from SSO
+ $scope.preventPassword = true
+
+ ## mapping of fields to disable
+ $scope.preventField = {}
+
+ ## CGU
+ $scope.cgu = cguFile.custom_asset
+
+ ## Angular-Bootstrap datepicker configuration for birthday
+ $scope.datePicker =
+ format: $locale.DATETIME_FORMATS.shortDate
+ opened: false # default: datePicker is not shown
+ options:
+ startingDay: Fablab.weekStartingDay
+
+
+
+ ##
+ # Callback to diplay the datepicker as a dropdown when clicking on the input field
+ # @param $event {Object} jQuery event object
+ ##
+ $scope.openDatePicker = ($event) ->
+ $event.preventDefault()
+ $event.stopPropagation()
+ $scope.datePicker.opened = true
+
+
+
+ ##
+ # 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's profile is updated and the user is
+ # redirected to the home page
+ # @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
+ $scope.user.profile.user_avatar = content.profile.user_avatar
+ Auth._currentUser.profile.user_avatar = content.profile.user_avatar
+ $scope.user.name = content.name
+ Auth._currentUser.name = content.name
+ $scope.user = content
+ Auth._currentUser = content
+ $rootScope.currentUser = content
+ $state.go('app.public.home')
+
+ ##
+ # 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'
+
+
+
+ ##
+ # Merge the current user into the account with the given auth_token
+ ##
+ $scope.registerAuthToken = ->
+ Member.merge {id: $rootScope.currentUser.id}, {user: {auth_token: $scope.user.auth_token}}, (user) ->
+ $scope.user = user
+ Auth._currentUser = user
+ $rootScope.currentUser = user
+ $state.go('app.public.home')
+ , (err) ->
+ if err.data.error
+ growl.error(err.data.error)
+ else
+ growl.error(_t('an_unexpected_error_occurred_check_your_authentication_code'))
+ console.error(err)
+
+ ##
+ # Return the email given by the SSO provider, parsed if needed
+ # @return {String} E-mail of the current user
+ ##
+ $scope.ssoEmail = ->
+ email = memberPromise.email
+ if email
+ duplicate = email.match(/^<([^>]+)>.{20}-duplicate$/)
+ if duplicate
+ return duplicate[1]
+ email
+
+
+
+ ### PRIVATE SCOPE ###
+
+
+
+ ##
+ # Kind of constructor: these actions will be realized first when the controller is loaded
+ ##
+ initialize = ->
+ CSRF.setMetaTags()
+
+ # init the birth date to JS object
+ $scope.user.profile.birthday = moment($scope.user.profile.birthday).toDate()
+
+ # bind fields protection with sso fields
+ angular.forEach activeProviderPromise.mapping, (map) ->
+ $scope.preventField[map] = true
+
+
+
+
+ ## !!! MUST BE CALLED AT THE END of the controller
+ initialize()
+
+]
\ No newline at end of file
diff --git a/app/assets/javascripts/controllers/projects.coffee b/app/assets/javascripts/controllers/projects.coffee.erb
similarity index 64%
rename from app/assets/javascripts/controllers/projects.coffee
rename to app/assets/javascripts/controllers/projects.coffee.erb
index 4fa8f55ae..700c79412 100644
--- a/app/assets/javascripts/controllers/projects.coffee
+++ b/app/assets/javascripts/controllers/projects.coffee.erb
@@ -144,30 +144,28 @@ class ProjectsController
##
# Controller used on projects listing page
##
-Application.Controllers.controller "projectsController", ["$scope", "$state", 'Project', 'Machine', 'Theme', 'Component', ($scope, $state, Project, Machine, Theme, Component) ->
-
-
+Application.Controllers.controller "ProjectsController", ["$scope", "$state", 'Project', 'machinesPromise', 'themesPromise', 'componentsPromise'
+, ($scope, $state, Project, machinesPromise, themesPromise, componentsPromise) ->
### PRIVATE STATIC CONSTANTS ###
# Number of notifications added to the page when the user clicks on 'load next notifications'
PROJECTS_PER_PAGE = 12
-
-
### PUBLIC SCOPE ###
+ $scope.search = { q: "", from: undefined, machine_id: undefined, component_id: undefined, theme_id: undefined }
## list of projects to display
$scope.projects = []
## list of machines / used for filtering
- $scope.machines = []
+ $scope.machines = machinesPromise
## list of themes / used for filtering
- $scope.themes = Theme.query()
+ $scope.themes = themesPromise
## list of components / used for filtering
- $scope.components = Component.query()
+ $scope.components = componentsPromise
## By default, the pagination mode is activated to limit the page size
$scope.paginateActive = true
@@ -175,19 +173,31 @@ Application.Controllers.controller "projectsController", ["$scope", "$state", 'P
## The currently displayed page number
$scope.page = 1
+ $scope.resetFilters = ->
+ $scope.search.q = ""
+ $scope.search.from = undefined
+ $scope.search.machine_id = undefined
+ $scope.search.component_id = undefined
+ $scope.search.theme_id = undefined
+ $scope.triggerSearch()
+ $scope.triggerSearch = ->
+ Project.search { search: $scope.search, page: 1 }, (projects)->
+ $scope.projects = projects
+ if projects.length < PROJECTS_PER_PAGE
+ $scope.paginateActive = false
+ else
+ $scope.paginateActive = true
+ $scope.page = 2
- ##
- # Request the server to retrieve the next undisplayed projects and add them
- # to the local projects list.
- ##
$scope.loadMoreProjects = ->
- Project.query {page: $scope.page}, (projects) ->
- $scope.projects = $scope.projects.concat projects
- $scope.paginateActive = false if projects.length < PROJECTS_PER_PAGE
-
- $scope.page += 1
-
+ # Project.query {page: $scope.page}, (projects) ->
+ # $scope.projects = $scope.projects.concat projects
+ # $scope.paginateActive = false if projects.length < PROJECTS_PER_PAGE
+ Project.search { search: $scope.search, page: $scope.page }, (projects)->
+ $scope.projects = $scope.projects.concat projects
+ $scope.paginateActive = false if projects.length < PROJECTS_PER_PAGE
+ $scope.page += 1
##
@@ -199,37 +209,9 @@ Application.Controllers.controller "projectsController", ["$scope", "$state", 'P
- ##
- # Callback to delete the provided project. Then, the projects list page is refreshed (admins only)
- ##
- $scope.delete = (project) ->
- # check the permissions
- if $scope.currentUser.role isnt 'admin'
- console.error 'Unauthorized operation'
- else
- # delete the project then refresh the projects list
- project.$delete ->
- $state.go('app.public.projects_list', {}, {reload: true})
+ ## initialization
+ $scope.triggerSearch()
-
-
- ### PRIVATE SCOPE ###
-
- ##
- # Kind of constructor: these actions will be realized first when the controller is loaded
- ##
- initialize = ->
- Machine.query().$promise.then (data)->
- $scope.machines = data.map (d) ->
- id: d.id
- name: d.name
-
- $scope.loadMoreProjects()
-
-
-
- ## !!! MUST BE CALLED AT THE END of the controller
- initialize()
]
@@ -237,7 +219,8 @@ Application.Controllers.controller "projectsController", ["$scope", "$state", 'P
##
# Controller used in the project creation page
##
-Application.Controllers.controller "newProjectController", ["$scope", "$state", 'Project', 'Machine', 'Member', 'Component', 'Theme', 'Licence', '$document', 'CSRF', ($scope, $state, Project, Machine, Member, Component, Theme, Licence, $document, CSRF) ->
+Application.Controllers.controller "NewProjectController", ["$scope", "$state", 'Project', 'Machine', 'Member', 'Component', 'Theme', 'Licence', '$document', 'CSRF'
+, ($scope, $state, Project, Machine, Member, Component, Theme, Licence, $document, CSRF) ->
CSRF.setMetaTags()
## API URL where the form will be posted
@@ -246,9 +229,6 @@ Application.Controllers.controller "newProjectController", ["$scope", "$state",
## Form action on the above URL
$scope.method = 'post'
- ## Button litteral text value
- $scope.submitName = 'Enregistrer comme brouillon'
-
## Default project parameters
$scope.project =
project_steps_attributes: []
@@ -271,7 +251,8 @@ Application.Controllers.controller "newProjectController", ["$scope", "$state",
##
# Controller used in the project edition page
##
-Application.Controllers.controller "editProjectController", ["$scope", "$state", '$stateParams', 'Project', 'Machine', 'Member', 'Component', 'Theme', 'Licence', '$document', 'CSRF', ($scope, $state, $stateParams, Project, Machine, Member, Component, Theme, Licence, $document, CSRF) ->
+Application.Controllers.controller "EditProjectController", ["$scope", "$state", '$stateParams', 'Project', 'Machine', 'Member', 'Component', 'Theme', 'Licence', '$document', 'CSRF', 'projectPromise'
+, ($scope, $state, $stateParams, Project, Machine, Member, Component, Theme, Licence, $document, CSRF, projectPromise) ->
CSRF.setMetaTags()
## API URL where the form will be posted
@@ -280,15 +261,8 @@ Application.Controllers.controller "editProjectController", ["$scope", "$state",
## Form action on the above URL
$scope.method = 'put'
- ## Button litteral text value
- $scope.submitName = 'Enregistrer'
-
## Retrieve the project's details, if an error occured, redirect the user to the projects list page
- $scope.project = Project.get {id: $stateParams.id}
- , -> # success
- return
- , -> # failed
- $state.go('app.public.projects_list')
+ $scope.project = projectPromise
## Other members list (project collaborators)
Member.query().$promise.then (data)->
@@ -307,18 +281,15 @@ Application.Controllers.controller "editProjectController", ["$scope", "$state",
##
# Controller used in the public project's details page
##
-Application.Controllers.controller "showProjectController", ["$scope", "$state", "$stateParams", "Project", '$location', ($scope, $state, $stateParams, Project, $location) ->
-
-
+Application.Controllers.controller "ShowProjectController", ["$scope", "$state", "projectPromise", '$location', '$uibModal', '_t'
+, ($scope, $state, projectPromise, $location, $uibModal, _t) ->
### PUBLIC SCOPE ###
- ## Will be set to true once the project details are loaded. Used to load the Disqus plugin at the right moment
- $scope.contentLoaded = false
-
## Store the project's details
- $scope.project = {}
-
+ $scope.project = projectPromise
+ $scope.projectUrl = $location.absUrl()
+ $scope.disqusShortname = Fablab.disqusShortname
##
@@ -336,23 +307,63 @@ Application.Controllers.controller "showProjectController", ["$scope", "$state",
- ### PRIVATE SCOPE ###
+ ##
+ # Test if the provided user has the deletion rights on the current project
+ # @param [user] {{id:number}} (optional) the user to check rights
+ # @returns boolean
+ ##
+ $scope.projectDeletableBy = (user) ->
+ return false if not user?
+ return true if $scope.project.author_id == user.id
+
+
##
- # Kind of constructor: these actions will be realized first when the controller is loaded
+ # Callback to delete the current project. Then, the user is redirected to the projects list page,
+ # which is refreshed. Admins and project owner only are allowed to delete a project
##
- initialize = ->
- ## Retrieve the project content
- $scope.project = Project.get {id: $stateParams.id}
- , -> # success
- $scope.contentLoaded = true
- $scope.project_url = $location.absUrl()
- return
- , -> # failed, redirect the user to the projects listing
- $state.go('app.public.projects_list')
+ $scope.deleteProject = ->
+ # check the permissions
+ if $scope.currentUser.role is 'admin' or $scope.projectDeletableBy($scope.currentUser)
+ # delete the project then refresh the projects list
+ $scope.project.$delete ->
+ $state.go('app.public.projects_list', {}, {reload: true})
+ else
+ console.error _t('unauthorized_operation')
+ ##
+ # Open a modal box containg a form that allow the end-user to signal an abusive content
+ # @param e {Object} jQuery event
+ ##
+ $scope.signalAbuse = (e) ->
+ e.preventDefault() if e
+ $uibModal.open
+ templateUrl: '<%= asset_path "shared/signalAbuseModal.html" %>'
+ size: 'md'
+ resolve:
+ project: -> $scope.project
+ controller: ['$scope', '$uibModalInstance', '_t', 'growl', 'Abuse', 'project', ($scope, $uibModalInstance, _t, growl, Abuse, project) ->
+
+ # signaler's profile & signalement infos
+ $scope.signaler = {
+ signaled_type: 'Project'
+ signaled_id: project.id
+ }
+
+ # callback for signaling cancellation
+ $scope.cancel = ->
+ $uibModalInstance.dismiss('cancel')
+
+ # callback for form validation
+ $scope.ok = ->
+ Abuse.save {}, {abuse: $scope.signaler}, (res) ->
+ # creation successful
+ growl.success(_t('your_report_was_successful_thanks'))
+ $uibModalInstance.close(res)
+ , (error) ->
+ # creation failed...
+ growl.error(_t('an_error_occured_while_sending_your_report'))
+ ]
- ## !!! MUST BE CALLED AT THE END of the controller
- initialize()
]
diff --git a/app/assets/javascripts/controllers/trainings.coffee.erb b/app/assets/javascripts/controllers/trainings.coffee.erb
new file mode 100644
index 000000000..ee1f678f4
--- /dev/null
+++ b/app/assets/javascripts/controllers/trainings.coffee.erb
@@ -0,0 +1,658 @@
+'use strict'
+
+##
+# Controller used in the training reservation agenda page.
+# This controller is very similar to the machine reservation controller with one major difference: here, ONLY ONE
+# 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', "$uibModal", 'Auth', 'dialogs', '$timeout', 'Price', 'Availability', 'Slot', 'Member', 'Setting', 'CustomAsset', '$compile', 'availabilityTrainingsPromise', 'plansPromise', 'groupsPromise', 'growl', 'settingsPromise', '_t',
+($scope, $state, $stateParams, $uibModal, Auth, dialogs, $timeout, Price, Availability, Slot, Member, Setting, CustomAsset, $compile, availabilityTrainingsPromise, plansPromise, groupsPromise, growl, settingsPromise, _t) ->
+
+
+
+ ### PRIVATE STATIC CONSTANTS ###
+
+ # The calendar is divided in slots of 60 minutes
+ BASE_SLOT = '01:00:00'
+
+ # The calendar will be initialized positioned under 9:00 AM
+ DEFAULT_CALENDAR_POSITION = '09:00:00'
+
+ # The user is unable to modify his already booked reservation 1 day before it occurs
+ PREVENT_BOOKING_MODIFICATION_DELAY = 1
+
+ # Color of the selected event backgound
+ SELECTED_EVENT_BG_COLOR = '#ffdd00'
+
+ # Slot already booked by the current user
+ FREE_SLOT_BORDER_COLOR = '#bd7ae9'
+
+
+
+ ### PUBLIC SCOPE ###
+
+ ## after fullCalendar loads, provides access to its methods through $scope.calendar.fullCalendar()
+ $scope.calendar = null
+
+ ## bind the trainings availabilities with full-Calendar events
+ $scope.eventSources = [ { events: availabilityTrainingsPromise, textColor: 'black' } ]
+
+ ## the user to deal with, ie. the current user for non-admins
+ $scope.ctrl =
+ member: {}
+
+ ## the full list of members, used by admin to select a user to interact with
+ $scope.members = []
+
+ ## 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)
+
+ ## indicates the state of the current view : calendar or plans informations
+ $scope.plansAreShown = false
+
+ ## indicates if the selected training was validated (ie. added to the shopping cart)
+ $scope.trainingIsValid = false
+
+ ## contains the selected training once it was payed, allows to display a firendly end-of-shopping message
+ $scope.paidTraining = null
+
+ ## will store the user's plan if he choosed to buy one
+ $scope.selectedPlan = null
+
+ ## fullCalendar event. Training slot that the user want to book
+ $scope.selectedTraining = null
+
+ ## fullCalendar event. An already booked slot that the user want to modify
+ $scope.slotToModify = null
+
+ ## Once a training reservation was modified, will contains {newReservedSlot:{}, oldReservedSlot:{}}
+ $scope.modifiedSlots = null
+
+ ## fullCalendar (v2) configuration
+ $scope.calendarConfig =
+ timezone: Fablab.timezone
+ lang: Fablab.fullcalendar_locale
+ header:
+ left: 'month agendaWeek'
+ center: 'title'
+ right: 'today prev,next'
+ firstDay: 1 # Week start on monday (France)
+ scrollTime: DEFAULT_CALENDAR_POSITION
+ slotDuration: BASE_SLOT
+ allDayDefault: false
+ minTime: '00:00:00'
+ maxTime: '24:00:00'
+ height: 'auto'
+ buttonIcons:
+ prev: 'left-single-arrow'
+ next: 'right-single-arrow'
+ timeFormat:
+ agenda:'H:mm'
+ month: 'H(:mm)'
+ axisFormat: 'H:mm'
+
+ allDaySlot: false
+ defaultView: 'agendaWeek'
+ editable: false
+ eventClick: (event, jsEvent, view) ->
+ calendarEventClickCb(event, jsEvent, view)
+ eventAfterAllRender: (view)->
+ $scope.events = $scope.calendar.fullCalendar 'clientEvents'
+ eventRender: (event, element, view) ->
+ eventRenderCb(event, element, view)
+
+ ## Custom settings
+ $scope.subscriptionExplicationsAlert = settingsPromise.subscription_explications_alert
+ $scope.trainingExplicationsAlert = settingsPromise.training_explications_alert
+ $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)
+ $scope.calendarConfig.minTime = moment.duration(moment(settingsPromise.booking_window_start).format('HH:mm:ss'))
+ $scope.calendarConfig.maxTime = moment.duration(moment(settingsPromise.booking_window_end).format('HH:mm:ss'))
+
+
+
+ ##
+ # 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
+ Availability.trainings {member_id: $scope.ctrl.member.id}, (trainings) ->
+ $scope.calendar.fullCalendar 'removeEvents'
+ $scope.eventSources.push
+ events: trainings
+ textColor: 'black'
+ $scope.trainingIsValid = false
+ $scope.paidTraining = null
+ $scope.plansAreShown = false
+ $scope.selectedPlan = null
+ $scope.selectedTraining = null
+ $scope.slotToModify = null
+ $scope.modifiedSlots = null
+
+
+
+ ##
+ # Callback to mark the selected training as validated (add it to the shopping cart).
+ ##
+ $scope.validTraining = ->
+ $scope.trainingIsValid = true
+ $scope.updatePrices()
+
+
+
+ ##
+ # Remove the training from the shopping cart
+ # @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
+ ##
+ $scope.removeTraining = (e) ->
+ e.preventDefault()
+
+ $scope.selectedTraining.backgroundColor = 'white'
+ $scope.selectedTraining = null
+ $scope.plansAreShown = false
+ $scope.selectedPlan = null
+ $scope.trainingIsValid = false
+ $timeout ->
+ $scope.calendar.fullCalendar 'refetchEvents'
+ $scope.calendar.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)
+
+ if $scope.currentUser.role isnt 'admin' and $scope.amountTotal > 0
+ payByStripe(reservation)
+ else
+ if $scope.currentUser.role is 'admin' or $scope.amountTotal 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'))
+
+
+
+ ##
+ # Add the provided plan to the current shopping cart
+ # @param plan {Object} the plan to subscribe
+ ##
+ $scope.selectPlan = (plan) ->
+ if $scope.isAuthenticated()
+ if $scope.selectedPlan != plan
+ $scope.selectedPlan = plan
+ $scope.updatePrices()
+ else
+ $scope.selectedPlan = null
+ $scope.updatePrices()
+ else
+ $scope.login null, ->
+ $scope.selectedPlan = plan
+ $scope.updatePrices()
+
+
+
+ ##
+ # 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.updatePrices()
+
+ ##
+ # Switch the user's view from the reservation agenda to the plan subscription
+ ##
+ $scope.showPlans = ->
+ $scope.plansAreShown = true
+
+ ##
+ # Cancel the current booking modification, removing the previously booked slot from the selection
+ # @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
+ ##
+ $scope.removeSlotToModify = (e) ->
+ e.preventDefault()
+ if $scope.slotToPlace
+ $scope.slotToPlace.backgroundColor = 'white'
+ $scope.slotToPlace.title = $scope.slotToPlace.training.name
+ $scope.slotToPlace = null
+ $scope.slotToModify.title = if $scope.currentUser.role isnt 'admin' then $scope.slotToModify.training.name + " - " + _t('i_ve_reserved') else $scope.slotToModify.training.name
+ $scope.slotToModify.backgroundColor = 'white'
+ $scope.slotToModify = null
+ $scope.calendar.fullCalendar 'rerenderEvents'
+
+
+
+ ##
+ # When modifying an already booked reservation, cancel the choice of the new slot
+ # @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
+ ##
+ $scope.removeSlotToPlace = (e)->
+ e.preventDefault()
+ $scope.slotToPlace.backgroundColor = 'white'
+ $scope.slotToPlace.title = $scope.slotToPlace.training.name
+ $scope.slotToPlace = null
+ $scope.calendar.fullCalendar 'rerenderEvents'
+
+
+
+ ##
+ # 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
+ $scope.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
+ $scope.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 {reservation: r}, (res) ->
+ $scope.amountTotal = res.price
+ else
+ $scope.amountTotal = null
+
+
+
+ ### 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
+ else
+ Member.query {requested_attributes:'[subscription,credits]'}, (members) ->
+ $scope.members = members
+
+
+
+ ##
+ # Create an hash map implementing the Reservation specs
+ # @param member {Object} User as retreived from the API: current user / selected user if current is admin
+ # @param training {Object} fullCalendar event: training slot selected on the calendar
+ # @param [plan] {Object} Plan as retrived from the API: plan to buy with the current reservation
+ # @return {{user_id:Number, reservable_id:Number, reservable_type:String, slots_attributes:Array, plan_id:Number|null}}
+ ##
+ mkReservation = (member, training, plan = null) ->
+ reservation =
+ user_id: member.id
+ reservable_id: training.training.id
+ reservable_type: 'Training'
+ slots_attributes: []
+ plan_id: (plan.id if plan)
+
+ reservation.slots_attributes.push
+ start_at: training.start
+ end_at: training.end
+ availability_id: training.id
+ offered: training.offered || false
+
+ reservation
+
+
+
+ ##
+ # 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) ->
+ if $scope.ctrl.member
+ # reserve a training if this training will not be reserved and is not about to move and not is completed
+ if !event.is_reserved && !$scope.slotToModify && !event.is_completed
+ if event != $scope.selectedTraining
+ $scope.selectedTraining = event
+ $scope.selectedTraining.offered = false
+ event.backgroundColor = SELECTED_EVENT_BG_COLOR
+ computeTrainingAmount($scope.selectedTraining)
+ else
+ $scope.selectedTraining = null
+ event.backgroundColor = 'white'
+ $scope.trainingIsValid = false
+ $scope.paidTraining = null
+ $scope.selectedPlan = null
+ $scope.modifiedSlots = null
+ # clean all others events background
+ angular.forEach $scope.events, (e)->
+ if event.id != e.id
+ e.backgroundColor = 'white'
+ $scope.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')
+ $scope.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')
+ $scope.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
+ $scope.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
+ # @see http://fullcalendar.io/docs/event_rendering/eventRender/
+ ##
+ eventRenderCb = (event, element, view)->
+ element.attr(
+ 'uib-popover': event.training.description
+ 'popover-trigger': 'mouseenter'
+ 'popover-append-to-body': true
+ )
+ $compile(element)($scope)
+
+
+
+ ##
+ # Open a modal window that allows the user to process a credit card payment for his current shopping cart.
+ ##
+ payByStripe = (reservation) ->
+
+ $uibModal.open
+ templateUrl: '<%= asset_path "stripe/payment_modal.html" %>'
+ size: 'md'
+ resolve:
+ reservation: ->
+ reservation
+ price: ->
+ Price.compute({reservation: reservation}).$promise
+ cgv: ->
+ CustomAsset.get({name: 'cgv-file'}).$promise
+ controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'cgv', 'Auth', 'Reservation', ($scope, $uibModalInstance, $state, reservation, price, cgv, Auth, Reservation) ->
+ # Price
+ $scope.amount = price.price
+
+ # CGV
+ $scope.cgv = cgv.custom_asset
+
+ # Reservation
+ $scope.reservation = reservation
+
+ ##
+ # 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 reservation: $scope.reservation, (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({reservation: reservation}).$promise
+ controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'Auth', 'Reservation', ($scope, $uibModalInstance, $state, reservation, price, Auth, Reservation) ->
+ # Price
+ $scope.amount = price.price
+
+ # Reservation
+ $scope.reservation = reservation
+
+ # Button label
+ if $scope.amount > 0
+ $scope.validButtonName = _t('confirm_(payment_on_site)')
+ else
+ $scope.validButtonName = _t('confirm')
+
+ ##
+ # Callback to process the local payment, triggered on button click
+ ##
+ $scope.ok = ->
+ $scope.attempting = true
+ Reservation.save reservation: $scope.reservation, (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 {reservation: 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
+
+ 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)
+
+ $scope.calendar.fullCalendar 'refetchEvents'
+ $scope.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
+
+
+
+ ##
+ # 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}
+ ##
+ updateTrainingSlotId = (slot, reservation)->
+ angular.forEach reservation.slots, (s)->
+ if slot.start_at == slot.start_at
+ slot.slot_id = s.id
+
+
+
+ ## !!! MUST BE CALLED AT THE END of the controller
+ initialize()
+
+]
diff --git a/app/assets/javascripts/directives/bs-jasny-fileinput.js b/app/assets/javascripts/directives/bs-jasny-fileinput.js
index b95a615fc..0c178aff6 100644
--- a/app/assets/javascripts/directives/bs-jasny-fileinput.js
+++ b/app/assets/javascripts/directives/bs-jasny-fileinput.js
@@ -28,7 +28,7 @@ Application.Directives.directive('bsJasnyFileinput', [function(){
ngModelCtrl.$setValidity('filetype', true);
else
ngModelCtrl.$setValidity('filetype', false);
- }
+ };
}
$scope.$apply();
});
diff --git a/app/assets/javascripts/directives/confirmation_needed.coffee b/app/assets/javascripts/directives/confirmation_needed.coffee
new file mode 100644
index 000000000..76b026112
--- /dev/null
+++ b/app/assets/javascripts/directives/confirmation_needed.coffee
@@ -0,0 +1,20 @@
+Application.Directives.directive 'confirmationNeeded', [->
+ return {
+ priority: 1
+ terminal: true
+ link: (scope, element, attrs)->
+ msg = attrs.confirmationNeeded || "Are you sure?"
+ clickAction = attrs.ngClick
+ element.bind 'click', ->
+ if attrs.confirmationNeededIf?
+ confirmNeededIf = scope.$eval(attrs.confirmationNeededIf)
+ if confirmNeededIf == true
+ if ( window.confirm(msg) )
+ scope.$eval(clickAction)
+ else
+ scope.$eval(clickAction)
+ else
+ if ( window.confirm(msg) )
+ scope.$eval(clickAction)
+ }
+]
diff --git a/app/assets/javascripts/directives/directives.coffee b/app/assets/javascripts/directives/directives.coffee
index 8c4d5a748..0d00b4986 100644
--- a/app/assets/javascripts/directives/directives.coffee
+++ b/app/assets/javascripts/directives/directives.coffee
@@ -21,6 +21,8 @@ Application.Directives.directive 'bsHolder', [ ->
{
link: (scope, element, attrs) ->
Holder.addTheme("icon", { background: "white", foreground: "#e9e9e9", size: 80, font: "FontAwesome"})
+ .addTheme("icon-xs", { background: "white", foreground: "#e0e0e0", size: 20, font: "FontAwesome"})
+ .addTheme("icon-black-xs", { background: "black", foreground: "white", size: 20, font: "FontAwesome"})
.addTheme("avatar", { background: "#eeeeee", foreground: "#555555", size: 16, font: "FontAwesome"})
.run(element[0])
return
@@ -66,3 +68,38 @@ Application.Directives.directive "disableAnimation", ($animate) ->
attrs.$observe "disableAnimation", (value) ->
$animate.enabled not value, elem
+
+##
+# Isolate a form's scope from its parent : no nested validation
+##
+Application.Directives.directive 'isolateForm', [ ->
+ {
+ restrict: 'A',
+ require: '?form'
+ link: (scope, elm, attrs, ctrl) ->
+ return unless ctrl
+
+ # Do a copy of the controller
+ ctrlCopy = {}
+ angular.copy(ctrl, ctrlCopy)
+
+ # Get the form's parent
+ parent = elm.parent().controller('form')
+ # Remove parent link to the controller
+ parent.$removeControl(ctrl)
+
+ # Replace form controller with a "isolated form"
+ isolatedFormCtrl =
+ $setValidity: (validationToken, isValid, control) ->
+ ctrlCopy.$setValidity(validationToken, isValid, control);
+ parent.$setValidity(validationToken, true, ctrl);
+
+ $setDirty: ->
+ elm.removeClass('ng-pristine').addClass('ng-dirty');
+ ctrl.$dirty = true;
+ ctrl.$pristine = false;
+
+ angular.extend(ctrl, isolatedFormCtrl)
+
+ }
+]
\ No newline at end of file
diff --git a/app/assets/javascripts/directives/stripe-angular.js b/app/assets/javascripts/directives/stripe-angular.js
new file mode 100644
index 000000000..9f9dbb182
--- /dev/null
+++ b/app/assets/javascripts/directives/stripe-angular.js
@@ -0,0 +1,24 @@
+'use strict';
+
+// https://github.com/gtramontina/stripe-angular
+
+Application.Directives.directive('stripeForm', ['$window',
+ function($window) {
+ var directive = { restrict: 'A' };
+ directive.link = function(scope, element, attributes) {
+ var form = angular.element(element);
+ form.bind('submit', function() {
+ var button = form.find('button');
+ button.prop('disabled', true);
+ $window.Stripe.createToken(form[0], function() {
+ var args = arguments;
+ scope.$apply(function() {
+ scope[attributes.stripeForm].apply(scope, args);
+ });
+ //button.prop('disabled', false);
+ });
+ });
+ };
+ return directive;
+
+ }]);
diff --git a/app/assets/javascripts/directives/validators.coffee b/app/assets/javascripts/directives/validators.coffee
new file mode 100644
index 000000000..71f19c80a
--- /dev/null
+++ b/app/assets/javascripts/directives/validators.coffee
@@ -0,0 +1,34 @@
+'use strict'
+
+Application.Directives.directive 'url', [ ->
+ URL_REGEXP = /^(https?:\/\/)([\da-z\.-]+)\.([-a-z0-9\.]{2,30})([\/\w \.-]*)*\/?$/
+ {
+ require: 'ngModel'
+ link: (scope, element, attributes, ctrl) ->
+ ctrl.$validators.url = (modelValue, viewValue) ->
+ if ctrl.$isEmpty(modelValue)
+ return true
+ if URL_REGEXP.test(viewValue)
+ return true
+
+ # otherwise, this is invalid
+ return false
+ }
+]
+
+
+Application.Directives.directive 'endpoint', [ ->
+ ENDPOINT_REGEXP = /^\/([-._~:?#\[\]@!$&'()*+,;=%\w]+\/?)*$/
+ {
+ require: 'ngModel'
+ link: (scope, element, attributes, ctrl) ->
+ ctrl.$validators.endpoint = (modelValue, viewValue) ->
+ if ctrl.$isEmpty(modelValue)
+ return true
+ if ENDPOINT_REGEXP.test(viewValue)
+ return true
+
+ # otherwise, this is invalid
+ return false
+ }
+]
\ No newline at end of file
diff --git a/app/assets/javascripts/filters/filters.coffee b/app/assets/javascripts/filters/filters.coffee
index e3277355a..2f3090870 100644
--- a/app/assets/javascripts/filters/filters.coffee
+++ b/app/assets/javascripts/filters/filters.coffee
@@ -1,7 +1,19 @@
'use strict'
+Application.Filters.filter 'array', [ ->
+ (arrayLength) ->
+ if (arrayLength)
+ arrayLength = Math.ceil(arrayLength)
+ arr = new Array(arrayLength)
+
+ for i in [0 ... arrayLength]
+ arr[i] = i
+
+ arr
+]
+
# filter for projects and trainings
-Application.Controllers.filter "machineFilter", [ ->
+Application.Filters.filter "machineFilter", [ ->
(elements, selectedMachine) ->
if !angular.isUndefined(elements) and !angular.isUndefined(selectedMachine) and elements? and selectedMachine?
filteredElements = []
@@ -13,7 +25,7 @@ Application.Controllers.filter "machineFilter", [ ->
elements
]
-Application.Controllers.filter "projectMemberFilter", [ "Auth", (Auth)->
+Application.Filters.filter "projectMemberFilter", [ "Auth", (Auth)->
(projects, selectedMember) ->
if !angular.isUndefined(projects) and angular.isDefined(selectedMember) and projects? and selectedMember? and selectedMember != ""
filteredProject = []
@@ -32,7 +44,7 @@ Application.Controllers.filter "projectMemberFilter", [ "Auth", (Auth)->
projects
]
-Application.Controllers.filter "themeFilter", [ ->
+Application.Filters.filter "themeFilter", [ ->
(projects, selectedTheme) ->
if !angular.isUndefined(projects) and !angular.isUndefined(selectedTheme) and projects? and selectedTheme?
filteredProjects = []
@@ -44,7 +56,7 @@ Application.Controllers.filter "themeFilter", [ ->
projects
]
-Application.Controllers.filter "componentFilter", [ ->
+Application.Filters.filter "componentFilter", [ ->
(projects, selectedComponent) ->
if !angular.isUndefined(projects) and !angular.isUndefined(selectedComponent) and projects? and selectedComponent?
filteredProjects = []
@@ -56,7 +68,7 @@ Application.Controllers.filter "componentFilter", [ ->
projects
]
-Application.Controllers.filter "projectsByAuthor", [ ->
+Application.Filters.filter "projectsByAuthor", [ ->
(projects, authorId) ->
if !angular.isUndefined(projects) and angular.isDefined(authorId) and projects? and authorId? and authorId != ""
filteredProject = []
@@ -68,7 +80,7 @@ Application.Controllers.filter "projectsByAuthor", [ ->
projects
]
-Application.Controllers.filter "projectsCollabored", [ ->
+Application.Filters.filter "projectsCollabored", [ ->
(projects, memberId) ->
if !angular.isUndefined(projects) and angular.isDefined(memberId) and projects? and memberId? and memberId != ""
filteredProject = []
@@ -81,24 +93,84 @@ Application.Controllers.filter "projectsCollabored", [ ->
]
# depend on humanize.js lib in /vendor
-Application.Controllers.filter "humanize", [ ->
+Application.Filters.filter "humanize", [ ->
(element, param) ->
Humanize.truncate(element, param, null)
]
-Application.Controllers.filter "breakFilter", [ ->
+Application.Filters.filter "breakFilter", [ ->
(text) ->
if text != undefined
text.replace(/\n/g, ' ')
]
-Application.Controllers.filter "toTrusted", [ "$sce", ($sce) ->
+Application.Filters.filter "toTrusted", [ "$sce", ($sce) ->
(text) ->
$sce.trustAsHtml text
]
-Application.Controllers.filter "eventsFilter", [ ->
+Application.Filters.filter "planIntervalFilter", [ ->
+ (interval, intervalCount) ->
+ if typeof intervalCount != 'number'
+ switch interval
+ when 'day' then return 'jour'
+ when 'week' then return 'semaine'
+ when 'month' then return 'mois'
+ when 'year' then return 'année'
+ else
+ if intervalCount == 1
+ switch interval
+ when 'day' then return 'un jour'
+ when 'week' then return 'une semaine'
+ when 'month' then return 'un mois'
+ when 'year' then return 'un an'
+ else
+ switch interval
+ when 'day' then return intervalCount+ ' jours'
+ when 'week' then return intervalCount+ ' semaines'
+ when 'month' then return intervalCount+ ' mois'
+ when 'year' then return intervalCount+ ' ans'
+]
+
+Application.Filters.filter "humanReadablePlanName", ['$filter', ($filter)->
+ (plan, groups, short) ->
+ if plan?
+ result = plan.base_name
+ if groups?
+ for group in groups
+ if group.id == plan.group_id
+ if short?
+ result += " - #{group.slug}"
+ else
+ result += " - #{group.name}"
+ result += " - #{$filter('planIntervalFilter')(plan.interval, plan.interval_count)}"
+ result
+]
+
+Application.Filters.filter "trainingReservationsFilter", [ ->
+ (elements, selectedScope) ->
+ if !angular.isUndefined(elements) and !angular.isUndefined(selectedScope) and elements? and selectedScope?
+ filteredElements = []
+ angular.forEach elements, (element)->
+ switch selectedScope
+ when "future"
+ if new Date(element.start_at) > new Date
+ filteredElements.push(element)
+ when "passed"
+ if new Date(element.start_at) <= new Date and !element.is_valid
+ filteredElements.push(element)
+ when "valided"
+ if new Date(element.start_at) <= new Date and element.is_valid
+ filteredElements.push(element)
+ else
+ return []
+ filteredElements
+ else
+ elements
+]
+
+Application.Filters.filter "eventsReservationsFilter", [ ->
(elements, selectedScope) ->
if !angular.isUndefined(elements) and !angular.isUndefined(selectedScope) and elements? and selectedScope? and selectedScope != ""
filteredElements = []
@@ -117,3 +189,43 @@ Application.Controllers.filter "eventsFilter", [ ->
else
elements
]
+
+Application.Filters.filter "groupFilter", [ ->
+ (elements, member) ->
+ if !angular.isUndefined(elements) and !angular.isUndefined(member) and elements? and member?
+ filteredElements = []
+ angular.forEach elements, (element)->
+ if member.group_id == element.id
+ filteredElements.push(element)
+ filteredElements
+ else
+ elements
+]
+
+Application.Filters.filter "groupByFilter", [ ->
+ _.memoize (elements, field)->
+ _.groupBy(elements, field)
+]
+
+Application.Filters.filter "capitalize", [->
+ (text)->
+ "#{text.charAt(0).toUpperCase()}#{text.slice(1).toLowerCase()}"
+]
+
+
+Application.Filters.filter 'reverse', [ ->
+ (items) ->
+ unless angular.isArray(items)
+ return items
+
+ items.slice().reverse()
+]
+
+Application.Filters.filter 'toArray', [ ->
+ (obj) ->
+ return obj unless (obj instanceof Object)
+ _.map obj, (val, key) ->
+ if angular.isObject(val)
+ Object.defineProperty(val, '$key', {__proto__: null, value: key})
+
+]
\ No newline at end of file
diff --git a/app/assets/javascripts/router.coffee.erb b/app/assets/javascripts/router.coffee.erb
index 96ec7a69b..d9b97fc97 100644
--- a/app/assets/javascripts/router.coffee.erb
+++ b/app/assets/javascripts/router.coffee.erb
@@ -1,222 +1,844 @@
angular.module('application.router', ['ui.router']).
- config ['$stateProvider', '$urlRouterProvider', '$locationProvider', ($stateProvider, $urlRouterProvider, $locationProvider) ->
- $locationProvider.hashPrefix('!')
- $urlRouterProvider.otherwise("/")
+ config ['$stateProvider', '$urlRouterProvider', '$locationProvider', ($stateProvider, $urlRouterProvider, $locationProvider) ->
+ $locationProvider.hashPrefix('!')
+ $urlRouterProvider.otherwise("/")
- # abstract root parents states
- # these states controls the access rights to the various routes inherited from them
- $stateProvider
- .state 'app',
- abstract: true
- views:
- 'header': { templateUrl: '<%= asset_path "shared/header.html" %>' }
- 'leftnav':
- templateUrl: '<%= asset_path "shared/leftnav.html" %>'
- controller: 'mainNavController'
- 'main':
- templateUrl: '<%= asset_path "home.html" %>'
- controller: 'homeController'
- .state 'app.public',
- abstract: true
- .state 'app.logged',
- abstract: true
- data:
- authorizedRoles: ['member', 'admin']
- resolve:
- currentUser: ['Auth', (Auth)->
- Auth.currentUser()
- ]
- onEnter: ["currentUser", "$rootScope", (currentUser, $rootScope)->
- $rootScope.currentUser = currentUser
- ]
- .state 'app.admin',
- abstract: true
- data:
- authorizedRoles: ['admin']
- resolve:
- currentUser: ['Auth', (Auth)->
- Auth.currentUser()
- ]
- onEnter: ["currentUser", "$rootScope", (currentUser, $rootScope)->
- $rootScope.currentUser = currentUser
- ]
+ # abstract root parents states
+ # these states controls the access rights to the various routes inherited from them
+ $stateProvider
+ .state 'app',
+ abstract: true
+ views:
+ 'header':
+ templateUrl: '<%= asset_path "shared/header.html" %>'
+ 'leftnav':
+ templateUrl: '<%= asset_path "shared/leftnav.html" %>'
+ controller: 'MainNavController'
+ 'main': {}
+ resolve:
+ logoFile: ['CustomAsset', (CustomAsset) ->
+ CustomAsset.get({name: 'logo-file'}).$promise
+ ]
+ logoBlackFile: ['CustomAsset', (CustomAsset) ->
+ CustomAsset.get({name: 'logo-black-file'}).$promise
+ ]
+ commonTranslations: [ 'Translations', (Translations) ->
+ Translations.query(['app.public.common', 'app.shared.buttons', 'app.shared.elements']).$promise
+ ]
+ onEnter: ['$rootScope', 'logoFile', 'logoBlackFile', ($rootScope, logoFile, logoBlackFile) ->
+ ## Application logo
+ $rootScope.logo = logoFile.custom_asset
+ $rootScope.logoBlack = logoBlackFile.custom_asset
+ ]
+ .state 'app.public',
+ abstract: true
+ .state 'app.logged',
+ abstract: true
+ data:
+ authorizedRoles: ['member', 'admin']
+ resolve:
+ currentUser: ['Auth', (Auth)->
+ Auth.currentUser()
+ ]
+ onEnter: ["$state", "$timeout", "currentUser", "$rootScope", ($state, $timeout, currentUser, $rootScope)->
+ $rootScope.currentUser = currentUser
+ ]
+ .state 'app.admin',
+ abstract: true
+ data:
+ authorizedRoles: ['admin']
+ resolve:
+ currentUser: ['Auth', (Auth)->
+ Auth.currentUser()
+ ]
+ onEnter: ["$state", "$timeout", "currentUser", "$rootScope", ($state, $timeout, currentUser, $rootScope)->
+ $rootScope.currentUser = currentUser
+ ]
- # main pages
- .state 'app.public.about',
- url: '/about'
- views:
- 'content@': { templateUrl: '<%= asset_path "shared/about.html" %>' }
- .state 'app.public.home',
- url: '/?reset_password_token'
- views:
- 'main':
- templateUrl: '<%= asset_path "home.html" %>'
- controller: 'homeController'
+ # main pages
+ .state 'app.public.about',
+ url: '/about'
+ views:
+ 'content@':
+ templateUrl: '<%= asset_path "shared/about.html" %>'
+ controller: 'AboutController'
+ resolve:
+ translations: [ 'Translations', (Translations) ->
+ Translations.query('app.public.about').$promise
+ ]
+ .state 'app.public.home',
+ url: '/?reset_password_token'
+ views:
+ 'main@':
+ templateUrl: '<%= asset_path "home.html" %>'
+ controller: 'HomeController'
+ resolve:
+ lastMembersPromise: ['Member', (Member)->
+ Member.lastSubscribed(limit: 4).$promise
+ ]
+ lastProjectsPromise: ['Project', (Project)->
+ Project.lastPublished().$promise
+ ]
+ upcomingEventsPromise: ['Event', (Event)->
+ Event.upcoming(limit: 3).$promise
+ ]
+ homeBlogpostPromise: ['Setting', (Setting)->
+ Setting.get(name: 'home_blogpost').$promise
+ ]
+ twitterNamePromise: ['Setting', (Setting)->
+ Setting.get(name: 'twitter_name').$promise
+ ]
+ translations: [ 'Translations', (Translations) ->
+ Translations.query('app.public.home').$promise
+ ]
-
- # dashboard
- .state 'app.logged.dashboard_profile',
- url: '/dashboard/profile'
- views:
- 'main@':
- templateUrl: '<%= asset_path "dashboard/profile.html" %>'
- controller: 'editProfileController'
- .state 'app.logged.dashboard_projects',
- url: '/dashboard/projects'
- views:
- 'main@':
- templateUrl: '<%= asset_path "dashboard/projects.html" %>'
- controller: 'dashboardProjectsController'
-
-
- # members
- .state 'app.logged.members_show',
- url: '/members/:id'
- views:
- 'main@':
- templateUrl: '<%= asset_path "members/show.html" %>'
- controller: 'showProfileController'
- .state 'app.logged.members',
- url: '/members'
- views:
- 'main@':
- templateUrl: '<%= asset_path "members/index.html" %>'
- controller: 'membersController'
-
-
- # projects
- .state 'app.public.projects_list',
- url: '/projects'
- views:
- 'main@':
- templateUrl: '<%= asset_path "projects/index.html" %>'
- controller: 'projectsController'
- .state 'app.public.projects_show',
- url: '/projects/:id'
- views:
- 'main@':
- templateUrl: '<%= asset_path "projects/show.html" %>'
- controller: 'showProjectController'
- .state 'app.logged.projects_new',
- url: '/projects/new'
- views:
- 'main@':
- templateUrl: '<%= asset_path "projects/new.html" %>'
- controller: 'newProjectController'
- .state 'app.logged.projects_edit',
- url: '/projects/:id/edit'
- views:
- 'main@':
- templateUrl: '<%= asset_path "projects/edit.html" %>'
- controller: 'editProjectController'
+ # profile completion (SSO import passage point)
+ .state 'app.logged.profileCompletion',
+ url: '/profile_completion'
+ views:
+ 'main@':
+ templateUrl: '<%= asset_path "profile/complete.html"%>'
+ controller: 'CompleteProfileController'
+ resolve:
+ settingsPromise: ['Setting', (Setting)->
+ Setting.query(names: "['fablab_name', 'name_genre']").$promise
+ ]
+ activeProviderPromise: ['AuthProvider', (AuthProvider) ->
+ AuthProvider.active().$promise
+ ]
+ groupsPromise: ['Group', (Group)->
+ Group.query().$promise
+ ]
+ cguFile: ['CustomAsset', (CustomAsset) ->
+ CustomAsset.get({name: 'cgu-file'}).$promise
+ ]
+ memberPromise: ['Member', 'currentUser', (Member, currentUser)->
+ Member.get(id: currentUser.id).$promise
+ ]
+ translations: [ 'Translations', (Translations) ->
+ Translations.query(['app.logged.profileCompletion', 'app.shared.user']).$promise
+ ]
- # machines
- .state 'app.public.machines_list',
- url: '/machines'
- views:
- 'main@':
- templateUrl: '<%= asset_path "machines/index.html" %>'
- controller: 'machinesController'
- .state 'app.public.machines_show',
- url: '/machines/:id'
- views:
- 'main@':
- templateUrl: '<%= asset_path "machines/show.html" %>'
- controller: 'showMachineController'
- .state 'app.admin.machines_new',
- url: '/machines/new'
- views:
- 'main@':
- templateUrl: '<%= asset_path "machines/new.html" %>'
- controller: 'newMachineController'
- .state 'app.admin.machines_edit',
- url: '/machines/:id/edit'
- views:
- 'main@':
- templateUrl: '<%= asset_path "machines/edit.html" %>'
- controller: 'editMachineController'
+ # dashboard
+ .state 'app.logged.dashboard',
+ abstract: true
+ url: '/dashboard'
+ resolve:
+ memberPromise: ['Member', 'currentUser', (Member, currentUser)->
+ Member.get(id: currentUser.id).$promise
+ ]
+ .state 'app.logged.dashboard.profile',
+ url: '/profile'
+ views:
+ 'main@':
+ templateUrl: '<%= asset_path "dashboard/profile.html" %>'
+ controller: 'EditProfileController'
+ resolve:
+ groups: ['Group', (Group)->
+ Group.query().$promise
+ ]
+ activeProviderPromise: ['AuthProvider', (AuthProvider) ->
+ AuthProvider.active().$promise
+ ]
+ translations: [ 'Translations', (Translations) ->
+ Translations.query(['app.logged.dashboard.profile', 'app.shared.user']).$promise
+ ]
+ .state 'app.logged.dashboard.projects',
+ url: '/projects'
+ views:
+ 'main@':
+ templateUrl: '<%= asset_path "dashboard/projects.html" %>'
+ controller: 'DashboardController'
+ resolve:
+ translations: [ 'Translations', (Translations) ->
+ Translations.query('app.logged.dashboard.projects').$promise
+ ]
+ .state 'app.logged.dashboard.trainings',
+ url: '/trainings'
+ views:
+ 'main@':
+ templateUrl: '<%= asset_path "dashboard/trainings.html" %>'
+ controller: 'DashboardController'
+ resolve:
+ translations: [ 'Translations', (Translations) ->
+ Translations.query('app.logged.dashboard.trainings').$promise
+ ]
+ .state 'app.logged.dashboard.events',
+ url: '/events'
+ views:
+ 'main@':
+ templateUrl: '<%= asset_path "dashboard/events.html" %>'
+ controller: 'DashboardController'
+ resolve:
+ translations: [ 'Translations', (Translations) ->
+ Translations.query('app.logged.dashboard.events').$promise
+ ]
+ .state 'app.logged.dashboard.invoices',
+ url: '/invoices'
+ views:
+ 'main@':
+ templateUrl: '<%= asset_path "dashboard/invoices.html" %>'
+ controller: 'DashboardController'
+ resolve:
+ translations: [ 'Translations', (Translations) ->
+ Translations.query('app.logged.dashboard.invoices').$promise
+ ]
- # notifications
- .state 'app.logged.notifications',
- url: '/notifications'
- views:
- 'main@':
- templateUrl: '<%= asset_path "notifications/index.html" %>'
- controller: 'notificationsController'
+ # members
+ .state 'app.logged.members_show',
+ url: '/members/:id'
+ views:
+ 'main@':
+ templateUrl: '<%= asset_path "members/show.html" %>'
+ controller: 'ShowProfileController'
+ resolve:
+ memberPromise: ['$stateParams', 'Member', ($stateParams, Member)->
+ Member.get(id: $stateParams.id).$promise
+ ]
+ translations: [ 'Translations', (Translations) ->
+ Translations.query('app.logged.members_show').$promise
+ ]
+ .state 'app.logged.members',
+ url: '/members'
+ views:
+ 'main@':
+ templateUrl: '<%= asset_path "members/index.html" %>'
+ controller: 'MembersController'
+ resolve:
+ membersPromise: ['Member', (Member)->
+ Member.query({requested_attributes:'[profile]'}).$promise
+ ]
+ translations: [ 'Translations', (Translations) ->
+ Translations.query('app.logged.members').$promise
+ ]
+
+ # projects
+ .state 'app.public.projects_list',
+ url: '/projects'
+ views:
+ 'main@':
+ templateUrl: '<%= asset_path "projects/index.html" %>'
+ controller: 'ProjectsController'
+ resolve:
+ themesPromise: ['Theme', (Theme)->
+ Theme.query().$promise
+ ]
+ componentsPromise: ['Component', (Component)->
+ Component.query().$promise
+ ]
+ machinesPromise: ['Machine', (Machine)->
+ Machine.query().$promise
+ ]
+ translations: [ 'Translations', (Translations) ->
+ Translations.query('app.public.projects_list').$promise
+ ]
+ .state 'app.logged.projects_new',
+ url: '/projects/new'
+ views:
+ 'main@':
+ templateUrl: '<%= asset_path "projects/new.html" %>'
+ controller: 'NewProjectController'
+ resolve:
+ translations: [ 'Translations', (Translations) ->
+ Translations.query(['app.logged.projects_new', 'app.shared.project']).$promise
+ ]
+ .state 'app.public.projects_show',
+ url: '/projects/:id'
+ views:
+ 'main@':
+ templateUrl: '<%= asset_path "projects/show.html" %>'
+ controller: 'ShowProjectController'
+ resolve:
+ projectPromise: ['$stateParams', 'Project', ($stateParams, Project)->
+ Project.get(id: $stateParams.id).$promise
+ ]
+ translations: [ 'Translations', (Translations) ->
+ Translations.query('app.public.projects_show').$promise
+ ]
+ .state 'app.logged.projects_edit',
+ url: '/projects/:id/edit'
+ views:
+ 'main@':
+ templateUrl: '<%= asset_path "projects/edit.html" %>'
+ controller: 'EditProjectController'
+ resolve:
+ projectPromise: ['$stateParams', 'Project', ($stateParams, Project)->
+ Project.get(id: $stateParams.id).$promise
+ ]
+ translations: [ 'Translations', (Translations) ->
+ Translations.query(['app.logged.projects_edit', 'app.shared.project']).$promise
+ ]
- # events
- .state 'app.public.events_list',
- url: '/events'
- views:
- 'main@':
- templateUrl: '<%= asset_path "events/index.html" %>'
- controller: 'eventsController'
- .state 'app.public.events_show',
- url: '/events/:id'
- views:
- 'main@':
- templateUrl: '<%= asset_path "events/show.html" %>'
- controller: 'showEventController'
+ # machines
+ .state 'app.public.machines_list',
+ url: '/machines'
+ views:
+ 'main@':
+ templateUrl: '<%= asset_path "machines/index.html" %>'
+ controller: 'MachinesController'
+ resolve:
+ machinesPromise: ['Machine', (Machine)->
+ Machine.query().$promise
+ ]
+ translations: [ 'Translations', (Translations) ->
+ Translations.query(['app.public.machines_list', 'app.shared.training_reservation_modal', 'app.shared.request_training_modal']).$promise
+ ]
+ .state 'app.admin.machines_new',
+ url: '/machines/new'
+ views:
+ 'main@':
+ templateUrl: '<%= asset_path "machines/new.html" %>'
+ controller: 'NewMachineController'
+ resolve:
+ translations: [ 'Translations', (Translations) ->
+ Translations.query(['app.admin.machines_new', 'app.shared.machine']).$promise
+ ]
+ .state 'app.public.machines_show',
+ url: '/machines/:id'
+ views:
+ 'main@':
+ templateUrl: '<%= asset_path "machines/show.html" %>'
+ controller: 'ShowMachineController'
+ resolve:
+ machinePromise: ['Machine', '$stateParams', (Machine, $stateParams)->
+ Machine.get(id: $stateParams.id).$promise
+ ]
+ translations: [ 'Translations', (Translations) ->
+ Translations.query(['app.public.machines_show', 'app.shared.training_reservation_modal', 'app.shared.request_training_modal']).$promise
+ ]
+ .state 'app.logged.machines_reserve',
+ url: '/machines/:id/reserve'
+ views:
+ 'main@':
+ templateUrl: '<%= asset_path "machines/reserve.html" %>'
+ controller: 'ReserveMachineController'
+ resolve:
+ plansPromise: ['Plan', (Plan)->
+ Plan.query(attributes_requested: "['machines_credits']").$promise
+ ]
+ groupsPromise: ['Group', (Group)->
+ Group.query().$promise
+ ]
+ settingsPromise: ['Setting', (Setting)->
+ Setting.query(names: "['machine_explications_alert',
+ 'booking_window_start',
+ 'booking_window_end',
+ 'booking_move_enable',
+ 'booking_move_delay',
+ 'booking_cancel_enable',
+ 'booking_cancel_delay',
+ 'subscription_explications_alert']").$promise
+ ]
+ translations: [ 'Translations', (Translations) ->
+ Translations.query(['app.logged.machines_reserve', 'app.shared.plan_subscribe', 'app.shared.member_select',
+ 'app.shared.stripe', 'app.shared.valid_reservation_modal', 'app.shared.confirm_modify_slot_modal']).$promise
+ ]
+ .state 'app.admin.machines_edit',
+ url: '/machines/:id/edit'
+ views:
+ 'main@':
+ templateUrl: '<%= asset_path "machines/edit.html" %>'
+ controller: 'EditMachineController'
+ resolve:
+ machinePromise: ['Machine', '$stateParams', (Machine, $stateParams)->
+ Machine.get(id: $stateParams.id).$promise
+ ]
+ translations: [ 'Translations', (Translations) ->
+ Translations.query(['app.admin.machines_edit', 'app.shared.machine']).$promise
+ ]
+
+ # trainings
+ .state 'app.logged.trainings_reserve',
+ url: '/trainings/reserve'
+ views:
+ 'main@':
+ templateUrl: '<%= asset_path "trainings/reserve.html" %>'
+ controller: 'ReserveTrainingController'
+ resolve:
+ explicationAlertPromise: ['Setting', (Setting)->
+ Setting.get(name: 'training_explications_alert').$promise
+ ]
+ plansPromise: ['Plan', (Plan)->
+ Plan.query(attributes_requested: "['trainings_credits']").$promise
+ ]
+ groupsPromise: ['Group', (Group)->
+ Group.query().$promise
+ ]
+ availabilityTrainingsPromise: ['Availability', (Availability)->
+ Availability.trainings().$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',
+ 'training_explications_alert',
+ 'training_information_message']").$promise
+ ]
+ translations: [ 'Translations', (Translations) ->
+ Translations.query(['app.logged.trainings_reserve', 'app.shared.plan_subscribe', 'app.shared.member_select',
+ 'app.shared.stripe', 'app.shared.valid_reservation_modal', 'app.shared.confirm_modify_slot_modal']).$promise
+ ]
+ # notifications
+ .state 'app.logged.notifications',
+ url: '/notifications'
+ views:
+ 'main@':
+ templateUrl: '<%= asset_path "notifications/index.html" %>'
+ controller: 'NotificationsController'
+ resolve:
+ translations: [ 'Translations', (Translations) ->
+ Translations.query('app.logged.notifications').$promise
+ ]
+
+ # pricing
+ .state 'app.public.plans',
+ url: '/plans'
+ abstract: Fablab.withoutPlans
+ views:
+ 'main@':
+ templateUrl: '<%= asset_path "plans/index.html" %>'
+ controller: 'PlansIndexController'
+ resolve:
+ subscriptionExplicationsPromise: ['Setting', (Setting)->
+ Setting.get(name: 'subscription_explications_alert').$promise
+ ]
+ plansPromise: ['Plan', (Plan)->
+ Plan.query(shallow: true).$promise
+ ]
+ groupsPromise: ['Group', (Group)->
+ Group.query().$promise
+ ]
+ translations: [ 'Translations', (Translations) ->
+ Translations.query(['app.public.plans', 'app.shared.member_select', 'app.shared.stripe']).$promise
+ ]
+
+ # events
+ .state 'app.public.events_list',
+ url: '/events'
+ views:
+ 'main@':
+ templateUrl: '<%= asset_path "events/index.html" %>'
+ controller: 'EventsController'
+ resolve:
+ translations: [ 'Translations', (Translations) ->
+ Translations.query('app.public.events_list').$promise
+ ]
+ .state 'app.public.events_show',
+ url: '/events/:id'
+ views:
+ 'main@':
+ templateUrl: '<%= asset_path "events/show.html" %>'
+ controller: 'ShowEventController'
+ resolve:
+ eventPromise: ['Event', '$stateParams', (Event, $stateParams)->
+ Event.get(id: $stateParams.id).$promise
+ ]
+ reducedAmountAlert: ['Setting', (Setting)->
+ Setting.get(name: 'event_reduced_amount_alert').$promise
+ ]
+ translations: [ 'Translations', (Translations) ->
+ Translations.query(['app.public.events_show', 'app.shared.member_select', 'app.shared.stripe', 'app.shared.valid_reservation_modal']).$promise
+ ]
+
+ # --- namespace /admin/... ---
+ # calendar
+ .state 'app.admin.calendar',
+ url: '/admin/calendar'
+ views:
+ 'main@':
+ templateUrl: '<%= asset_path "admin/calendar/calendar.html" %>'
+ controller: 'AdminCalendarController'
+ resolve:
+ availabilitiesPromise: ['Availability', (Availability)->
+ Availability.query().$promise
+ ]
+ bookingWindowStart: ['Setting', (Setting)->
+ Setting.get(name: 'booking_window_start').$promise
+ ]
+ bookingWindowEnd: ['Setting', (Setting)->
+ Setting.get(name: 'booking_window_end').$promise
+ ]
+ machinesPromise: ['Machine', (Machine) ->
+ Machine.query().$promise
+ ]
+ translations: [ 'Translations', (Translations) ->
+ Translations.query('app.admin.calendar').$promise
+ ]
+
+ # project's elements
+ .state 'app.admin.project_elements',
+ url: '/admin/project_elements'
+ views:
+ 'main@':
+ templateUrl: '<%= asset_path "admin/project_elements/index.html" %>'
+ controller: 'ProjectElementsController'
+ resolve:
+ componentsPromise: ['Component', (Component)->
+ Component.query().$promise
+ ]
+ licencesPromise: ['Licence', (Licence)->
+ Licence.query().$promise
+ ]
+ themesPromise: ['Theme', (Theme)->
+ Theme.query().$promise
+ ]
+ translations: [ 'Translations', (Translations) ->
+ Translations.query('app.admin.project_elements').$promise
+ ]
+
+ # trainings
+ .state 'app.admin.trainings',
+ url: '/admin/trainings'
+ views:
+ 'main@':
+ templateUrl: '<%= asset_path "admin/trainings/index.html" %>'
+ controller: 'TrainingsController'
+ resolve:
+ trainingsPromise: ['Training', (Training)->
+ Training.query().$promise
+ ]
+ machinesPromise: ['Machine', (Machine)->
+ Machine.query().$promise
+ ]
+ translations: [ 'Translations', (Translations) ->
+ Translations.query('app.admin.trainings').$promise
+ ]
+
+ # events
+ .state 'app.admin.events',
+ url: '/admin/events'
+ views:
+ 'main@':
+ templateUrl: '<%= asset_path "admin/events/index.html" %>'
+ controller: 'AdminEventsController'
+ resolve:
+ eventsPromise: ['Event', (Event)->
+ Event.query(page: 1).$promise
+ ]
+ translations: [ 'Translations', (Translations) ->
+ Translations.query('app.admin.events').$promise
+ ]
+ .state 'app.admin.events_new',
+ url: '/admin/events/new'
+ views:
+ 'main@':
+ templateUrl: '<%= asset_path "events/new.html" %>'
+ controller: 'NewEventController'
+ resolve:
+ translations: [ 'Translations', (Translations) ->
+ Translations.query(['app.admin.events_new', 'app.shared.event']).$promise
+ ]
+ .state 'app.admin.events_edit',
+ url: '/admin/events/:id/edit'
+ views:
+ 'main@':
+ templateUrl: '<%= asset_path "events/edit.html" %>'
+ controller: 'EditEventController'
+ resolve:
+ eventPromise: ['Event', '$stateParams', (Event, $stateParams)->
+ Event.get(id: $stateParams.id).$promise
+ ]
+ translations: [ 'Translations', (Translations) ->
+ Translations.query(['app.admin.events_edit', 'app.shared.event']).$promise
+ ]
+ .state 'app.admin.event_reservations',
+ url: '/admin/events/:id/reservations'
+ views:
+ 'main@':
+ templateUrl: '<%= asset_path "admin/events/reservations.html" %>'
+ controller: 'ShowEventReservationsController'
+ resolve:
+ eventPromise: ['Event', '$stateParams', (Event, $stateParams)->
+ Event.get(id: $stateParams.id).$promise
+ ]
+ reservationsPromise: ['Reservation', '$stateParams', (Reservation, $stateParams)->
+ Reservation.query(reservable_id: $stateParams.id, reservable_type: 'Event').$promise
+ ]
+ translations: [ 'Translations', (Translations) ->
+ Translations.query('app.admin.event_reservations').$promise
+ ]
+
+ # pricing
+ .state 'app.admin.pricing',
+ url: '/admin/pricing'
+ views:
+ 'main@':
+ templateUrl: '<%= asset_path "admin/pricing/index.html" %>'
+ controller: 'EditPricingController'
+ resolve:
+ plans: ['Plan', (Plan) ->
+ Plan.query().$promise
+ ]
+ groups: ['Group', (Group) ->
+ Group.query().$promise
+ ]
+ machinesPricesPromise: ['Price', (Price)->
+ Price.query(priceable_type: 'Machine', plan_id: 'null').$promise
+ ]
+ trainingsPricingsPromise: ['TrainingsPricing', (TrainingsPricing)->
+ TrainingsPricing.query().$promise
+ ]
+ translations: [ 'Translations', (Translations) ->
+ Translations.query('app.admin.pricing').$promise
+ ]
+
+ # plans
+ .state 'app.admin.plans',
+ abstract: true
+ resolve:
+ prices: ['Pricing', (Pricing) ->
+ Pricing.query().$promise
+ ]
+ machines: ['Machine', (Machine) ->
+ Machine.query().$promise
+ ]
+ groups: ['Group', (Group) ->
+ Group.query().$promise
+ ]
+ plans: ['Plan', (Plan) ->
+ Plan.query().$promise
+ ]
+ partners: ['User', (User) ->
+ User.query({role: 'partner'}).$promise
+ ]
+ .state 'app.admin.plans.new',
+ url: '/admin/plans/new'
+ views:
+ 'main@':
+ templateUrl: '<%= asset_path "admin/plans/new.html" %>'
+ controller: 'NewPlanController'
+ resolve:
+ translations: [ 'Translations', (Translations) ->
+ Translations.query(['app.admin.plans.new', 'app.shared.plan']).$promise
+ ]
+ .state 'app.admin.plans.edit',
+ url: '/admin/plans/:id/edit'
+ views:
+ 'main@':
+ templateUrl: '<%= asset_path "admin/plans/edit.html" %>'
+ controller: 'EditPlanController'
+ resolve:
+ planPromise: ['Plan', '$stateParams', (Plan, $stateParams) ->
+ Plan.get({id: $stateParams.id}).$promise
+ ]
+ translations: [ 'Translations', (Translations) ->
+ Translations.query(['app.admin.plans.edit', 'app.shared.plan']).$promise
+ ]
- # --- namespace /admin/... ---
- # project's elements
- .state 'app.admin.project_elements',
- url: '/admin/project_elements'
- views:
- 'main@':
- templateUrl: '<%= asset_path "admin/project_elements/index.html" %>'
- controller: 'projectElementsController'
+ # invoices
+ .state 'app.admin.invoices',
+ url: '/admin/invoices'
+ views:
+ 'main@':
+ templateUrl: '<%= asset_path "admin/invoices/index.html" %>'
+ controller: 'InvoicesController'
+ resolve:
+ settings: ['Setting', (Setting)->
+ Setting.query(names: "[
+ 'invoice_legals',
+ 'invoice_text',
+ 'invoice_VAT-rate',
+ 'invoice_VAT-active',
+ 'invoice_order-nb',
+ 'invoice_code-value',
+ 'invoice_code-active',
+ 'invoice_reference',
+ 'invoice_logo'
+ ]").$promise
+ ]
+ translations: [ 'Translations', (Translations) ->
+ Translations.query('app.admin.invoices').$promise
+ ]
- # events
- .state 'app.admin.events',
- url: '/admin/events'
- views:
- 'main@':
- templateUrl: '<%= asset_path "admin/events/index.html" %>'
- controller: 'adminEventsController'
- .state 'app.admin.events_new',
- url: '/admin/events/new'
- views:
- 'main@':
- templateUrl: '<%= asset_path "events/new.html" %>'
- controller: 'newEventController'
- .state 'app.admin.events_edit',
- url: '/admin/events/:id/edit'
- views:
- 'main@':
- templateUrl: '<%= asset_path "events/edit.html" %>'
- controller: 'editEventController'
+ # members
+ .state 'app.admin.members',
+ url: '/admin/members'
+ views:
+ 'main@':
+ templateUrl: '<%= asset_path "admin/members/index.html" %>'
+ controller: 'AdminMembersController'
+ 'groups@app.admin.members':
+ templateUrl: '<%= asset_path "admin/groups/index.html" %>'
+ controller: 'GroupsController'
+ 'tags@app.admin.members':
+ templateUrl: '<%= asset_path "admin/tags/index.html" %>'
+ controller: 'TagsController'
+ 'authentification@app.admin.members':
+ templateUrl: '<%= asset_path "admin/authentications/index.html" %>'
+ controller: 'AuthentificationController'
+ resolve:
+ membersPromise: ['Member', (Member)->
+ Member.query({requested_attributes:'[profile,group,subscription]'}).$promise
+ ]
+ adminsPromise: ['Admin', (Admin)->
+ Admin.query().$promise
+ ]
+ groupsPromise: ['Group', (Group)->
+ Group.query().$promise
+ ]
+ tagsPromise: ['Tag', (Tag)->
+ Tag.query().$promise
+ ]
+ authProvidersPromise: ['AuthProvider', (AuthProvider)->
+ AuthProvider.query().$promise
+ ]
+ translations: [ 'Translations', (Translations) ->
+ Translations.query('app.admin.members').$promise
+ ]
+ .state 'app.admin.members_new',
+ url: '/admin/members/new'
+ views:
+ 'main@':
+ templateUrl: '<%= asset_path "admin/members/new.html" %>'
+ controller: 'NewMemberController'
+ resolve:
+ translations: [ 'Translations', (Translations) ->
+ Translations.query(['app.admin.members_new', 'app.shared.user', 'app.shared.user_admin']).$promise
+ ]
+ .state 'app.admin.members_edit',
+ url: '/admin/members/:id/edit'
+ views:
+ 'main@':
+ templateUrl: '<%= asset_path "admin/members/edit.html" %>'
+ controller: 'EditMemberController'
+ resolve:
+ memberPromise: ['Member', '$stateParams', (Member, $stateParams)->
+ Member.get(id: $stateParams.id).$promise
+ ]
+ tagsPromise: ['Tag', (Tag)->
+ Tag.query().$promise
+ ]
+ translations: [ 'Translations', (Translations) ->
+ Translations.query(['app.admin.members_edit', 'app.shared.user', 'app.shared.user_admin']).$promise
+ ]
+ .state 'app.admin.admins_new',
+ url: '/admin/admins/new'
+ views:
+ 'main@':
+ templateUrl: '<%= asset_path "admin/admins/new.html" %>'
+ controller: 'NewAdminController'
+ resolve:
+ translations: [ 'Translations', (Translations) ->
+ Translations.query('app.admin.admins_new').$promise
+ ]
- # members
- .state 'app.admin.members',
- url: '/admin/members'
- views:
- 'main@':
- templateUrl: '<%= asset_path "admin/members/index.html" %>'
- controller: 'membersController'
- .state 'app.admin.members_new',
- url: '/admin/members/new'
- views:
- 'main@':
- templateUrl: '<%= asset_path "admin/members/new.html" %>'
- controller: 'newMemberController'
- .state 'app.admin.members_edit',
- url: '/admin/members/:id/edit'
- views:
- 'main@':
- templateUrl: '<%= asset_path "admin/members/edit.html" %>'
- controller: 'editMemberController'
+ # authentification providers
+ .state 'app.admin.authentication_new',
+ url: '/admin/authentications/new'
+ views:
+ 'main@':
+ templateUrl: '<%= asset_path "admin/authentications/new.html" %>'
+ controller: 'NewAuthenticationController'
+ resolve:
+ mappingFieldsPromise: ['AuthProvider', (AuthProvider)->
+ AuthProvider.mapping_fields().$promise
+ ]
+ authProvidersPromise: ['AuthProvider', (AuthProvider)->
+ AuthProvider.query().$promise
+ ]
+ translations: [ 'Translations', (Translations) ->
+ Translations.query(['app.admin.authentication_new', 'app.shared.authentication', 'app.shared.oauth2']).$promise
+ ]
+ .state 'app.admin.authentication_edit',
+ url: '/admin/authentications/:id/edit'
+ views:
+ 'main@':
+ templateUrl: '<%= asset_path "admin/authentications/edit.html" %>'
+ controller: 'EditAuthenticationController'
+ resolve:
+ providerPromise: ['AuthProvider', '$stateParams', (AuthProvider, $stateParams)->
+ AuthProvider.get(id: $stateParams.id).$promise
+ ]
+ mappingFieldsPromise: ['AuthProvider', (AuthProvider)->
+ AuthProvider.mapping_fields().$promise
+ ]
+ translations: [ 'Translations', (Translations) ->
+ Translations.query(['app.admin.authentication_edit', 'app.shared.authentication', 'app.shared.oauth2']).$promise
+ ]
+
+
+
+ # statistics
+ .state 'app.admin.statistics',
+ url: '/admin/statistics'
+ views:
+ 'main@':
+ templateUrl: '<%= asset_path "admin/statistics/index.html" %>'
+ controller: 'StatisticsController'
+ resolve:
+ translations: [ 'Translations', (Translations) ->
+ Translations.query('app.admin.statistics').$promise
+ ]
+ .state 'app.admin.stats_graphs',
+ url: '/admin/statistics/evolution'
+ views:
+ 'main@':
+ templateUrl: '<%= asset_path "admin/statistics/graphs.html" %>'
+ controller: 'GraphsController'
+ resolve:
+ translations: [ 'Translations', (Translations) ->
+ Translations.query('app.admin.stats_graphs').$promise
+ ]
+
+ # configurations
+ .state 'app.admin.settings',
+ url: '/admin/settings'
+ views:
+ 'main@':
+ templateUrl: '<%= asset_path "admin/settings/index.html" %>'
+ controller: 'SettingsController'
+ resolve:
+ settingsPromise: ['Setting', (Setting)->
+ Setting.query(names: "[
+ 'twitter_name',
+ 'about_title',
+ 'about_body',
+ 'about_contacts',
+ 'home_blogpost',
+ 'machine_explications_alert',
+ 'training_explications_alert',
+ 'training_information_message',
+ 'subscription_explications_alert',
+ 'event_reduced_amount_alert',
+ 'booking_window_start',
+ 'booking_window_end',
+ 'booking_move_enable',
+ 'booking_move_delay',
+ 'booking_cancel_enable',
+ 'booking_cancel_delay',
+ 'main_color',
+ 'secondary_color',
+ 'fablab_name',
+ 'name_genre'
+ ]").$promise
+ ]
+ cguFile: ['CustomAsset', (CustomAsset) ->
+ CustomAsset.get({name: 'cgu-file'}).$promise
+ ]
+ cgvFile: ['CustomAsset', (CustomAsset) ->
+ CustomAsset.get({name: 'cgv-file'}).$promise
+ ]
+ faviconFile: ['CustomAsset', (CustomAsset) ->
+ CustomAsset.get({name: 'favicon-file'}).$promise
+ ]
+ translations: [ 'Translations', (Translations) ->
+ Translations.query('app.admin.settings').$promise
+ ]
]
diff --git a/app/assets/javascripts/services/_t.coffee b/app/assets/javascripts/services/_t.coffee
new file mode 100644
index 000000000..b282aa427
--- /dev/null
+++ b/app/assets/javascripts/services/_t.coffee
@@ -0,0 +1,6 @@
+'use strict'
+
+Application.Services.factory '_t', ["$filter", ($filter)->
+ (key, interpolation = undefined, options = undefined) ->
+ $filter('translate')(key, interpolation, options)
+]
diff --git a/app/assets/javascripts/services/abuse.coffee b/app/assets/javascripts/services/abuse.coffee
new file mode 100644
index 000000000..9445c6f14
--- /dev/null
+++ b/app/assets/javascripts/services/abuse.coffee
@@ -0,0 +1,8 @@
+'use strict'
+
+Application.Services.factory 'Abuse', ["$resource", ($resource)->
+ $resource "/api/abuses/:id",
+ {id: "@id"},
+ update:
+ method: 'PUT'
+]
diff --git a/app/assets/javascripts/services/admin.coffee b/app/assets/javascripts/services/admin.coffee
new file mode 100644
index 000000000..ce46ee2c5
--- /dev/null
+++ b/app/assets/javascripts/services/admin.coffee
@@ -0,0 +1,8 @@
+'use strict'
+
+Application.Services.factory 'Admin', ["$resource", ($resource)->
+ $resource "/api/admins/:id",
+ {id: "@id"},
+ query:
+ isArray: false
+]
diff --git a/app/assets/javascripts/services/authProvider.coffee b/app/assets/javascripts/services/authProvider.coffee
new file mode 100644
index 000000000..4acb0b731
--- /dev/null
+++ b/app/assets/javascripts/services/authProvider.coffee
@@ -0,0 +1,14 @@
+'use strict'
+
+Application.Services.factory 'AuthProvider', ["$resource", ($resource)->
+ $resource "/api/auth_providers/:id",
+ {id: "@id"},
+ update:
+ method: 'PUT'
+ mapping_fields:
+ method: 'GET'
+ url: '/api/auth_providers/mapping_fields'
+ active:
+ method: 'GET'
+ url: '/api/auth_providers/active'
+]
diff --git a/app/assets/javascripts/services/availability.coffee b/app/assets/javascripts/services/availability.coffee
new file mode 100644
index 000000000..c96ca8e7b
--- /dev/null
+++ b/app/assets/javascripts/services/availability.coffee
@@ -0,0 +1,21 @@
+'use strict'
+
+Application.Services.factory 'Availability', ["$resource", ($resource)->
+ $resource "/api/availabilities/:id",
+ {id: "@id"},
+ machine:
+ method: 'GET'
+ url: '/api/availabilities/machines/:machineId'
+ params: {machineId: "@machineId"}
+ isArray: true
+ reservations:
+ method: 'GET'
+ url: '/api/availabilities/:id/reservations'
+ isArray: true
+ trainings:
+ method: 'GET'
+ url: '/api/availabilities/trainings'
+ isArray: true
+ update:
+ method: 'PUT'
+]
diff --git a/app/assets/javascripts/services/credit.coffee b/app/assets/javascripts/services/credit.coffee
new file mode 100644
index 000000000..9e1aae2a6
--- /dev/null
+++ b/app/assets/javascripts/services/credit.coffee
@@ -0,0 +1,8 @@
+'use strict'
+
+Application.Services.factory 'Credit', ["$resource", ($resource)->
+ $resource "/api/credits/:id",
+ {id: "@id"},
+ update:
+ method: 'PUT'
+]
diff --git a/app/assets/javascripts/services/customAsset.coffee b/app/assets/javascripts/services/customAsset.coffee
new file mode 100644
index 000000000..65c8e64fc
--- /dev/null
+++ b/app/assets/javascripts/services/customAsset.coffee
@@ -0,0 +1,6 @@
+'use strict'
+
+Application.Services.factory 'CustomAsset', ["$resource", ($resource)->
+ $resource "/api/custom_assets/:name",
+ {name: "@name"}
+]
diff --git a/app/assets/javascripts/services/dialogs.coffee.erb b/app/assets/javascripts/services/dialogs.coffee.erb
index d10983921..0422624be 100644
--- a/app/assets/javascripts/services/dialogs.coffee.erb
+++ b/app/assets/javascripts/services/dialogs.coffee.erb
@@ -1,6 +1,6 @@
'use strict'
-Application.Services.factory 'dialogs', ["$modal", ($modal) ->
+Application.Services.factory 'dialogs', ["$uibModal", ($uibModal) ->
confirm: (options, success, error)->
defaultOpts =
templateUrl: '<%= asset_path "shared/confirm_modal.html" %>'
@@ -8,20 +8,20 @@ Application.Services.factory 'dialogs', ["$modal", ($modal) ->
resolve:
object: ->
title: 'Titre de confirmation'
- msg: 'Message de confiramtion'
- controller: ['$scope', '$modalInstance', '$state', 'object', ($scope, $modalInstance, $state, object) ->
+ msg: 'Message de confirmation'
+ controller: ['$scope', '$uibModalInstance', '$state', 'object', ($scope, $uibModalInstance, $state, object) ->
$scope.object = object
- $scope.ok = ->
- $modalInstance.close()
+ $scope.ok = (info) ->
+ $uibModalInstance.close( info )
$scope.cancel = ->
- $modalInstance.dismiss('cancel')
+ $uibModalInstance.dismiss('cancel')
]
angular.extend(defaultOpts, options) if angular.isObject options
- $modal.open defaultOpts
- .result['finally'](null).then ->
+ $uibModal.open defaultOpts
+ .result['finally'](null).then (info)->
if angular.isFunction(success)
- success()
- , ->
+ success(info)
+ , (reason)->
if angular.isFunction(error)
- error()
+ error(reason)
]
diff --git a/app/assets/javascripts/services/elastic.js.erb b/app/assets/javascripts/services/elastic.js.erb
new file mode 100644
index 000000000..7bed59acd
--- /dev/null
+++ b/app/assets/javascripts/services/elastic.js.erb
@@ -0,0 +1,3 @@
+Application.Services.service('es', function (esFactory) {
+ return esFactory({ host: window.location.origin });
+});
diff --git a/app/assets/javascripts/services/group.coffee b/app/assets/javascripts/services/group.coffee
index a4192f7d6..c444531e3 100644
--- a/app/assets/javascripts/services/group.coffee
+++ b/app/assets/javascripts/services/group.coffee
@@ -2,5 +2,7 @@
Application.Services.factory 'Group', ["$resource", ($resource)->
$resource "/api/groups/:id",
- {id: "@id"}
+ {id: "@id"},
+ update:
+ method: 'PUT'
]
diff --git a/app/assets/javascripts/services/invoice.coffee b/app/assets/javascripts/services/invoice.coffee
new file mode 100644
index 000000000..d2b3c34ff
--- /dev/null
+++ b/app/assets/javascripts/services/invoice.coffee
@@ -0,0 +1,8 @@
+'use strict'
+
+Application.Services.factory 'Invoice', ["$resource", ($resource)->
+ $resource "/api/invoices/:id",
+ {id: "@id"},
+ update:
+ method: 'PUT'
+]
diff --git a/app/assets/javascripts/services/member.coffee b/app/assets/javascripts/services/member.coffee
index 4d2a7afd3..bd1fbf8dc 100644
--- a/app/assets/javascripts/services/member.coffee
+++ b/app/assets/javascripts/services/member.coffee
@@ -3,9 +3,14 @@
Application.Services.factory 'Member', ["$resource", ($resource)->
$resource "/api/members/:id",
{id: "@id"},
+ update:
+ method: 'PUT'
lastSubscribed:
method: 'GET'
url: '/api/last_subscribed/:limit'
params: {limit: "@limit"}
isArray: true
+ merge:
+ method: 'PUT'
+ url: '/api/members/:id/merge'
]
diff --git a/app/assets/javascripts/services/plan.coffee b/app/assets/javascripts/services/plan.coffee
new file mode 100644
index 000000000..d761cf267
--- /dev/null
+++ b/app/assets/javascripts/services/plan.coffee
@@ -0,0 +1,8 @@
+'use strict'
+
+Application.Services.factory 'Plan', ["$resource", ($resource)->
+ $resource "/api/plans/:id",
+ {id: "@id"},
+ update:
+ method: 'PUT'
+]
diff --git a/app/assets/javascripts/services/price.coffee b/app/assets/javascripts/services/price.coffee
new file mode 100644
index 000000000..66b8270fe
--- /dev/null
+++ b/app/assets/javascripts/services/price.coffee
@@ -0,0 +1,14 @@
+'use strict'
+
+Application.Services.factory 'Price', ["$resource", ($resource)->
+ $resource "/api/prices/:id",
+ {},
+ query:
+ isArray: false
+ update:
+ method: 'PUT'
+ compute:
+ method: 'POST'
+ url: '/api/prices/compute'
+ isArray: false
+]
diff --git a/app/assets/javascripts/services/pricing.coffee b/app/assets/javascripts/services/pricing.coffee
new file mode 100644
index 000000000..a49cdb732
--- /dev/null
+++ b/app/assets/javascripts/services/pricing.coffee
@@ -0,0 +1,8 @@
+'use strict'
+
+Application.Services.factory 'Pricing', ["$resource", ($resource)->
+ $resource "/api/pricing",
+ {},
+ update:
+ method: 'PUT'
+]
diff --git a/app/assets/javascripts/services/project.coffee b/app/assets/javascripts/services/project.coffee
index 637f59b53..94471b2a0 100644
--- a/app/assets/javascripts/services/project.coffee
+++ b/app/assets/javascripts/services/project.coffee
@@ -7,4 +7,8 @@ Application.Services.factory 'Project', ["$resource", ($resource)->
method: 'GET'
url: '/api/projects/last_published'
isArray: true
+ search:
+ method: 'GET'
+ url: '/api/projects/search'
+ isArray: true
]
diff --git a/app/assets/javascripts/services/reservation.coffee b/app/assets/javascripts/services/reservation.coffee
new file mode 100644
index 000000000..8f216950e
--- /dev/null
+++ b/app/assets/javascripts/services/reservation.coffee
@@ -0,0 +1,8 @@
+'use strict'
+
+Application.Services.factory 'Reservation', ["$resource", ($resource)->
+ $resource "/api/reservations/:id",
+ {id: "@id"},
+ update:
+ method: 'PUT'
+]
diff --git a/app/assets/javascripts/services/setting.coffee b/app/assets/javascripts/services/setting.coffee
new file mode 100644
index 000000000..258781124
--- /dev/null
+++ b/app/assets/javascripts/services/setting.coffee
@@ -0,0 +1,10 @@
+'use strict'
+
+Application.Services.factory 'Setting', ["$resource", ($resource)->
+ $resource "/api/settings/:name",
+ {name: "@name"},
+ update:
+ method: 'PUT'
+ query:
+ isArray: false
+]
diff --git a/app/assets/javascripts/services/slot.coffee b/app/assets/javascripts/services/slot.coffee
new file mode 100644
index 000000000..0050b179c
--- /dev/null
+++ b/app/assets/javascripts/services/slot.coffee
@@ -0,0 +1,11 @@
+'use strict'
+
+Application.Services.factory 'Slot', ["$resource", ($resource)->
+ $resource "/api/slots/:id",
+ {id: "@id"},
+ update:
+ method: 'PUT'
+ cancel:
+ method: 'PUT'
+ url: '/api/slots/:id/cancel'
+]
diff --git a/app/assets/javascripts/services/statistics.coffee b/app/assets/javascripts/services/statistics.coffee
new file mode 100644
index 000000000..5e1c2969d
--- /dev/null
+++ b/app/assets/javascripts/services/statistics.coffee
@@ -0,0 +1,5 @@
+'use strict'
+
+Application.Services.factory 'Statistics', ["$resource", ($resource)->
+ $resource "/api/statistics"
+]
diff --git a/app/assets/javascripts/services/subscription.coffee b/app/assets/javascripts/services/subscription.coffee
new file mode 100644
index 000000000..5606fefb9
--- /dev/null
+++ b/app/assets/javascripts/services/subscription.coffee
@@ -0,0 +1,8 @@
+'use strict'
+
+Application.Services.factory 'Subscription', ["$resource", ($resource)->
+ $resource "/api/subscriptions/:id",
+ {id: "@id"},
+ update:
+ method: 'PUT'
+]
diff --git a/app/assets/javascripts/services/tag.coffee b/app/assets/javascripts/services/tag.coffee
new file mode 100644
index 000000000..ab1cf29ba
--- /dev/null
+++ b/app/assets/javascripts/services/tag.coffee
@@ -0,0 +1,8 @@
+'use strict'
+
+Application.Services.factory 'Tag', ["$resource", ($resource)->
+ $resource "/api/tags/:id",
+ {id: "@id"},
+ update:
+ method: 'PUT'
+]
diff --git a/app/assets/javascripts/services/training.coffee b/app/assets/javascripts/services/training.coffee
new file mode 100644
index 000000000..c62f277d0
--- /dev/null
+++ b/app/assets/javascripts/services/training.coffee
@@ -0,0 +1,8 @@
+'use strict'
+
+Application.Services.factory 'Training', ["$resource", ($resource)->
+ $resource "/api/trainings/:id",
+ {id: "@id"},
+ update:
+ method: 'PUT'
+]
diff --git a/app/assets/javascripts/services/trainings_pricing.coffee b/app/assets/javascripts/services/trainings_pricing.coffee
new file mode 100644
index 000000000..9e75a4165
--- /dev/null
+++ b/app/assets/javascripts/services/trainings_pricing.coffee
@@ -0,0 +1,8 @@
+'use strict'
+
+Application.Services.factory 'TrainingsPricing', ["$resource", ($resource)->
+ $resource "/api/trainings_pricings/:id",
+ {},
+ update:
+ method: 'PUT'
+]
diff --git a/app/assets/javascripts/services/translations.coffee b/app/assets/javascripts/services/translations.coffee
new file mode 100644
index 000000000..68abb76bf
--- /dev/null
+++ b/app/assets/javascripts/services/translations.coffee
@@ -0,0 +1,13 @@
+'use strict'
+
+Application.Services.factory 'Translations', ["$translatePartialLoader", "$translate", ($translatePartialLoader, $translate)->
+ return {
+ query: (stateName) ->
+ if angular.isArray (stateName)
+ angular.forEach stateName, (state) ->
+ $translatePartialLoader.addPart(state)
+ else
+ $translatePartialLoader.addPart(stateName)
+ $translate.refresh()
+ }
+]
diff --git a/app/assets/javascripts/services/user.coffee b/app/assets/javascripts/services/user.coffee
new file mode 100644
index 000000000..a93234915
--- /dev/null
+++ b/app/assets/javascripts/services/user.coffee
@@ -0,0 +1,8 @@
+'use strict'
+
+Application.Services.factory 'User', ["$resource", ($resource)->
+ $resource "/api/users",
+ {},
+ query:
+ isArray: false
+]
diff --git a/app/assets/stylesheets/app.base.scss b/app/assets/stylesheets/app.base.scss
index 6d415b0c2..4ff669009 100644
--- a/app/assets/stylesheets/app.base.scss
+++ b/app/assets/stylesheets/app.base.scss
@@ -16,7 +16,7 @@ h1, .page-title {
font-weight: 900;
}
h2 {
- color: $red;
+ //color: $red;
line-height: rem-calc(24);
font-weight: 900;
}
@@ -26,7 +26,7 @@ h5 {
display: inline-block;
position: relative;
line-height: rem-calc(18);
- color: $red;
+ //color: $red;
font-size: rem-calc(16);
&:after {
position: absolute;
@@ -35,7 +35,7 @@ h5 {
content: '';
width: 35%;
height: 1px;
- background-color: $red;
+ //background-color: $red;
}
}
@@ -43,12 +43,12 @@ h5 {
// -------------------------
a {
- color: $link-color;
+ //color: $link-color;
text-decoration: none;
}
a:hover,
a:focus {
- color: $link-hover-color;
+ //color: $link-hover-color;
text-decoration: none;
}
@@ -127,7 +127,8 @@ dd {
// transition:0.5s linear all;
// }
-[ui-view].ng-enter, [ui-view].ng-leave {
+// only for main content
+#content-main.ng-enter, #content-main.ng-leave {
position: absolute;
left: 0;
right: 0;
diff --git a/app/assets/stylesheets/app.buttons.scss b/app/assets/stylesheets/app.buttons.scss
index cf6f2299f..49f97dd1c 100644
--- a/app/assets/stylesheets/app.buttons.scss
+++ b/app/assets/stylesheets/app.buttons.scss
@@ -17,8 +17,8 @@
.btn-warning-full {
outline: 0;
text-transform: uppercase;
- border: 3px solid $yellow;
- background-color: $yellow;
+ //border: 3px solid $yellow;
+ //background-color: $yellow;
&:hover {
background-color: white;
}
@@ -41,4 +41,18 @@
.btn-inactive{
-webkit-box-shadow: none !important;
box-shadow: none !important;
-}
\ No newline at end of file
+}
+
+.btn-loading:after {
+ margin-left: 1em;
+ display: inline-block;
+ content: "\f110";
+ font-family: FontAwesome;
+ -webkit-animation:spin 4s linear infinite;
+ -moz-animation:spin 4s linear infinite;
+ animation:spin 4s linear infinite;
+}
+
+@-moz-keyframes spin { 100% { -moz-transform: rotate(360deg); } }
+@-webkit-keyframes spin { 100% { -webkit-transform: rotate(360deg); } }
+@keyframes spin { 100% { -webkit-transform: rotate(360deg); transform:rotate(360deg); } }
diff --git a/app/assets/stylesheets/app.colors.scss b/app/assets/stylesheets/app.colors.scss
index def64aeb0..41a7bcbd5 100644
--- a/app/assets/stylesheets/app.colors.scss
+++ b/app/assets/stylesheets/app.colors.scss
@@ -1,8 +1,9 @@
.bg-light { background-color: $brand-light; }
-.bg-red { background-color: $red; color: white; }
-.bg-red-dark { background-color: $red-dark; }
-.bg-yellow { background-color: $yellow !important; }
+//.bg-red { background-color: $red; color: white; }
+//.bg-red-dark { background-color: $red-dark; }
+//.bg-yellow { background-color: $yellow !important; }
+.bg-token { background-color: rgba(230, 208, 137, 0.49); }
.bg-machine { background-color: $beige; }
.bg-formation { background-color: $violet; }
.bg-atelier { background-color: $blue; }
@@ -30,7 +31,7 @@
.text-black-light { color: #424242 !important; }
.text-gray { color: #5a5a5a !important; }
.text-white { color: #fff !important; }
-.text-yellow { color: $yellow !important; }
+//.text-yellow { color: $yellow !important; }
.text-blue { color: $blue; }
.text-muted { color: $text-muted; }
.text-danger, .red { color: $red !important; }
diff --git a/app/assets/stylesheets/app.components.scss b/app/assets/stylesheets/app.components.scss
index aa38f91bf..f4b3b2076 100644
--- a/app/assets/stylesheets/app.components.scss
+++ b/app/assets/stylesheets/app.components.scss
@@ -7,11 +7,11 @@
font-weight: 600;
color: black;
}
- h1 {
- font-size: rem-calc(16); text-transform: uppercase;
+ h1 {
+ font-size: rem-calc(16); text-transform: uppercase;
}
h2 { font-weight: bold; }
- h3 { color: $red; }
+ //h3 { color: $red; }
h4 {
font-size: rem-calc(12);
margin: 8px 0;
@@ -50,13 +50,14 @@
left: 0;
right: 0;
margin: 0 auto;
+ max-height: 44px;
}
h1 {
margin: 25px 0 20px 0;
font-weight: bold;
text-transform: uppercase;
text-align: center;
- color: $red;
+ //color: $red;
}
}
@@ -129,14 +130,14 @@
}
.article-thumbnail {
- max-height: 400px;
+ // max-height: 400px;
overflow: hidden;
}
}
.label-staging {
position: absolute;
- top: 50px;
+ top: 50px;
}
.notification-open {
@@ -197,6 +198,7 @@
margin: 10px 0;
font-size: rem-calc(16);
text-transform: uppercase;
+ color: black;
}
.content {
padding: 15px 0;
@@ -206,7 +208,7 @@
display: inline-block;
background: white;
@include border-radius(50%);
- border: 3px solid $yellow;
+ border: 3px solid;// $yellow;
}
.price {
position: relative;
@@ -238,15 +240,15 @@
background-color: white;
padding-left: 30px;
padding-right: 30px;
- &:hover { background-color: $yellow; }
+ //&:hover { background-color: $yellow; }
}
}
}
.well {
&.well-warning {
- border-color: #ffdc4e;
- background-color: #ffdc4e;
+ //border-color: #ffdc4e;
+ //background-color: #ffdc4e;
@include border-radius(3px);
padding: 5px 10px;
}
@@ -324,10 +326,10 @@
.block-link {
cursor: pointer;
- &:hover { background-color: $yellow; }
+ //&:hover { background-color: $yellow; }
}
-.form-control.form-control-ui-select .select2-choices .select2-search-choice {
+.form-control .ui-select-choices, .form-control .ui-select-match {
font-size: 85% !important;
}
@@ -351,16 +353,15 @@
.about-picture {
padding: 70px 0;
height: 326px;
- background: white asset-url("about-fablab.jpg") no-repeat;
background-size: cover;
margin-bottom: 30px;
}
- .about-title {
+ .about-title, .about-title p {
margin: 0;
font-size: rem-calc(50);
line-height: rem-calc(48);
color: #fff;
- font-weight: 900; //black
+ font-weight: 900; //black
}
.about-title-aside {
@@ -393,7 +394,7 @@
}
.event:hover {
-background-color: #cb1117;
+//background-color: #cb1117;
color: white;
}
@@ -441,3 +442,25 @@ padding: 10px;
}
}
+
+// angular-bootstrap accordions (enlightened version)
+.light-accordion > .panel-heading {
+ padding-top: 0.2em;
+ padding-bottom: 0.2em;
+}
+.light-accordion > .panel-heading > .panel-title {
+ font-size: 12pt;
+}
+
+.app-generator {
+ position: absolute;
+ bottom: 0; right: 10px;
+ z-index: 100;
+ padding: 3px 15px;
+ border: 1px solid $border-color;
+ border-top-left-radius: 8px;
+ background: $bg-gray;
+ @media only screen and (max-width: 768px) {
+ display: none;
+ }
+}
diff --git a/app/assets/stylesheets/app.layout.scss b/app/assets/stylesheets/app.layout.scss
index ed17d7543..3fe0d7907 100644
--- a/app/assets/stylesheets/app.layout.scss
+++ b/app/assets/stylesheets/app.layout.scss
@@ -76,11 +76,17 @@
cursor: pointer;
color: black;
&:hover {
- background-color: $yellow;
+ //background-color: $yellow;
}
i:before { content: "\f177"; }
}
}
+ .heading-icon {
+ width: 100%;
+ padding: 35px 40%;
+ display: inline-block;
+ color: black;
+ }
.heading-title {
overflow: hidden;
height: 94px;
@@ -341,4 +347,131 @@ body.container{
}
}
}
+}
+
+.customMenuButton {
+ min-width: 15em;
+ max-width: 15em;
+ overflow-x: hidden;
+}
+
+.customMenuInput {
+ width:100% !important;
+}
+
+
+.reservation-canceled {
+ color: #606060;
+ border-radius: 0.2em;
+ background-color: #e4e4e4;
+ padding: 0.7em 0.7em;
+ font-size: 90%;
+ display:inline-block;
+ vertical-align:middle;
+
+ .reservation-time {
+ color: #606060;
+ }
+
+ &:before {
+ content: "Annulée";
+ display: inline-block;
+ background-color: #c44242;
+ border-radius: 0.25em;
+ padding: 0.1em 0.5em;
+ font-weight: bold;
+ color: #fff;
+ float: left;
+ margin-right: 1em;
+ }
+
+}
+
+.custom-logo-container {
+ max-width: 240px;
+ height: 100%;
+
+ .custom-logo {
+ height: 100px;
+ width: 100%;
+ position: relative;
+ background-size: cover;
+ background-repeat: no-repeat;
+ border: 1px dashed #c4c4c4;
+ border-radius: 0.7em;
+ padding: 1.6em;
+ margin-left: 1em;
+
+ img {
+ display: block;
+ width: auto;
+ max-height: 44px;
+ max-width: 100%;
+ margin:auto;
+ }
+
+ &:hover .tools-box {
+ opacity: 1;
+ }
+
+ .tools-box {
+ opacity: 0;
+ position: absolute;
+ bottom: 10px;
+ left: 0;
+ right: 0;
+ text-align: center;
+ }
+ }
+
+ .bg-dark {
+ background-color: #000;
+ opacity: 0.9;
+ }
+}
+
+.custom-favicon-container {
+ max-width: 70px;
+ height: 100%;
+
+ .custom-favicon {
+ height: 70px;
+ width: 100%;
+ position: relative;
+ background-size: cover;
+ background-repeat: no-repeat;
+ border: 1px dashed #c4c4c4;
+ border-radius: 0.7em;
+ padding: 1.6em;
+ margin-left: 1em;
+
+ img {
+ display: block;
+ width: auto;
+ max-height: 16px;
+ max-width: 16px;
+ margin:auto;
+ }
+
+ &:hover .tools-box {
+ opacity: 1;
+ }
+
+ .tools-box {
+ opacity: 0;
+ position: absolute;
+ bottom: -7px;
+ left: 51px;
+ right: 0;
+ text-align: center;
+ }
+ }
+}
+
+.flash-message {
+ position: absolute;
+ top: 1%;
+ z-index: 1001;
+ width: 33%;
+ left: 33%;
}
\ No newline at end of file
diff --git a/app/assets/stylesheets/app.nav.scss b/app/assets/stylesheets/app.nav.scss
index 85ce4d539..097b83f52 100644
--- a/app/assets/stylesheets/app.nav.scss
+++ b/app/assets/stylesheets/app.nav.scss
@@ -425,7 +425,7 @@
#nav {
// border-right: 1px solid $red-dark;
.nav {
- background-color: $red;
+ //background-color: $red;
> li {
> a {
padding: 13px 17px;
@@ -433,11 +433,11 @@
color: white;
&:hover,
&:focus, &.active {
- background-color: $red-light;
+ //background-color: $red-light;
color: white;
}
&.active {
- border-left: 3px solid #870003;
+ border-left: 3px solid;// #870003;
}
}
}
diff --git a/app/assets/stylesheets/app.plugins.scss b/app/assets/stylesheets/app.plugins.scss
index 08c4b3241..9d39c291c 100644
--- a/app/assets/stylesheets/app.plugins.scss
+++ b/app/assets/stylesheets/app.plugins.scss
@@ -1,37 +1,108 @@
+// medium editor placeholder
+.medium-editor-placeholder {
+ min-height: 30px; // fix for firefox
+}
+
+//xeditable
+.editable-buttons{
+ button[type=submit].btn-primary{
+ @extend .btn-warning;
+ }
+}
+
//summernote
-
.note-editor .note-editable {
background-color: white;
}
+
// Growl
.growl {
top: 90px;
z-index: 1100;
}
+// fullcalendar
-
-// UI Select
-
-.form-control {
- &.form-control-ui-select {
- height: auto;
- .select2-choices {
- border: none;
- background: transparent;
- .select2-search-choice {
- @extend .label;
- padding-left: .9em;
- font-size: 100%;
- font-weight: normal;
-
- }
- }
- }
+.fc-view-container .fc-body tr {
+ height: 40px !important;
}
+.fc-toolbar {
+ height: 40px;
+ background-color: #fff;
+}
+
+.fc-toolbar .fc-button {
+ background: #F2F2F2;
+ border: none;
+ box-shadow: none;
+ text-shadow: none;
+ margin: 0;
+ height: 40px;
+ line-height: 18px;
+ padding: 10px;
+ //&:hover, &:active, &.fc-state-active { background-color: $yellow; }
+}
+
+.fc-toolbar h2 {
+ font-size: 15px;
+ line-height: 40px;
+ margin: 0;
+}
+
+.fc-view-container .fc-widget-header,
+.fc-view-container .fc-widget-content {
+ border-color: #e8e8e8;
+ font-weight: normal;
+}
+
+.fc-content-skeleton .fc-event {
+ padding: 2px;
+ border-left: solid 3px;
+}
+
+.fc-event {
+ -webkit-box-sizing: content-box;
+ -moz-box-sizing: content-box;
+ box-sizing: content-box;
+}
+
+.fc-event .fc-time span, .fc-event .fc-title {
+ font-size: rem-calc(10);
+ line-height: rem-calc(12);
+}
+
+.fc-event .fc-time span.label {
+ font-size: rem-calc(8);
+ margin-left: 0.7em;
+}
+
+// croix de suppression pour un créneau de disponibilité
+.remove-event {
+ position: absolute;
+ float: right;
+ right: 0;
+ top: 0;
+ padding: 0;
+ font-size: 11px;
+ color: black;
+ cursor: pointer;
+ z-index: 9999;
+ text-align: right;
+ .training-reserve &, .machine-reserve & { display: none; }
+}
+
+.fc-v-event.fc-end {
+ border-bottom-width: 2px;
+}
+
+.fc-divider {
+ display: none !important;
+}
+
+
@@ -59,6 +130,7 @@
line-height: rem-calc(24);
color: white;
font-weight: 800;
+ text-align: center;
a {
color: white;
&:hover { color: $yellow; }
@@ -77,7 +149,7 @@
background: none;
@include border-radius($border-radius-base);
&:hover, &:focus {
- color: $yellow;
+ //color: $yellow;
}
.glyphicon-chevron-left {
@@ -99,6 +171,31 @@
display: none;
}
+// .carousel-control {
+// // position: absolute;
+// display: block;
+// margin-bottom: -20px;
+// padding: 20px;
+// color: white;
+// width: 58px;
+// height: 58px;
+// border: 3px solid white;
+// border-radius: 50%;
+
+// .glyphicon-chevron-right:before {
+// // //Reset the icon
+// // content: " ";
+// // //Give layout
+// // display:block;
+// // //Your image as background
+// // background:url('http://yourarrow.png') no-repeat;
+// // //To show full image set the dimensions
+// // width:30px;
+// // height:30px;
+// }
+// }
+
+
.banner { }
diff --git a/app/assets/stylesheets/app.printer.scss b/app/assets/stylesheets/app.printer.scss
index f87ded6ed..5a7e2c1db 100644
--- a/app/assets/stylesheets/app.printer.scss
+++ b/app/assets/stylesheets/app.printer.scss
@@ -1,3 +1,3 @@
/*
- * Require here your print media stylesheets
+ *= require fullcalendar/dist/fullcalendar.print
*/
\ No newline at end of file
diff --git a/app/assets/stylesheets/app.utilities.scss b/app/assets/stylesheets/app.utilities.scss
index 2e077f4b5..962289652 100644
--- a/app/assets/stylesheets/app.utilities.scss
+++ b/app/assets/stylesheets/app.utilities.scss
@@ -111,7 +111,7 @@ p, .widget p {
.b{border: 1px solid rgba(0, 0, 0, 0.05)}
.b-a{border: 1px solid $border-color}
.b-t{border-top: 1px solid $border-color}
-.b-r{border-right: 1px solid $border-color}
+.b-r{border-right: 1px solid $border-color !important;}
.b-b{border-bottom: 1px solid $border-color}
.b-l{border-left: 1px solid $border-color}
.b-light{border-color: darken($brand-light, 5%)}
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index 014cf9206..bb4619d2b 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -1,17 +1,23 @@
/*
*= require_self
- *= require select2/select2
+ *= require angular-ui-select/dist/select
+ *= require fullcalendar/dist/fullcalendar
*= require jasny-bootstrap/dist/css/jasny-bootstrap
- *= require angular-growl/build/angular-growl.min.css
+ *= require angular-growl-v2/build/angular-growl
*= require angular-xeditable/dist/css/xeditable
*= require angular-loading-bar/src/loading-bar
+ *= require nvd3/build/nv.d3
*= require font-awesome
+ *= require medium-editor/dist/css/medium-editor
+ *= require medium-editor/dist/css/themes/default
+ *= require bootstrap-switch/dist/css/bootstrap3/bootstrap-switch.min
*= require summernote/dist/summernote
+ *= require jquery-minicolors/jquery.minicolors.css
*/
-
-
@import "app.functions";
+@import "bootstrap-compass";
+@import "bootstrap-sprockets";
@import "compass";
@import "bootstrap_and_overrides";
@@ -25,5 +31,6 @@
@import "app.buttons";
@import "app.components";
@import "app.plugins";
+@import "modules/invoice";
@import "app.responsive";
diff --git a/app/assets/stylesheets/bootstrap_and_overrides.scss b/app/assets/stylesheets/bootstrap_and_overrides.scss
index aa27906b0..6dda4f54d 100644
--- a/app/assets/stylesheets/bootstrap_and_overrides.scss
+++ b/app/assets/stylesheets/bootstrap_and_overrides.scss
@@ -78,10 +78,10 @@ $link-hover-decoration: underline;
// Semibold = 600, Bold = 700, ExtraB = 800
-$font-family-sans-serif: "Open Sans", Helvetica, Arial, sans-serif !default;
-$font-proxima-condensed: "Open Sans Condensed", Helvetica, Arial, sans-serif !default;
-$font-family-serif: Georgia, "Times New Roman", Times, serif !default;
-$font-felt: "Loved by the King", sans-serif;
+$font-family-sans-serif: 'proxima-nova', 'Open Sans', Helvetica, Arial, sans-serif !default;
+$font-proxima-condensed: 'proxima-nova-condensed', 'Open Sans Condensed', Helvetica, Arial, sans-serif !default;
+$font-family-serif: Georgia, 'Times New Roman', Times, serif !default;
+$font-felt: 'felt-tip-roman', 'Loved by the King', cursive, sans-serif;
//** Default monospace fonts for ``, ``, and ``.
// $font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace !default;
@@ -952,16 +952,16 @@ $hr-border: $gray-lighter !default;
@import "bootstrap/input-groups";
@import "bootstrap/navs";
@import "bootstrap/navbar";
-// @import "bootstrap/breadcrumbs";
+@import "bootstrap/breadcrumbs";
@import "bootstrap/pagination";
-// @import "bootstrap/pager";
+@import "bootstrap/pager";
@import "bootstrap/labels";
@import "bootstrap/badges";
-//@import "bootstrap/jumbotron";
+@import "bootstrap/jumbotron";
@import "bootstrap/thumbnails";
@import "bootstrap/alerts";
@import "bootstrap/progress-bars";
-// @import "bootstrap/media";
+@import "bootstrap/media";
@import "bootstrap/list-group";
@import "bootstrap/panels";
@import "bootstrap/responsive-embed";
diff --git a/app/assets/stylesheets/modules/invoice.scss b/app/assets/stylesheets/modules/invoice.scss
new file mode 100644
index 000000000..452138f54
--- /dev/null
+++ b/app/assets/stylesheets/modules/invoice.scss
@@ -0,0 +1,181 @@
+
+// admin invoices
+
+.invoice-placeholder {
+ width: 80%;
+ max-width: 800px;
+ height: 100%;
+ margin: auto;
+ margin-top: 2em;
+ border-width: 1px;
+ border-style: solid;
+ border-color: #aeaeae #979797 #7b7b7b;
+ box-shadow: 2px 3px 6px 0 #898989,
+ -2px 3px 6px 0 #898989;
+ padding: 2em;
+
+ .invoice-buyer-infos {
+ float: right;
+ text-align: right;
+ margin-left: 1em;
+ }
+
+ .invoice-logo {
+ height: 6em;
+ width: 100%;
+ position: relative;
+ background-size: cover;
+ background-repeat: no-repeat;
+
+ img {
+ display: block;
+ width: auto;
+ max-height: 100%;
+ max-width: 60%;
+ }
+
+ &:hover .tools-box {
+ opacity: 1;
+ }
+
+ .tools-box {
+ opacity: 0;
+ position: absolute;
+ bottom: 10px;
+ left: 0;
+ right: 0;
+ text-align: center;
+ }
+ }
+
+ .invoice-object, .invoice-data, .invoice-data p, .invoice-text, .invoice-legals {
+ margin-top: 2em;
+ }
+
+ .invoice-data table {
+ width: 100%
+ }
+
+ .invoice-data tr, .invoice-data th, .invoice-data td {
+ border: 1px solid;
+ padding: 4px;
+ }
+
+ .invoice-text {
+ font-weight: bold;
+ }
+
+ .invoice-legals {
+ text-align: right;
+ }
+
+ .invoice-editable:hover {
+ background-color: $yellow;
+ overflow-x: hidden;
+ }
+
+ .invoice-activable {
+ font-style: italic;
+ color: #c4c4c4;
+ overflow-x: hidden;
+ }
+
+ .invoice-activable:hover {
+ background-color: $yellow;
+ border: 1px dashed #c4c4c4;
+ }
+
+
+ &:after {
+ content:"";
+ display:block;
+ margin-top:30%;
+ }
+
+ .vat-line {
+ background-color: #e4e4e4;
+ text-align: right;
+ }
+
+ .bold {
+ font-weight: bold;
+ }
+
+ .italic {
+ font-style: italic;
+ }
+
+ .right {
+ text-align: right;
+ }
+
+}
+
+.custom-invoice {
+ .modal-header {
+ @extend .modal-header;
+ // padding-left: 4em;
+ text-align: center;
+ background-color: #e6e6e6;
+ }
+
+ .modal-body {
+
+ .elements ul {
+ @extend .list-unstyled;
+ }
+
+ .elements li {
+ @extend .btn;
+ @extend .btn-default;
+ width: 100%;
+ }
+
+ table.invoice-element-legend {
+ min-width: 15em;
+ }
+
+ .invoice-element-legend tr, .invoice-element-legend th, .invoice-element-legend td {
+ border: 1px solid;
+ padding: 4px;
+ }
+
+ .bottom-notes {
+ font-style: italic;
+ }
+
+ }
+
+}
+
+
+.partial-avoir-table tr {
+ float:left;
+
+ .input-col { min-width: 2em; }
+ .label-col { min-width: 18em; }
+ .amount-col { min-width: 6em; }
+}
+
+.partial-avoir-selected-item {
+ background-color: $yellow;
+ display: block;
+ position: relative;
+
+ &:after {
+ content:"Rembourser";
+ display:inline-block;
+ position: absolute;
+ right: 0.8em;
+ top: 1.8em;
+ color: $red;
+ width: 6.7em;
+ transform: rotate(-22.5deg);
+ overflow: visible;
+ font-weight: bold;
+ border: 1px solid;
+ padding: 0 4px;
+ border-radius: 5px;
+ font-size: small;
+ }
+}
\ No newline at end of file
diff --git a/app/assets/templates/admin/admins/new.html.erb b/app/assets/templates/admin/admins/new.html.erb
new file mode 100644
index 000000000..a53506394
--- /dev/null
+++ b/app/assets/templates/admin/admins/new.html.erb
@@ -0,0 +1,34 @@
+
+
+
+
+
+ {{ 'add_an_administrator' }}
+
+
+
+
+
+
+
diff --git a/app/assets/templates/admin/authentications/_form.html.erb b/app/assets/templates/admin/authentications/_form.html.erb
new file mode 100644
index 000000000..efe2dc71b
--- /dev/null
+++ b/app/assets/templates/admin/authentications/_form.html.erb
@@ -0,0 +1,30 @@
+
+
+
\ No newline at end of file
diff --git a/app/assets/templates/admin/authentications/_oauth2.html.erb b/app/assets/templates/admin/authentications/_oauth2.html.erb
new file mode 100644
index 000000000..50d0300e8
--- /dev/null
+++ b/app/assets/templates/admin/authentications/_oauth2.html.erb
@@ -0,0 +1,93 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/assets/templates/admin/authentications/_oauth2_mapping.html.erb b/app/assets/templates/admin/authentications/_oauth2_mapping.html.erb
new file mode 100644
index 000000000..bb16e52f1
--- /dev/null
+++ b/app/assets/templates/admin/authentications/_oauth2_mapping.html.erb
@@ -0,0 +1,76 @@
+{{ 'define_the_fields_mapping' }}
+
+
+ {{ 'add_a_match' | translate }}
+
\ No newline at end of file
diff --git a/app/assets/templates/admin/authentications/edit.html.erb b/app/assets/templates/admin/authentications/edit.html.erb
new file mode 100644
index 000000000..43e282f9d
--- /dev/null
+++ b/app/assets/templates/admin/authentications/edit.html.erb
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+ {{ 'provider' | translate }} {{provider.name}}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/assets/templates/admin/authentications/index.html.erb b/app/assets/templates/admin/authentications/index.html.erb
new file mode 100644
index 000000000..d0d9b5313
--- /dev/null
+++ b/app/assets/templates/admin/authentications/index.html.erb
@@ -0,0 +1,40 @@
+
+
+
{{ 'add_a_new_authentication_provider' }}
+
+
+
+
+ {{ 'name' }}
+
+ {{ 'type' }}
+
+ {{ 'state' }}
+
+
+
+
+
+
+ {{ provider.name }}
+ {{ getType(provider.providable_type) }}
+ {{ getState(provider.status) }}
+
+
+ {{ 'edit' | translate }}
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/assets/templates/admin/authentications/new.html.erb b/app/assets/templates/admin/authentications/new.html.erb
new file mode 100644
index 000000000..bdc47246b
--- /dev/null
+++ b/app/assets/templates/admin/authentications/new.html.erb
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+ {{ 'add_a_new_authentication_provider' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/assets/templates/admin/calendar/calendar.html.erb b/app/assets/templates/admin/calendar/calendar.html.erb
new file mode 100644
index 000000000..7cde3e4dd
--- /dev/null
+++ b/app/assets/templates/admin/calendar/calendar.html.erb
@@ -0,0 +1,66 @@
+
+
+
+
+
+ {{ 'calendar_management' }}
+
+
+
+
+
+ {{ 'trainings' }}
+ {{ 'machines' }}
+
+
+
+
+
+
+
+
diff --git a/app/assets/templates/admin/calendar/eventModal.html.erb b/app/assets/templates/admin/calendar/eventModal.html.erb
new file mode 100644
index 000000000..6cc3bda96
--- /dev/null
+++ b/app/assets/templates/admin/calendar/eventModal.html.erb
@@ -0,0 +1,65 @@
+
+
+
{{ 'you_can_define_a_training_on_that_slot' }}
+
+
+ {{ 'link_a_training' | translate }}
+
+
+
+
{{ 'or_' }} {{ '_select_some_machines' | translate }}
+
+
+
+ {{machine.name}} (id {{machine.id}})
+
+
+
+
+
+
{{ 'adjust_the_opening_hours' }}
+
+
+
+
+
{{ 'to_time' }}
+
+
+
+
+
+
+
{{ 'restrict_this_slot_with_labels_(optional)' }}
+
+
+
+
diff --git a/app/assets/templates/admin/events/index.html.erb b/app/assets/templates/admin/events/index.html.erb
index fa538b729..d8bdf9dd8 100644
--- a/app/assets/templates/admin/events/index.html.erb
+++ b/app/assets/templates/admin/events/index.html.erb
@@ -7,13 +7,13 @@
- Les Stages et ateliers du Fab Lab
+ {{ 'fablab_courses_and_workshops' }}
@@ -24,43 +24,43 @@
- Tous les évènements
- Les évènements déjà passés
- Les évènements à venir
+ {{ 'all_events' }}
+ {{ 'passed_events' }}
+ {{ 'events_to_come' }}
- Titre
- Dates
+ {{ 'title' }}
+ {{ 'dates' }}
-
+
{{ event.title }}
- Du {{event.start_date | amDateFormat:'DD/MM/YYYY'}} au {{event.end_date | amDateFormat:'DD/MM/YYYY'}}
+ {{ 'from_DATE' | translate:{DATE:(event.start_date | amDateFormat:'LL')} }} {{ 'to_date' }} {{event.end_date | amDateFormat:'LL'}}
- Toute la journée
+ {{ 'all_day' }}
- De {{event.start_date | date:'HH:mm'}}
- à
- {{event.end_date | date:'HH:mm'}}
+ {{ 'from_TIME' | translate:{TIME:(event.start_date | amDateFormat:'LT')} }}
+ {{ 'to_time' }}
+ {{event.end_date | amDateFormat:'LT'}}
-
- Éditer
+
+ {{ 'view_reservations' | translate }}
+
+
+ {{ 'edit' | translate }}
- <%#
-
- %>
@@ -71,7 +71,7 @@
diff --git a/app/assets/templates/admin/events/reservations.html.erb b/app/assets/templates/admin/events/reservations.html.erb
new file mode 100644
index 000000000..f21bad55f
--- /dev/null
+++ b/app/assets/templates/admin/events/reservations.html.erb
@@ -0,0 +1,52 @@
+
+
+
+
+
+ {{ 'reservations' | translate }} {{event.title}}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ 'user' }}
+ {{ 'payment_date' }}
+ {{ 'reserved_tickets' }}
+
+
+
+
+
+
+ {{ reservation.user_full_name }}
+
+ {{ reservation.created_at | amDateFormat:'LL LTS' }}
+ {{ 'full_price_' | translate }} {{reservation.nb_reserve_places}} {{ 'reduced_rate_' | translate }} {{reservation.nb_reserve_reduced_places}}
+
+
+
+ {{ 'show_the_event' | translate }}
+
+
+
+
+
+
+
{{ 'no_reservations_for_now' }}
+
+
{{ 'back_to_monitoring' }}
+
+
+
+
diff --git a/app/assets/templates/admin/groups/index.html.erb b/app/assets/templates/admin/groups/index.html.erb
new file mode 100644
index 000000000..498c29c1c
--- /dev/null
+++ b/app/assets/templates/admin/groups/index.html.erb
@@ -0,0 +1,37 @@
+{{ 'add_a_group' }}
+
+
+
+ {{ 'group_name' }}
+
+
+
+
+
+
+
+ {{group.name}}
+
+
+
+
+
+
+
+ {{ 'edit' }}
+
+
+
+
+
+
+
+
+
diff --git a/app/assets/templates/admin/invoices/avoirModal.html.erb b/app/assets/templates/admin/invoices/avoirModal.html.erb
new file mode 100644
index 000000000..56e72b16c
--- /dev/null
+++ b/app/assets/templates/admin/invoices/avoirModal.html.erb
@@ -0,0 +1,60 @@
+
+
+
\ No newline at end of file
diff --git a/app/assets/templates/admin/invoices/index.html.erb b/app/assets/templates/admin/invoices/index.html.erb
new file mode 100644
index 000000000..4c96d3789
--- /dev/null
+++ b/app/assets/templates/admin/invoices/index.html.erb
@@ -0,0 +1,383 @@
+
+
+
+
+
+
+
+ {{ 'filter_invoices' | translate }}
+
+
+
+
+
+
+
+
{{ 'no_invoices_for_now' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ 'john_smith' }}
+
{{ 'john_smith@example_com' }}
+
+ {{ 'invoice_reference_' | translate }} {{mkReference()}}
+ {{ 'code_' | translate }} {{invoice.code.model}}
+ {{ 'code_disabled' }}
+ {{ 'order_#' | translate }} {{mkNumber()}}
+ {{ 'invoice_issued_on_DATE_at_TIME' | translate:{DATE:(today | amDateFormat:'L'), TIME:(today | amDateFormat:'LT')} }}
+
+ {{ 'object_reservation_of_john_smith_on_DATE_at_TIME' | translate:{DATE:(inOneWeek | amDateFormat:'L'), TIME:(inOneWeek | amDateFormat:'LT')} }}
+
+
+ {{ 'order_summary' | translate }}
+
+
+
+ {{ 'details' }}
+ {{ 'amount' }}
+
+
+
+
+ {{ 'machine_booking-3D_printer' | translate }} {{inOneWeek | amDateFormat:'LLL'}} - {{inOneWeekAndOneHour | amDateFormat:'LT'}}
+ {{30.0 | currency}}
+
+
+
+ {{ 'total_amount' }}
+ {{ 'total_including_all_taxes' }}
+ {{30.0 | currency}}
+
+
+
+ {{ 'VAT_disabled' }}
+
+
+
+
+ {{ 'including_VAT' | translate }} {{invoice.VAT.rate}} %
+ {{30*invoice.VAT.rate/100 | currency}}
+
+
+ {{ 'including_total_excluding_taxes' }}
+ {{30-(30*invoice.VAT.rate/100) | currency}}
+
+
+ {{ 'including_amount_payed_on_ordering' }}
+ {{30.0 | currency}}
+
+
+
+
+
+ {{ 'settlement_by_debit_card_on_DATE_at_TIME_for_an_amount_of_AMOUNT' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/assets/templates/admin/members/_form.html.erb b/app/assets/templates/admin/members/_form.html.erb
index 6de0a9c35..d7c1831d0 100644
--- a/app/assets/templates/admin/members/_form.html.erb
+++ b/app/assets/templates/admin/members/_form.html.erb
@@ -1,9 +1,61 @@
+
+
+
+
+
+
diff --git a/app/assets/templates/admin/members/edit.html.erb b/app/assets/templates/admin/members/edit.html.erb
index 3e26192af..92107c960 100644
--- a/app/assets/templates/admin/members/edit.html.erb
+++ b/app/assets/templates/admin/members/edit.html.erb
@@ -9,15 +9,15 @@
- Utilisateur : {{ user.name }}
+ {{ 'user' | translate }} {{ user.name }}
-
- Annuler
+
+ {{ 'cancel' }}
@@ -31,22 +31,191 @@
diff --git a/app/assets/templates/admin/members/index.html.erb b/app/assets/templates/admin/members/index.html.erb
index ed0a9e35e..3d0f6df69 100644
--- a/app/assets/templates/admin/members/index.html.erb
+++ b/app/assets/templates/admin/members/index.html.erb
@@ -7,7 +7,7 @@
- Liste des membres
+ {{ 'users_management' }}
@@ -16,56 +16,127 @@
-
-
-
-
Ajouter un nouveau membre
-
-
-
-
-
-
- Nom
-
- Prénom
-
- Email
-
- Tel.
-
- Type utilisateur
-
-
-
-
-
-
- {{ member.profile.last_name }}
- {{ member.profile.first_name }}
- {{ member.email }}
- {{ member.profile.phone }}
- {{ member.group.name }}
-
-
-
- Éditer
-
-
-
-
-
-
-
diff --git a/app/assets/templates/admin/members/new.html.erb b/app/assets/templates/admin/members/new.html.erb
index 9a141a64b..30c049de1 100644
--- a/app/assets/templates/admin/members/new.html.erb
+++ b/app/assets/templates/admin/members/new.html.erb
@@ -9,15 +9,15 @@
- Ajouter un membre
+ {{ 'add_a_member' }}
-
- Annuler
+
+ {{ 'cancel' }}
@@ -42,7 +42,7 @@
diff --git a/app/assets/templates/admin/plans/_form.html.erb b/app/assets/templates/admin/plans/_form.html.erb
new file mode 100644
index 000000000..03d38582d
--- /dev/null
+++ b/app/assets/templates/admin/plans/_form.html.erb
@@ -0,0 +1,158 @@
+
{{ 'general_informations' }}
+
+
+
+ {{ 'name' | translate }} *
+
+ {{ 'name_is_required' }}
+ {{ 'name_length_must_be_less_than_24_characters' }}
+
+
+ {{ 'type' | translate }} *
+
+ {{ 'standard' }}
+ {{ 'partner' }}
+
+ {{ 'type_is_required' }}
+
+
+ {{ 'group' | translate }} *
+
+ {{ 'transversal_(all_groups)' }}
+
+ {{group.name}}
+
+
+ {{ 'group_is_required' }}
+
+
+
+ {{ 'period' | translate }} *
+
+ {{ 'month' }}
+ {{ 'year' }}
+
+ {{ 'period_is_required' }}
+
+
+
+ {{ 'number_of_periods' | translate }} *
+
+ {{ 'number_of_periods_is_required' }}
+
+
+
+
+
+ {{ 'visual_prominence_of_the_subscription' }}
+
+
+ {{ '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 }}
+
+
+
+
+ {{ 'rolling_subscription' | translate }} *
+
+ {{ (plan.is_rolling ? 'yes' : 'no') | translate }}
+
+
+ {{ 'a_rolling_subscription_will_begin_the_day_of_the_first_training' | translate }}
+ {{ 'otherwise_it_will_begin_as_soon_as_it_is_bought' | translate }}
+
+
+
+
+
+
+
+
{{ 'information_sheet' }}
+
+
+
+
+
+ Partenaire notifié
+
+
+
+
\ No newline at end of file
diff --git a/app/assets/templates/admin/plans/edit.html.erb b/app/assets/templates/admin/plans/edit.html.erb
new file mode 100644
index 000000000..5c9020129
--- /dev/null
+++ b/app/assets/templates/admin/plans/edit.html.erb
@@ -0,0 +1,69 @@
+
+
+
+
+
+ {{ 'subscription_plan' | translate }} {{ plan.base_name }}
+
+
+
+
+
+
+
+
+
+
diff --git a/app/assets/templates/admin/plans/new.html.erb b/app/assets/templates/admin/plans/new.html.erb
new file mode 100644
index 000000000..bd5621fb8
--- /dev/null
+++ b/app/assets/templates/admin/plans/new.html.erb
@@ -0,0 +1,33 @@
+
+
+
+
+
+ {{ 'add_a_subscription_plan' }}
+
+
+
+
+
+
+
diff --git a/app/assets/templates/admin/pricing/index.html.erb b/app/assets/templates/admin/pricing/index.html.erb
new file mode 100644
index 000000000..4a5ce4acf
--- /dev/null
+++ b/app/assets/templates/admin/pricing/index.html.erb
@@ -0,0 +1,219 @@
+
+
+
+
+
+ {{ 'pricing_management' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ 'list_of_the_subscription_plans' }}
+
+
+ {{ '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 }}
+ {{ 'for_safety_reasons_please_dont_create_subscriptions_if_you_dont_want_intend_to_use_them_later' | translate }}
+
+
+ {{ 'add_a_new_subscription_plan' }}
+
+
+
+
+
+
+
+
+ {{ 'trainings' }}
+
+ {{group.name}}
+
+
+
+
+
+
+ {{ training.name }}
+
+
+
+ {{ findTrainingsPricing(trainingsPricings, training.id, group.id).amount | currency}}
+
+
+
+
+
+
+
+
+
+ {{ 'these_prices_match_machine_hours_rates_' | translate }} {{ '_without_subscriptions' }} .
+
+
+
+
+ {{ 'machines' }}
+
+ {{group.name}}
+
+
+
+
+
+
+ {{ machine.name }}
+
+
+
+ {{ findPriceBy(machinesPrices, machine.id, group.id).amount | currency}}
+
+
+
+
+
+
+
+
+ {{ 'trainings' }}
+
+
+
+ {{ 'subscription' }}
+ {{ 'credits' }}
+ {{ 'related_trainings' }}
+
+
+
+
+
+
+
+ {{ plan | humanReadablePlanName: groups }}
+
+
+
+ {{ plan.training_credit_nb }}
+
+
+
+
+ {{ showTrainings(trainingIds) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ 'edit' | translate }}
+
+
+
+
+
+
+
+ {{ 'machines' }}
+
+ {{ 'add_a_machine_credit' }}
+
+
+
+
+
+ {{ 'machine' }}
+ {{ 'hours' }}
+ {{ 'related_subscriptions' }}
+
+
+
+
+
+
+
+ {{ showCreditableName(mc) }}
+
+
+
+
+ {{ mc.hours }}
+
+
+
+
+ {{ getPlanFromId(mc.plan_id) | humanReadablePlanName: groups: 'short' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ 'edit' | translate }}
+
+
+ {{ 'delete' | translate }} (!)
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/assets/templates/admin/project_elements/index.html.erb b/app/assets/templates/admin/project_elements/index.html.erb
index 3254f262f..b4264d2bc 100644
--- a/app/assets/templates/admin/project_elements/index.html.erb
+++ b/app/assets/templates/admin/project_elements/index.html.erb
@@ -7,7 +7,7 @@
- Gestion des éléments projets
+ {{ 'projects_elements_management' }}
@@ -19,14 +19,14 @@
-
-
- Ajouter un matériau
+
+
+ {{ 'add_a_material' }}
- Nom
+ {{ 'name' }}
@@ -49,7 +49,7 @@
- Éditer
+ {{ 'edit' }}
@@ -59,14 +59,14 @@
-
-
- Ajouter une nouvelle thématique
+
+
+ {{ 'add_a_new_theme' }}
- Nom
+ {{ 'name' }}
@@ -89,7 +89,7 @@
- Éditer
+ {{ 'edit' }}
@@ -99,15 +99,15 @@
-
-
- Ajouter une nouvelle licence
+
+
+ {{ 'add_a_new_licence' }}
- Nom
- Description
+ {{ 'name' }}
+ {{ 'description' }}
@@ -135,7 +135,7 @@
- Éditer
+ {{ 'edit' }}
@@ -145,8 +145,8 @@
-
-
+
+
diff --git a/app/assets/templates/admin/settings/index.html b/app/assets/templates/admin/settings/index.html
new file mode 100644
index 000000000..2fa5158c7
--- /dev/null
+++ b/app/assets/templates/admin/settings/index.html
@@ -0,0 +1,463 @@
+
+
+
+
+
+ {{ 'customize_the_application' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ 'title' }}
+
+
+
+
+
+ {{ 'fablab_title' }}
+
+ {{ 'save' }}
+
+
+
+
+
+
+
+
+
+
+ {{ 'customize_information_messages' }}
+
+
+
+
+
+
+
{{ 'message_of_the_machine_booking_page' }}
+
+
+
+
{{ 'save' }}
+
+
+
{{ 'warning_message_of_the_training_booking_page'}}
+
+
+
+
{{ 'save' }}
+
+
+
{{ 'information_message_of_the_training_reservation_page'}}
+
+
+
+
{{ 'save' }}
+
+
+
{{ 'message_of_the_subscriptions_page' }}
+
+
+
{{ 'save' }}
+
+
+
+
+
{{ 'message_of_the_event_page_relative_to_the_reduced_rate_availability_conditions' }}
+
+
+
{{ 'save' }}
+
+
+
+
+
+
+
+
+ {{ 'legal_documents'}}
+
+
+
+ {{ 'if_these_documents_are_not_filled_no_consent_about_them_will_be_asked_to_the_user' }}
+
+
+
+
+
+ {{ 'general_terms_and_conditions_(T&C)' }}
+
+ {{ 'save' }}
+
+
+
+
+
+
+ {{ 'terms_of_service_(TOS)' }}
+
+ {{ 'save' }}
+
+
+
+
+
+
+
+ {{ 'customize_the_graphics' }}
+
+
+
+ {{ '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' }}
+ {{ 'concerning_the_favicon_it_must_be_at_ICO_format_with_a_size_of_16x16_pixels' }}
+
+ {{ 'remember_to_refresh_the_page_for_the_changes_to_take_effect' }}
+
+
+
+
+
+
+ {{ 'logo_(white_background)' }}
+
+ {{ 'save' }}
+
+
+
+
+
+
+ {{ 'logo_(black_background)' }}
+
+ {{ 'save' }}
+
+
+
+
+
+
+ {{ 'favicon' }}
+
+ {{ 'save' }}
+
+
+
+
+
+
{{ 'main_colour' }}
+
+
+
+ {{ 'save' }}
+
+
+
+
+
{{ 'secondary_colour' }}
+
+
+
+ {{ 'save' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ 'news_of_the_home_page' }}
+
+
{{ 'leave_it_empty_to_not_bring_up_any_news_on_the_home_page' | translate }}
+
{{ 'save' }}
+
+
+
{{ 'twitter_stream' }}
+
+
+
+ {{ 'save' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ 'shift_enter_to_force_carriage_return' | translate }}
+ {{ 'save' }}
+
+
+
+
+
+
+
+
+
+
+
{{ 'shift_enter_to_force_carriage_return' | translate }}
+
{{ 'save' }}
+
+
+
+
+
+
+
+
+
+
+ {{ 'reservations_parameters' }}
+
+
+
+
+
{{ 'confine_the_booking_agenda' }}
+
+
{{ 'opening_time' }}
+
+
+
+ {{ 'save' }}
+
+
+
{{ 'closing_time' }}
+
+
+
+ {{ 'save' }}
+
+
+
+
{{ 'ability_for_the_users_to_move_their_reservations' }}
+
+ {{ 'reservations_shifting' }}
+
+ {{ 'save' }}
+
+
+
+
+
{{ 'prior_period_(hours)' }}
+
+
{{ 'save' }}
+
+
+
+
{{ 'ability_for_the_users_to_cancel_their_reservations' }}
+
+ {{ 'reservations_cancelling' }}
+
+ {{ 'save' }}
+
+
+
+
+
{{ 'prior_period_(hours)' }}
+
+
{{ 'save' }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/assets/templates/admin/statistics/graphs.html.erb b/app/assets/templates/admin/statistics/graphs.html.erb
new file mode 100644
index 000000000..6095fab90
--- /dev/null
+++ b/app/assets/templates/admin/statistics/graphs.html.erb
@@ -0,0 +1,147 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ 'type' }}
+ {{field.label}}
+
+
+
+
+
+ {{ 'revenue' }}
+ {{t.label}}
+
+
+
+
+
+
{{ 'top_list_of' | translate}} {{stat.label}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/assets/templates/admin/statistics/index.html.erb b/app/assets/templates/admin/statistics/index.html.erb
new file mode 100644
index 000000000..82743039a
--- /dev/null
+++ b/app/assets/templates/admin/statistics/index.html.erb
@@ -0,0 +1,287 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ 'apply' | translate }}
+
+
+
+
+
+
+
+
+ {{ 'entries' | translate }} {{data.length}}
+ {{ 'revenue_' | translate }} {{sumCA | currency}}
+ {{ 'average_age' | translate }} {{averageAge}} {{ 'years_old' | translate }}
+ {{ 'total' | translate }} {{type.active.label}} : {{sumStat}}
+
+
+
+
+
+
+ {{ 'date' }}
+ {{ 'user' }}
+ {{ 'gender' }}
+ {{ 'age' }}
+ {{ 'type' }}
+ {{type.active.label}}
+ {{field.label}}
+ {{ 'revenue' | translate }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{formatDate(datum._source.date)}}
+ {{getUserNameFromId(datum._source.userId)}}
+ {{formatSex(datum._source.gender)}}
+ {{datum._source.age}} {{ 'years_old' | translate }} {{ 'unknown' }}
+ {{formatSubtype(datum._source.subType)}}
+ {{datum._source.stat}}
+
+
+ {{formatDate(datum._source[field.key])}}
+
+ {{datum._source[field.key]}}
+
+
+ {{datum._source.ca | currency}} {{ 'unknown' }}
+
+
+
+
+
+
+
+
+
+
diff --git a/app/assets/templates/admin/subscriptions/create_modal.html.erb b/app/assets/templates/admin/subscriptions/create_modal.html.erb
new file mode 100644
index 000000000..7ff44ecdc
--- /dev/null
+++ b/app/assets/templates/admin/subscriptions/create_modal.html.erb
@@ -0,0 +1,20 @@
+
+
+
+
+ {{ 'you_are_about_to_purchase_a_subscription_to_NAME' }}
+
+
+
+
+
+
+
+
+
+
diff --git a/app/assets/templates/admin/subscriptions/expired_at_modal.html.erb b/app/assets/templates/admin/subscriptions/expired_at_modal.html.erb
new file mode 100644
index 000000000..b2c945523
--- /dev/null
+++ b/app/assets/templates/admin/subscriptions/expired_at_modal.html.erb
@@ -0,0 +1,33 @@
+
+
+
+
+ {{ 'you_intentionally_decide_to_extend_the_user_s_subscription_by_offering_him_free_days' }}
+
+
+ {{ 'you_intentionally_decide_to_extend_the_user_s_subscription_by_charging_him_again_for_his_current_subscription' }}
+
+
+
+
+ {{ 'until_(expiration_date)' }}
+
+
+
+
+
diff --git a/app/assets/templates/admin/tags/index.html.erb b/app/assets/templates/admin/tags/index.html.erb
new file mode 100644
index 000000000..dd734e01a
--- /dev/null
+++ b/app/assets/templates/admin/tags/index.html.erb
@@ -0,0 +1,37 @@
+{{ 'add_a_tag'}}
+
+
+
+ {{ 'tag_name' }}
+
+
+
+
+
+
+
+ {{tag.name}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ 'edit' }}
+
+
+
+
+
+
+
+
+
diff --git a/app/assets/templates/admin/trainings/index.html.erb b/app/assets/templates/admin/trainings/index.html.erb
new file mode 100644
index 000000000..0b218b814
--- /dev/null
+++ b/app/assets/templates/admin/trainings/index.html.erb
@@ -0,0 +1,124 @@
+
+
+
+
+
+ {{ 'trainings_monitoring' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ 'add_a_new_training' }}
+
+ {{ 'beware_when_creating_a_training_its_reservation_prices_are_initialized_to_zero' | translate }}
+ {{ 'dont_forget_to_change_them_before_creating_slots_for_this_training' | translate }}
+
+
+
+
+
+ {{ 'name' }}
+ {{ 'associated_machines' }}
+ {{ 'number_of_tickets' }}
+
+
+
+
+
+
+
+ {{ training.name }}
+
+
+
+
+ {{ showMachines(training) }}
+
+
+
+
+ {{ training.nb_total_places }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ 'edit' | translate }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ 'training' }}
+ {{ 'date' }}
+
+
+
+
+ {{training_name}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/assets/templates/admin/trainings/modal_edit.html.erb b/app/assets/templates/admin/trainings/modal_edit.html.erb
new file mode 100644
index 000000000..2c51871fa
--- /dev/null
+++ b/app/assets/templates/admin/trainings/modal_edit.html.erb
@@ -0,0 +1,14 @@
+
+
+
+
+
+ {{ 'description_is_limited_to_255_characters' }}
+
+
+
+
diff --git a/app/assets/templates/admin/trainings/validTrainingModal.html.erb b/app/assets/templates/admin/trainings/validTrainingModal.html.erb
new file mode 100644
index 000000000..99608fef3
--- /dev/null
+++ b/app/assets/templates/admin/trainings/validTrainingModal.html.erb
@@ -0,0 +1,18 @@
+
+
+
{{ 'training_of_the_' | translate }}{{ availability.start_at | amDateFormat:'LLL' }} - {{ availability.end_at | amDateFormat:'LT' }}
+ {{ 'you_can_validate_the_training_of_the_following_members' | translate }}
+
+
{{ 'no_reservation' }}
+
+
\ No newline at end of file
diff --git a/app/assets/templates/dashboard/events.html.erb b/app/assets/templates/dashboard/events.html.erb
new file mode 100644
index 000000000..0903ba9dd
--- /dev/null
+++ b/app/assets/templates/dashboard/events.html.erb
@@ -0,0 +1,47 @@
+
diff --git a/app/assets/templates/dashboard/invoices.html.erb b/app/assets/templates/dashboard/invoices.html.erb
new file mode 100644
index 000000000..5d103fc15
--- /dev/null
+++ b/app/assets/templates/dashboard/invoices.html.erb
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ 'reference_number' }}
+ {{ 'date' }}
+ {{ 'price' }}
+
+
+
+
+
+ {{ invoice.reference }}
+ {{ invoice.date | amDateFormat:'L LTS' }}
+ {{ invoice.date | amDateFormat:'L' }}
+ {{ invoice.total | currency}}
+
+
+
+
+
+
+
{{ 'no_invoices_for_now' }}
+
+
+
+
diff --git a/app/assets/templates/dashboard/nav.html.erb b/app/assets/templates/dashboard/nav.html.erb
index 9204b1145..0c4d5f34e 100644
--- a/app/assets/templates/dashboard/nav.html.erb
+++ b/app/assets/templates/dashboard/nav.html.erb
@@ -1,6 +1,6 @@
-
+
- Tableau de bord
+ {{ 'dashboard' }}
-
-
diff --git a/app/assets/templates/dashboard/profile.html.erb b/app/assets/templates/dashboard/profile.html.erb
index b22152862..ed3b13fd1 100644
--- a/app/assets/templates/dashboard/profile.html.erb
+++ b/app/assets/templates/dashboard/profile.html.erb
@@ -13,19 +13,67 @@
{{user.name}}
{{user.email}}
- Dernière activité le {{user.last_sign_in_at | amDateFormat: 'Do MMMM '}}
+ {{ 'last_activity_on_' | translate }} {{user.last_sign_in_at | amDateFormat: 'LL'}}
-
+
+
+
{{ 'group' }}
+
+
+ {{getUserGroup().name}}
+
+ {{ 'i_want_to_change_group' }}
+
+
+
+ Changer mon groupe
+
+
+
+
{{ 'subscription' }}
+
+
+ {{ user.subscribed_plan | humanReadablePlanName }}
+ {{ 'your_subscription_expires_on_' | translate }} {{user.subscription.expired_at | amDateFormat: 'LL'}}
+
+
+
+
+
-
Projets
+
{{ 'trainings' }}
+
+
+ {{r.reservable.name}} - {{ 'to_come' | translate }}
+
+
+ {{t.name}} - {{ 'approved' | translate }}
+
+
+
{{ 'no_trainings' }}
+
+
+
+
{{ 'projects' }}
-
Aucun projet
+
{{ 'no_projects' }}
+
+
+
{{ 'labels' }}
+
+ {{t.name}}
+
+
{{ 'no_labels' }}
+
+
+
+ {{ 'delete_my_account' | translate }}
@@ -34,18 +82,39 @@