1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-03-21 12:29:03 +01:00

Merge branch 'es6' into dev

This commit is contained in:
Sylvain 2018-11-21 16:49:43 +01:00
commit 727b205815
214 changed files with 17366 additions and 14117 deletions

3
.eslintignore Normal file
View File

@ -0,0 +1,3 @@
node_modules/**
vendor/**

12
.eslintrc Normal file
View File

@ -0,0 +1,12 @@
{
"extends": "standard",
"rules": {
"semi": ["error", "always"]
},
"globals": {
"Application": true,
"angular": true,
"Fablab": true
}
}

View File

@ -1 +0,0 @@
2.6.7

4
.gitignore vendored
View File

@ -45,3 +45,7 @@
# Plugins are versioned is their own repository
/plugins/*
# Ignore node.js dev dependencies
/node_modules
npm-debug.log

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
10.13.0

2
.rubocop.yml Normal file
View File

@ -0,0 +1,2 @@
Metrics/LineLength:
Max: 120

View File

@ -1,5 +1,8 @@
# Changelog Fab Manager
- Nom using standard [package.json](package.json) file to save application version number
- Migrated front-end application from CoffeeScript to ECMAScript 6 (JS)
- Integration of Eslint and Rubocop coding rules
- Fix a bug: on small screens, display of button "change group" overflow
## v2.6.7 2018 October 4

41
Gemfile
View File

@ -1,15 +1,13 @@
source 'https://rubygems.org'
gem 'compass-rails', '2.0.4'
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '4.2.10'
# Use SCSS for stylesheets
gem 'sass-rails', '5.0.1'
gem 'compass-rails', '2.0.4'
# Use Uglifier as compressor for JavaScript assets
gem 'uglifier', '>= 1.3.0'
# Use CoffeeScript for .js.coffee assets and views
gem 'coffee-rails', '~> 4.1.0'
# See https://github.com/sstephenson/execjs#readme for more supported runtimes
gem 'therubyracer', '= 0.12.0', platforms: :ruby
@ -39,37 +37,33 @@ group :development, :test do
end
group :development do
gem 'active_record_query_trace'
gem 'awesome_print'
gem 'capistrano'
gem 'capistrano-maintenance', '0.0.5', require: false
gem 'capistrano-sidekiq', require: false
gem 'coveralls', require: false
gem 'foreman'
# Preview mail in the browser
gem 'mailcatcher'
gem 'awesome_print'
gem 'puma'
gem 'foreman'
gem 'capistrano'
gem 'rvm-capistrano', require: false
gem 'capistrano-sidekiq', require: false
gem 'capistrano-maintenance', '0.0.5', require: false
gem 'active_record_query_trace'
gem 'coveralls', require: false
end
group :test do
gem 'byebug'
gem 'database_cleaner'
gem 'faker'
gem 'test_after_commit'
gem 'minitest-reporters'
gem 'webmock'
gem 'vcr'
gem 'byebug'
gem 'pdf-reader'
gem 'test_after_commit'
gem 'vcr'
gem 'webmock'
end
group :production do
gem 'unicorn'
gem 'rails_12factor'
gem 'unicorn'
end
gem 'seed_dump'
@ -91,9 +85,6 @@ gem 'figaro'
gem 'bootstrap-sass'
gem 'font-awesome-rails'
#using bower instead
#gem 'angularjs-rails'
# Image processing ruby wrapper for ImageMagick
gem 'mini_magick'
# upload files
@ -122,9 +113,9 @@ gem 'recurrence'
gem 'prawn'
gem 'prawn-table'
gem 'elasticsearch-rails', '~> 5'
gem 'elasticsearch-model', '~> 5'
gem 'elasticsearch-persistence', '~> 5'
gem 'elasticsearch-rails', '~> 5'
gem 'notify_with'
@ -145,11 +136,11 @@ gem 'message_format'
gem 'openlab_ruby'
gem 'api-pagination'
gem 'has_secure_token'
gem 'apipie-rails'
gem 'has_secure_token'
# XLS files generation
gem 'axlsx', git: 'https://github.com/randym/axlsx', branch: 'release-3.0.0'
gem 'axlsx_rails'
gem "rack-protection", "1.5.5"
gem "rack-protection", "1.5.5"

View File

@ -103,13 +103,6 @@ GEM
cldr-plurals-runtime-rb (1.0.1)
coercible (1.0.0)
descendants_tracker (~> 0.0.1)
coffee-rails (4.1.1)
coffee-script (>= 2.2.0)
railties (>= 4.0.0, < 5.1.x)
coffee-script (2.4.1)
coffee-script-source
execjs
coffee-script-source (1.10.0)
compass (1.0.3)
chunky_png (~> 1.2)
compass-core (~> 1.0.2)
@ -514,7 +507,6 @@ DEPENDENCIES
capistrano-sidekiq
carrierwave
chroma
coffee-rails (~> 4.1.0)
compass-rails (= 2.0.4)
coveralls
database_cleaner

View File

@ -5,6 +5,7 @@
* creating namespaces and moduled for controllers, filters, services, and directives.
*/
// eslint-disable-next-line no-use-before-define
var Application = Application || {};
Application.Constants = angular.module('application.constants', []);
@ -13,58 +14,49 @@ Application.Controllers = angular.module('application.controllers', []);
Application.Filters = angular.module('application.filters', []);
Application.Directives = angular.module('application.directives', []);
angular.module('application', ['ngCookies', 'ngResource', 'ngSanitize', 'ngCookies', 'ui.router', 'ui.bootstrap',
'ngUpload', 'duScroll', 'application.filters','application.services', 'application.directives',
'frapontillo.bootstrap-switch', 'application.constants', 'application.controllers', 'application.router',
'ui.select', 'ui.calendar', 'angularMoment', 'Devise', 'DeviseModal', 'angular-growl', 'xeditable',
'checklist-model', 'unsavedChanges', 'angular-loading-bar', 'ngTouch', 'angular-google-analytics',
'angularUtils.directives.dirDisqus', 'summernote', 'elasticsearch', 'angular-medium-editor', 'naif.base64',
'minicolors', 'pascalprecht.translate', 'ngFitText', 'ngAside', 'ngCapsLock']).
config(['$httpProvider', 'AuthProvider', "growlProvider", "unsavedWarningsConfigProvider", "AnalyticsProvider", "uibDatepickerPopupConfig", "$provide", "$translateProvider",
function($httpProvider, AuthProvider, growlProvider, unsavedWarningsConfigProvider, AnalyticsProvider, uibDatepickerPopupConfig, $provide, $translateProvider) {
'ngUpload', 'duScroll', 'application.filters', 'application.services', 'application.directives',
'frapontillo.bootstrap-switch', 'application.constants', 'application.controllers', 'application.router',
'ui.select', 'ui.calendar', 'angularMoment', 'Devise', 'DeviseModal', 'angular-growl', 'xeditable',
'checklist-model', 'unsavedChanges', 'angular-loading-bar', 'ngTouch', 'angular-google-analytics',
'angularUtils.directives.dirDisqus', 'summernote', 'elasticsearch', 'angular-medium-editor', 'naif.base64',
'minicolors', 'pascalprecht.translate', 'ngFitText', 'ngAside', 'ngCapsLock'])
.config(['$httpProvider', 'AuthProvider', 'growlProvider', 'unsavedWarningsConfigProvider', 'AnalyticsProvider', 'uibDatepickerPopupConfig', '$provide', '$translateProvider',
function ($httpProvider, AuthProvider, growlProvider, unsavedWarningsConfigProvider, AnalyticsProvider, uibDatepickerPopupConfig, $provide, $translateProvider) {
// Google analytics
<% #if Rails.env.production? %>
AnalyticsProvider.setAccount(Fablab.gaId);
// track all routes (or not)
AnalyticsProvider.trackPages(true);
AnalyticsProvider.setDomainName(Fablab.defaultHost);
AnalyticsProvider.useAnalytics(true);
AnalyticsProvider.setPageEvent('$stateChangeSuccess');
<% #else %>
//AnalyticsProvider.setAccount('DISABLED');
<% #end %>
// Custom messages for the date-picker widget
uibDatepickerPopupConfig.closeText = Fablab.translations.app.shared.buttons.close;
uibDatepickerPopupConfig.clearText = Fablab.translations.app.shared.buttons.clear;
uibDatepickerPopupConfig.currentText = Fablab.translations.app.shared.buttons.today;
// Custom messages for the date-picker widget
uibDatepickerPopupConfig.closeText = Fablab.translations.app.shared.buttons.close;
uibDatepickerPopupConfig.clearText = Fablab.translations.app.shared.buttons.clear;
uibDatepickerPopupConfig.currentText = Fablab.translations.app.shared.buttons.today;
// Custom messages for angular-unsavedChanges
unsavedWarningsConfigProvider.navigateMessage = Fablab.translations.app.shared.messages.you_will_lose_any_unsaved_modification_if_you_quit_this_page;
unsavedWarningsConfigProvider.reloadMessage = Fablab.translations.app.shared.messages.you_will_lose_any_unsaved_modification_if_you_reload_this_page;
// Custom messages for angular-unsavedChanges
unsavedWarningsConfigProvider.navigateMessage = Fablab.translations.app.shared.messages.you_will_lose_any_unsaved_modification_if_you_quit_this_page;
unsavedWarningsConfigProvider.reloadMessage = Fablab.translations.app.shared.messages.you_will_lose_any_unsaved_modification_if_you_reload_this_page;
// Set how long the popup messages (growl) will remain
growlProvider.globalTimeToLive(5000);
// Configure the i18n module to load the partial translations from the given API URL
$translateProvider.useLoader('$translatePartialLoader', {
urlTemplate: '/api/translations/{lang}/{part}'
});
// Enable the cache to speed-up the loading times on already seen pages
$translateProvider.useLoaderCache(true);
// Secure i18n module against XSS attacks by escaping the output
$translateProvider.useSanitizeValueStrategy('escapeParameters');
// Enable the MessageFormat interpolation (used for pluralization)
$translateProvider.addInterpolation('$translateMessageFormatInterpolation');
// Set the langage of the instance (from ruby configuration)
$translateProvider.preferredLanguage(Fablab.locale);
}]).run(["$rootScope", "$log", "AuthService", "Auth", "amMoment", "$state", "editableOptions", 'Analytics',
function($rootScope, $log, AuthService, Auth, amMoment, $state, editableOptions, Analytics) {
// Set how long the popup messages (growl) will remain
growlProvider.globalTimeToLive(5000);
// Configure the i18n module to load the partial translations from the given API URL
$translateProvider.useLoader('$translatePartialLoader', {
urlTemplate: '/api/translations/{lang}/{part}'
});
// Enable the cache to speed-up the loading times on already seen pages
$translateProvider.useLoaderCache(true);
// Secure i18n module against XSS attacks by escaping the output
$translateProvider.useSanitizeValueStrategy('escapeParameters');
// Enable the MessageFormat interpolation (used for pluralization)
$translateProvider.addInterpolation('$translateMessageFormatInterpolation');
// Set the langage of the instance (from ruby configuration)
$translateProvider.preferredLanguage(Fablab.locale);
}]).run(['$rootScope', '$log', 'AuthService', 'Auth', 'amMoment', '$state', 'editableOptions', 'Analytics',
function ($rootScope, $log, AuthService, Auth, amMoment, $state, editableOptions, Analytics) {
// Angular-moment (date-time manipulations library)
amMoment.changeLocale(Fablab.moment_locale);
@ -73,7 +65,7 @@ config(['$httpProvider', 'AuthProvider', "growlProvider", "unsavedWarningsConfig
// Alter the UI-Router's $state, registering into some information concerning the previous $state.
// This is used to allow the user to navigate to the previous state
$rootScope.$on('$stateChangeSuccess', function(event, toState, toParams, fromState, fromParams){
$rootScope.$on('$stateChangeSuccess', function (event, toState, toParams, fromState, fromParams) {
$state.prevState = fromState;
$state.prevParams = fromParams;
});
@ -85,11 +77,11 @@ config(['$httpProvider', 'AuthProvider', "growlProvider", "unsavedWarningsConfig
// Global function to allow the user to navigate to the previous screen (ie. $state).
// If no previous $state were recorded, navigate to the home page
$rootScope.backPrevLocation = function(event){
$rootScope.backPrevLocation = function (event) {
event.preventDefault();
event.stopPropagation();
if($state.prevState.name == ""){
$state.prevState = "app.public.home";
if ($state.prevState.name === '') {
$state.prevState = 'app.public.home';
}
$state.go($state.prevState, $state.prevParams);
};
@ -116,8 +108,8 @@ config(['$httpProvider', 'AuthProvider', "growlProvider", "unsavedWarningsConfig
// Prevent the usage of the application for members with incomplete profiles: they will be redirected to
// the 'profile completion' page. This is especially useful for user's accounts imported through SSO.
$rootScope.$on('$stateChangeStart', function (event, toState) {
Auth.currentUser().then(function(currentUser) {
if (currentUser.need_completion && toState.name != 'app.logged.profileCompletion') {
Auth.currentUser().then(function (currentUser) {
if (currentUser.need_completion && toState.name !== 'app.logged.profileCompletion') {
$state.go('app.logged.profileCompletion');
}
});
@ -127,7 +119,6 @@ config(['$httpProvider', 'AuthProvider', "growlProvider", "unsavedWarningsConfig
// see https://github.com/revolunet/angular-google-analytics#automatic-page-view-tracking
Analytics.pageView();
/**
* This helper method builds and return an array contaning every integers between
* the provided start and end.
@ -135,16 +126,15 @@ config(['$httpProvider', 'AuthProvider', "growlProvider", "unsavedWarningsConfig
* @param end {number}
* @return {Array} [start .. end]
*/
$rootScope.intArray = function(start, end) {
$rootScope.intArray = function (start, end) {
var arr = [];
for (var i = start; i < end; i++) { arr.push(i); }
return arr;
};
}]).constant('angularMomentConfig', {
}]).constant('angularMomentConfig', {
timezone: Fablab.timezone
});
angular.isUndefinedOrNull = function(val) {
return angular.isUndefined(val) || val === null
angular.isUndefinedOrNull = function (val) {
return angular.isUndefined(val) || val === null;
};

View File

@ -1,23 +0,0 @@
'use strict'
Application.Controllers.controller "AboutController", ['$scope', 'Setting', 'CustomAsset', ($scope, Setting, CustomAsset)->
### PUBLIC SCOPE ###
Setting.get { name: 'about_title'}, (data)->
$scope.aboutTitle = data.setting
Setting.get { name: 'about_body'}, (data)->
$scope.aboutBody = data.setting
Setting.get { name: 'about_contacts'}, (data)->
$scope.aboutContacts = data.setting
# retrieve the CGU
CustomAsset.get {name: 'cgu-file'}, (cgu) ->
$scope.cgu = cgu.custom_asset
# retrieve the CGV
CustomAsset.get {name: 'cgv-file'}, (cgv) ->
$scope.cgv = cgv.custom_asset
]

View File

@ -0,0 +1,29 @@
/* eslint-disable
no-return-assign,
no-undef,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
'use strict';
Application.Controllers.controller('AboutController', ['$scope', 'Setting', 'CustomAsset', function ($scope, Setting, CustomAsset) {
/* PUBLIC SCOPE */
Setting.get({ name: 'about_title' }, data => $scope.aboutTitle = data.setting);
Setting.get({ name: 'about_body' }, data => $scope.aboutBody = data.setting);
Setting.get({ name: 'about_contacts' }, data => $scope.aboutContacts = data.setting);
// retrieve the CGU
CustomAsset.get({ name: 'cgu-file' }, cgu => $scope.cgu = cgu.custom_asset);
// retrieve the CGV
return CustomAsset.get({ name: 'cgv-file' }, cgv => $scope.cgv = cgv.custom_asset);
}
]);

View File

@ -0,0 +1,2 @@
// TODO: This file was created by bulk-decaffeinate.
// Sanity-check the conversion and remove this comment.

View File

@ -1,303 +0,0 @@
'use strict'
### COMMON CODE ###
## list of supported authentication methods
METHODS = {
'DatabaseProvider' : 'local_database',
'OAuth2Provider' : 'o_auth2',
}
##
# Iterate through the provided array and return the index of the requested element
# @param elements {Array<{id:*}>}
# @param id {*} id of the element to retrieve in the list
# @returns {number} index of the requested element, in the provided array
##
findIdxById = (elements, id)->
(elements.map (elem)->
elem.id
).indexOf(id)
##
# For OAuth2 ententications, mapping the user's ID is mendatory. This function will check that this mapping
# is effective and will return false otherwise
# @param mappings {Array<Object>} 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
##
# Provides a set of common callback methods and data to the $scope parameter. These methods are used
# in the various authentication providers' controllers.
#
# Provides :
# - $scope.authMethods
# - $scope.mappingFields
# - $scope.cancel()
# - $scope.defineDataMapping(mapping)
#
# Requires :
# - mappingFieldsPromise: retrieved by AuthProvider.mapping_fields()
# - $state (Ui-Router) [ 'app.admin.members' ]
##
class AuthenticationController
constructor: ($scope, $state, $uibModal, mappingFieldsPromise)->
## list of supported authentication methods
$scope.authMethods = METHODS
## list of fields that can be mapped through the SSO
$scope.mappingFields = mappingFieldsPromise
##
# Changes the admin's view to the members list page
##
$scope.cancel = ->
$state.go('app.admin.members')
##
# Open a modal allowing to specify the data mapping for the given field
##
$scope.defineDataMapping = (mapping) ->
$uibModal.open
templateUrl: '<%= asset_path "admin/authentications/_data_mapping.html" %>'
size: 'md'
resolve:
field: -> mapping
datatype: ->
for field in $scope.mappingFields[mapping.local_model]
if field[0] == mapping.local_field
return field[1]
controller: ['$scope', '$uibModalInstance', 'field', 'datatype', ($scope, $uibModalInstance, field, datatype) ->
## parent field
$scope.field = field
## expected data type
$scope.datatype = datatype
## data transformation rules
$scope.transformation =
rules: field.transformation || {type: datatype}
## available transformation formats
$scope.formats =
date: [
{
label: 'ISO 8601',
value: 'iso8601'
},
{
label: 'RFC 2822',
value: 'rfc2822'
},
{
label: 'RFC 3339',
value: 'rfc3339'
},
{
label: 'Timestamp (s)'
value: 'timestamp-s'
},
{
label: 'Timestamp (ms)',
value: 'timestamp-ms'
}
]
## Create a new mapping between anything and an expected integer
$scope.addIntegerMapping = ->
unless angular.isArray $scope.transformation.rules.mapping
$scope.transformation.rules.mapping = []
$scope.transformation.rules.mapping.push({from:'', to:0})
## close and save the modifications
$scope.ok = ->
$uibModalInstance.close($scope.transformation.rules)
## do not save the modifications
$scope.cancel = ->
$uibModalInstance.dismiss()
]
.result['finally'](null).then (transfo_rules) ->
mapping.transformation = transfo_rules
##
# 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 _t(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", "$uibModal", "dialogs", "growl", "mappingFieldsPromise", "authProvidersPromise", "AuthProvider", '_t'
, ($scope, $state, $rootScope, $uibModal, dialogs, growl, mappingFieldsPromise, authProvidersPromise, AuthProvider, _t) ->
$scope.mode = 'creation'
## default parameters for the new authentication provider
$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')
## Using the AuthenticationController
new AuthenticationController($scope, $state, $uibModal, mappingFieldsPromise)
]
##
# Page to edit an already added authentication provider
##
Application.Controllers.controller "EditAuthenticationController", ["$scope", "$state", "$stateParams", "$rootScope", "$uibModal", "dialogs", "growl", 'providerPromise', 'mappingFieldsPromise', 'AuthProvider', '_t'
, ($scope, $state, $stateParams, $rootScope, $uibModal, dialogs, growl, providerPromise, mappingFieldsPromise, AuthProvider, _t) ->
## parameters of the currently edited authentication provider
$scope.provider = providerPromise
$scope.mode = 'edition'
##
# 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'))
## Using the AuthenticationController
new AuthenticationController($scope, $state, $uibModal, mappingFieldsPromise)
]

View File

@ -0,0 +1,342 @@
/* eslint-disable
camelcase,
no-return-assign,
no-undef,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
'use strict';
/* COMMON CODE */
// list of supported authentication methods
const METHODS = {
'DatabaseProvider': 'local_database',
'OAuth2Provider': 'o_auth2'
};
/**
* Iterate through the provided array and return the index of the requested element
* @param elements {Array<{id:*}>}
* @param id {*} id of the element to retrieve in the list
* @returns {number} index of the requested element, in the provided array
*/
const findIdxById = function (elements, id) {
return (elements.map(function (elem) { return elem.id; })).indexOf(id);
};
/**
* For OAuth2 authentications, mapping the user's ID is mandatory. This function will check that this mapping
* is effective and will return false otherwise
* @param mappings {Array<Object>} expected: $scope.provider.providable_attributes.o_auth2_mappings_attributes
* @returns {Boolean} true if the mapping is declared
*/
const check_oauth2_id_is_mapped = function (mappings) {
for (let mapping of Array.from(mappings)) {
if ((mapping.local_model === 'user') && (mapping.local_field === 'uid') && !mapping._destroy) {
return true;
}
}
return false;
};
/**
* Provides a set of common callback methods and data to the $scope parameter. These methods are used
* in the various authentication providers' controllers.
*
* Provides :
* - $scope.authMethods
* - $scope.mappingFields
* - $scope.cancel()
* - $scope.defineDataMapping(mapping)
*
* Requires :
* - mappingFieldsPromise: retrieved by AuthProvider.mapping_fields()
* - $state (Ui-Router) [ 'app.admin.members' ]
*/
class AuthenticationController {
constructor ($scope, $state, $uibModal, mappingFieldsPromise) {
// list of supported authentication methods
$scope.authMethods = METHODS;
// list of fields that can be mapped through the SSO
$scope.mappingFields = mappingFieldsPromise;
/**
* Changes the admin's view to the members list page
*/
$scope.cancel = function () { $state.go('app.admin.members'); };
/**
* Open a modal allowing to specify the data mapping for the given field
*/
$scope.defineDataMapping = function (mapping) {
$uibModal.open({
templateUrl: '<%= asset_path "admin/authentications/_data_mapping.html" %>',
size: 'md',
resolve: {
field () { return mapping; },
datatype () {
for (let field of Array.from($scope.mappingFields[mapping.local_model])) {
if (field[0] === mapping.local_field) {
return field[1];
}
}
}
},
controller: ['$scope', '$uibModalInstance', 'field', 'datatype', function ($scope, $uibModalInstance, field, datatype) {
// parent field
$scope.field = field;
// expected data type
$scope.datatype = datatype;
// data transformation rules
$scope.transformation =
{ rules: field.transformation || { type: datatype } };
// available transformation formats
$scope.formats = {
date: [
{
label: 'ISO 8601',
value: 'iso8601'
},
{
label: 'RFC 2822',
value: 'rfc2822'
},
{
label: 'RFC 3339',
value: 'rfc3339'
},
{
label: 'Timestamp (s)',
value: 'timestamp-s'
},
{
label: 'Timestamp (ms)',
value: 'timestamp-ms'
}
]
};
// Create a new mapping between anything and an expected integer
$scope.addIntegerMapping = function () {
if (!angular.isArray($scope.transformation.rules.mapping)) {
$scope.transformation.rules.mapping = [];
}
return $scope.transformation.rules.mapping.push({ from: '', to: 0 });
};
// close and save the modifications
$scope.ok = function () { $uibModalInstance.close($scope.transformation.rules); };
// do not save the modifications
return $scope.cancel = function () { $uibModalInstance.dismiss(); };
}
] })
.result['finally'](null).then(function (transfo_rules) { mapping.transformation = transfo_rules; });
};
}
}
/**
* Page listing all authentication providers
*/
Application.Controllers.controller('AuthentificationController', ['$scope', '$state', '$rootScope', 'dialogs', 'growl', 'authProvidersPromise', 'AuthProvider', '_t',
function ($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 = function (type) {
const text = METHODS[type];
if (typeof text !== 'undefined') {
return _t(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 = function (status) {
switch (status) {
case 'active': return _t('active');
case 'pending': return _t('pending');
case 'previous': return _t('previous_provider');
default: return _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 = function (providers, provider) {
dialogs.confirm(
{
resolve: {
object () {
return {
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 })
};
}
}
},
function () {
// the admin has confirmed, delete
AuthProvider.delete(
{ id: provider.id },
function () {
providers.splice(findIdxById(providers, provider.id), 1);
growl.success(_t('authentication_provider_successfully_deleted'));
},
function () { 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', '$uibModal', 'dialogs', 'growl', 'mappingFieldsPromise', 'authProvidersPromise', 'AuthProvider', '_t',
function ($scope, $state, $rootScope, $uibModal, dialogs, growl, mappingFieldsPromise, authProvidersPromise, AuthProvider, _t) {
$scope.mode = 'creation';
// default parameters for the new authentication provider
$scope.provider = {
name: '',
providable_type: '',
providable_attributes: {}
};
/**
* Initialize some provider's specific properties when selecting the provider type
*/
$scope.updateProvidable = function () {
// === OAuth2Provider ===
if ($scope.provider.providable_type === 'OAuth2Provider') {
if (typeof $scope.provider.providable_attributes.o_auth2_mappings_attributes === 'undefined') {
return $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 = function () {
// === DatabaseProvider ===
let provider;
if ($scope.provider.providable_type === 'DatabaseProvider') {
// prevent from adding mode than 1
for (provider of Array.from(authProvidersPromise)) {
if (provider.providable_type === 'DatabaseProvider') {
growl.error(_t('a_local_database_provider_already_exists_unable_to_create_another'));
return false;
}
}
return AuthProvider.save({ auth_provider: $scope.provider }, function (provider) {
growl.success(_t('local_provider_successfully_saved'));
return $state.go('app.admin.members');
});
// === OAuth2Provider ===
} else if ($scope.provider.providable_type === 'OAuth2Provider') {
// check the ID mapping
if (!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
if (!($scope.provider.providable_attributes.base_url.indexOf('https://') > -1)) {
dialogs.confirm(
{
size: 'l',
resolve: {
object () {
return {
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')
};
}
}
},
function () { // unsecured http confirmed
AuthProvider.save({ auth_provider: $scope.provider }, function (provider) {
growl.success(_t('unsecured_oauth2_provider_successfully_added'));
return $state.go('app.admin.members');
});
}
);
} else {
AuthProvider.save({ auth_provider: $scope.provider }, function (provider) {
growl.success(_t('oauth2_provider_successfully_added'));
return $state.go('app.admin.members');
});
}
}
};
// Using the AuthenticationController
return new AuthenticationController($scope, $state, $uibModal, mappingFieldsPromise);
}
]);
/**
* Page to edit an already added authentication provider
*/
Application.Controllers.controller('EditAuthenticationController', ['$scope', '$state', '$stateParams', '$rootScope', '$uibModal', 'dialogs', 'growl', 'providerPromise', 'mappingFieldsPromise', 'AuthProvider', '_t',
function ($scope, $state, $stateParams, $rootScope, $uibModal, dialogs, growl, providerPromise, mappingFieldsPromise, AuthProvider, _t) {
// parameters of the currently edited authentication provider
$scope.provider = providerPromise;
$scope.mode = 'edition';
/**
* Update the current provider with the new inputs
*/
$scope.updateProvider = function () {
// check the ID mapping
if (!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;
}
return AuthProvider.update(
{ id: $scope.provider.id },
{ auth_provider: $scope.provider },
function (provider) {
growl.success(_t('provider_successfully_updated'));
$state.go('app.admin.members');
},
function () { growl.error(_t('an_error_occurred_unable_to_update_the_provider')); }
);
};
// Using the AuthenticationController
return new AuthenticationController($scope, $state, $uibModal, mappingFieldsPromise);
}
]);

View File

@ -1,474 +0,0 @@
'use strict'
##
# Controller used in the calendar management page
##
Application.Controllers.controller "AdminCalendarController", ["$scope", "$state", "$uibModal", "moment", "Availability", 'Slot', 'Setting', 'Export', 'growl', 'dialogs', 'bookingWindowStart', 'bookingWindowEnd', 'machinesPromise', '_t', 'uiCalendarConfig', 'CalendarConfig'
($scope, $state, $uibModal, moment, Availability, Slot, Setting, Export, growl, dialogs, bookingWindowStart, bookingWindowEnd, machinesPromise, _t, uiCalendarConfig, CalendarConfig) ->
### 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'
# 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
url: '/api/availabilities'
textColor: 'black'
## fullCalendar (v2) configuration
$scope.calendarConfig = CalendarConfig
slotDuration: BASE_SLOT
snapDuration: BOOKING_SNAP
selectable: true
selecHelper: true
minTime: moment.duration(moment(bookingWindowStart.setting.value).format('HH:mm:ss'))
maxTime: moment.duration(moment(bookingWindowEnd.setting.value).format('HH:mm:ss'))
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)
loading: (isLoading, view ) ->
loadingCb(isLoading, view)
##
# 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('admin_calendar.confirmation_required')
msg: _t("admin_calendar.do_you_really_want_to_cancel_the_USER_s_reservation_the_DATE_at_TIME_concerning_RESERVATION"
, { GENDER:getGender($scope.currentUser), USER:slot.user.name, DATE:moment(slot.start_at).format('L'), TIME:moment(slot.start_at).format('LT'), RESERVATION:slot.reservable.name }
, 'messageformat')
, ->
# 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('admin_calendar.reservation_was_successfully_cancelled'))
, (data, status) -> # failed
growl.error(_t('admin_calendar.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('admin_calendar.unable_to_remove_the_last_machine_of_the_slot_delete_the_slot_rather'))
else
# open a confirmation dialog
dialogs.confirm
resolve:
object: ->
title: _t('admin_calendar.confirmation_required')
msg: _t('admin_calendar.do_you_really_want_to_remove_MACHINE_from_this_slot', {GENDER:getGender($scope.currentUser), MACHINE:machine.name}, "messageformat") + ' ' +
_t('admin_calendar.this_will_prevent_any_new_reservation_on_this_slot_but_wont_cancel_those_existing') + ' ' +
_t('admin_calendar.beware_this_cannot_be_reverted')
, ->
# the admin has confirmed, remove the machine
machines = $scope.availability.machine_ids
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
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
# notify the admin
growl.success(_t('admin_calendar.the_machine_was_successfully_removed_from_the_slot'))
, (data, status) -> # failed
growl.error(_t('admin_calendar.deletion_failed'))
##
# Callback to alert the admin that the export request was acknowledged and is
# processing right now.
##
$scope.alertExport = (type) ->
Export.status({category: 'availabilities', type: type}).then (res) ->
unless (res.data.exists)
growl.success _t('admin_calendar.export_is_running_you_ll_be_notified_when_its_ready')
##
# Mark the selected slot as unavailable for new reservations or allow reservations again on it
##
$scope.toggleLockReservations = ->
# first, define a shortcut to the lock property
locked = $scope.availability.lock
# then check if we'll allow reservations locking
prevent = !locked # if currently locked, allow unlock anyway
if (!locked)
prevent = false
angular.forEach $scope.reservations, (r) ->
if r.canceled_at == null
prevent = true # if currently unlocked and has any non-cancelled reservation, disallow locking
if (!prevent)
# open a confirmation dialog
dialogs.confirm
resolve:
object: ->
title: _t('admin_calendar.confirmation_required')
msg: if locked then _t("admin_calendar.do_you_really_want_to_allow_reservations") else _t("admin_calendar.do_you_really_want_to_block_this_slot")
, ->
# the admin has confirmed, lock/unlock the slot
Availability.lock {id: $scope.availability.id}, {lock: !locked}
, (data) -> # success
$scope.availability = data
growl.success(if locked then _t('admin_calendar.unlocking_success') else _t('admin_calendar.locking_success') )
uiCalendarConfig.calendars.calendar.fullCalendar 'refetchEvents'
, (error) -> # failed
growl.error(if locked then _t('admin_calendar.unlocking_failed') else _t('admin_calendar.locking_failed'))
else
growl.error(_t('admin_calendar.unlockable_because_reservations'))
##
# Confirm and destroy the slot in $scope.availability
##
$scope.removeSlot = ->
# open a confirmation dialog
dialogs.confirm
resolve:
object: ->
title: _t('admin_calendar.confirmation_required')
msg: _t("admin_calendar.do_you_really_want_to_delete_this_slot")
, ->
# the admin has confirmed, delete the slot
Availability.delete id: $scope.availability.id, ->
uiCalendarConfig.calendars.calendar.fullCalendar 'removeEvents', $scope.availability.id
growl.success(_t('admin_calendar.the_slot_START-END_has_been_successfully_deleted', {START:moment(event.start).format('LL LT'), END:moment(event.end).format('LT')}))
$scope.availability = null
,->
growl.error(_t('admin_calendar.unable_to_delete_the_slot_START-END_because_it_s_already_reserved_by_a_member', {START:moment(event.start).format('LL LT'), END:moment(event.end).format('LT')}))
### 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
machinesPromise: ['Machine', (Machine)->
Machine.query().$promise
]
trainingsPromise: ['Training', (Training)->
Training.query().$promise
]
spacesPromise: ['Space', (Space)->
Space.query().$promise
]
# when the modal is closed, we send the slot to the server for saving
modalInstance.result.then (availability) ->
uiCalendarConfig.calendars.calendar.fullCalendar 'renderEvent',
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
tags: availability.tags
machine_ids: availability.machine_ids
, true
, ->
uiCalendarConfig.calendars.calendar.fullCalendar('unselect')
uiCalendarConfig.calendars.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'))
$scope.removeSlot()
# 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) ->
element.find('.fc-content').prepend('<span class="remove-event">x&nbsp;</span>')
if event.tags.length > 0
html = ''
for tag in event.tags
html += "<span class='label label-success text-white'>#{tag.name}</span> "
element.find('.fc-title').append("<br/>"+html)
# force return to prevent coffee-script auto-return to return random value (possiblity falsy)
return
##
# Triggered when resource fetching starts/stops.
# @see https://fullcalendar.io/docs/resource_data/loading/
##
loadingCb = (isLoading, view) ->
if (isLoading)
# we remove existing events when fetching starts to prevent duplicates
uiCalendarConfig.calendars.calendar.fullCalendar('removeEvents')
]
##
# Controller used in the slot creation modal window
##
Application.Controllers.controller 'CreateEventModalController', ["$scope", "$uibModalInstance", "moment", "start", "end", "machinesPromise", "Availability", "trainingsPromise", "spacesPromise", 'Tag', 'growl', '_t'
, ($scope, $uibModalInstance, moment, start, end, machinesPromise, Availability, trainingsPromise, spacesPromise, Tag, growl, _t) ->
## $uibModal parameter
$scope.start = start
## $uibModal parameter
$scope.end = end
## machines list
$scope.machines = machinesPromise.filter (m) -> !m.disabled
## trainings list
$scope.trainings = trainingsPromise.filter (t) -> !t.disabled
## spaces list
$scope.spaces = spacesPromise.filter (s) -> !s.disabled
## machines associated with the created slot
$scope.selectedMachines = []
## training associated with the created slot
$scope.selectedTraining = null
## space associated with the created slot
$scope.selectedSpace = null
## UI step
$scope.step = 1
## the user is not able to edit the ending time of the availability, unless he set the type to 'training'
$scope.endDateReadOnly = true
## 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('admin_calendar.you_should_select_at_least_a_machine'))
return
else if $scope.availability.available_type == 'training'
$scope.availability.training_ids = [$scope.selectedTraining.id]
else if $scope.availability.available_type == 'space'
$scope.availability.space_ids = [$scope.selectedSpace.id]
Availability.save
availability: $scope.availability
, (availability) ->
$uibModalInstance.close(availability)
##
# Move the modal UI to the next step
##
$scope.next = ->
$scope.setNbTotalPlaces() if $scope.step == 1
$scope.step++
##
# Move the modal UI to the next step
##
$scope.previous = ->
$scope.step--
##
# Callback to cancel the slot creation
##
$scope.cancel = ->
$uibModalInstance.dismiss('cancel')
##
# For training avaiabilities, set the maximum number of people allowed to register on this slot
##
$scope.setNbTotalPlaces = ->
if $scope.availability.available_type == 'training'
$scope.availability.nb_total_places = $scope.selectedTraining.nb_total_places
else if $scope.availability.available_type == 'space'
$scope.availability.nb_total_places = $scope.selectedSpace.default_places
### PRIVATE SCOPE ###
##
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
if $scope.trainings.length > 0
$scope.selectedTraining = $scope.trainings[0]
if $scope.spaces.length > 0
$scope.selectedSpace = $scope.spaces[0]
Tag.query().$promise.then (data) ->
$scope.tags = data
## 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' or newValue == 'space'
$scope.endDateReadOnly = true
diff = moment($scope.end).diff($scope.start, 'hours') # the result is rounded down by moment.js
$scope.end = moment($scope.start).add(diff, 'hours').toDate()
$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 or space availabilities, adjust the end time
if $scope.availability.available_type == 'machines' or $scope.availability.available_type == 'space'
end = moment($scope.end)
end.add(moment(newValue).diff(oldValue), 'milliseconds')
$scope.end = end.toDate()
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()
]

View File

@ -0,0 +1,546 @@
/* eslint-disable
camelcase,
handle-callback-err,
no-return-assign,
no-undef,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
'use strict';
/**
* Controller used in the calendar management page
*/
Application.Controllers.controller('AdminCalendarController', ['$scope', '$state', '$uibModal', 'moment', 'Availability', 'Slot', 'Setting', 'Export', 'growl', 'dialogs', 'bookingWindowStart', 'bookingWindowEnd', 'machinesPromise', '_t', 'uiCalendarConfig', 'CalendarConfig',
function ($scope, $state, $uibModal, moment, Availability, Slot, Setting, Export, growl, dialogs, bookingWindowStart, bookingWindowEnd, machinesPromise, _t, uiCalendarConfig, CalendarConfig) {
/* PRIVATE STATIC CONSTANTS */
// The calendar is divided in slots of 30 minutes
let loadingCb;
const BASE_SLOT = '00:30:00';
// The bookings can be positioned every half hours
const BOOKING_SNAP = '00:30:00';
// We do not allow the creation of slots that are not a multiple of 60 minutes
const 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({
url: '/api/availabilities',
textColor: 'black'
});
// fullCalendar (v2) configuration
$scope.calendarConfig = CalendarConfig({
slotDuration: BASE_SLOT,
snapDuration: BOOKING_SNAP,
selectable: true,
selecHelper: true,
minTime: moment.duration(moment(bookingWindowStart.setting.value).format('HH:mm:ss')),
maxTime: moment.duration(moment(bookingWindowEnd.setting.value).format('HH:mm:ss')),
select (start, end, jsEvent, view) {
return calendarSelectCb(start, end, jsEvent, view);
},
eventClick (event, jsEvent, view) {
return calendarEventClickCb(event, jsEvent, view);
},
eventRender (event, element, view) {
return eventRenderCb(event, element);
},
loading (isLoading, view) {
return loadingCb(isLoading, view);
}
});
/**
* 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 = function (slot) {
// open a confirmation dialog
dialogs.confirm(
{
resolve: {
object () {
return {
title: _t('admin_calendar.confirmation_required'),
msg: _t('admin_calendar.do_you_really_want_to_cancel_the_USER_s_reservation_the_DATE_at_TIME_concerning_RESERVATION'
, { GENDER: getGender($scope.currentUser), USER: slot.user.name, DATE: moment(slot.start_at).format('L'), TIME: moment(slot.start_at).format('LT'), RESERVATION: slot.reservable.name }
, 'messageformat')
};
}
}
},
function () {
// the admin has confirmed, cancel the subscription
Slot.cancel(
{ id: slot.slot_id },
function (data, status) { // success
// update the canceled_at attribute
for (let resa of Array.from($scope.reservations)) {
if (resa.slot_id === data.id) {
resa.canceled_at = data.canceled_at;
break;
}
}
// notify the admin
return growl.success(_t('admin_calendar.reservation_was_successfully_cancelled'));
},
function (data, status) { // failed
growl.error(_t('admin_calendar.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 = function (machine) {
if ($scope.availability.machine_ids.length === 1) {
return growl.error(_t('admin_calendar.unable_to_remove_the_last_machine_of_the_slot_delete_the_slot_rather'));
} else {
// open a confirmation dialog
return dialogs.confirm({
resolve: {
object () {
return {
title: _t('admin_calendar.confirmation_required'),
msg: _t('admin_calendar.do_you_really_want_to_remove_MACHINE_from_this_slot', { GENDER: getGender($scope.currentUser), MACHINE: machine.name }, 'messageformat') + ' ' +
_t('admin_calendar.this_will_prevent_any_new_reservation_on_this_slot_but_wont_cancel_those_existing') + ' ' +
_t('admin_calendar.beware_this_cannot_be_reverted')
};
}
}
}
, function () {
// the admin has confirmed, remove the machine
const machines = $scope.availability.machine_ids;
for (let m_id = 0; m_id < machines.length; m_id++) {
const key = machines[m_id];
if (m_id === machine.id) {
machines.splice(key, 1);
}
}
return Availability.update({ id: $scope.availability.id }, { availability: { machines_attributes: [{ id: machine.id, _destroy: true }] } }
, function (data, status) { // success
// update the machine_ids attribute
$scope.availability.machine_ids = data.machine_ids;
$scope.availability.title = data.title;
uiCalendarConfig.calendars.calendar.fullCalendar('rerenderEvents');
// notify the admin
return growl.success(_t('admin_calendar.the_machine_was_successfully_removed_from_the_slot'));
}
, function (data, status) { // failed
growl.error(_t('admin_calendar.deletion_failed'));
}
);
});
}
};
/**
* Callback to alert the admin that the export request was acknowledged and is
* processing right now.
*/
$scope.alertExport = function (type) {
Export.status({ category: 'availabilities', type }).then(function (res) {
if (!res.data.exists) {
return growl.success(_t('admin_calendar.export_is_running_you_ll_be_notified_when_its_ready'));
}
});
};
/**
* Mark the selected slot as unavailable for new reservations or allow reservations again on it
*/
$scope.toggleLockReservations = function () {
// first, define a shortcut to the lock property
const locked = $scope.availability.lock;
// then check if we'll allow reservations locking
let prevent = !locked; // if currently locked, allow unlock anyway
if (!locked) {
prevent = false;
angular.forEach($scope.reservations, function (r) {
if (r.canceled_at === null) {
return prevent = true;
}
}); // if currently unlocked and has any non-cancelled reservation, disallow locking
}
if (!prevent) {
// open a confirmation dialog
dialogs.confirm(
{
resolve: {
object () {
return {
title: _t('admin_calendar.confirmation_required'),
msg: locked ? _t('admin_calendar.do_you_really_want_to_allow_reservations') : _t('admin_calendar.do_you_really_want_to_block_this_slot')
};
}
}
},
function () {
// the admin has confirmed, lock/unlock the slot
Availability.lock(
{ id: $scope.availability.id },
{ lock: !locked },
function (data) { // success
$scope.availability = data;
growl.success(locked ? _t('admin_calendar.unlocking_success') : _t('admin_calendar.locking_success'));
uiCalendarConfig.calendars.calendar.fullCalendar('refetchEvents');
},
function (error) { // failed
growl.error(locked ? _t('admin_calendar.unlocking_failed') : _t('admin_calendar.locking_failed'));
console.error(error);
}
);
}
);
} else {
return growl.error(_t('admin_calendar.unlockable_because_reservations'));
}
};
/**
* Confirm and destroy the slot in $scope.availability
*/
$scope.removeSlot = function () {
// open a confirmation dialog
dialogs.confirm(
{
resolve: {
object () {
return {
title: _t('admin_calendar.confirmation_required'),
msg: _t('admin_calendar.do_you_really_want_to_delete_this_slot')
};
}
}
},
function () {
// the admin has confirmed, delete the slot
Availability.delete(
{ id: $scope.availability.id },
function () {
uiCalendarConfig.calendars.calendar.fullCalendar('removeEvents', $scope.availability.id);
growl.success(_t('admin_calendar.the_slot_START-END_has_been_successfully_deleted', { START: moment(event.start).format('LL LT'), END: moment(event.end).format('LT') }));
$scope.availability = null;
},
function () {
growl.error(_t('admin_calendar.unable_to_delete_the_slot_START-END_because_it_s_already_reserved_by_a_member', { START: moment(event.start).format('LL LT'), END: moment(event.end).format('LT') }));
});
}
);
};
/* 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'
*/
var getGender = function (user) {
if (user.profile) {
if (user.profile.gender === 'true') { return 'male'; } else { return 'female'; }
} else { return 'other'; }
};
// Triggered when the admin drag on the agenda to create a new reservable slot.
// @see http://fullcalendar.io/docs/selection/select_callback/
//
var calendarSelectCb = function (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)) {
const 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
const modalInstance = $uibModal.open({
templateUrl: '<%= asset_path "admin/calendar/eventModal.html" %>',
controller: 'CreateEventModalController',
resolve: {
start () { return start; },
end () { return end; },
machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }],
trainingsPromise: ['Training', function (Training) { return Training.query().$promise; }],
spacesPromise: ['Space', function (Space) { return Space.query().$promise; }]
} });
// when the modal is closed, we send the slot to the server for saving
modalInstance.result.then(
function (availability) {
uiCalendarConfig.calendars.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,
tags: availability.tags,
machine_ids: availability.machine_ids
},
true
);
},
function () { uiCalendarConfig.calendars.calendar.fullCalendar('unselect'); }
);
}
}
return uiCalendarConfig.calendars.calendar.fullCalendar('unselect');
};
/**
* Triggered when the admin clicks on a availability slot in the agenda.
* @see http://fullcalendar.io/docs/mouse/eventClick/
*/
var calendarEventClickCb = function (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')) {
return $scope.removeSlot();
// if the user has only clicked on the event, display its reservations
} else {
return Availability.reservations({ id: event.id }, function (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/
*/
var eventRenderCb = function (event, element) {
element.find('.fc-content').prepend('<span class="remove-event">x&nbsp;</span>');
if (event.tags.length > 0) {
let html = '';
for (let tag of Array.from(event.tags)) {
html += `<span class='label label-success text-white'>${tag.name}</span> `;
}
element.find('.fc-title').append(`<br/>${html}`);
}
// force return to prevent coffee-script auto-return to return random value (possiblity falsy)
};
/**
* Triggered when resource fetching starts/stops.
* @see https://fullcalendar.io/docs/resource_data/loading/
*/
return loadingCb = function (isLoading, view) {
if (isLoading) {
// we remove existing events when fetching starts to prevent duplicates
return uiCalendarConfig.calendars.calendar.fullCalendar('removeEvents');
}
};
}
]);
/**
* Controller used in the slot creation modal window
*/
Application.Controllers.controller('CreateEventModalController', ['$scope', '$uibModalInstance', 'moment', 'start', 'end', 'machinesPromise', 'Availability', 'trainingsPromise', 'spacesPromise', 'Tag', 'growl', '_t',
function ($scope, $uibModalInstance, moment, start, end, machinesPromise, Availability, trainingsPromise, spacesPromise, Tag, growl, _t) {
// $uibModal parameter
$scope.start = start;
// $uibModal parameter
$scope.end = end;
// machines list
$scope.machines = machinesPromise.filter(function (m) { return !m.disabled; });
// trainings list
$scope.trainings = trainingsPromise.filter(function (t) { return !t.disabled; });
// spaces list
$scope.spaces = spacesPromise.filter(function (s) { return !s.disabled; });
// machines associated with the created slot
$scope.selectedMachines = [];
// training associated with the created slot
$scope.selectedTraining = null;
// space associated with the created slot
$scope.selectedSpace = null;
// UI step
$scope.step = 1;
// the user is not able to edit the ending time of the availability, unless he set the type to 'training'
$scope.endDateReadOnly = true;
// 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 = function (machine) {
const index = $scope.selectedMachines.indexOf(machine);
if (index > -1) {
return $scope.selectedMachines.splice(index, 1);
} else {
return $scope.selectedMachines.push(machine);
}
};
/**
* Callback for the modal window validation: save the slot and closes the modal
*/
$scope.ok = function () {
if ($scope.availability.available_type === 'machines') {
if ($scope.selectedMachines.length > 0) {
$scope.availability.machine_ids = $scope.selectedMachines.map(function (m) { return m.id; });
} else {
growl.error(_t('admin_calendar.you_should_select_at_least_a_machine'));
return;
}
} else if ($scope.availability.available_type === 'training') {
$scope.availability.training_ids = [$scope.selectedTraining.id];
} else if ($scope.availability.available_type === 'space') {
$scope.availability.space_ids = [$scope.selectedSpace.id];
}
return Availability.save(
{ availability: $scope.availability }
, function (availability) { $uibModalInstance.close(availability); });
};
/**
* Move the modal UI to the next step
*/
$scope.next = function () {
if ($scope.step === 1) { $scope.setNbTotalPlaces(); }
return $scope.step++;
};
/**
* Move the modal UI to the next step
*/
$scope.previous = function () { return $scope.step--; };
/**
* Callback to cancel the slot creation
*/
$scope.cancel = function () { $uibModalInstance.dismiss('cancel'); };
/**
* For training avaiabilities, set the maximum number of people allowed to register on this slot
*/
$scope.setNbTotalPlaces = function () {
if ($scope.availability.available_type === 'training') {
return $scope.availability.nb_total_places = $scope.selectedTraining.nb_total_places;
} else if ($scope.availability.available_type === 'space') {
return $scope.availability.nb_total_places = $scope.selectedSpace.default_places;
}
};
/* PRIVATE SCOPE */
/**
* Kind of constructor: these actions will be realized first when the controller is loaded
*/
const initialize = function () {
if ($scope.trainings.length > 0) {
$scope.selectedTraining = $scope.trainings[0];
}
if ($scope.spaces.length > 0) {
$scope.selectedSpace = $scope.spaces[0];
}
Tag.query().$promise.then(function (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', function (newValue, oldValue, scope) {
if ((newValue === 'machines') || (newValue === 'space')) {
$scope.endDateReadOnly = true;
const 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();
return $scope.availability.end_at = $scope.end;
} else {
return $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', function (newValue, oldValue, scope) {
// for machine or space availabilities, adjust the end time
if (($scope.availability.available_type === 'machines') || ($scope.availability.available_type === 'space')) {
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
return $scope.availability.start_at = $scope.start;
});
// Maintain consistency between the end time and the date object in the availability object
return $scope.$watch('end', function (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
return $scope.availability.end_at = $scope.end;
});
};
// !!! MUST BE CALLED AT THE END of the controller
return initialize();
}
]);

View File

@ -1,127 +0,0 @@
### COMMON CODE ###
# The validity per user defines how many time a user may ba able to use the same coupon
# Here are the various options for this parameter
userValidities = ['once', 'forever']
##
# Controller used in the coupon creation page
##
Application.Controllers.controller "NewCouponController", ["$scope", "$state", 'Coupon', 'growl', '_t'
, ($scope, $state, Coupon, growl, _t) ->
## Values for the coupon currently created
$scope.coupon =
active: true
type: 'percent_off'
## Options for the validity per user
$scope.validities = userValidities
## Default parameters for AngularUI-Bootstrap datepicker (used for coupon validity limit selection)
$scope.datePicker =
format: Fablab.uibDateFormat
opened: false # default: datePicker is not shown
minDate: moment().toDate()
options:
startingDay: Fablab.weekStartingDay
##
# Shows/hides the validity limit datepicker
# @param $event {Object} jQuery event object
##
$scope.toggleDatePicker = ($event) ->
$event.preventDefault()
$event.stopPropagation()
$scope.datePicker.opened = !$scope.datePicker.opened
##
# Callback to save the new coupon in $scope.coupon and redirect the user to the listing page
##
$scope.saveCoupon = ->
Coupon.save coupon: $scope.coupon, (coupon) ->
$state.go('app.admin.pricing')
, (err)->
growl.error(_t('unable_to_create_the_coupon_check_code_already_used'))
console.error(err)
]
##
# Controller used in the coupon edition page
##
Application.Controllers.controller "EditCouponController", ["$scope", "$state", 'Coupon', 'couponPromise', '_t', 'growl'
, ($scope, $state, Coupon, couponPromise, _t, growl) ->
### PUBLIC SCOPE ###
## Used in the form to freeze unmodifiable fields
$scope.mode = 'EDIT'
## Coupon to edit
$scope.coupon = couponPromise
## Options for the validity per user
$scope.validities = userValidities
## Mapping for validation errors
$scope.errors = {}
## Default parameters for AngularUI-Bootstrap datepicker (used for coupon validity limit selection)
$scope.datePicker =
format: Fablab.uibDateFormat
opened: false # default: datePicker is not shown
minDate: moment().toDate()
options:
startingDay: Fablab.weekStartingDay
##
# Shows/hides the validity limit datepicker
# @param $event {Object} jQuery event object
##
$scope.toggleDatePicker = ($event) ->
$event.preventDefault()
$event.stopPropagation()
$scope.datePicker.opened = !$scope.datePicker.opened
##
# Callback to save the coupon's changes to the API
##
$scope.updateCoupon = ->
$scope.errors = {}
Coupon.update {id: $scope.coupon.id}, coupon: $scope.coupon, (coupon) ->
$state.go('app.admin.pricing')
, (err)->
growl.error(_t('unable_to_update_the_coupon_an_error_occurred'))
$scope.errors = err.data
### PRIVATE SCOPE ###
##
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
# parse the date if any
if (couponPromise.valid_until)
$scope.coupon.valid_until = moment(couponPromise.valid_until).toDate()
## !!! MUST BE CALLED AT THE END of the controller
initialize()
]

View File

@ -0,0 +1,130 @@
/* eslint-disable
no-return-assign,
no-undef,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/* COMMON CODE */
// The validity per user defines how many time a user may ba able to use the same coupon
// Here are the various options for this parameter
const userValidities = ['once', 'forever'];
/**
* Controller used in the coupon creation page
*/
Application.Controllers.controller('NewCouponController', ['$scope', '$state', 'Coupon', 'growl', '_t',
function ($scope, $state, Coupon, growl, _t) {
// Values for the coupon currently created
$scope.coupon = {
active: true,
type: 'percent_off'
};
// Options for the validity per user
$scope.validities = userValidities;
// Default parameters for AngularUI-Bootstrap datepicker (used for coupon validity limit selection)
$scope.datePicker = {
format: Fablab.uibDateFormat,
opened: false, // default: datePicker is not shown
minDate: moment().toDate(),
options: {
startingDay: Fablab.weekStartingDay
}
};
/**
* Shows/hides the validity limit datepicker
* @param $event {Object} jQuery event object
*/
$scope.toggleDatePicker = function ($event) {
$event.preventDefault();
$event.stopPropagation();
return $scope.datePicker.opened = !$scope.datePicker.opened;
};
/**
* Callback to save the new coupon in $scope.coupon and redirect the user to the listing page
*/
return $scope.saveCoupon = () =>
Coupon.save({ coupon: $scope.coupon }, coupon => $state.go('app.admin.pricing')
, function (err) {
growl.error(_t('unable_to_create_the_coupon_check_code_already_used'));
return console.error(err);
});
}
]);
/**
* Controller used in the coupon edition page
*/
Application.Controllers.controller('EditCouponController', ['$scope', '$state', 'Coupon', 'couponPromise', '_t', 'growl',
function ($scope, $state, Coupon, couponPromise, _t, growl) {
/* PUBLIC SCOPE */
// Used in the form to freeze unmodifiable fields
$scope.mode = 'EDIT';
// Coupon to edit
$scope.coupon = couponPromise;
// Options for the validity per user
$scope.validities = userValidities;
// Mapping for validation errors
$scope.errors = {};
// Default parameters for AngularUI-Bootstrap datepicker (used for coupon validity limit selection)
$scope.datePicker = {
format: Fablab.uibDateFormat,
opened: false, // default: datePicker is not shown
minDate: moment().toDate(),
options: {
startingDay: Fablab.weekStartingDay
}
};
/**
* Shows/hides the validity limit datepicker
* @param $event {Object} jQuery event object
*/
$scope.toggleDatePicker = function ($event) {
$event.preventDefault();
$event.stopPropagation();
return $scope.datePicker.opened = !$scope.datePicker.opened;
};
/**
* Callback to save the coupon's changes to the API
*/
$scope.updateCoupon = function () {
$scope.errors = {};
return Coupon.update({ id: $scope.coupon.id }, { coupon: $scope.coupon }, coupon => $state.go('app.admin.pricing')
, function (err) {
growl.error(_t('unable_to_update_the_coupon_an_error_occurred'));
return $scope.errors = err.data;
});
};
/* PRIVATE SCOPE */
/**
* Kind of constructor: these actions will be realized first when the controller is loaded
*/
const initialize = function () {
// parse the date if any
if (couponPromise.valid_until) {
return $scope.coupon.valid_until = moment(couponPromise.valid_until).toDate();
}
};
// !!! MUST BE CALLED AT THE END of the controller
return initialize();
}
]);

View File

@ -1,523 +0,0 @@
'use strict'
### COMMON CODE ###
##
# Provides a set of common properties and methods to the $scope parameter. They are used
# in the various events' admin controllers.
#
# Provides :
# - $scope.datePicker = {}
# - $scope.submited(content)
# - $scope.cancel()
# - $scope.addFile()
# - $scope.deleteFile(file)
# - $scope.fileinputClass(v)
# - $scope.toggleStartDatePicker($event)
# - $scope.toggleEndDatePicker($event)
# - $scope.toggleRecurrenceEnd(e)
# - $scope.addPrice()
# - $scope.removePrice(price, $event)
#
# Requires :
# - $scope.event.event_files_attributes = []
# - $state (Ui-Router) [ 'app.public.events_list' ]
##
class EventsController
constructor: ($scope, $state) ->
## default parameters for AngularUI-Bootstrap datepicker
$scope.datePicker =
format: Fablab.uibDateFormat
startOpened: false # default: datePicker is not shown
endOpened: false
recurrenceEndOpened: false
options:
startingDay: Fablab.weekStartingDay
##
# 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 project 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.public.events_list')
##
# Changes the user's view to the events list page
##
$scope.cancel = ->
$state.go('app.public.events_list')
##
# For use with 'ng-class', returns the CSS class name for the uploads previews.
# The preview may show a placeholder or the content of the file depending on the upload state.
# @param v {*} any attribute, will be tested for truthiness (see JS evaluation rules)
##
$scope.fileinputClass = (v)->
if v
'fileinput-exists'
else
'fileinput-new'
##
# This will create a single new empty entry into the event's attachements list.
##
$scope.addFile = ->
$scope.event.event_files_attributes.push {}
##
# This will remove the given file from the event's attachements list. If the file was previously uploaded
# to the server, it will be marked for deletion on the server. Otherwise, it will be simply truncated from
# the attachements array.
# @param file {Object} the file to delete
##
$scope.deleteFile = (file) ->
index = $scope.event.event_files_attributes.indexOf(file)
if file.id?
file._destroy = true
else
$scope.event.event_files_attributes.splice(index, 1)
##
# Show/Hide the "start" datepicker (open the drop down/close it)
##
$scope.toggleStartDatePicker = ($event) ->
$event.preventDefault()
$event.stopPropagation()
$scope.datePicker.startOpened = !$scope.datePicker.startOpened
##
# Show/Hide the "end" datepicker (open the drop down/close it)
##
$scope.toggleEndDatePicker = ($event) ->
$event.preventDefault()
$event.stopPropagation()
$scope.datePicker.endOpened = !$scope.datePicker.endOpened
##
# Masks/displays the recurrence pane allowing the admin to set the current event as recursive
##
$scope.toggleRecurrenceEnd = (e)->
e.preventDefault()
e.stopPropagation()
$scope.datePicker.recurrenceEndOpened = !$scope.datePicker.recurrenceEndOpened
##
# Initialize a new price item in the additional prices list
##
$scope.addPrice = ->
$scope.event.prices.push({
category: null,
amount: null,
})
##
# Remove the price or mark it as 'to delete'
##
$scope.removePrice = (price, event) ->
event.preventDefault()
event.stopPropagation()
if price.id
price._destroy = true
else
index = $scope.event.prices.indexOf(price)
$scope.event.prices.splice(index, 1)
##
# Controller used in the events listing page (admin view)
##
Application.Controllers.controller "AdminEventsController", ["$scope", "$state", 'dialogs', '$uibModal', 'growl', 'Event', 'Category', 'EventTheme', 'AgeRange', 'PriceCategory', 'eventsPromise', 'categoriesPromise', 'themesPromise', 'ageRangesPromise', 'priceCategoriesPromise', '_t'
, ($scope, $state, dialogs, $uibModal, growl, Event, Category, EventTheme, AgeRange, PriceCategory, eventsPromise, categoriesPromise, themesPromise, ageRangesPromise, priceCategoriesPromise, _t) ->
### PUBLIC SCOPE ###
## By default, the pagination mode is activated to limit the page size
$scope.paginateActive = true
## The events displayed on the page
$scope.events = eventsPromise
## Current virtual page
$scope.page = 1
## Temporary datastore for creating new elements
$scope.inserted =
category: null
theme: null
age_range: null
## List of categories for the events
$scope.categories = categoriesPromise
## List of events themes
$scope.themes = themesPromise
## List of age ranges
$scope.ageRanges = ageRangesPromise
## List of price categories for the events
$scope.priceCategories = priceCategoriesPromise
## Default: we display all events (no restriction)
$scope.eventsScope =
selected: ''
##
# Adds a bucket of events to the bottom of the page, grouped by month
##
$scope.loadMoreEvents = ->
$scope.page += 1
Event.query { page: $scope.page, scope: $scope.eventsScope.selected }, (data)->
$scope.events = $scope.events.concat data
paginationCheck(data, $scope.events)
##
# Saves a new element / Update an existing one to the server (form validation callback)
# @param model {string} model name
# @param data {Object} element name
# @param [id] {number} element id, in case of update
##
$scope.saveElement = (model, data, id) ->
if id?
getModel(model)[0].update {id: id}, data
else
getModel(model)[0].save data, (resp)->
getModel(model)[1][getModel(model)[1].length-1].id = resp.id
##
# Deletes the element at the specified index
# @param model {string} model name
# @param index {number} element index in the $scope[model] array
##
$scope.removeElement = (model, index) ->
if model == 'category' and getModel(model)[1].length == 1
growl.error(_t('at_least_one_category_is_required')+' '+_t('unable_to_delete_the_last_one'))
return false
if getModel(model)[1][index].related_to > 0
growl.error(_t('unable_to_delete_ELEMENT_already_in_use_NUMBER_times', {ELEMENT:model, NUMBER:getModel(model)[1][index].related_to}, "messageformat"))
return false
dialogs.confirm
resolve:
object: ->
title: _t('confirmation_required')
msg: _t('do_you_really_want_to_delete_this_ELEMENT', {ELEMENT:model}, "messageformat")
, -> # delete confirmed
getModel(model)[0].delete getModel(model)[1][index], null, ->
getModel(model)[1].splice(index, 1)
, ->
growl.error(_t('unable_to_delete_an_error_occured'))
##
# Creates a new empty entry in the $scope[model] array
# @param model {string} model name
##
$scope.addElement = (model) ->
$scope.inserted[model] =
name: ''
related_to: 0
getModel(model)[1].push($scope.inserted[model])
##
# Removes the newly inserted but not saved element / Cancel the current element modification
# @param model {string} model name
# @param rowform {Object} see http://vitalets.github.io/angular-xeditable/
# @param index {number} element index in the $scope[model] array
##
$scope.cancelElement = (model, rowform, index) ->
if getModel(model)[1][index].id?
rowform.$cancel()
else
getModel(model)[1].splice(index, 1)
##
# Open a modal dialog allowing the definition of a new price category.
# Save it once filled and handle the result.
##
$scope.newPriceCategory = ->
$uibModal.open
templateUrl: '<%= asset_path "admin/events/price_form.html" %>'
size: 'md'
resolve:
category: -> {}
controller: 'PriceCategoryController'
.result['finally'](null).then (p_cat) ->
# save the price category to the API
PriceCategory.save p_cat, (cat) ->
$scope.priceCategories.push(cat)
growl.success(_t('price_category_successfully_created'))
, (err)->
growl.error(_t('unable_to_add_the_price_category_check_name_already_used'))
console.error(err)
##
# Update the given price category with the new properties
# to specify in a modal dialog
# @param index {number} index of the caterory in the $scope.priceCategories array
# @param id {number} price category ID, must match the ID of the category at the index specified above
##
$scope.editPriceCategory = (id, index) ->
if $scope.priceCategories[index].id != id
growl.error(_t('unexpected_error_occurred_please_refresh'))
else
$uibModal.open
templateUrl: '<%= asset_path "admin/events/price_form.html" %>'
size: 'md'
resolve:
category: -> $scope.priceCategories[index]
controller: 'PriceCategoryController'
.result['finally'](null).then (p_cat) ->
# update the price category to the API
PriceCategory.update {id: id}, {price_category: p_cat}, (cat) ->
$scope.priceCategories[index] = cat
growl.success(_t('price_category_successfully_updated'))
, (err)->
growl.error(_t('unable_to_update_the_price_category'))
console.error(err)
##
# Delete the given price category from the API
# @param index {number} index of the caterory in the $scope.priceCategories array
# @param id {number} price category ID, must match the ID of the category at the index specified above
##
$scope.removePriceCategory = (id, index) ->
if $scope.priceCategories[index].id != id
growl.error(_t('unexpected_error_occurred_please_refresh'))
else if $scope.priceCategories[index].events > 0
growl.error(_t('unable_to_delete_this_price_category_because_it_is_already_used'))
else
dialogs.confirm
resolve:
object: ->
title: _t('confirmation_required')
msg: _t('do_you_really_want_to_delete_this_price_category')
, -> # delete confirmed
PriceCategory.remove {id: id}, -> # successfully deleted
growl.success _t('price_category_successfully_deleted')
$scope.priceCategories.splice(index, 1)
, ->
growl.error _t('price_category_deletion_failed')
##
# Triggered when the admin changes the events filter (all, passed, future).
# We request the first page of corresponding events to the API
##
$scope.changeScope = ->
Event.query {page: 1, scope: $scope.eventsScope.selected}, (data)->
$scope.events = data
paginationCheck(data, $scope.events)
$scope.page = 1
### PRIVATE SCOPE ###
##
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
paginationCheck(eventsPromise, $scope.events)
##
# 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
if events.length >= lastEvents[0].nb_total_events
$scope.paginateActive = false
else
$scope.paginateActive = true
else
$scope.paginateActive = false
##
# Return the model and the datastore matching the given name
# @param name {string} 'category', 'theme' or 'age_range'
# @return {[Object, Array]} model and datastore
##
getModel = (name) ->
switch name
when 'category' then [Category, $scope.categories]
when 'theme' then [EventTheme, $scope.themes]
when 'age_range' then [AgeRange, $scope.ageRanges]
else [null, []]
# 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
]
##
# Controller used in the event creation page
##
Application.Controllers.controller "NewEventController", ["$scope", "$state", 'CSRF', 'categoriesPromise', 'themesPromise', 'ageRangesPromise', 'priceCategoriesPromise', '_t'
, ($scope, $state, CSRF, categoriesPromise, themesPromise, ageRangesPromise, priceCategoriesPromise, _t) ->
CSRF.setMetaTags()
## API URL where the form will be posted
$scope.actionUrl = "/api/events/"
## Form action on the above URL
$scope.method = 'post'
## List of categories for the events
$scope.categories = categoriesPromise
## List of events themes
$scope.themes = themesPromise
## List of age ranges
$scope.ageRanges = ageRangesPromise
## List of availables price's categories
$scope.priceCategories = priceCategoriesPromise
## Default event parameters
$scope.event =
event_files_attributes: []
start_date: new Date()
end_date: new Date()
start_time: new Date()
end_time: new Date()
all_day: 'false'
recurrence: 'none'
category_id: null
prices: []
## Possible types of recurrences for an event
$scope.recurrenceTypes = [
{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'}
]
## Using the EventsController
new EventsController($scope, $state)
]
##
# Controller used in the events edition page
##
Application.Controllers.controller "EditEventController", ["$scope", "$state", "$stateParams", 'CSRF', 'eventPromise', 'categoriesPromise', 'themesPromise', 'ageRangesPromise', 'priceCategoriesPromise'
, ($scope, $state, $stateParams, CSRF, eventPromise, categoriesPromise, themesPromise, ageRangesPromise, priceCategoriesPromise) ->
### PUBLIC SCOPE ###
## API URL where the form will be posted
$scope.actionUrl = "/api/events/" + $stateParams.id
## Form action on the above URL
$scope.method = 'put'
## Retrieve the event details, in case of error the user is redirected to the events listing
$scope.event = eventPromise
## List of categories for the events
$scope.categories = categoriesPromise
## List of availables price's categories
$scope.priceCategories = priceCategoriesPromise
## List of events themes
$scope.themes = themesPromise
## List of age ranges
$scope.ageRanges = ageRangesPromise
### 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)
## !!! MUST BE CALLED AT THE END of the controller
initialize()
]

View File

@ -0,0 +1,536 @@
/* eslint-disable
camelcase,
no-return-assign,
no-undef,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
'use strict';
/* COMMON CODE */
/**
* Provides a set of common properties and methods to the $scope parameter. They are used
* in the various events' admin controllers.
*
* Provides :
* - $scope.datePicker = {}
* - $scope.submited(content)
* - $scope.cancel()
* - $scope.addFile()
* - $scope.deleteFile(file)
* - $scope.fileinputClass(v)
* - $scope.toggleStartDatePicker($event)
* - $scope.toggleEndDatePicker($event)
* - $scope.toggleRecurrenceEnd(e)
* - $scope.addPrice()
* - $scope.removePrice(price, $event)
*
* Requires :
* - $scope.event.event_files_attributes = []
* - $state (Ui-Router) [ 'app.public.events_list' ]
*/
class EventsController {
constructor ($scope, $state) {
// default parameters for AngularUI-Bootstrap datepicker
$scope.datePicker = {
format: Fablab.uibDateFormat,
startOpened: false, // default: datePicker is not shown
endOpened: false,
recurrenceEndOpened: false,
options: {
startingDay: Fablab.weekStartingDay
}
};
/**
* 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 project page.
* @param content {Object} JSON - The upload's result
*/
$scope.submited = function (content) {
if ((content.id == null)) {
$scope.alerts = [];
angular.forEach(content, function (v, k) {
angular.forEach(v, function (err) { $scope.alerts.push({ msg: k + ': ' + err, type: 'danger' }); });
});
} else {
$state.go('app.public.events_list');
}
};
/**
* Changes the user's view to the events list page
*/
$scope.cancel = function () { $state.go('app.public.events_list'); };
/**
* For use with 'ng-class', returns the CSS class name for the uploads previews.
* The preview may show a placeholder or the content of the file depending on the upload state.
* @param v {*} any attribute, will be tested for truthiness (see JS evaluation rules)
*/
$scope.fileinputClass = function (v) {
if (v) {
return 'fileinput-exists';
} else {
return 'fileinput-new';
}
};
/**
* This will create a single new empty entry into the event's attachements list.
*/
$scope.addFile = function () { $scope.event.event_files_attributes.push({}); };
/**
* This will remove the given file from the event's attachements list. If the file was previously uploaded
* to the server, it will be marked for deletion on the server. Otherwise, it will be simply truncated from
* the attachements array.
* @param file {Object} the file to delete
*/
$scope.deleteFile = function (file) {
const index = $scope.event.event_files_attributes.indexOf(file);
if (file.id != null) {
return file._destroy = true;
} else {
return $scope.event.event_files_attributes.splice(index, 1);
}
};
/**
* Show/Hide the "start" datepicker (open the drop down/close it)
*/
$scope.toggleStartDatePicker = function ($event) {
$event.preventDefault();
$event.stopPropagation();
return $scope.datePicker.startOpened = !$scope.datePicker.startOpened;
};
/**
* Show/Hide the "end" datepicker (open the drop down/close it)
*/
$scope.toggleEndDatePicker = function ($event) {
$event.preventDefault();
$event.stopPropagation();
return $scope.datePicker.endOpened = !$scope.datePicker.endOpened;
};
/**
* Masks/displays the recurrence pane allowing the admin to set the current event as recursive
*/
$scope.toggleRecurrenceEnd = function (e) {
e.preventDefault();
e.stopPropagation();
return $scope.datePicker.recurrenceEndOpened = !$scope.datePicker.recurrenceEndOpened;
};
/**
* Initialize a new price item in the additional prices list
*/
$scope.addPrice = function () {
$scope.event.prices.push({
category: null,
amount: null
});
};
/**
* Remove the price or mark it as 'to delete'
*/
$scope.removePrice = function (price, event) {
event.preventDefault();
event.stopPropagation();
if (price.id) {
price._destroy = true;
} else {
const index = $scope.event.prices.indexOf(price);
$scope.event.prices.splice(index, 1);
}
};
}
}
/**
* Controller used in the events listing page (admin view)
*/
Application.Controllers.controller('AdminEventsController', ['$scope', '$state', 'dialogs', '$uibModal', 'growl', 'Event', 'Category', 'EventTheme', 'AgeRange', 'PriceCategory', 'eventsPromise', 'categoriesPromise', 'themesPromise', 'ageRangesPromise', 'priceCategoriesPromise', '_t',
function ($scope, $state, dialogs, $uibModal, growl, Event, Category, EventTheme, AgeRange, PriceCategory, eventsPromise, categoriesPromise, themesPromise, ageRangesPromise, priceCategoriesPromise, _t) {
/* PUBLIC SCOPE */
// By default, the pagination mode is activated to limit the page size
$scope.paginateActive = true;
// The events displayed on the page
$scope.events = eventsPromise;
// Current virtual page
$scope.page = 1;
// Temporary datastore for creating new elements
$scope.inserted = {
category: null,
theme: null,
age_range: null
};
// List of categories for the events
$scope.categories = categoriesPromise;
// List of events themes
$scope.themes = themesPromise;
// List of age ranges
$scope.ageRanges = ageRangesPromise;
// List of price categories for the events
$scope.priceCategories = priceCategoriesPromise;
// Default: we display all events (no restriction)
$scope.eventsScope =
{ selected: '' };
/**
* Adds a bucket of events to the bottom of the page, grouped by month
*/
$scope.loadMoreEvents = function () {
$scope.page += 1;
return Event.query({ page: $scope.page, scope: $scope.eventsScope.selected }, function (data) {
$scope.events = $scope.events.concat(data);
return paginationCheck(data, $scope.events);
});
};
/**
* Saves a new element / Update an existing one to the server (form validation callback)
* @param model {string} model name
* @param data {Object} element name
* @param [id] {number} element id, in case of update
*/
$scope.saveElement = function (model, data, id) {
if (id != null) {
return getModel(model)[0].update({ id }, data);
} else {
return getModel(model)[0].save(data, function (resp) { getModel(model)[1][getModel(model)[1].length - 1].id = resp.id; });
}
};
/**
* Deletes the element at the specified index
* @param model {string} model name
* @param index {number} element index in the $scope[model] array
*/
$scope.removeElement = function (model, index) {
if ((model === 'category') && (getModel(model)[1].length === 1)) {
growl.error(_t('at_least_one_category_is_required') + ' ' + _t('unable_to_delete_the_last_one'));
return false;
}
if (getModel(model)[1][index].related_to > 0) {
growl.error(_t('unable_to_delete_ELEMENT_already_in_use_NUMBER_times', { ELEMENT: model, NUMBER: getModel(model)[1][index].related_to }, 'messageformat'));
return false;
}
return dialogs.confirm({
resolve: {
object () {
return {
title: _t('confirmation_required'),
msg: _t('do_you_really_want_to_delete_this_ELEMENT', { ELEMENT: model }, 'messageformat')
};
}
}
}
, function () { // delete confirmed
getModel(model)[0].delete(getModel(model)[1][index], null, function () { getModel(model)[1].splice(index, 1); }
, function () { growl.error(_t('unable_to_delete_an_error_occured')); });
});
};
/**
* Creates a new empty entry in the $scope[model] array
* @param model {string} model name
*/
$scope.addElement = function (model) {
$scope.inserted[model] = {
name: '',
related_to: 0
};
return getModel(model)[1].push($scope.inserted[model]);
};
/**
* Removes the newly inserted but not saved element / Cancel the current element modification
* @param model {string} model name
* @param rowform {Object} see http://vitalets.github.io/angular-xeditable/
* @param index {number} element index in the $scope[model] array
*/
$scope.cancelElement = function (model, rowform, index) {
if (getModel(model)[1][index].id != null) {
return rowform.$cancel();
} else {
return getModel(model)[1].splice(index, 1);
}
};
/**
* Open a modal dialog allowing the definition of a new price category.
* Save it once filled and handle the result.
*/
$scope.newPriceCategory = function () {
$uibModal.open({
templateUrl: '<%= asset_path "admin/events/price_form.html" %>',
size: 'md',
resolve: {
category () { return {}; }
},
controller: 'PriceCategoryController' }).result['finally'](null).then(function (p_cat) {
// save the price category to the API
PriceCategory.save(p_cat, function (cat) {
$scope.priceCategories.push(cat);
return growl.success(_t('price_category_successfully_created'));
}
, function (err) {
growl.error(_t('unable_to_add_the_price_category_check_name_already_used'));
return console.error(err);
});
});
};
/**
* Update the given price category with the new properties
* to specify in a modal dialog
* @param index {number} index of the caterory in the $scope.priceCategories array
* @param id {number} price category ID, must match the ID of the category at the index specified above
*/
$scope.editPriceCategory = function (id, index) {
if ($scope.priceCategories[index].id !== id) {
return growl.error(_t('unexpected_error_occurred_please_refresh'));
} else {
return $uibModal.open({
templateUrl: '<%= asset_path "admin/events/price_form.html" %>',
size: 'md',
resolve: {
category () { return $scope.priceCategories[index]; }
},
controller: 'PriceCategoryController' }).result['finally'](null).then(function (p_cat) {
// update the price category to the API
PriceCategory.update({ id }, { price_category: p_cat }, function (cat) {
$scope.priceCategories[index] = cat;
return growl.success(_t('price_category_successfully_updated'));
}
, function (err) {
growl.error(_t('unable_to_update_the_price_category'));
return console.error(err);
});
});
}
};
/**
* Delete the given price category from the API
* @param index {number} index of the caterory in the $scope.priceCategories array
* @param id {number} price category ID, must match the ID of the category at the index specified above
*/
$scope.removePriceCategory = function (id, index) {
if ($scope.priceCategories[index].id !== id) {
return growl.error(_t('unexpected_error_occurred_please_refresh'));
} else if ($scope.priceCategories[index].events > 0) {
return growl.error(_t('unable_to_delete_this_price_category_because_it_is_already_used'));
} else {
return dialogs.confirm(
{
resolve: {
object () {
return {
title: _t('confirmation_required'),
msg: _t('do_you_really_want_to_delete_this_price_category')
};
}
}
},
function () { // delete confirmed
PriceCategory.remove(
{ id },
function () { // successfully deleted
growl.success(_t('price_category_successfully_deleted'));
$scope.priceCategories.splice(index, 1);
},
function () { growl.error(_t('price_category_deletion_failed')); }
);
}
);
}
};
/**
* Triggered when the admin changes the events filter (all, passed, future).
* We request the first page of corresponding events to the API
*/
$scope.changeScope = function () {
Event.query({ page: 1, scope: $scope.eventsScope.selected }, function (data) {
$scope.events = data;
return paginationCheck(data, $scope.events);
});
return $scope.page = 1;
};
/* PRIVATE SCOPE */
/**
* Kind of constructor: these actions will be realized first when the controller is loaded
*/
const initialize = function () { paginationCheck(eventsPromise, $scope.events); };
/**
* 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)
*/
var paginationCheck = function (lastEvents, events) {
if (lastEvents.length > 0) {
if (events.length >= lastEvents[0].nb_total_events) {
return $scope.paginateActive = false;
} else {
return $scope.paginateActive = true;
}
} else {
return $scope.paginateActive = false;
}
};
/**
* Return the model and the datastore matching the given name
* @param name {string} 'category', 'theme' or 'age_range'
* @return {[Object, Array]} model and datastore
*/
var getModel = function (name) {
switch (name) {
case 'category': return [Category, $scope.categories];
case 'theme': return [EventTheme, $scope.themes];
case 'age_range': return [AgeRange, $scope.ageRanges];
default: return [null, []];
}
};
// init the controller (call at the end !)
return initialize();
}
]);
/**
* Controller used in the reservations listing page for a specific event
*/
Application.Controllers.controller('ShowEventReservationsController', ['$scope', 'eventPromise', 'reservationsPromise', function ($scope, eventPromise, reservationsPromise) {
// retrieve the event from the ID provided in the current URL
$scope.event = eventPromise;
// list of reservations for the current event
return $scope.reservations = reservationsPromise;
}]);
/**
* Controller used in the event creation page
*/
Application.Controllers.controller('NewEventController', ['$scope', '$state', 'CSRF', 'categoriesPromise', 'themesPromise', 'ageRangesPromise', 'priceCategoriesPromise', '_t',
function ($scope, $state, CSRF, categoriesPromise, themesPromise, ageRangesPromise, priceCategoriesPromise, _t) {
CSRF.setMetaTags();
// API URL where the form will be posted
$scope.actionUrl = '/api/events/';
// Form action on the above URL
$scope.method = 'post';
// List of categories for the events
$scope.categories = categoriesPromise;
// List of events themes
$scope.themes = themesPromise;
// List of age ranges
$scope.ageRanges = ageRangesPromise;
// List of availables price's categories
$scope.priceCategories = priceCategoriesPromise;
// Default event parameters
$scope.event = {
event_files_attributes: [],
start_date: new Date(),
end_date: new Date(),
start_time: new Date(),
end_time: new Date(),
all_day: 'false',
recurrence: 'none',
category_id: null,
prices: []
};
// Possible types of recurrences for an event
$scope.recurrenceTypes = [
{ 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' }
];
// Using the EventsController
return new EventsController($scope, $state);
}
]);
/**
* Controller used in the events edition page
*/
Application.Controllers.controller('EditEventController', ['$scope', '$state', '$stateParams', 'CSRF', 'eventPromise', 'categoriesPromise', 'themesPromise', 'ageRangesPromise', 'priceCategoriesPromise',
function ($scope, $state, $stateParams, CSRF, eventPromise, categoriesPromise, themesPromise, ageRangesPromise, priceCategoriesPromise) {
/* PUBLIC SCOPE */
// API URL where the form will be posted
$scope.actionUrl = `/api/events/${$stateParams.id}`;
// Form action on the above URL
$scope.method = 'put';
// Retrieve the event details, in case of error the user is redirected to the events listing
$scope.event = eventPromise;
// List of categories for the events
$scope.categories = categoriesPromise;
// List of availables price's categories
$scope.priceCategories = priceCategoriesPromise;
// List of events themes
$scope.themes = themesPromise;
// List of age ranges
$scope.ageRanges = ageRangesPromise;
/* PRIVATE SCOPE */
/**
* Kind of constructor: these actions will be realized first when the controller is loaded
*/
const initialize = function () {
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
return new EventsController($scope, $state);
};
// !!! MUST BE CALLED AT THE END of the controller
return initialize();
}
]);

View File

@ -1,657 +0,0 @@
'use strict'
Application.Controllers.controller "GraphsController", ["$scope", "$state", "$rootScope", 'es', 'Statistics', '_t'
, ($scope, $state, $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: Fablab.uibDateFormat
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: Fablab.uibDateFormat
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 information
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, (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": "query_then_fetch"
"size": 0
"stat-type": statType
"custom-query": ''
"start-date": moment($scope.datePickerStart.selected).format()
"end-date": moment($scope.datePickerEnd.selected).format()
"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 groupKey {string} statistics subtype or custom field
# @param sortKey {string} statistics type or 'ca'
# @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, callback) ->
# handle invalid callback
if typeof(callback) != "function"
return console.error('[graphsController::queryElasticRanking] Error: invalid callback provided')
if !esType or !groupKey or !sortKey
return callback([], '[graphsController::queryElasticRanking] Error: invalid parameters provided')
# run query
es.search
"index": "stats"
"type": esType
"searchType": "query_then_fetch"
"size": 0
"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['offset'] = '-1d'
# scale days to UTC time
else if interval == 'day'
offset = moment().utcOffset()
q.aggregations.subgroups.aggregations.intervals.date_histogram['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": groupKey
"size": 10
"order":
"total": "desc"
"aggregations":
"top_events":
"top_hits":
"size": 1
"sort": [
{ "ca": "desc" }
]
"total":
"sum":
"field": "stat"
# 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()
]

View File

@ -0,0 +1,750 @@
/* eslint-disable
camelcase,
no-return-assign,
no-undef,
no-unreachable,
no-unused-vars,
standard/no-callback-literal,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS205: Consider reworking code to avoid use of IIFEs
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
'use strict';
Application.Controllers.controller('GraphsController', ['$scope', '$state', '$rootScope', 'es', 'Statistics', '_t',
function ($scope, $state, $rootScope, es, Statistics, _t) {
/* PRIVATE STATIC CONSTANTS */
// height of the HTML/SVG charts elements in pixels
const CHART_HEIGHT = 500;
// Label of the charts' horizontal axes
const X_AXIS_LABEL = _t('date');
// Label of the charts' vertical axes
const Y_AXIS_LABEL = _t('number');
// Colors for the line charts. Each new line uses the next color in this array
const 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: Fablab.uibDateFormat,
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: Fablab.uibDateFormat,
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 = function (tab) {
$scope.selectedIndex = tab;
$scope.ranking.groupCriterion = 'subType';
if (tab.ca) {
$scope.ranking.sortCriterion = 'ca';
} else {
$scope.ranking.sortCriterion = tab.types[0].key;
}
return refreshChart();
};
/**
* Callback to close the date-picking popup and refresh the results
*/
$scope.validateDateChange = function () {
$scope.datePicker.show = false;
return refreshChart();
};
/* PRIVATE SCOPE */
/**
* Kind of constructor: these actions will be realized first when the controller is loaded
*/
const initialize = function () {
Statistics.query(function (stats) {
$scope.statistics = stats;
// watch the interval changes to refresh the graph
$scope.$watch(scope => scope.display.interval
, (newValue, oldValue) => refreshChart());
$scope.$watch(scope => scope.ranking.sortCriterion
, (newValue, oldValue) => refreshChart());
$scope.$watch(scope => scope.ranking.groupCriterion
, (newValue, oldValue) => refreshChart());
return 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
return $rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams) {
if ((fromState.name === 'app.admin.stats_graphs') && (Object.keys(fromParams).length === 0)) {
return $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
*/
var toggleDatePicker = function ($event, datePicker) {
$event.preventDefault();
$event.stopPropagation();
return datePicker.opened = !datePicker.opened;
};
/**
* Query elasticSearch according to the current parameters and update the chart
*/
var refreshChart = function () {
if ($scope.selectedIndex && !$scope.preventRefresh) {
return query($scope.selectedIndex, function (aggregations, error) {
if (error) {
return console.error(error);
} else {
if ($scope.selectedIndex.graph.chart_type !== 'discreteBarChart') {
$scope.data = formatAggregations(aggregations);
return 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);
return 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
*/
const xAxisTickFormatFunction = function (d, x, y) {
/* WARNING !! These tests (typeof/instanceof) may become broken on nvd3 update */
if ($scope.display.interval === 'day') {
if ((typeof d === 'number') || d instanceof Date) {
return d3.time.format(Fablab.d3DateFormat)(moment(d).toDate());
} else { // typeof d == 'string'
return d;
}
} else if ($scope.display.interval === 'week') {
if ((typeof x === 'number') || d instanceof Date) {
return d3.time.format(_t('week_short') + ' %U')(moment(d).toDate());
} else if (typeof d === 'number') {
return _t('week_of_START_to_END', { START: moment(d).format('L'), END: moment(d).add(6, 'days').format('L') });
} else { // typeof d == 'string'
return d;
}
} else if ($scope.display.interval === 'month') {
if (typeof d === 'number') {
const label = moment(d).format('MMMM YYYY');
return label.substr(0, 1).toUpperCase() + label.substr(1).toLowerCase();
} else { // typeof d == 'string'
return d;
}
}
};
/**
* Format aggregations as retuned by elasticSearch to an understandable format for NVD3
* @param aggs {Object} as returned by elasticsearch
*/
var formatAggregations = function (aggs) {
const format = {};
angular.forEach(aggs, function (type, type_key) { // go through aggs[$TYPE] where $TYPE = month|year|hour|booking|...
format[type_key] = [];
if (type.subgroups) {
return angular.forEach(type.subgroups.buckets, subgroup => // go through aggs.$TYPE.subgroups.buckets where each bucket represent a $SUBTYPE
angular.forEach($scope.selectedIndex.types, function (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
return (() => {
const result = [];
for (let it_st = 0, end = cur_type.subtypes.length - 1; it_st <= end; it_st++) { // when we've found it, iterate over its subtypes ...
const cur_subtype = cur_type.subtypes[it_st];
if (subgroup.key === cur_subtype.key) { // ... which match $SUBTYPE
// then we construct NVD3 dataSource according to these information
var 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, function (interval) {
dataSource.values.push({
x: interval.key,
y: interval.total.value
});
return dataSource.total += parseInt(interval.total.value);
});
dataSource.key += ` (${dataSource.total})`;
result.push(format[type_key].push(dataSource));
} else {
result.push(undefined);
}
}
return result;
})();
}
})
);
}
});
return 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
*/
var formatRankingAggregations = function (aggs, limit, typeKey) {
const format =
{ ranking: [] };
let it = 0;
while (it < aggs.subgroups.buckets.length) {
const bucket = aggs.subgroups.buckets[it];
const 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++;
}
const getY = object => object.values[0].y;
format.ranking = stableSort(format.ranking, 'DESC', getY).slice(0, limit);
for (let i = 0, end = format.ranking.length; i <= end; i++) {
if (typeof format.ranking[i] === 'undefined') { format.ranking.splice(i, 1); }
}
return 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
*/
var getRankingLabel = function (key, typeKey) {
if ($scope.selectedIndex) {
if (typeKey === 'subType') {
for (let type of Array.from($scope.selectedIndex.types)) {
for (let subtype of Array.from(type.subtypes)) {
if (subtype.key === key) {
return subtype.label;
}
}
}
} else {
for (let field of Array.from($scope.selectedIndex.additional_fields)) {
if (field.key === typeKey) {
switch (field.data_type) {
case 'date': return moment(key).format('LL'); break;
case 'list': return key.name; break;
default: 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)
*/
var query = function (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
const stat_types = [];
for (let t of Array.from(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');
}
let type_it = 0;
const results = {};
let error = '';
var recursiveCb = function () {
if (type_it < stat_types.length) {
return queryElasticStats(index.es_type_key, stat_types[type_it], function (prevResults, prevError) {
if (prevError) {
console.error(`[graphsController::query] ${prevError}`);
error += `\n${prevError}`;
}
results[stat_types[type_it]] = prevResults;
type_it++;
return recursiveCb();
});
} else {
return callback(results);
}
};
return recursiveCb();
} else { // palmares (ranking)
return queryElasticRanking(index.es_type_key, $scope.ranking.groupCriterion, $scope.ranking.sortCriterion, function (results, error) {
if (error) {
return callback([], error);
} else {
return 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)
*/
var queryElasticStats = function (esType, statType, callback) {
// handle invalid callback
if (typeof (callback) !== 'function') {
console.error('[graphsController::queryElasticStats] Error: invalid callback provided');
return;
}
if (!esType || !statType) {
callback([], '[graphsController::queryElasticStats] Error: invalid parameters provided');
}
// run query
return es.search({
'index': 'stats',
'type': esType,
'searchType': 'query_then_fetch',
'size': 0,
'stat-type': statType,
'custom-query': '',
'start-date': moment($scope.datePickerStart.selected).format(),
'end-date': moment($scope.datePickerEnd.selected).format(),
'body': buildElasticAggregationsQuery(statType, $scope.display.interval, moment($scope.datePickerStart.selected), moment($scope.datePickerEnd.selected))
}
, function (error, response) {
if (error) {
return callback([], `Error: something unexpected occurred during elasticSearch query: ${error}`);
} else {
return 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 groupKey {string} statistics subtype or custom field
* @param sortKey {string} statistics type or 'ca'
* @param callback {function} function be to run after results were retrieved,
* it will receive two parameters : results {Array}, error {String} (if any)
*/
var queryElasticRanking = function (esType, groupKey, sortKey, callback) {
// handle invalid callback
if (typeof (callback) !== 'function') {
return console.error('[graphsController::queryElasticRanking] Error: invalid callback provided');
}
if (!esType || !groupKey || !sortKey) {
return callback([], '[graphsController::queryElasticRanking] Error: invalid parameters provided');
}
// run query
return es.search({
'index': 'stats',
'type': esType,
'searchType': 'query_then_fetch',
'size': 0,
'body': buildElasticAggregationsRankingQuery(groupKey, sortKey, moment($scope.datePickerStart.selected), moment($scope.datePickerEnd.selected))
}
, function (error, response) {
if (error) {
return callback([], `Error: something unexpected occurred during elasticSearch query: ${error}`);
} else {
return 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
*/
const 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)
*/
var buildElasticAggregationsQuery = function (type, interval, intervalBegin, intervalEnd) {
const 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['offset'] = '-1d';
// scale days to UTC time
} else if (interval === 'day') {
const offset = moment().utcOffset();
q.aggregations.subgroups.aggregations.intervals.date_histogram['offset'] = (-offset) + 'm';
}
return 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)
*/
var buildElasticAggregationsRankingQuery = function (groupKey, sortKey, intervalBegin, intervalEnd) {
const q = {
'query': {
'bool': {
'must': [
{
'range': {
'date': {
'gte': intervalBegin.format(),
'lte': intervalEnd.format()
}
}
},
{
'term': {
'type': 'booking'
}
}
]
}
},
'aggregations': {
'subgroups': {
'terms': {
'field': groupKey,
'size': 10,
'order': {
'total': 'desc'
}
},
'aggregations': {
'top_events': {
'top_hits': {
'size': 1,
'sort': [
{ 'ca': 'desc' }
]
}
},
'total': {
'sum': {
'field': 'stat'
}
}
}
}
}
};
// results must be sorted and limited later by angular
if (sortKey !== 'ca') {
angular.forEach(q.query.bool.must, function (must) {
if (must.term) {
return must.term.type = sortKey;
}
});
} else {
q.aggregations.subgroups.aggregations.total.sum.field = sortKey;
}
return 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)
*/
var updateChart = function (chart_type, data, type) {
const id = `#chart-${type} svg`;
// clean old charts
d3.selectAll(id + ' > *').remove();
return nv.addGraph(function () {
// no data or many dates, display line charts
let chart;
if ((data.length === 0) || ((data[0].values.length > 1) && (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
return chart;
});
};
/**
* Given an NVD3 line chart axis, scale it to display ordinated dates, according to the given arguments
*/
var setTimeScale = function (nvd3Axis, nvd3Scale, argsArray) {
const scale = d3.time.scale();
nvd3Axis.scale(scale);
nvd3Scale(scale);
if (!argsArray && !argsArray.length) {
const oldTicks = nvd3Axis.axis.ticks;
return nvd3Axis.axis.ticks = () => oldTicks.apply(nvd3Axis.axis, argsArray);
}
};
/**
* Translate line chart data in dates row to bar chart data, one bar per type.
*/
var prepareDataForBarChart = function (data, type) {
const newData = [{
key: type,
values: []
}
];
for (let info of Array.from(data)) {
if (info) {
newData[0].values.push({
'label': info.key,
'value': info.values[0].y,
'color': info.color
});
}
}
return 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}
*/
var stableSort = function (array, order, getValue) {
// prepare sorting
const keys_order = [];
const result = [];
for (let i = 0, end = array.length; i <= end; i++) {
keys_order[array[i]] = i;
result.push(array[i]);
}
// callback for javascript native Array.sort()
const sort_fc = function (a, b) {
const val_a = getValue(a);
const val_b = getValue(b);
if (val_a === val_b) {
return keys_order[a] - keys_order[b];
}
if (val_a < val_b) {
if (order === 'ASC') {
return -1;
} else { return 1; }
} else {
if (order === 'ASC') {
return 1;
} else { return -1; }
}
};
// finish the sort
result.sort(sort_fc);
return result;
};
// !!! MUST BE CALLED AT THE END of the controller
return initialize();
}
]);

View File

@ -1,90 +0,0 @@
Application.Controllers.controller "GroupsController", ["$scope", 'groupsPromise', 'Group', 'growl', '_t', ($scope, groupsPromise, Group, growl, _t) ->
## List of users groups
$scope.groups = groupsPromise
## Default: we show only enabled groups
$scope.groupFiltering = 'enabled'
## Available options for filtering groups by status
$scope.filterDisabled = [
'enabled',
'disabled',
'all',
]
##
# 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 [id] {number} group id, in case of update
##
$scope.saveGroup = (data, id) ->
if id?
Group.update {id: id}, { group: data }, (response) ->
growl.success(_t('group_form.changes_successfully_saved'))
, (error) ->
growl.error(_t('group_form.an_error_occurred_while_saving_changes'))
else
Group.save { group: data }, (resp)->
growl.success(_t('group_form.new_group_successfully_saved'))
$scope.groups[$scope.groups.length-1].id = resp.id
, (error) ->
growl.error(_t('.group_forman_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_form.group_successfully_deleted'))
$scope.groups.splice(index, 1)
, (error) ->
growl.error(_t('group_form.unable_to_delete_group_because_some_users_and_or_groups_are_still_linked_to_it'))
##
# Enable/disable the group at the specified index
# @param index {number} group index in the $scope.groups array
##
$scope.toggleDisableGroup = (index) ->
group = $scope.groups[index]
if (!group.disabled && group.users > 0)
growl.error(_t('group_form.unable_to_disable_group_with_users', { USERS: group.users }, 'messageformat'))
else
Group.update {id: group.id}, { group: { disabled: !group.disabled } }, (response) ->
$scope.groups[index] = response
growl.success(_t('group_form.group_successfully_enabled_disabled', { STATUS: response.disabled }, 'messageformat'))
, (error) ->
growl.error(_t('group_form.unable_to_enable_disable_group', { STATUS: !group.disabled }, 'messageformat'))
]

View File

@ -0,0 +1,100 @@
/* eslint-disable
handle-callback-err,
no-return-assign,
no-undef,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
Application.Controllers.controller('GroupsController', ['$scope', 'groupsPromise', 'Group', 'growl', '_t', function ($scope, groupsPromise, Group, growl, _t) {
// List of users groups
$scope.groups = groupsPromise;
// Default: we show only enabled groups
$scope.groupFiltering = 'enabled';
// Available options for filtering groups by status
$scope.filterDisabled = [
'enabled',
'disabled',
'all'
];
/**
* 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 = function (rowform, index) {
if ($scope.groups[index].id != null) {
return rowform.$cancel();
} else {
return $scope.groups.splice(index, 1);
}
};
/**
* Creates a new empty entry in the $scope.groups array
*/
$scope.addGroup = function () {
$scope.inserted =
{ name: '' };
return $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 [id] {number} group id, in case of update
*/
$scope.saveGroup = function (data, id) {
if (id != null) {
return Group.update({ id }, { group: data }, response => growl.success(_t('group_form.changes_successfully_saved'))
, error => growl.error(_t('group_form.an_error_occurred_while_saving_changes')));
} else {
return Group.save({ group: data }, function (resp) {
growl.success(_t('group_form.new_group_successfully_saved'));
return $scope.groups[$scope.groups.length - 1].id = resp.id;
}
, function (error) {
growl.error(_t('.group_forman_error_occurred_when_saving_the_new_group'));
return $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 }, function (resp) {
growl.success(_t('group_form.group_successfully_deleted'));
return $scope.groups.splice(index, 1);
}
, error => growl.error(_t('group_form.unable_to_delete_group_because_some_users_and_or_groups_are_still_linked_to_it')));
/**
* Enable/disable the group at the specified index
* @param index {number} group index in the $scope.groups array
*/
return $scope.toggleDisableGroup = function (index) {
const group = $scope.groups[index];
if (!group.disabled && (group.users > 0)) {
return growl.error(_t('group_form.unable_to_disable_group_with_users', { USERS: group.users }, 'messageformat'));
} else {
return Group.update({ id: group.id }, { group: { disabled: !group.disabled } }, function (response) {
$scope.groups[index] = response;
return growl.success(_t('group_form.group_successfully_enabled_disabled', { STATUS: response.disabled }, 'messageformat'));
}
, error => growl.error(_t('group_form.unable_to_enable_disable_group', { STATUS: !group.disabled }, 'messageformat')));
}
};
}
]);

View File

@ -1,574 +0,0 @@
'use strict'
##
# Controller used in the admin invoices listing page
##
Application.Controllers.controller "InvoicesController", ["$scope", "$state", 'Invoice', 'invoices', '$uibModal', "growl", "$filter", 'Setting', 'settings', '_t'
, ($scope, $state, Invoice, invoices, $uibModal, growl, $filter, Setting, settings, _t) ->
### PRIVATE STATIC CONSTANTS ###
# number of invoices loaded each time we click on 'load more...'
INVOICES_PER_PAGE = 20
### PUBLIC SCOPE ###
## List of all users invoices
$scope.invoices = invoices
# Invoices filters
$scope.searchInvoice =
date: null
name: ''
reference: ''
# currently displayed page of invoices (search results)
$scope.page = 1
# true when all invoices are loaded
$scope.noMoreResults = false
## 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
resetSearchInvoice()
invoiceSearch()
##
# 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 information
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 wallet (W[text]) - does not apply here
sample = sample.replace(/W\[([^\]]+)\]/g, "")
# 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 information
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 information 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)
##
# Callback when any of the filters changes.
# Full reload the results list
##
$scope.handleFilterChange = ->
resetSearchInvoice()
invoiceSearch()
##
# Callback for the 'load more' button.
# Will load the next results of the current search, if any
##
$scope.showNextInvoices = ->
$scope.page += 1
invoiceSearch(true)
### PRIVATE SCOPE ###
##
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
if (!invoices[0] || invoices[0].maxInvoices <= $scope.invoices.length)
$scope.noMoreResults = true
# 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 <p>, <span>, ...).
# The supported tags are <b>, <u>, <i> and <br>.
# @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
''
)
##
# Reinitialize the context of invoices' search to display new results set
##
resetSearchInvoice = ->
$scope.page = 1
$scope.noMoreResults = false
##
# Run a search query with the current parameters set concerning invoices, then affect or concat the results
# to $scope.invoices
# @param concat {boolean} if true, the result will be append to $scope.invoices instead of being affected
##
invoiceSearch = (concat) ->
Invoice.list {
query:
number: $scope.searchInvoice.reference
customer: $scope.searchInvoice.name
date: $scope.searchInvoice.date
order_by: $scope.orderInvoice
page: $scope.page
size: INVOICES_PER_PAGE
}, (invoices) ->
if concat
$scope.invoices = $scope.invoices.concat(invoices)
else
$scope.invoices = invoices
if (!invoices[0] || invoices[0].maxInvoices <= $scope.invoices.length)
$scope.noMoreResults = true
## !!! MUST BE CALLED AT THE END of the controller
initialize()
]
##
# Controller used in the invoice refunding modal window
##
Application.Controllers.controller 'AvoirModalController', ["$scope", "$uibModalInstance", "invoice", "Invoice", "growl", '_t'
, ($scope, $uibModalInstance, 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'}
{name: _t('by_wallet'), value: 'wallet'}
]
## 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: Fablab.uibDateFormat
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()
]

View File

@ -0,0 +1,602 @@
/* eslint-disable
handle-callback-err,
no-return-assign,
no-undef,
no-useless-escape,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
'use strict';
/**
* Controller used in the admin invoices listing page
*/
Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'Invoice', 'invoices', '$uibModal', 'growl', '$filter', 'Setting', 'settings', '_t',
function ($scope, $state, Invoice, invoices, $uibModal, growl, $filter, Setting, settings, _t) {
/* PRIVATE STATIC CONSTANTS */
// number of invoices loaded each time we click on 'load more...'
const INVOICES_PER_PAGE = 20;
/* PUBLIC SCOPE */
// List of all users invoices
$scope.invoices = invoices;
// Invoices filters
$scope.searchInvoice = {
date: null,
name: '',
reference: ''
};
// currently displayed page of invoices (search results)
$scope.page = 1;
// true when all invoices are loaded
$scope.noMoreResults = false;
// 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 = function (orderBy) {
if ($scope.orderInvoice === orderBy) {
$scope.orderInvoice = `-${orderBy}`;
} else {
$scope.orderInvoice = orderBy;
}
resetSearchInvoice();
return invoiceSearch();
};
/**
* 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 = function (invoice) {
// open modal
const modalInstance = $uibModal.open({
templateUrl: '<%= asset_path "admin/invoices/avoirModal.html" %>',
controller: 'AvoirModalController',
resolve: {
invoice () { return invoice; }
}
});
// once done, update the invoice model and inform the admin
return modalInstance.result.then(function (res) {
$scope.invoices.unshift(res.avoir);
return Invoice.get({ id: invoice.id }, function (data) {
invoice.has_avoir = data.has_avoir;
return growl.success(_t('refund_invoice_successfully_created'));
});
});
};
/**
* Generate an invoice reference sample from the parametrized model
* @returns {string} invoice reference sample
*/
$scope.mkReference = function () {
let sample = $scope.invoice.reference.model;
if (sample) {
// invoice number per day (dd..dd)
sample = sample.replace(/d+(?![^\[]*])/g, function (match, offset, string) { return padWithZeros(2, match.length); });
// invoice number per month (mm..mm)
sample = sample.replace(/m+(?![^\[]*])/g, function (match, offset, string) { return padWithZeros(12, match.length); });
// invoice number per year (yy..yy)
sample = sample.replace(/y+(?![^\[]*])/g, function (match, offset, string) { return padWithZeros(8, match.length); });
// date information
sample = sample.replace(/[YMD]+(?![^\[]*])/g, function (match, offset, string) { return $scope.today.format(match); });
// information about online selling (X[text])
sample = sample.replace(/X\[([^\]]+)\]/g, function (match, p1, offset, string) { return p1; });
// information about wallet (W[text]) - does not apply here
sample = sample.replace(/W\[([^\]]+)\]/g, '');
// information about refunds (R[text]) - does not apply here
sample = sample.replace(/R\[([^\]]+)\]/g, '');
}
return sample;
};
/**
* Generate an order nmuber sample from the parametrized model
* @returns {string} invoice reference sample
*/
$scope.mkNumber = function () {
let sample = $scope.invoice.number.model;
if (sample) {
// global order number (nn..nn)
sample = sample.replace(/n+(?![^\[]*])/g, function (match, offset, string) { return padWithZeros(327, match.length); });
// order number per year (yy..yy)
sample = sample.replace(/y+(?![^\[]*])/g, function (match, offset, string) { return padWithZeros(8, match.length); });
// order number per month (mm..mm)
sample = sample.replace(/m+(?![^\[]*])/g, function (match, offset, string) { return padWithZeros(12, match.length); });
// order number per day (dd..dd)
sample = sample.replace(/d+(?![^\[]*])/g, function (match, offset, string) { return padWithZeros(2, match.length); });
// date information
sample = sample.replace(/[YMD]+(?![^\[]*])/g, function (match, offset, string) { return $scope.today.format(match); });
}
return sample;
};
/**
* Open a modal dialog allowing the user to edit the invoice reference generation template
*/
$scope.openEditReference = function () {
const modalInstance = $uibModal.open({
animation: true,
templateUrl: $scope.invoice.reference.templateUrl,
size: 'lg',
resolve: {
model () {
return $scope.invoice.reference.model;
}
},
controller: ['$scope', '$uibModalInstance', 'model', function ($scope, $uibModalInstance, model) {
$scope.model = model;
$scope.ok = function () { $uibModalInstance.close($scope.model); };
$scope.cancel = function () { $uibModalInstance.dismiss('cancel'); };
}]
});
modalInstance.result.then(function (model) {
Setting.update({ name: 'invoice_reference' }, { value: model }, function (data) {
$scope.invoice.reference.model = model;
growl.success(_t('invoice_reference_successfully_saved'));
}
, function (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 = function () {
const modalInstance = $uibModal.open({
animation: true,
templateUrl: $scope.invoice.code.templateUrl,
size: 'lg',
resolve: {
model () {
return $scope.invoice.code.model;
},
active () {
return $scope.invoice.code.active;
}
},
controller: ['$scope', '$uibModalInstance', 'model', 'active', function ($scope, $uibModalInstance, model, active) {
$scope.codeModel = model;
$scope.isSelected = active;
$scope.ok = function () { $uibModalInstance.close({ model: $scope.codeModel, active: $scope.isSelected }); };
$scope.cancel = function () { $uibModalInstance.dismiss('cancel'); };
}]
});
return modalInstance.result.then(function (result) {
Setting.update({ name: 'invoice_code-value' }, { value: result.model }, function (data) {
$scope.invoice.code.model = result.model;
if (result.active) {
return growl.success(_t('invoicing_code_succesfully_saved'));
}
}
, function (error) {
growl.error(_t('an_error_occurred_while_saving_the_invoicing_code'));
return console.error(error);
});
return Setting.update({ name: 'invoice_code-active' }, { value: result.active ? 'true' : 'false' }, function (data) {
$scope.invoice.code.active = result.active;
if (result.active) {
return growl.success(_t('code_successfully_activated'));
} else {
return growl.success(_t('code_successfully_disabled'));
}
}
, function (error) {
growl.error(_t('an_error_occurred_while_activating_the_invoicing_code'));
return console.error(error);
});
});
};
/**
* Open a modal dialog allowing the user to edit the invoice number
*/
$scope.openEditInvoiceNb = function () {
const modalInstance = $uibModal.open({
animation: true,
templateUrl: $scope.invoice.number.templateUrl,
size: 'lg',
resolve: {
model () {
return $scope.invoice.number.model;
}
},
controller: ['$scope', '$uibModalInstance', 'model', function ($scope, $uibModalInstance, model) {
$scope.model = model;
$scope.ok = function () { $uibModalInstance.close($scope.model); };
$scope.cancel = function () { $uibModalInstance.dismiss('cancel'); };
}]
});
return modalInstance.result.then(function (model) {
Setting.update({ name: 'invoice_order-nb' }, { value: model }, function (data) {
$scope.invoice.number.model = model;
return growl.success(_t('order_number_successfully_saved'));
}
, function (error) {
growl.error(_t('an_error_occurred_while_saving_the_order_number'));
return 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 = function () {
const modalInstance = $uibModal.open({
animation: true,
templateUrl: $scope.invoice.VAT.templateUrl,
size: 'lg',
resolve: {
rate () {
return $scope.invoice.VAT.rate;
},
active () {
return $scope.invoice.VAT.active;
}
},
controller: ['$scope', '$uibModalInstance', 'rate', 'active', function ($scope, $uibModalInstance, rate, active) {
$scope.rate = rate;
$scope.isSelected = active;
$scope.ok = function () { $uibModalInstance.close({ rate: $scope.rate, active: $scope.isSelected }); };
return $scope.cancel = function () { $uibModalInstance.dismiss('cancel'); };
}]
});
return modalInstance.result.then(function (result) {
Setting.update({ name: 'invoice_VAT-rate' }, { value: result.rate + '' }, function (data) {
$scope.invoice.VAT.rate = result.rate;
if (result.active) {
return growl.success(_t('VAT_rate_successfully_saved'));
}
}
, function (error) {
growl.error(_t('an_error_occurred_while_saving_the_VAT_rate'));
return console.error(error);
});
return Setting.update({ name: 'invoice_VAT-active' }, { value: result.active ? 'true' : 'false' }, function (data) {
$scope.invoice.VAT.active = result.active;
if (result.active) {
return growl.success(_t('VAT_successfully_activated'));
} else {
return growl.success(_t('VAT_successfully_disabled'));
}
}
, function (error) {
growl.error(_t('an_error_occurred_while_activating_the_VAT'));
return console.error(error);
});
});
};
/**
* Callback to save the value of the text zone when editing is done
*/
$scope.textEditEnd = function (event) {
const parsed = parseHtml($scope.invoice.text.content);
return Setting.update({ name: 'invoice_text' }, { value: parsed }, function (data) {
$scope.invoice.text.content = parsed;
return growl.success(_t('text_successfully_saved'));
}
, function (error) {
growl.error(_t('an_error_occurred_while_saving_the_text'));
return console.error(error);
});
};
/**
* Callback to save the value of the legal information zone when editing is done
*/
$scope.legalsEditEnd = function (event) {
const parsed = parseHtml($scope.invoice.legals.content);
return Setting.update({ name: 'invoice_legals' }, { value: parsed }, function (data) {
$scope.invoice.legals.content = parsed;
return growl.success(_t('address_and_legal_information_successfully_saved'));
}
, function (error) {
growl.error(_t('an_error_occurred_while_saving_the_address_and_the_legal_information'));
return console.error(error);
});
};
/**
* Callback when any of the filters changes.
* Full reload the results list
*/
$scope.handleFilterChange = function () {
resetSearchInvoice();
return invoiceSearch();
};
/**
* Callback for the 'load more' button.
* Will load the next results of the current search, if any
*/
$scope.showNextInvoices = function () {
$scope.page += 1;
return invoiceSearch(true);
};
/* PRIVATE SCOPE */
/**
* Kind of constructor: these actions will be realized first when the controller is loaded
*/
const initialize = function () {
if (!invoices[0] || (invoices[0].maxInvoices <= $scope.invoices.length)) {
$scope.noMoreResults = true;
}
// 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
return $scope.$watch('invoice.logo', function () {
if ($scope.invoice.logo && $scope.invoice.logo.filesize) {
return Setting.update(
{ name: 'invoice_logo' },
{ value: $scope.invoice.logo.base64 },
function (data) { growl.success(_t('logo_successfully_saved')); },
function (error) {
growl.error(_t('an_error_occurred_while_saving_the_logo'));
return 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.
*/
var padWithZeros = function (value, length) { return (1e15 + value + '').slice(-length); };
/**
* Remove every unsupported html tag from the given html text (like <p>, <span>, ...).
* The supported tags are <b>, <u>, <i> and <br>.
* @param html {string} single line html text
* @return {string} multi line simplified html text
*/
var parseHtml = function (html) {
return html.replace(/<\/?(\w+)((\s+\w+(\s*=\s*(?:".*?"|'.*?'|[^'">\s]+))?)+\s*|\s*)\/?>/g, function (match, p1, offset, string) {
if (['b', 'u', 'i', 'br'].includes(p1)) {
return match;
} else {
return '';
}
});
};
/**
* Reinitialize the context of invoices' search to display new results set
*/
var resetSearchInvoice = function () {
$scope.page = 1;
return $scope.noMoreResults = false;
};
/**
* Run a search query with the current parameters set concerning invoices, then affect or concat the results
* to $scope.invoices
* @param concat {boolean} if true, the result will be append to $scope.invoices instead of being affected
*/
var invoiceSearch = function (concat) {
Invoice.list({
query: {
number: $scope.searchInvoice.reference,
customer: $scope.searchInvoice.name,
date: $scope.searchInvoice.date,
order_by: $scope.orderInvoice,
page: $scope.page,
size: INVOICES_PER_PAGE
}
}, function (invoices) {
if (concat) {
$scope.invoices = $scope.invoices.concat(invoices);
} else {
$scope.invoices = invoices;
}
if (!invoices[0] || (invoices[0].maxInvoices <= $scope.invoices.length)) {
return $scope.noMoreResults = true;
}
});
};
// !!! MUST BE CALLED AT THE END of the controller
return initialize();
}
]);
/**
* Controller used in the invoice refunding modal window
*/
Application.Controllers.controller('AvoirModalController', ['$scope', '$uibModalInstance', 'invoice', 'Invoice', 'growl', '_t',
function ($scope, $uibModalInstance, 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' },
{ name: _t('by_wallet'), value: 'wallet' }
];
// 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: Fablab.uibDateFormat,
opened: false, // default: datePicker is not shown
options: {
startingDay: Fablab.weekStartingDay
}
};
/**
* Callback to open the datepicker
*/
$scope.openDatePicker = function ($event) {
$event.preventDefault();
$event.stopPropagation();
return $scope.datePicker.opened = true;
};
/**
* Validate the refunding and generate a refund invoice
*/
$scope.ok = function () {
// check that at least 1 element of the invoice is refunded
$scope.avoir.invoice_items_ids = [];
for (let itemId in $scope.partial) {
const refundItem = $scope.partial[itemId];
if (refundItem) { $scope.avoir.invoice_items_ids.push(parseInt(itemId)); }
}
if ($scope.avoir.invoice_items_ids.length === 0) {
return growl.error(_t('you_must_select_at_least_one_element_to_create_a_refund'));
} else {
return Invoice.save(
{ avoir: $scope.avoir },
function (avoir) { // success
$uibModalInstance.close({ avoir, invoice: $scope.invoice });
},
function (err) { // failed
growl.error(_t('unable_to_create_the_refund'));
}
);
}
};
/**q
* Cancel the refund, dismiss the modal window
*/
$scope.cancel = function () { $uibModalInstance.dismiss('cancel'); };
/* PRIVATE SCOPE */
/**
* Kind of constructor: these actions will be realized first when the controller is loaded
*/
const initialize = function () {
// if the invoice was payed with stripe, allow to refund through stripe
Invoice.get({ id: invoice.id }, function (data) {
$scope.invoice = data;
// default : all elements of the invoice are refund
return Array.from(data.items).map(function (item) {
return ($scope.partial[item.id] = (typeof item.avoir_item_id !== 'number'));
});
});
if (invoice.stripe) {
return $scope.avoirModes.push({ name: _t('online_payment'), value: 'stripe' });
}
};
// !!! MUST BE CALLED AT THE END of the controller
return initialize();
}
]);

View File

@ -1,617 +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.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, Group, Training) ->
## Retrieve the profiles groups (eg. students ...)
Group.query (groups) ->
$scope.groups = groups.filter (g) -> g.slug != 'admins' && !g.disabled
## Retrieve the list of available trainings
Training.query().$promise.then (data)->
$scope.trainings = data.map (d) ->
id: d.id
name: d.name
disabled: d.disabled
## Default parameters for AngularUI-Bootstrap datepicker
$scope.datePicker =
format: Fablab.uibDateFormat
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","$sce", 'membersPromise', 'adminsPromise', 'growl', 'Admin', 'dialogs', '_t', 'Member', 'Export'
, ($scope, $sce, membersPromise, adminsPromise, growl, Admin, dialogs, _t, Member, Export) ->
### PRIVATE STATIC CONSTANTS ###
# number of users loaded each time we click on 'load more...'
USERS_PER_PAGE = 20
### PUBLIC SCOPE ###
## members list
$scope.members = membersPromise
$scope.member =
## Members plain-text filtering. Default: not filtered
searchText: ''
## Members ordering/sorting. Default: not sorted
order: 'id'
## currently displayed page of members
page: 1
## true when all members where loaded
noMore: false
## admins list
$scope.admins = adminsPromise.admins
## 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.member.order == orderBy
$scope.member.order = '-'+orderBy
else
$scope.member.order = orderBy
resetSearchMember()
memberSearch()
##
# 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: $sce.trustAsHtml(_t('do_you_really_want_to_delete_this_administrator_this_cannot_be_undone') + '<br/><br/>' +_t('this_may_take_a_while_please_wait'))
, -> # 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'))
##
# Callback for the 'load more' button.
# Will load the next results of the current search, if any
##
$scope.showNextMembers = ->
$scope.member.page += 1
memberSearch(true)
##
# Callback when the search field content changes: reload the search results
##
$scope.updateTextSearch = ->
resetSearchMember()
memberSearch()
##
# Callback to alert the admin that the export request was acknowledged and is
# processing right now.
##
$scope.alertExport = (type) ->
Export.status({category: 'users', type: type}).then (res) ->
unless (res.data.exists)
growl.success _t('export_is_running_you_ll_be_notified_when_its_ready')
### PRIVATE SCOPE ###
##
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
if (!membersPromise[0] || membersPromise[0].maxMembers <= $scope.members.length)
$scope.member.noMore = true
##
# 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)
##
# Reinitialize the context of members's search to display new results set
##
resetSearchMember = ->
$scope.member.noMore = false
$scope.member.page = 1
##
# Run a search query with the current parameters set ($scope.member[searchText,order,page])
# and affect or append the result in $scope.members, depending on the concat parameter
# @param concat {boolean} if true, the result will be append to $scope.members instead of being affected
##
memberSearch = (concat) ->
Member.list { query: { search: $scope.member.searchText, order_by: $scope.member.order, page: $scope.member.page, size: USERS_PER_PAGE } }, (members) ->
if concat
$scope.members = $scope.members.concat(members)
else
$scope.members = members;
if (!members[0] || members[0].maxMembers <= $scope.members.length)
$scope.member.noMore = true
## !!! MUST BE CALLED AT THE END of the controller
initialize()
]
##
# Controller used in the member edition page
##
Application.Controllers.controller "EditMemberController", ["$scope", "$state", "$stateParams", "Member", 'Training', 'dialogs', 'growl', 'Group', 'Subscription', 'CSRF', 'memberPromise', 'tagsPromise', '$uibModal', 'Plan', '$filter', '_t', 'walletPromise', 'transactionsPromise', 'activeProviderPromise', 'Wallet'
, ($scope, $state, $stateParams, Member, Training, dialogs, growl, Group, Subscription, CSRF, memberPromise, tagsPromise, $uibModal, Plan, $filter, _t, walletPromise, transactionsPromise, activeProviderPromise, Wallet) ->
### 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
## Should the passord be modified?
$scope.password =
change: false
## 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 = []
## the user wallet
$scope.wallet = walletPromise
## user wallet transactions
$scope.transactions = transactionsPromise
## used in wallet partial template to identify parent view
$scope.view = 'member_edit'
# current active authentication provider
$scope.activeProvider = activeProviderPromise
##
# 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: Fablab.uibDateFormat
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
$scope.createWalletCreditModal = (user, wallet)->
modalInstance = $uibModal.open
animation: true,
templateUrl: '<%= asset_path "wallet/credit_modal.html" %>'
controller: ['$scope', '$uibModalInstance', 'Wallet', ($scope, $uibModalInstance, Wallet) ->
# default: do not generate a refund invoice
$scope.generate_avoir = false
# date of the generated refund invoice
$scope.avoir_date = null
# optional description shown on the refund invoice
$scope.description = ''
# default configuration for the avoir date selector widget
$scope.datePicker =
format: Fablab.uibDateFormat
opened: false
options:
startingDay: Fablab.weekStartingDay
##
# Callback to open/close the date picker
##
$scope.toggleDatePicker = ($event) ->
$event.preventDefault()
$event.stopPropagation()
$scope.datePicker.opened = !$scope.datePicker.opened
##
# Modal dialog validation callback
##
$scope.ok = ->
Wallet.credit { id: wallet.id },
amount: $scope.amount
avoir: $scope.generate_avoir
avoir_date: $scope.avoir_date
avoir_description: $scope.description
, (_wallet)->
growl.success(_t('wallet_credit_successfully'))
$uibModalInstance.close(_wallet)
, (error)->
growl.error(_t('a_problem_occurred_for_wallet_credit'))
##
# Modal dialog cancellation callback
##
$scope.cancel = ->
$uibModalInstance.dismiss('cancel')
]
# once the form was validated succesfully ...
modalInstance.result.then (wallet) ->
$scope.wallet = wallet
Wallet.transactions {id: wallet.id}, (transactions) ->
$scope.transactions = transactions
##
# To use as callback in Array.prototype.filter to get only enabled plans
##
$scope.filterDisabledPlans = (plan) ->
!plan.disabled
### 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, 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", "$stateParams", "Member", 'Training', 'Group', 'CSRF'
, ($scope, $state, $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'
## Should the passord be set manually or generated?
$scope.password =
change: false
## Default member's profile parameters
$scope.user =
plan_interval: ''
## Callback when the admin check/unckeck the box telling that the new user is an organization.
## Disable or enable the organization fields in the form, accordingly
$scope.toggleOrganization = ->
if $scope.user.organization
$scope.user.profile = {} unless $scope.user.profile
$scope.user.profile.organization = {}
else
$scope.user.profile.organization = undefined
## Using the MembersController
new MembersController($scope, $state, Group, Training)
]
##
# Controller used in the admin's creation page (admin view)
##
Application.Controllers.controller 'NewAdminController', ['$state', '$scope', 'Admin', 'growl', '_t', ($state, $scope, Admin, growl, _t)->
## default admin profile
$scope.admin =
profile_attributes:
gender: true
## Default parameters for AngularUI-Bootstrap datepicker
$scope.datePicker =
format: Fablab.uibDateFormat
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'
]

View File

@ -0,0 +1,661 @@
/* eslint-disable
handle-callback-err,
no-return-assign,
no-self-assign,
no-undef,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
'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, Group, Training) {
// Retrieve the profiles groups (eg. students ...)
Group.query(function (groups) { $scope.groups = groups.filter(function (g) { return (g.slug !== 'admins') && !g.disabled; }); });
// Retrieve the list of available trainings
Training.query().$promise.then(function (data) {
$scope.trainings = data.map(function (d) {
return ({
id: d.id,
name: d.name,
disabled: d.disabled
});
});
});
// Default parameters for AngularUI-Bootstrap datepicker
$scope.datePicker = {
format: Fablab.uibDateFormat,
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 = function ($event) {
$event.preventDefault();
$event.stopPropagation();
return $scope.datePicker.opened = true;
};
/**
* Shows the end of subscription datepicker
* @param $event {Object} jQuery event object
*/
$scope.openSubscriptionDatePicker = function ($event) {
$event.preventDefault();
$event.stopPropagation();
return $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 = function (content) {
if ((content.id == null)) {
$scope.alerts = [];
return angular.forEach(content, function (v, k) {
angular.forEach(v, function (err) {
$scope.alerts.push({
msg: k + ': ' + err,
type: 'danger'
});
});
});
} else {
return $state.go('app.admin.members');
}
};
/**
* Changes the admin's view to the members list page
*/
$scope.cancel = function () { $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 = function (v) {
if (v) {
return 'fileinput-exists';
} else {
return 'fileinput-new';
}
};
}
}
/**
* Controller used in the members/groups management page
*/
Application.Controllers.controller('AdminMembersController', ['$scope', '$sce', 'membersPromise', 'adminsPromise', 'growl', 'Admin', 'dialogs', '_t', 'Member', 'Export',
function ($scope, $sce, membersPromise, adminsPromise, growl, Admin, dialogs, _t, Member, Export) {
/* PRIVATE STATIC CONSTANTS */
// number of users loaded each time we click on 'load more...'
const USERS_PER_PAGE = 20;
/* PUBLIC SCOPE */
// members list
$scope.members = membersPromise;
$scope.member = {
// Members plain-text filtering. Default: not filtered
searchText: '',
// Members ordering/sorting. Default: not sorted
order: 'id',
// currently displayed page of members
page: 1,
// true when all members where loaded
noMore: false
};
// admins list
$scope.admins = adminsPromise.admins;
// 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 = function (orderBy) {
if ($scope.member.order === orderBy) {
$scope.member.order = `-${orderBy}`;
} else {
$scope.member.order = orderBy;
}
resetSearchMember();
return memberSearch();
};
/**
* Change the admins ordering criterion to the one provided
* @param orderAdmin {string} ordering criterion
*/
$scope.setOrderAdmin = function (orderAdmin) {
if ($scope.orderAdmin === orderAdmin) {
return $scope.orderAdmin = `-${orderAdmin}`;
} else {
return $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 = function (admins, admin) {
dialogs.confirm(
{
resolve: {
object () {
return {
title: _t('confirmation_required'),
msg: $sce.trustAsHtml(_t('do_you_really_want_to_delete_this_administrator_this_cannot_be_undone') + '<br/><br/>' + _t('this_may_take_a_while_please_wait'))
};
}
}
},
function () { // cancel confirmed
Admin.delete(
{ id: admin.id },
function () {
admins.splice(findAdminIdxById(admins, admin.id), 1);
return growl.success(_t('administrator_successfully_deleted'));
},
function (error) { growl.error(_t('unable_to_delete_the_administrator')); }
);
}
);
};
/**
* Callback for the 'load more' button.
* Will load the next results of the current search, if any
*/
$scope.showNextMembers = function () {
$scope.member.page += 1;
return memberSearch(true);
};
/**
* Callback when the search field content changes: reload the search results
*/
$scope.updateTextSearch = function () {
resetSearchMember();
return memberSearch();
};
/**
* Callback to alert the admin that the export request was acknowledged and is
* processing right now.
*/
$scope.alertExport = function (type) {
Export.status({ category: 'users', type }).then(function (res) {
if (!res.data.exists) {
return growl.success(_t('export_is_running_you_ll_be_notified_when_its_ready'));
}
});
};
/* PRIVATE SCOPE */
/**
* Kind of constructor: these actions will be realized first when the controller is loaded
*/
const initialize = function () {
if (!membersPromise[0] || (membersPromise[0].maxMembers <= $scope.members.length)) {
return $scope.member.noMore = true;
}
};
/**
* 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
*/
var findAdminIdxById = function (admins, id) {
return (admins.map(function (admin) { return admin.id; })).indexOf(id);
};
/**
* Reinitialize the context of members's search to display new results set
*/
var resetSearchMember = function () {
$scope.member.noMore = false;
return $scope.member.page = 1;
};
/**
* Run a search query with the current parameters set ($scope.member[searchText,order,page])
* and affect or append the result in $scope.members, depending on the concat parameter
* @param concat {boolean} if true, the result will be append to $scope.members instead of being affected
*/
var memberSearch = function (concat) {
Member.list({
query: {
search: $scope.member.searchText,
order_by: $scope.member.order,
page: $scope.member.page,
size: USERS_PER_PAGE
}
}, function (members) {
if (concat) {
$scope.members = $scope.members.concat(members);
} else {
$scope.members = members;
}
if (!members[0] || (members[0].maxMembers <= $scope.members.length)) {
return $scope.member.noMore = true;
}
});
};
// !!! MUST BE CALLED AT THE END of the controller
return initialize();
}
]);
/**
* Controller used in the member edition page
*/
Application.Controllers.controller('EditMemberController', ['$scope', '$state', '$stateParams', 'Member', 'Training', 'dialogs', 'growl', 'Group', 'Subscription', 'CSRF', 'memberPromise', 'tagsPromise', '$uibModal', 'Plan', '$filter', '_t', 'walletPromise', 'transactionsPromise', 'activeProviderPromise', 'Wallet',
function ($scope, $state, $stateParams, Member, Training, dialogs, growl, Group, Subscription, CSRF, memberPromise, tagsPromise, $uibModal, Plan, $filter, _t, walletPromise, transactionsPromise, activeProviderPromise, Wallet) {
/* 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;
// Should the passord be modified?
$scope.password =
{ change: false };
// the user subscription
if (($scope.user.subscribed_plan != null) && ($scope.user.subscription != null)) {
$scope.subscription = $scope.user.subscription;
$scope.subscription.expired_at = $scope.subscription.expired_at;
} else {
Plan.query({ group_id: $scope.user.group_id }, function (plans) {
$scope.plans = plans;
return Array.from($scope.plans).map(function (plan) {
return (plan.nameToDisplay = $filter('humanReadablePlanName')(plan));
});
});
}
// Available trainings list
$scope.trainings = [];
// Profiles types (student/standard/...)
$scope.groups = [];
// the user wallet
$scope.wallet = walletPromise;
// user wallet transactions
$scope.transactions = transactionsPromise;
// used in wallet partial template to identify parent view
$scope.view = 'member_edit';
// current active authentication provider
$scope.activeProvider = activeProviderPromise;
/**
* 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 = function (subscription, free) {
const modalInstance = $uibModal.open({
animation: true,
templateUrl: '<%= asset_path "admin/subscriptions/expired_at_modal.html" %>',
size: 'lg',
controller: ['$scope', '$uibModalInstance', 'Subscription', function ($scope, $uibModalInstance, Subscription) {
$scope.new_expired_at = angular.copy(subscription.expired_at);
$scope.free = free;
$scope.datePicker = {
opened: false,
format: Fablab.uibDateFormat,
options: {
startingDay: Fablab.weekStartingDay
},
minDate: new Date()
};
$scope.openDatePicker = function (ev) {
ev.preventDefault();
ev.stopPropagation();
return $scope.datePicker.opened = true;
};
$scope.ok = function () {
Subscription.update(
{ id: subscription.id },
{ subscription: { expired_at: $scope.new_expired_at, free } },
function (_subscription) {
growl.success(_t('you_successfully_changed_the_expiration_date_of_the_user_s_subscription'));
return $uibModalInstance.close(_subscription);
},
function (error) { growl.error(_t('a_problem_occurred_while_saving_the_date')); }
);
};
$scope.cancel = function () { $uibModalInstance.dismiss('cancel'); };
}]
});
// once the form was validated successfully ...
return modalInstance.result.then(function (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 = function (user, plans) {
const modalInstance = $uibModal.open({
animation: true,
templateUrl: '<%= asset_path "admin/subscriptions/create_modal.html" %>',
size: 'lg',
controller: ['$scope', '$uibModalInstance', 'Subscription', 'Group', function ($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 human-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 = function (plan, groups, short) { return `${$filter('humanReadablePlanName')(plan, groups, short)}`; };
/**
* Modal dialog validation callback
*/
$scope.ok = function () {
$scope.subscription.user_id = user.id;
return Subscription.save({ }, { subscription: $scope.subscription }, function (_subscription) {
growl.success(_t('subscription_successfully_purchased'));
$uibModalInstance.close(_subscription);
return $state.reload();
}
, function (error) {
growl.error(_t('a_problem_occurred_while_taking_the_subscription'));
console.error(error);
});
};
/**
* Modal dialog cancellation callback
*/
$scope.cancel = function () { $uibModalInstance.dismiss('cancel'); };
}]
});
// once the form was validated succesfully ...
return modalInstance.result.then(function (subscription) { $scope.subscription = subscription; });
};
$scope.createWalletCreditModal = function (user, wallet) {
const modalInstance = $uibModal.open({
animation: true,
templateUrl: '<%= asset_path "wallet/credit_modal.html" %>',
controller: ['$scope', '$uibModalInstance', 'Wallet', function ($scope, $uibModalInstance, Wallet) {
// default: do not generate a refund invoice
$scope.generate_avoir = false;
// date of the generated refund invoice
$scope.avoir_date = null;
// optional description shown on the refund invoice
$scope.description = '';
// default configuration for the avoir date selector widget
$scope.datePicker = {
format: Fablab.uibDateFormat,
opened: false,
options: {
startingDay: Fablab.weekStartingDay
}
};
/**
* Callback to open/close the date picker
*/
$scope.toggleDatePicker = function ($event) {
$event.preventDefault();
$event.stopPropagation();
return $scope.datePicker.opened = !$scope.datePicker.opened;
};
/**
* Modal dialog validation callback
*/
$scope.ok = function () {
Wallet.credit(
{ id: wallet.id },
{
amount: $scope.amount,
avoir: $scope.generate_avoir,
avoir_date: $scope.avoir_date,
avoir_description: $scope.description
},
function (_wallet) {
growl.success(_t('wallet_credit_successfully'));
return $uibModalInstance.close(_wallet);
},
function (error) {
growl.error(_t('a_problem_occurred_for_wallet_credit'));
console.error(error);
}
);
};
/**
* Modal dialog cancellation callback
*/
$scope.cancel = function () { $uibModalInstance.dismiss('cancel'); };
}
] });
// once the form was validated succesfully ...
return modalInstance.result.then(function (wallet) {
$scope.wallet = wallet;
return Wallet.transactions({ id: wallet.id }, function (transactions) { $scope.transactions = transactions; });
});
};
/**
* To use as callback in Array.prototype.filter to get only enabled plans
*/
$scope.filterDisabledPlans = function (plan) { return !plan.disabled; };
/* PRIVATE SCOPE */
/**
* Kind of constructor: these actions will be realized first when the controller is loaded
*/
const initialize = function () {
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 != null) && ($scope.user.subscription != null)) {
$scope.subscription = $scope.user.subscription;
$scope.subscription.expired_at = $scope.subscription.expired_at;
} else {
Plan.query({ group_id: $scope.user.group_id }, function (plans) {
$scope.plans = plans;
return Array.from($scope.plans).map(function (plan) {
return (plan.nameToDisplay = `${plan.base_name} - ${plan.interval}`);
});
});
}
// Using the MembersController
return new MembersController($scope, $state, Group, Training);
};
// !!! MUST BE CALLED AT THE END of the controller
return initialize();
}
]);
/**
* Controller used in the member's creation page (admin view)
*/
Application.Controllers.controller('NewMemberController', ['$scope', '$state', '$stateParams', 'Member', 'Training', 'Group', 'CSRF',
function ($scope, $state, $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';
// Should the passord be set manually or generated?
$scope.password =
{ change: false };
// Default member's profile parameters
$scope.user =
{ plan_interval: '' };
// Callback when the admin check/unckeck the box telling that the new user is an organization.
// Disable or enable the organization fields in the form, accordingly
$scope.toggleOrganization = function () {
if ($scope.user.organization) {
if (!$scope.user.profile) { $scope.user.profile = {}; }
return $scope.user.profile.organization = {};
} else {
return $scope.user.profile.organization = undefined;
}
};
// Using the MembersController
return new MembersController($scope, $state, Group, Training);
}
]);
/**
* Controller used in the admin's creation page (admin view)
*/
Application.Controllers.controller('NewAdminController', ['$state', '$scope', 'Admin', 'growl', '_t', function ($state, $scope, Admin, growl, _t) {
// default admin profile
let getGender;
$scope.admin = {
profile_attributes: {
gender: true
}
};
// Default parameters for AngularUI-Bootstrap datepicker
$scope.datePicker = {
format: Fablab.uibDateFormat,
opened: false,
options: {
startingDay: Fablab.weekStartingDay
}
};
/**
* Shows the birth day datepicker
* @param $event {Object} jQuery event object
*/
$scope.openDatePicker = function ($event) { $scope.datePicker.opened = true; };
/**
* Send the new admin, currently stored in $scope.admin, to the server for database saving
*/
$scope.saveAdmin = function () {
Admin.save(
{},
{ admin: $scope.admin },
function () {
growl.success(_t('administrator_successfully_created_he_will_receive_his_connection_directives_by_email', { GENDER: getGender($scope.admin) }, 'messageformat'));
return $state.go('app.admin.members');
}
, function (error) {
console.log(error);
}
);
};
/* PRIVATE SCOPE */
/**
* Return an enumerable meaningful string for the gender of the provider user
* @param user {Object} Database user record
* @return {string} 'male' or 'female'
*/
return getGender = function (user) {
if (user.profile_attributes) {
if (user.profile_attributes.gender) { return 'male'; } else { return 'female'; }
} else { return 'other'; }
};
}
]);

View File

@ -1,69 +0,0 @@
Application.Controllers.controller "OpenAPIClientsController", ["$scope", 'clientsPromise', 'growl', 'OpenAPIClient', 'dialogs', '_t'
, ($scope, clientsPromise, growl, OpenAPIClient, dialogs, _t) ->
### PUBLIC SCOPE ###
## clients list
$scope.clients = clientsPromise
$scope.order = null
$scope.clientFormVisible = false
$scope.client = {}
$scope.toggleForm = ->
$scope.clientFormVisible = !$scope.clientFormVisible
# Change the order criterion to the one provided
# @param orderBy {string} ordering criterion
##
$scope.setOrder = (orderBy)->
if $scope.order == orderBy
$scope.order = '-'+orderBy
else
$scope.order = orderBy
$scope.saveClient = (client)->
if client.id?
OpenAPIClient.update { id: client.id }, open_api_client: client, (clientResp)->
client = clientResp
growl.success(_t('client_successfully_updated'))
else
OpenAPIClient.save open_api_client: client, (client)->
$scope.clients.push client
growl.success(_t('client_successfully_created'))
$scope.clientFormVisible = false
$scope.clientForm.$setPristine()
$scope.client = {}
$scope.editClient = (client)->
$scope.clientFormVisible = true
$scope.client = client
$scope.deleteClient = (index)->
dialogs.confirm
resolve:
object: ->
title: _t('confirmation_required')
msg: _t('do_you_really_want_to_delete_this_open_api_client')
, ->
OpenAPIClient.delete { id: $scope.clients[index].id }, ->
$scope.clients.splice(index, 1)
growl.success(_t('client_successfully_deleted'))
$scope.resetToken = (client)->
dialogs.confirm
resolve:
object: ->
title: _t('confirmation_required')
msg: _t('do_you_really_want_to_revoke_this_open_api_access')
, ->
OpenAPIClient.resetToken { id: client.id }, {}, (clientResp)->
client.token = clientResp.token
growl.success(_t('access_successfully_revoked'))
]

View File

@ -0,0 +1,96 @@
/* eslint-disable
no-return-assign,
no-undef,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
Application.Controllers.controller('OpenAPIClientsController', ['$scope', 'clientsPromise', 'growl', 'OpenAPIClient', 'dialogs', '_t',
function ($scope, clientsPromise, growl, OpenAPIClient, dialogs, _t) {
/* PUBLIC SCOPE */
// clients list
$scope.clients = clientsPromise;
$scope.order = null;
$scope.clientFormVisible = false;
$scope.client = {};
$scope.toggleForm = () => $scope.clientFormVisible = !$scope.clientFormVisible;
// Change the order criterion to the one provided
// @param orderBy {string} ordering criterion
//
$scope.setOrder = function (orderBy) {
if ($scope.order === orderBy) {
return $scope.order = `-${orderBy}`;
} else {
return $scope.order = orderBy;
}
};
$scope.saveClient = function (client) {
if (client.id != null) {
OpenAPIClient.update({ id: client.id }, { open_api_client: client }, function (clientResp) {
client = clientResp;
return growl.success(_t('client_successfully_updated'));
});
} else {
OpenAPIClient.save({ open_api_client: client }, function (client) {
$scope.clients.push(client);
return growl.success(_t('client_successfully_created'));
});
}
$scope.clientFormVisible = false;
$scope.clientForm.$setPristine();
return $scope.client = {};
};
$scope.editClient = function (client) {
$scope.clientFormVisible = true;
return $scope.client = client;
};
$scope.deleteClient = index =>
dialogs.confirm({
resolve: {
object () {
return {
title: _t('confirmation_required'),
msg: _t('do_you_really_want_to_delete_this_open_api_client')
};
}
}
}
, () =>
OpenAPIClient.delete({ id: $scope.clients[index].id }, function () {
$scope.clients.splice(index, 1);
return growl.success(_t('client_successfully_deleted'));
})
);
return $scope.resetToken = client =>
dialogs.confirm({
resolve: {
object () {
return {
title: _t('confirmation_required'),
msg: _t('do_you_really_want_to_revoke_this_open_api_access')
};
}
}
}
, () =>
OpenAPIClient.resetToken({ id: client.id }, {}, function (clientResp) {
client.token = clientResp.token;
return growl.success(_t('access_successfully_revoked'));
})
);
}
]);

View File

@ -1,269 +0,0 @@
'use strict'
### COMMON CODE ###
class PlanController
constructor: ($scope, groups, prices, partners, CSRF) ->
# protection against request forgery
CSRF.setMetaTags()
## groups list
$scope.groups = groups.filter (g) -> g.slug != 'admins' && !g.disabled
## 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
##
# Controller used in the plan creation form
##
Application.Controllers.controller 'NewPlanController', ['$scope', '$uibModal', 'groups', 'prices', 'partners', 'CSRF', '$state', 'growl', '_t'
, ($scope, $uibModal, groups, prices, partners, CSRF, $state, growl, _t) ->
### 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
is_rolling: 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'
##
# 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('new_plan.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('new_plan.unable_to_create_the_subscription_please_try_again'))
else
growl.success(_t('new_plan.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, prices, partners, CSRF)
]
##
# Controller used in the plan edition form
##
Application.Controllers.controller 'EditPlanController', ['$scope', 'groups', 'plans', 'planPromise', 'machines', 'spaces', 'prices', 'partners', 'CSRF', '$state', '$stateParams', 'growl', '$filter', '_t', 'Plan'
, ($scope, groups, plans, planPromise, machines, spaces, prices, partners, CSRF, $state, $stateParams, growl, $filter, _t, Plan) ->
### PUBLIC SCOPE ###
## List of spaces
$scope.spaces = spaces
## List of plans
$scope.plans = plans
## List of machines
$scope.machines = machines
## List of groups
$scope.groups = groups
## current form is used for edition mode
$scope.mode = 'edition'
## edited plan data
$scope.plan = planPromise
$scope.plan.type = "Plan" if $scope.plan.type == null
$scope.plan.disabled = 'true' if $scope.plan.disabled
## API URL where the form will be posted
$scope.actionUrl = "/api/plans/" + $stateParams.id
## HTTP method for the rest API
$scope.method = 'PATCH'
##
# 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
Plan.get {id: $scope.plan.parent}, (parentPlan) ->
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('edit_plan.unable_to_save_subscription_changes_please_try_again'))
else
growl.success(_t('edit_plan.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)}"
##
# Retrieve the machine from its ID
# @param machine_id {number} machine identifier
# @returns {Object} Machine
##
$scope.getMachine = (machine_id) ->
for machine in $scope.machines
if machine.id == machine_id
return machine
##
# Retrieve the space from its ID
# @param space_id {number} space identifier
# @returns {Object} Space
##
$scope.getSpace = (space_id) ->
for space in $scope.spaces
if space.id == space_id
return space
### PRIVATE SCOPE ###
##
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
# Using the PlansController
new PlanController($scope, groups, prices, partners, CSRF)
## !!! MUST BE CALLED AT THE END of the controller
initialize()
]

View File

@ -0,0 +1,286 @@
/* eslint-disable
camelcase,
handle-callback-err,
no-return-assign,
no-undef,
no-unused-expressions,
no-unused-vars,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS205: Consider reworking code to avoid use of IIFEs
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
'use strict';
/* COMMON CODE */
class PlanController {
constructor ($scope, groups, prices, partners, CSRF) {
// protection against request forgery
CSRF.setMetaTags();
// groups list
$scope.groups = groups.filter(function (g) { return (g.slug !== 'admins') && !g.disabled; });
// 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 = function (v) {
if (v) {
return 'fileinput-exists';
} else {
return 'fileinput-new';
}
};
/**
* Mark the provided file for deletion
* @param file {Object}
*/
$scope.deleteFile = function (file) {
if ((file != null) && (file.id != null)) {
return file._destroy = true;
}
};
}
}
/**
* Controller used in the plan creation form
*/
Application.Controllers.controller('NewPlanController', ['$scope', '$uibModal', 'groups', 'prices', 'partners', 'CSRF', '$state', 'growl', '_t',
function ($scope, $uibModal, groups, prices, partners, CSRF, $state, growl, _t) {
/* 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,
is_rolling: 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';
/**
* Checks if the partner contact is a valid data. Used in the form validation process
* @returns {boolean}
*/
$scope.partnerIsValid = function () { return ($scope.plan.type === 'Plan') || ($scope.plan.partnerId || ($scope.plan.partnerContact && $scope.plan.partnerContact.email)); };
/**
* Open a modal dialog allowing the admin to create a new partner user
*/
$scope.openPartnerNewModal = function (subscription) {
const modalInstance = $uibModal.open({
animation: true,
templateUrl: '<%= asset_path "shared/_partner_new_modal.html" %>',
size: 'lg',
controller: ['$scope', '$uibModalInstance', 'User', function ($scope, $uibModalInstance, User) {
$scope.partner = {};
$scope.ok = function () {
User.save(
{},
{ user: $scope.partner },
function (user) {
$scope.partner.id = user.id;
$scope.partner.name = `${user.first_name} ${user.last_name}`;
$uibModalInstance.close($scope.partner);
},
function (error) {
growl.error(_t('new_plan.unable_to_save_this_user_check_that_there_isnt_an_already_a_user_with_the_same_name'));
console.error(error);
}
);
};
$scope.cancel = function () { $uibModalInstance.dismiss('cancel'); };
}]
});
// once the form was validated successfully ...
return modalInstance.result.then(function (partner) {
$scope.partners.push(partner);
return $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 = function (content) {
if ((content.id == null) && (content.plan_ids == null)) {
return growl.error(_t('new_plan.unable_to_create_the_subscription_please_try_again'));
} else {
growl.success(_t('new_plan.successfully_created_subscription(s)_dont_forget_to_redefine_prices'));
if (content.plan_ids != null) {
return $state.go('app.admin.pricing');
} else {
if (content.id != null) {
return $state.go('app.admin.plans.edit', { id: content.id });
}
}
}
};
return new PlanController($scope, groups, prices, partners, CSRF);
}
]);
/**
* Controller used in the plan edition form
*/
Application.Controllers.controller('EditPlanController', ['$scope', 'groups', 'plans', 'planPromise', 'machines', 'spaces', 'prices', 'partners', 'CSRF', '$state', '$stateParams', 'growl', '$filter', '_t', 'Plan',
function ($scope, groups, plans, planPromise, machines, spaces, prices, partners, CSRF, $state, $stateParams, growl, $filter, _t, Plan) {
/* PUBLIC SCOPE */
// List of spaces
$scope.spaces = spaces;
// List of plans
$scope.plans = plans;
// List of machines
$scope.machines = machines;
// List of groups
$scope.groups = groups;
// current form is used for edition mode
$scope.mode = 'edition';
// edited plan data
$scope.plan = planPromise;
if ($scope.plan.type === null) { $scope.plan.type = 'Plan'; }
if ($scope.plan.disabled) { $scope.plan.disabled = 'true'; }
// API URL where the form will be posted
$scope.actionUrl = `/api/plans/${$stateParams.id}`;
// HTTP method for the rest API
$scope.method = 'PATCH';
/**
* 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 = function () {
if ($scope.plan.parent) {
return Plan.get({ id: $scope.plan.parent }, function (parentPlan) {
Array.from(parentPlan.prices).map(function (parentPrice) {
return (function () {
const result = [];
for (let childKey in $scope.plan.prices) {
const childPrice = $scope.plan.prices[childKey];
if ((childPrice.priceable_type === parentPrice.priceable_type) && (childPrice.priceable_id === parentPrice.priceable_id)) {
$scope.plan.prices[childKey].amount = parentPrice.amount;
break;
} else {
result.push(undefined);
}
}
return result;
})();
});
}
);
// if no plan were selected, unset every prices
} else {
return (function () {
const result = [];
for (let key in $scope.plan.prices) {
const price = $scope.plan.prices[key];
result.push($scope.plan.prices[key].amount = 0);
}
return result;
})();
}
};
/**
* Display some messages once the form was submitted, depending on the result status (failed/succeeded)
* @param content {Object}
*/
$scope.afterSubmit = function (content) {
if ((content.id == null) && (content.plan_ids == null)) {
return growl.error(_t('edit_plan.unable_to_save_subscription_changes_please_try_again'));
} else {
growl.success(_t('edit_plan.subscription_successfully_changed'));
return $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 = function (plan, groups, short) { return `${$filter('humanReadablePlanName')(plan, groups, short)}`; };
/**
* Retrieve the machine from its ID
* @param machine_id {number} machine identifier
* @returns {Object} Machine
*/
$scope.getMachine = function (machine_id) {
for (let machine of Array.from($scope.machines)) {
if (machine.id === machine_id) {
return machine;
}
}
};
/**
* Retrieve the space from its ID
* @param space_id {number} space identifier
* @returns {Object} Space
*/
$scope.getSpace = function (space_id) {
for (let space of Array.from($scope.spaces)) {
if (space.id === space_id) {
return space;
}
}
};
// Using the PlansController
return new PlanController($scope, groups, prices, partners, CSRF);
}
]);

View File

@ -1,23 +0,0 @@
'use strict'
##
# Controller used in price category creation/edition form dialog
##
Application.Controllers.controller "PriceCategoryController", ["$scope", "$uibModalInstance", "category"
, ($scope, $uibModalInstance, category) ->
## Price category to edit/empty object for new category
$scope.category = category
##
# Callback for form validation
##
$scope.ok = ->
$uibModalInstance.close($scope.category)
##
# Do not validate the modifications, hide the modal
##
$scope.cancel = ->
$uibModalInstance.dismiss('cancel')
]

View File

@ -0,0 +1,32 @@
/* eslint-disable
no-return-assign,
no-undef,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
'use strict';
/**
* Controller used in price category creation/edition form dialog
*/
Application.Controllers.controller('PriceCategoryController', ['$scope', '$uibModalInstance', 'category',
function ($scope, $uibModalInstance, category) {
// Price category to edit/empty object for new category
$scope.category = category;
/**
* Callback for form validation
*/
$scope.ok = () => $uibModalInstance.close($scope.category);
/**
* Do not validate the modifications, hide the modal
*/
return $scope.cancel = () => $uibModalInstance.dismiss('cancel');
}
]);

View File

@ -1,562 +0,0 @@
'use strict'
##
# Controller used in the prices edition page
##
Application.Controllers.controller "EditPricingController", ["$scope", "$state", '$uibModal', '$filter', 'TrainingsPricing', 'Credit', 'Pricing', 'Plan', 'Coupon', 'plans', 'groups', 'growl', 'machinesPricesPromise', 'Price', 'dialogs', 'trainingsPricingsPromise', 'trainingsPromise', 'machineCreditsPromise', 'machinesPromise', 'trainingCreditsPromise', 'couponsPromise', 'spacesPromise', 'spacesPricesPromise', 'spacesCreditsPromise', '_t'
, ($scope, $state, $uibModal, $filter, TrainingsPricing, Credit, Pricing, Plan, Coupon, plans, groups, growl, machinesPricesPromise, Price, dialogs, trainingsPricingsPromise, trainingsPromise, machineCreditsPromise, machinesPromise, trainingCreditsPromise, couponsPromise, spacesPromise, spacesPricesPromise, spacesCreditsPromise, _t) ->
### PUBLIC SCOPE ###
## List of machines prices (not considering any plan)
$scope.machinesPrices = machinesPricesPromise
## List of trainings pricing
$scope.trainingsPricings = trainingsPricingsPromise
## List of available subscriptions plans (eg. student/month, PME/year ...)
$scope.plans = plans
$scope.enabledPlans = plans.filter (p) -> !p.disabled
## List of groups (eg. normal, student ...)
$scope.groups = groups.filter (g) -> g.slug != 'admins'
$scope.enabledGroups = groups.filter (g) -> g.slug != 'admins' && !g.disabled
## Associate free machine hours with subscriptions
$scope.machineCredits = machineCreditsPromise
## Array of associations (plan <-> training)
$scope.trainingCredits = trainingCreditsPromise
## Associate a plan with all its trainings ids
$scope.trainingCreditsGroups = {}
## List of trainings
$scope.trainings = trainingsPromise.filter (t) -> !t.disabled
## List of machines
$scope.machines = machinesPromise
$scope.enabledMachines = machinesPromise.filter (m) -> !m.disabled
## List of coupons
$scope.coupons = couponsPromise
## List of spaces
$scope.spaces = spacesPromise
$scope.enabledSpaces = spacesPromise.filter (s) -> !s.disabled
## Associate free space hours with subscriptions
$scope.spaceCredits = spacesCreditsPromise
## List of spaces prices (not considering any plan)
$scope.spacesPrices = spacesPricesPromise
## The plans list ordering. Default: by group
$scope.orderPlans = 'group_id'
## Status of the drop-down menu in Credits tab
$scope.status =
isopen: false
## Default: we show only enabled plans
$scope.planFiltering = 'enabled'
## Available options for filtering plans by status
$scope.filterDisabled = [
'enabled',
'disabled',
'all',
]
$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('pricing.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<number>} trainings IDs array
##
$scope.showTrainings = (trainings) ->
unless angular.isArray(trainings) and trainings.length > 0
return _t('pricing.none')
selected = []
angular.forEach $scope.trainings, (t) ->
if trainings.indexOf(t.id) >= 0
selected.push t.name
return if selected.length then selected.join(' | ') else _t('pricing.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('pricing.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('pricing.an_error_occurred_while_deleting_credit_with_the_TRAINING', {TRAINING:tc.creditable.name}))
else
growl.error(_t('pricing.an_error_occurred_unable_to_find_the_credit_to_revoke'))
# iterate through the new credits to add
angular.forEach newdata.training_ids, (newTrainingId) ->
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('pricing.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, return the name of the machine/space associated with the given credit
# @param credit {Object} credit object, inherited from $resource
# @returns {String}
##
$scope.showCreditableName = (credit) ->
selected = _t('pricing.not_set')
if credit and credit.creditable_id
object = $scope.getCreditable(credit)
selected = object.name
if credit.creditable_type == 'Machine'
selected += ' ( id. ' + object.id + ' )'
return selected
##
# In the Credits tab, return the machine/space associated with the given credit
# @param credit {Object} credit object, inherited from $resource
# @returns {Object}
##
$scope.getCreditable = (credit) ->
selected = undefined
if credit and credit.creditable_id
if credit.creditable_type == 'Machine'
angular.forEach $scope.machines, (m)->
if m.id == credit.creditable_id
selected = m
else if credit.creditable_type == 'Space'
angular.forEach $scope.spaces, (s)->
if s.id == credit.creditable_id
selected = s
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('pricing.error_a_credit_linking_this_machine_with_that_subscription_already_exists'))
unless id
$scope.machineCredits.pop()
return false
if id?
Credit.update {id: id}, credit: data, ->
growl.success(_t('pricing.changes_have_been_successfully_saved'))
else
data.creditable_type = 'Machine'
Credit.save
credit: data
, (resp) ->
$scope.machineCredits[$scope.machineCredits.length-1].id = resp.id
growl.success(_t('pricing.credit_was_successfully_saved'))
, (err) ->
$scope.machineCredits.pop()
growl.error(_t('pricing.error_creating_credit'))
##
# 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} credit 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)
##
# Create a new empty entry in the $scope.spaceCredits array
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
##
$scope.addSpaceCredit = (e)->
e.preventDefault()
e.stopPropagation()
$scope.inserted =
creditable_type: 'Space'
$scope.spaceCredits.push($scope.inserted)
$scope.status.isopen = !$scope.status.isopen
##
# Validation callback when editing space's credits. Save the changes.
# This will prevent the creation of two credits associated with the same space and plan.
# @param data {Object} space, associated plan and number of credit hours.
# @param [id] {number} credit id for edition, create a new credit object if not provided
##
$scope.saveSpaceCredit = (data, id) ->
for sc in $scope.spaceCredits
if sc.plan_id == data.plan_id and sc.creditable_id == data.creditable_id and (id == null or sc.id != id)
growl.error(_t('pricing.error_a_credit_linking_this_space_with_that_subscription_already_exists'))
unless id
$scope.spaceCredits.pop()
return false
if id?
Credit.update {id: id}, credit: data, ->
growl.success(_t('pricing.changes_have_been_successfully_saved'))
else
data.creditable_type = 'Space'
Credit.save
credit: data
, (resp) ->
$scope.spaceCredits[$scope.spaceCredits.length - 1].id = resp.id
growl.success(_t('pricing.credit_was_successfully_saved'))
, (err) ->
$scope.spaceCredits.pop()
growl.error(_t('pricing.error_creating_credit'))
##
# Removes the newly inserted but not saved space credit / Cancel the current space credit modification
# @param rowform {Object} see http://vitalets.github.io/angular-xeditable/
# @param index {number} credit index in the $scope.spaceCredits array
##
$scope.cancelSpaceCredit = (rowform, index) ->
if $scope.spaceCredits[index].id?
rowform.$cancel()
else
$scope.spaceCredits.splice(index, 1)
##
# Deletes the space credit at the specified index
# @param index {number} space credit index in the $scope.spaceCredits array
##
$scope.removeSpaceCredit = (index) ->
Credit.delete $scope.spaceCredits[index]
$scope.spaceCredits.splice(index, 1)
##
# If the plan does not have a type, return a default value for display purposes
# @param type {string|undefined|null} plan's type (eg. 'partner')
# @returns {string}
##
$scope.getPlanType = (type) ->
if type == 'PartnerPlan'
return _t('pricing.partner')
else return _t('pricing.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('pricing.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('pricing.confirmation_required')
msg: _t('pricing.do_you_really_want_to_delete_this_subscription_plan')
, ->
# the admin has confirmed, delete the plan
Plan.delete {id: id}, (res) ->
growl.success(_t('pricing.subscription_plan_was_successfully_deleted'))
$scope.plans.splice(findItemIdxById(plans, id), 1)
, (error) ->
console.error('[EditPricingController::deletePlan] Error: '+error.statusText) if error.statusText
growl.error(_t('pricing.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)}"
##
# Delete a coupon from the server's database and, in case of success, from the list in memory
# @param coupons {Array<Object>} should be called with $scope.coupons
# @param id {number} ID of the coupon to delete
##
$scope.deleteCoupon = (coupons, id) ->
if typeof id != 'number'
console.error('[EditPricingController::deleteCoupon] Error: invalid id parameter')
else
# open a confirmation dialog
dialogs.confirm
resolve:
object: ->
title: _t('pricing.confirmation_required')
msg: _t('pricing.do_you_really_want_to_delete_this_coupon')
, ->
# the admin has confirmed, delete the coupon
Coupon.delete {id: id}, (res) ->
growl.success(_t('coupon_was_successfully_deleted'))
$scope.coupons.splice(findItemIdxById(coupons, id), 1)
, (error) ->
console.error('[EditPricingController::deleteCoupon] Error: '+error.statusText) if error.statusText
if error.status == 422
growl.error(_t('pricing.unable_to_delete_the_specified_coupon_already_in_use'))
else
growl.error(_t('pricing.unable_to_delete_the_specified_coupon_an_unexpected_error_occurred'))
##
# Open a modal allowing to select an user and send him the details of the provided coupon
# @param coupon {Object} The coupon to send
##
$scope.sendCouponToUser = (coupon) ->
$uibModal.open
templateUrl: '<%= asset_path "admin/pricing/sendCoupon.html" %>'
resolve:
coupon: -> coupon
size: 'md'
controller: ['$scope', '$uibModalInstance', 'Coupon', 'coupon', '_t', ($scope, $uibModalInstance, Coupon, coupon, _t) ->
## Member, receiver of the coupon
$scope.ctrl =
member: null
## Details of the coupon to send
$scope.coupon = coupon
## Callback to validate sending of the coupon
$scope.ok = ->
Coupon.send {coupon_code: coupon.code, user_id: $scope.ctrl.member.id}, (res) ->
growl.success(_t('pricing.coupon_successfully_sent_to_USER', {USER: $scope.ctrl.member.name}))
$uibModalInstance.close({user_id: $scope.ctrl.member.id})
, (err) ->
growl.error(_t('pricing.an_error_occurred_unable_to_send_the_coupon'))
## Callback to close the modal and cancel the sending process
$scope.cancel = ->
$uibModalInstance.dismiss('cancel')
]
### PRIVATE SCOPE ###
##
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
$scope.trainingCreditsGroups = groupCreditsByPlan($scope.trainingCredits)
## 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] = []
##
# Retrieve an item index by its ID from the given array of objects
# @param items {Array<{id:number}>}
# @param id {number}
# @returns {number} item index in the provided array
##
findItemIdxById = (items, id)->
(items.map (item)->
item.id
).indexOf(id)
##
# 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()
]

View File

@ -0,0 +1,647 @@
/* eslint-disable
handle-callback-err,
no-return-assign,
no-undef,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS205: Consider reworking code to avoid use of IIFEs
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
'use strict';
/**
* Controller used in the prices edition page
*/
Application.Controllers.controller('EditPricingController', ['$scope', '$state', '$uibModal', '$filter', 'TrainingsPricing', 'Credit', 'Pricing', 'Plan', 'Coupon', 'plans', 'groups', 'growl', 'machinesPricesPromise', 'Price', 'dialogs', 'trainingsPricingsPromise', 'trainingsPromise', 'machineCreditsPromise', 'machinesPromise', 'trainingCreditsPromise', 'couponsPromise', 'spacesPromise', 'spacesPricesPromise', 'spacesCreditsPromise', '_t',
function ($scope, $state, $uibModal, $filter, TrainingsPricing, Credit, Pricing, Plan, Coupon, plans, groups, growl, machinesPricesPromise, Price, dialogs, trainingsPricingsPromise, trainingsPromise, machineCreditsPromise, machinesPromise, trainingCreditsPromise, couponsPromise, spacesPromise, spacesPricesPromise, spacesCreditsPromise, _t) {
/* PUBLIC SCOPE */
// List of machines prices (not considering any plan)
$scope.machinesPrices = machinesPricesPromise;
// List of trainings pricing
$scope.trainingsPricings = trainingsPricingsPromise;
// List of available subscriptions plans (eg. student/month, PME/year ...)
$scope.plans = plans;
$scope.enabledPlans = plans.filter(function (p) { return !p.disabled; });
// List of groups (eg. normal, student ...)
$scope.groups = groups.filter(function (g) { return g.slug !== 'admins'; });
$scope.enabledGroups = groups.filter(function (g) { return (g.slug !== 'admins') && !g.disabled; });
// Associate free machine hours with subscriptions
$scope.machineCredits = machineCreditsPromise;
// Array of associations (plan <-> training)
$scope.trainingCredits = trainingCreditsPromise;
// Associate a plan with all its trainings ids
$scope.trainingCreditsGroups = {};
// List of trainings
$scope.trainings = trainingsPromise.filter(function (t) { return !t.disabled; });
// List of machines
$scope.machines = machinesPromise;
$scope.enabledMachines = machinesPromise.filter(function (m) { return !m.disabled; });
// List of coupons
$scope.coupons = couponsPromise;
// List of spaces
$scope.spaces = spacesPromise;
$scope.enabledSpaces = spacesPromise.filter(function (s) { return !s.disabled; });
// Associate free space hours with subscriptions
$scope.spaceCredits = spacesCreditsPromise;
// List of spaces prices (not considering any plan)
$scope.spacesPrices = spacesPricesPromise;
// The plans list ordering. Default: by group
$scope.orderPlans = 'group_id';
// Status of the drop-down menu in Credits tab
$scope.status =
{ isopen: false };
// Default: we show only enabled plans
$scope.planFiltering = 'enabled';
// Available options for filtering plans by status
$scope.filterDisabled = [
'enabled',
'disabled',
'all'
];
$scope.findTrainingsPricing = function (trainingsPricings, trainingId, groupId) {
for (let trainingsPricing of Array.from(trainingsPricings)) {
if ((trainingsPricing.training_id === trainingId) && (trainingsPricing.group_id === groupId)) {
return trainingsPricing;
}
}
};
$scope.updateTrainingsPricing = function (data, trainingsPricing) {
if (data != null) {
return TrainingsPricing.update({ id: trainingsPricing.id }, { trainings_pricing: { amount: data } }).$promise;
} else {
return _t('pricing.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 = function (id) {
for (let plan of Array.from($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 = function (groups, id) {
for (let group of Array.from(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<number>} trainings IDs array
*/
$scope.showTrainings = function (trainings) {
if (!angular.isArray(trainings) || !(trainings.length > 0)) {
return _t('pricing.none');
}
const selected = [];
angular.forEach($scope.trainings, function (t) {
if (trainings.indexOf(t.id) >= 0) {
return selected.push(t.name);
}
});
if (selected.length) { return selected.join(' | '); } else { return _t('pricing.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 = function (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
, function (error) {
growl.error(_t('pricing.an_error_occurred_while_saving_the_number_of_credits'));
console.error(error);
}
);
// save the associated trainings
return angular.forEach($scope.trainingCreditsGroups, function (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, function (oldTrainingId) {
if (newdata.training_ids.indexOf(oldTrainingId) === -1) {
const tc = findTrainingCredit(oldTrainingId, planId);
if (tc) {
return tc.$delete({}
, function () {
$scope.trainingCredits.splice($scope.trainingCredits.indexOf(tc), 1);
return $scope.trainingCreditsGroups[planId].splice($scope.trainingCreditsGroups[planId].indexOf(tc.id), 1);
}
, function (error) {
growl.error(_t('pricing.an_error_occurred_while_deleting_credit_with_the_TRAINING', { TRAINING: tc.creditable.name }));
console.error(error);
});
} else {
return growl.error(_t('pricing.an_error_occurred_unable_to_find_the_credit_to_revoke'));
}
}
});
// iterate through the new credits to add
return angular.forEach(newdata.training_ids, function (newTrainingId) {
if (original.indexOf(newTrainingId) === -1) {
return Credit.save({
credit: {
creditable_id: newTrainingId,
creditable_type: 'Training',
plan_id: planId
}
}
, function (newTc) { // success
$scope.trainingCredits.push(newTc);
return $scope.trainingCreditsGroups[newTc.plan_id].push(newTc.creditable_id);
}
, function (error) { // failed
const training = getTrainingFromId(newTrainingId);
growl.error(_t('pricing.an_error_occurred_while_creating_credit_with_the_TRAINING', { TRAINING: training.name }));
return console.error(error);
});
}
});
}
}
});
};
/**
* Cancel the current training credit modification
* @param rowform {Object} see http://vitalets.github.io/angular-xeditable/
*/
$scope.cancelTrainingCredit = function (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 = function (e) {
e.preventDefault();
e.stopPropagation();
$scope.inserted =
{ creditable_type: 'Machine' };
$scope.machineCredits.push($scope.inserted);
return $scope.status.isopen = !$scope.status.isopen;
};
/**
* In the Credits tab, return the name of the machine/space associated with the given credit
* @param credit {Object} credit object, inherited from $resource
* @returns {String}
*/
$scope.showCreditableName = function (credit) {
let selected = _t('pricing.not_set');
if (credit && credit.creditable_id) {
const object = $scope.getCreditable(credit);
selected = object.name;
if (credit.creditable_type === 'Machine') {
selected += ` ( id. ${object.id} )`;
}
}
return selected;
};
/**
* In the Credits tab, return the machine/space associated with the given credit
* @param credit {Object} credit object, inherited from $resource
* @returns {Object}
*/
$scope.getCreditable = function (credit) {
let selected;
if (credit && credit.creditable_id) {
if (credit.creditable_type === 'Machine') {
angular.forEach($scope.machines, function (m) {
if (m.id === credit.creditable_id) {
return selected = m;
}
});
} else if (credit.creditable_type === 'Space') {
angular.forEach($scope.spaces, function (s) {
if (s.id === credit.creditable_id) {
return selected = s;
}
});
}
}
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 = function (data, id) {
for (let mc of Array.from($scope.machineCredits)) {
if ((mc.plan_id === data.plan_id) && (mc.creditable_id === data.creditable_id) && ((id === null) || (mc.id !== id))) {
growl.error(_t('pricing.error_a_credit_linking_this_machine_with_that_subscription_already_exists'));
if (!id) {
$scope.machineCredits.pop();
}
return false;
}
}
if (id != null) {
return Credit.update({ id }, { credit: data }, function () { growl.success(_t('pricing.changes_have_been_successfully_saved')); });
} else {
data.creditable_type = 'Machine';
return Credit.save(
{ credit: data }
, function (resp) {
$scope.machineCredits[$scope.machineCredits.length - 1].id = resp.id;
return growl.success(_t('pricing.credit_was_successfully_saved'));
}
, function (err) {
$scope.machineCredits.pop();
growl.error(_t('pricing.error_creating_credit'));
console.error(err);
});
}
};
/**
* 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} credit index in the $scope.machineCredits array
*/
$scope.cancelMachineCredit = function (rowform, index) {
if ($scope.machineCredits[index].id != null) {
return rowform.$cancel();
} else {
return $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 = function (index) {
Credit.delete($scope.machineCredits[index]);
$scope.machineCredits.splice(index, 1);
};
/**
* Create a new empty entry in the $scope.spaceCredits array
* @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
*/
$scope.addSpaceCredit = function (e) {
e.preventDefault();
e.stopPropagation();
$scope.inserted =
{ creditable_type: 'Space' };
$scope.spaceCredits.push($scope.inserted);
$scope.status.isopen = !$scope.status.isopen;
};
/**
* Validation callback when editing space's credits. Save the changes.
* This will prevent the creation of two credits associated with the same space and plan.
* @param data {Object} space, associated plan and number of credit hours.
* @param [id] {number} credit id for edition, create a new credit object if not provided
*/
$scope.saveSpaceCredit = function (data, id) {
for (let sc of Array.from($scope.spaceCredits)) {
if ((sc.plan_id === data.plan_id) && (sc.creditable_id === data.creditable_id) && ((id === null) || (sc.id !== id))) {
growl.error(_t('pricing.error_a_credit_linking_this_space_with_that_subscription_already_exists'));
if (!id) {
$scope.spaceCredits.pop();
}
return false;
}
}
if (id != null) {
return Credit.update({ id }, { credit: data }, function () { growl.success(_t('pricing.changes_have_been_successfully_saved')); });
} else {
data.creditable_type = 'Space';
return Credit.save(
{ credit: data }
, function (resp) {
$scope.spaceCredits[$scope.spaceCredits.length - 1].id = resp.id;
return growl.success(_t('pricing.credit_was_successfully_saved'));
}
, function (err) {
$scope.spaceCredits.pop();
return growl.error(_t('pricing.error_creating_credit'));
});
}
};
/**
* Removes the newly inserted but not saved space credit / Cancel the current space credit modification
* @param rowform {Object} see http://vitalets.github.io/angular-xeditable/
* @param index {number} credit index in the $scope.spaceCredits array
*/
$scope.cancelSpaceCredit = function (rowform, index) {
if ($scope.spaceCredits[index].id != null) {
return rowform.$cancel();
} else {
return $scope.spaceCredits.splice(index, 1);
}
};
/**
* Deletes the space credit at the specified index
* @param index {number} space credit index in the $scope.spaceCredits array
*/
$scope.removeSpaceCredit = function (index) {
Credit.delete($scope.spaceCredits[index]);
return $scope.spaceCredits.splice(index, 1);
};
/**
* If the plan does not have a type, return a default value for display purposes
* @param type {string|undefined|null} plan's type (eg. 'partner')
* @returns {string}
*/
$scope.getPlanType = function (type) {
if (type === 'PartnerPlan') {
return _t('pricing.partner');
} else { return _t('pricing.standard'); }
};
/**
* Change the plans ordering criterion to the one provided
* @param orderBy {string} ordering criterion
*/
$scope.setOrderPlans = function (orderBy) {
if ($scope.orderPlans === orderBy) {
return $scope.orderPlans = `-${orderBy}`;
} else {
return $scope.orderPlans = orderBy;
}
};
/**
* Retrieve a price from prices array by a machineId and a groupId
*/
$scope.findPriceBy = function (prices, machineId, groupId) {
for (let price of Array.from(prices)) {
if ((price.priceable_id === machineId) && (price.group_id === groupId)) {
return price;
}
}
};
/**
* update a price for a machine and a group, not considering any plan
*/
$scope.updatePrice = function (data, price) {
if (data != null) {
return Price.update({ id: price.id }, { price: { amount: data } }).$promise;
} else {
return _t('pricing.please_specify_a_number');
}
};
/**
* Delete the specified subcription plan
* @param id {number} plan id
*/
$scope.deletePlan = function (plans, id) {
if (typeof id !== 'number') {
return console.error('[EditPricingController::deletePlan] Error: invalid id parameter');
} else {
// open a confirmation dialog
return dialogs.confirm(
{
resolve: {
object () {
return {
title: _t('pricing.confirmation_required'),
msg: _t('pricing.do_you_really_want_to_delete_this_subscription_plan')
};
}
}
},
function () {
// the admin has confirmed, delete the plan
Plan.delete(
{ id },
function (res) {
growl.success(_t('pricing.subscription_plan_was_successfully_deleted'));
return $scope.plans.splice(findItemIdxById(plans, id), 1);
},
function (error) {
if (error.statusText) { console.error(`[EditPricingController::deletePlan] Error: ${error.statusText}`); }
growl.error(_t('pricing.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 = function (plan, groups, short) { return `${$filter('humanReadablePlanName')(plan, groups, short)}`; };
/**
* Delete a coupon from the server's database and, in case of success, from the list in memory
* @param coupons {Array<Object>} should be called with $scope.coupons
* @param id {number} ID of the coupon to delete
*/
$scope.deleteCoupon = function (coupons, id) {
if (typeof id !== 'number') {
return console.error('[EditPricingController::deleteCoupon] Error: invalid id parameter');
} else {
// open a confirmation dialog
return dialogs.confirm({
resolve: {
object () {
return {
title: _t('pricing.confirmation_required'),
msg: _t('pricing.do_you_really_want_to_delete_this_coupon')
};
}
}
}
, function () {
// the admin has confirmed, delete the coupon
Coupon.delete({ id }, function (res) {
growl.success(_t('coupon_was_successfully_deleted'));
return $scope.coupons.splice(findItemIdxById(coupons, id), 1);
}
, function (error) {
if (error.statusText) { console.error(`[EditPricingController::deleteCoupon] Error: ${error.statusText}`); }
if (error.status === 422) {
return growl.error(_t('pricing.unable_to_delete_the_specified_coupon_already_in_use'));
} else {
return growl.error(_t('pricing.unable_to_delete_the_specified_coupon_an_unexpected_error_occurred'));
}
});
});
}
};
/**
* Open a modal allowing to select an user and send him the details of the provided coupon
* @param coupon {Object} The coupon to send
*/
$scope.sendCouponToUser = function (coupon) {
$uibModal.open({
templateUrl: '<%= asset_path "admin/pricing/sendCoupon.html" %>',
resolve: {
coupon () { return coupon; }
},
size: 'md',
controller: ['$scope', '$uibModalInstance', 'Coupon', 'coupon', '_t', function ($scope, $uibModalInstance, Coupon, coupon, _t) {
// Member, receiver of the coupon
$scope.ctrl =
{ member: null };
// Details of the coupon to send
$scope.coupon = coupon;
// Callback to validate sending of the coupon
$scope.ok = function () {
Coupon.send({ coupon_code: coupon.code, user_id: $scope.ctrl.member.id }, function (res) {
growl.success(_t('pricing.coupon_successfully_sent_to_USER', { USER: $scope.ctrl.member.name }));
return $uibModalInstance.close({ user_id: $scope.ctrl.member.id });
}
, function (err) {
growl.error(_t('pricing.an_error_occurred_unable_to_send_the_coupon'));
console.error(err);
});
};
// Callback to close the modal and cancel the sending process
$scope.cancel = function () { $uibModalInstance.dismiss('cancel'); };
}]
});
};
/* PRIVATE SCOPE */
/**
* Kind of constructor: these actions will be realized first when the controller is loaded
*/
const initialize = function () {
$scope.trainingCreditsGroups = groupCreditsByPlan($scope.trainingCredits);
// adds empty array for plan which hasn't any credits yet
return (function () {
const result = [];
for (let plan of Array.from($scope.plans)) {
if ($scope.trainingCreditsGroups[plan.id] == null) {
result.push($scope.trainingCreditsGroups[plan.id] = []);
} else {
result.push(undefined);
}
}
return result;
})();
};
/**
* Retrieve an item index by its ID from the given array of objects
* @param items {Array<{id:number}>}
* @param id {number}
* @returns {number} item index in the provided array
*/
var findItemIdxById = function (items, id) {
return (items.map(function (item) { return item.id; })).indexOf(id);
};
/**
* Group the given credits array into a map associating the plan ID with its associated trainings/machines
* @return {Object} the association map
*/
var groupCreditsByPlan = function (credits) {
const creditsMap = {};
angular.forEach(credits, function (c) {
if (!creditsMap[c.plan_id]) {
creditsMap[c.plan_id] = [];
}
return creditsMap[c.plan_id].push(c.creditable_id);
});
return 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
*/
var findTrainingCredit = function (trainingId, planId) {
trainingId = parseInt(trainingId);
planId = parseInt(planId);
for (let credit of Array.from($scope.trainingCredits)) {
if ((credit.plan_id === planId) && (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
*/
var getTrainingFromId = function (id) {
for (let training of Array.from($scope.trainings)) {
if (training.id === parseInt(id)) {
return training;
}
}
};
// !!! MUST BE CALLED AT THE END of the controller
return initialize();
}
]);

View File

@ -1,154 +0,0 @@
'use strict'
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 = componentsPromise
## Licences list (Creative Common ...)
$scope.licences = licencesPromise
## Themes list (cooking, sport ...)
$scope.themes = themesPromise
##
# Saves a new component / Update an existing material to the server (form validation callback)
# @param data {Object} component name
# @param [data] {number} component id, in case of update
##
$scope.saveComponent = (data, id) ->
if id?
Component.update {id: id}, data
else
Component.save data, (resp)->
$scope.components[$scope.components.length-1].id = resp.id
##
# Deletes the component at the specified index
# @param index {number} component index in the $scope.components array
##
$scope.removeComponent = (index) ->
Component.delete $scope.components[index]
$scope.components.splice(index, 1)
##
# Creates a new empty entry in the $scope.components array
##
$scope.addComponent = ->
$scope.inserted =
name: ''
$scope.components.push($scope.inserted)
##
# Removes the newly inserted but not saved component / Cancel the current component modification
# @param rowform {Object} see http://vitalets.github.io/angular-xeditable/
# @param index {number} component index in the $scope.components array
##
$scope.cancelComponent = (rowform, index) ->
if $scope.components[index].id?
rowform.$cancel()
else
$scope.components.splice(index, 1)
##
# Saves a new theme / Update an existing theme to the server (form validation callback)
# @param data {Object} theme name
# @param [data] {number} theme id, in case of update
##
$scope.saveTheme = (data, id) ->
if id?
Theme.update {id: id}, data
else
Theme.save data, (resp)->
$scope.themes[$scope.themes.length-1].id = resp.id
##
# Deletes the theme at the specified index
# @param index {number} theme index in the $scope.themes array
##
$scope.removeTheme = (index) ->
Theme.delete $scope.themes[index]
$scope.themes.splice(index, 1)
##
# Creates a new empty entry in the $scope.themes array
##
$scope.addTheme = ->
$scope.inserted =
name: ''
$scope.themes.push($scope.inserted)
##
# Removes the newly inserted but not saved theme / Cancel the current theme modification
# @param rowform {Object} see http://vitalets.github.io/angular-xeditable/
# @param index {number} theme index in the $scope.themes array
##
$scope.cancelTheme = (rowform, index) ->
if $scope.themes[index].id?
rowform.$cancel()
else
$scope.themes.splice(index, 1)
##
# Saves a new licence / Update an existing licence to the server (form validation callback)
# @param data {Object} licence name and description
# @param [data] {number} licence id, in case of update
##
$scope.saveLicence = (data, id) ->
if id?
Licence.update {id: id}, data
else
Licence.save data, (resp)->
$scope.licences[$scope.licences.length-1].id = resp.id
##
# Deletes the licence at the specified index
# @param index {number} licence index in the $scope.licences array
##
$scope.removeLicence = (index) ->
Licence.delete $scope.licences[index]
$scope.licences.splice(index, 1)
##
# Creates a new empty entry in the $scope.licences array
##
$scope.addLicence = ->
$scope.inserted =
name: ''
description: ''
$scope.licences.push($scope.inserted)
##
# Removes the newly inserted but not saved licence / Cancel the current licence modification
# @param rowform {Object} see http://vitalets.github.io/angular-xeditable/
# @param index {number} licence index in the $scope.licences array
##
$scope.cancelLicence = (rowform, index) ->
if $scope.licences[index].id?
rowform.$cancel()
else
$scope.licences.splice(index, 1)
]

View File

@ -0,0 +1,160 @@
/* eslint-disable
no-return-assign,
no-undef,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
'use strict';
Application.Controllers.controller('ProjectElementsController', ['$scope', '$state', 'Component', 'Licence', 'Theme', 'componentsPromise', 'licencesPromise', 'themesPromise',
function ($scope, $state, Component, Licence, Theme, componentsPromise, licencesPromise, themesPromise) {
// Materials list (plastic, wood ...)
$scope.components = componentsPromise;
// Licences list (Creative Common ...)
$scope.licences = licencesPromise;
// Themes list (cooking, sport ...)
$scope.themes = themesPromise;
/**
* Saves a new component / Update an existing material to the server (form validation callback)
* @param data {Object} component name
* @param [data] {number} component id, in case of update
*/
$scope.saveComponent = function (data, id) {
if (id != null) {
return Component.update({ id }, data);
} else {
return Component.save(data, resp => $scope.components[$scope.components.length - 1].id = resp.id);
}
};
/**
* Deletes the component at the specified index
* @param index {number} component index in the $scope.components array
*/
$scope.removeComponent = function (index) {
Component.delete($scope.components[index]);
return $scope.components.splice(index, 1);
};
/**
* Creates a new empty entry in the $scope.components array
*/
$scope.addComponent = function () {
$scope.inserted =
{ name: '' };
return $scope.components.push($scope.inserted);
};
/**
* Removes the newly inserted but not saved component / Cancel the current component modification
* @param rowform {Object} see http://vitalets.github.io/angular-xeditable/
* @param index {number} component index in the $scope.components array
*/
$scope.cancelComponent = function (rowform, index) {
if ($scope.components[index].id != null) {
return rowform.$cancel();
} else {
return $scope.components.splice(index, 1);
}
};
/**
* Saves a new theme / Update an existing theme to the server (form validation callback)
* @param data {Object} theme name
* @param [data] {number} theme id, in case of update
*/
$scope.saveTheme = function (data, id) {
if (id != null) {
return Theme.update({ id }, data);
} else {
return Theme.save(data, resp => $scope.themes[$scope.themes.length - 1].id = resp.id);
}
};
/**
* Deletes the theme at the specified index
* @param index {number} theme index in the $scope.themes array
*/
$scope.removeTheme = function (index) {
Theme.delete($scope.themes[index]);
return $scope.themes.splice(index, 1);
};
/**
* Creates a new empty entry in the $scope.themes array
*/
$scope.addTheme = function () {
$scope.inserted =
{ name: '' };
return $scope.themes.push($scope.inserted);
};
/**
* Removes the newly inserted but not saved theme / Cancel the current theme modification
* @param rowform {Object} see http://vitalets.github.io/angular-xeditable/
* @param index {number} theme index in the $scope.themes array
*/
$scope.cancelTheme = function (rowform, index) {
if ($scope.themes[index].id != null) {
return rowform.$cancel();
} else {
return $scope.themes.splice(index, 1);
}
};
/**
* Saves a new licence / Update an existing licence to the server (form validation callback)
* @param data {Object} licence name and description
* @param [data] {number} licence id, in case of update
*/
$scope.saveLicence = function (data, id) {
if (id != null) {
return Licence.update({ id }, data);
} else {
return Licence.save(data, resp => $scope.licences[$scope.licences.length - 1].id = resp.id);
}
};
/**
* Deletes the licence at the specified index
* @param index {number} licence index in the $scope.licences array
*/
$scope.removeLicence = function (index) {
Licence.delete($scope.licences[index]);
return $scope.licences.splice(index, 1);
};
/**
* Creates a new empty entry in the $scope.licences array
*/
$scope.addLicence = function () {
$scope.inserted = {
name: '',
description: ''
};
return $scope.licences.push($scope.inserted);
};
/**
* Removes the newly inserted but not saved licence / Cancel the current licence modification
* @param rowform {Object} see http://vitalets.github.io/angular-xeditable/
* @param index {number} licence index in the $scope.licences array
*/
return $scope.cancelLicence = function (rowform, index) {
if ($scope.licences[index].id != null) {
return rowform.$cancel();
} else {
return $scope.licences.splice(index, 1);
}
};
}
]);

View File

@ -1,228 +0,0 @@
'use strict'
Application.Controllers.controller "SettingsController", ["$scope", 'Setting', 'growl', 'settingsPromise', 'cgvFile', 'cguFile', 'logoFile', 'logoBlackFile', 'faviconFile', 'profileImageFile', 'CSRF', '_t'
($scope, Setting, growl, settingsPromise, cgvFile, cguFile, logoFile, logoBlackFile, faviconFile, profileImageFile, 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"
profileImage: "/api/custom_assets"
## Form actions on the above URL
$scope.methods =
cgu: "post"
cgv: "post"
logo: "post"
logoBlack: "post"
favicon: "post"
profileImage: "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.eventExplicationsAlert = {name: 'event_explications_alert', value: settingsPromise.event_explications_alert }
$scope.spaceExplicationsAlert = { name: 'space_explications_alert', value: settingsPromise.space_explications_alert }
$scope.windowStart = { name: 'booking_window_start', value: settingsPromise.booking_window_start }
$scope.windowEnd = { name: 'booking_window_end', value: settingsPromise.booking_window_end }
$scope.mainColorSetting = { name: 'main_color', value: settingsPromise.main_color }
$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.machinesSortBy = { name: 'machines_sort_by', value: settingsPromise.machines_sort_by }
$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.profileImage = profileImageFile.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, 10)
$scope.enableCancel =
name: 'booking_cancel_enable'
value: (settingsPromise.booking_cancel_enable == "true")
$scope.cancelDelay =
name: 'booking_cancel_delay'
value: parseInt(settingsPromise.booking_cancel_delay, 10)
$scope.enableReminder =
name: 'reminder_enable'
value: (settingsPromise.reminder_enable == 'true')
$scope.reminderDelay =
name: 'reminder_delay'
value: parseInt(settingsPromise.reminder_delay, 10)
$scope.visibilityYearly =
name: 'visibility_yearly'
value: parseInt(settingsPromise.visibility_yearly, 10)
$scope.visibilityOthers =
name: 'visibility_others'
value: parseInt(settingsPromise.visibility_others, 10)
$scope.displayNameEnable =
name: 'display_name_enable'
value: (settingsPromise.display_name_enable == 'true')
##
# 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 == "<br>" or setting.value == "<p><br></p>"
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('settings.customization_of_SETTING_successfully_saved', { SETTING:_t('settings.' + 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('settings.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.customLogo = 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.customLogoBlack = 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.customFavicon = content.custom_asset
$scope.methods.favicon = 'put'
$scope.actionUrl.favicon += '/favicon-file' unless $scope.actionUrl.favicon.indexOf('/favicon-file') > 0
else if content.custom_asset.name is 'profile-image-file'
$scope.profileImage = content.custom_asset
$scope.methods.profileImage = 'put'
$scope.actionUrl.profileImage += '/profile-image-file' unless $scope.actionUrl.profileImage.indexOf('/profile-image-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'
if profileImageFile.custom_asset
$scope.methods.profileImage = 'put'
$scope.actionUrl.profileImage += '/profile-image-file'
# init the controller (call at the end !)
initialize()
]

View File

@ -0,0 +1,256 @@
/* eslint-disable
no-return-assign,
no-undef,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
'use strict';
Application.Controllers.controller('SettingsController', ['$scope', 'Setting', 'growl', 'settingsPromise', 'cgvFile', 'cguFile', 'logoFile', 'logoBlackFile', 'faviconFile', 'profileImageFile', 'CSRF', '_t',
function ($scope, Setting, growl, settingsPromise, cgvFile, cguFile, logoFile, logoBlackFile, faviconFile, profileImageFile, 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',
profileImage: '/api/custom_assets'
};
// Form actions on the above URL
$scope.methods = {
cgu: 'post',
cgv: 'post',
logo: 'post',
logoBlack: 'post',
favicon: 'post',
profileImage: '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.eventExplicationsAlert = { name: 'event_explications_alert', value: settingsPromise.event_explications_alert };
$scope.spaceExplicationsAlert = { name: 'space_explications_alert', value: settingsPromise.space_explications_alert };
$scope.windowStart = { name: 'booking_window_start', value: settingsPromise.booking_window_start };
$scope.windowEnd = { name: 'booking_window_end', value: settingsPromise.booking_window_end };
$scope.mainColorSetting = { name: 'main_color', value: settingsPromise.main_color };
$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.machinesSortBy = { name: 'machines_sort_by', value: settingsPromise.machines_sort_by };
$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.profileImage = profileImageFile.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, 10)
};
$scope.enableCancel = {
name: 'booking_cancel_enable',
value: (settingsPromise.booking_cancel_enable === 'true')
};
$scope.cancelDelay = {
name: 'booking_cancel_delay',
value: parseInt(settingsPromise.booking_cancel_delay, 10)
};
$scope.enableReminder = {
name: 'reminder_enable',
value: (settingsPromise.reminder_enable === 'true')
};
$scope.reminderDelay = {
name: 'reminder_delay',
value: parseInt(settingsPromise.reminder_delay, 10)
};
$scope.visibilityYearly = {
name: 'visibility_yearly',
value: parseInt(settingsPromise.visibility_yearly, 10)
};
$scope.visibilityOthers = {
name: 'visibility_others',
value: parseInt(settingsPromise.visibility_others, 10)
};
$scope.displayNameEnable = {
name: 'display_name_enable',
value: (settingsPromise.display_name_enable === 'true')
};
/**
* 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 = function (v) {
if (v) {
return 'fileinput-exists';
} else {
return '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 = function (setting) {
// trim empty html
let value;
if ((setting.value === '<br>') || (setting.value === '<p><br></p>')) {
setting.value = '';
}
// convert dates to ISO format
if (setting.value instanceof Date) {
setting.value = setting.value.toISOString();
}
if (setting.value !== null) {
value = setting.value.toString();
} else {
({ value } = setting);
}
return Setting.update({ name: setting.name }, { value }, data => growl.success(_t('settings.customization_of_SETTING_successfully_saved', { SETTING: _t(`settings.${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 = function (content) {
if ((content.custom_asset == null)) {
$scope.alerts = [];
return angular.forEach(content, (v, k) =>
angular.forEach(v, err => growl.error(err))
);
} else {
growl.success(_t('settings.file_successfully_updated'));
if (content.custom_asset.name === 'cgu-file') {
$scope.cguFile = content.custom_asset;
$scope.methods.cgu = 'put';
if (!($scope.actionUrl.cgu.indexOf('/cgu-file') > 0)) { $scope.actionUrl.cgu += '/cgu-file'; }
return $scope.loader.cgu = false;
} else if (content.custom_asset.name === 'cgv-file') {
$scope.cgvFile = content.custom_asset;
$scope.methods.cgv = 'put';
if (!($scope.actionUrl.cgv.indexOf('/cgv-file') > 0)) { $scope.actionUrl.cgv += '/cgv-file'; }
return $scope.loader.cgv = false;
} else if (content.custom_asset.name === 'logo-file') {
$scope.customLogo = content.custom_asset;
$scope.methods.logo = 'put';
if (!($scope.actionUrl.logo.indexOf('/logo-file') > 0)) { return $scope.actionUrl.logo += '/logo-file'; }
} else if (content.custom_asset.name === 'logo-black-file') {
$scope.customLogoBlack = content.custom_asset;
$scope.methods.logoBlack = 'put';
if (!($scope.actionUrl.logoBlack.indexOf('/logo-black-file') > 0)) { return $scope.actionUrl.logoBlack += '/logo-black-file'; }
} else if (content.custom_asset.name === 'favicon-file') {
$scope.customFavicon = content.custom_asset;
$scope.methods.favicon = 'put';
if (!($scope.actionUrl.favicon.indexOf('/favicon-file') > 0)) { return $scope.actionUrl.favicon += '/favicon-file'; }
} else if (content.custom_asset.name === 'profile-image-file') {
$scope.profileImage = content.custom_asset;
$scope.methods.profileImage = 'put';
if (!($scope.actionUrl.profileImage.indexOf('/profile-image-file') > 0)) { return $scope.actionUrl.profileImage += '/profile-image-file'; }
}
}
};
/**
* @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
*/
const initialize = function () {
// 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', function (newValue, oldValue, scope) {
if ($scope.windowStart && moment($scope.windowStart.value).isAfter(newValue)) {
return $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';
}
if (profileImageFile.custom_asset) {
$scope.methods.profileImage = 'put';
return $scope.actionUrl.profileImage += '/profile-image-file';
}
};
// init the controller (call at the end !)
return initialize();
}
]);

View File

@ -1,684 +0,0 @@
'use strict'
Application.Controllers.controller "StatisticsController", ["$scope", "$state", "$rootScope", '$uibModal', "es", "Member", '_t', 'membersPromise', 'statisticsPromise'
, ($scope, $state, $rootScope, $uibModal, es, Member, _t, membersPromise, statisticsPromise) ->
### PRIVATE STATIC CONSTANTS ###
## search window size
RESULTS_PER_PAGE = 20
## keep search context for (delay in minutes) ...
ES_SCROLL_TIME = 1
### PUBLIC SCOPE ###
## ui-view transitions optimization: if true, the stats will never be refreshed
$scope.preventRefresh = false
## statistics structure in elasticSearch
$scope.statistics = statisticsPromise
## fablab users list
$scope.members = membersPromise
## statistics data recovered from elasticSearch
$scope.data = null
## when did the search was triggered
$scope.searchDate = null
## id of the elastic search context
$scope.scrollId = null
## total number of results for the current query
$scope.totalHits = 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
## Results of custom aggregations for the current type
$scope.customAggs = {}
## default: results are not sorted
$scope.sorting =
ca: 'none'
date: 'desc'
## 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: Fablab.uibDateFormat
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: Fablab.uibDateFormat
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: Fablab.uibDateFormat
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 (from statistic_indices table)
##
$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'
$scope.sorting.date = 'desc'
buildCustomFiltersList()
refreshStats()
##
# Returns true if the provided tab must be hidden due to some global or local configuration
# @param tab {Object} elasticsearch statistic structure (from statistic_indices table)
##
$scope.hiddenTab = (tab) ->
if tab.table
if tab.es_type_key == 'subscription' && $rootScope.fablabWithoutPlans
true
else if tab.es_type_key == 'space' && $rootScope.fablabWithoutSpaces
true
else
false
else
true
##
# Callback to validate the filters and send a new request to elastic
##
$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) ->
name = $scope.members[id]
return (if name then name else "ID "+id)
##
# Run a scroll query to elasticsearch to append the next packet of results to those displayed.
# If the ES search context has expired when the user ask for more results, we re-run the whole query.
##
$scope.showMoreResults = ->
# if all results were retrieved, do nothing
if $scope.data.length >= $scope.totalHits
return
if moment($scope.searchDate).add(ES_SCROLL_TIME, 'minutes').isBefore(moment())
# elastic search context has expired, so we run again the whole query
refreshStats()
else
es.scroll
"scroll": ES_SCROLL_TIME+'m'
"body": {scrollId: $scope.scrollId}
, (error, response) ->
if (error)
console.error "Error: something unexpected occurred during elasticSearch scroll query: "+error
else
$scope.scrollId = response._scroll_id
$scope.data = $scope.data.concat(response.hits.hits)
##
# Open a modal dialog asking the user for details about exporting the statistics tables to an excel file
##
$scope.exportToExcel = ->
options =
templateUrl: '<%= asset_path "admin/statistics/export.html" %>'
size: 'sm'
controller: 'ExportStatisticsController'
resolve:
dates: ->
start: $scope.datePickerStart.selected
end: $scope.datePickerEnd.selected
query: ->
custom = buildCustomFilterQuery()
buildElasticDataQuery($scope.type.active.key, custom, $scope.agePicker.start, $scope.agePicker.end, moment($scope.datePickerStart.selected), moment($scope.datePickerEnd.selected), $scope.sorting)
index: ->
key: $scope.selectedIndex.es_type_key
type: ->
key: $scope.type.active.key
$uibModal.open options
.result['finally'](null).then (info)->
console.log(info)
### PRIVATE SCOPE ###
##
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
# 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
$scope.customAggs = {}
$scope.totalHits = null
$scope.searchDate = new Date()
custom = buildCustomFilterQuery()
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.hits
$scope.totalHits = res.hits.total
$scope.sumCA = res.aggregations.total_ca.value
$scope.averageAge = Math.round(res.aggregations.average_age.value * 100) / 100
$scope.sumStat = res.aggregations.total_stat.value
$scope.scrollId = res._scroll_id
for custom in $scope.type.active.custom_aggregations
$scope.customAggs[custom.field] = res.aggregations[custom.field].value
##
# 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 {Object}, 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": RESULTS_PER_PAGE
"scroll": ES_SCROLL_TIME+'m'
"stat-type": type
"custom-query": if custom then JSON.stringify(Object.assign({exclude: custom.exclude}, buildElasticCustomCriterion(custom))) else ''
"start-date": moment($scope.datePickerStart.selected).format()
"end-date": moment($scope.datePickerEnd.selected).format()
"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)
##
# 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 typeof ageMin == 'number' && typeof ageMax == 'number'
q.query.bool.must.push
"range":
"age":
"gte": ageMin
"lte": ageMax
# optional criterion
if custom
criterion = buildElasticCustomCriterion(custom)
if (custom.exclude)
q.query.bool.must_not = [
"term": criterion.match
]
else
q.query.bool.must.push(criterion)
if sortings
q["sort"] = buildElasticSortCriteria(sortings)
# aggregations (avg age & CA sum)
q["aggs"] = {
"total_ca":
"sum":
"field": "ca"
"average_age":
"avg":
"field": "age"
"total_stat":
"sum":
"field": "stat"
}
q
##
# Build the elasticSearch query DSL to match the selected cutom filter
# @param custom {Object} if custom is empty or undefined, an empty string will be returned
# @returns {{match:{*}}|string}
##
buildElasticCustomCriterion = (custom) ->
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
criterion
else
''
##
# 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)
##
# Build and return an object according to the custom filter set by the user, used to request elasticsearch
# @return {Object|null}
##
buildCustomFilterQuery = ->
custom = null
if !angular.isUndefinedOrNull($scope.customFilter.criterion) and
!angular.isUndefinedOrNull($scope.customFilter.criterion.key) and
!angular.isUndefinedOrNull($scope.customFilter.value)
custom = {}
custom.key = $scope.customFilter.criterion.key
custom.value = $scope.customFilter.value
custom.exclude = $scope.customFilter.exclude
custom
# init the controller (call at the end !)
initialize()
]
Application.Controllers.controller 'ExportStatisticsController', [ '$scope', '$uibModalInstance', 'Export','dates', 'query', 'index', 'type', 'CSRF', 'growl', '_t'
, ($scope, $uibModalInstance, Export, dates, query, index, type, CSRF, growl, _t) ->
## Retrieve Anti-CSRF tokens from cookies
CSRF.setMetaTags()
## Bindings for date range
$scope.dates = dates
## Body of the query to export
$scope.query = JSON.stringify(query)
## API URL where the form will be posted
$scope.actionUrl = '/stats/'+index.key+'/export'
## Key of the current search' statistic type
$scope.typeKey = type.key
## Form action on the above URL
$scope.method = "post"
## Anti-CSRF token to inject into the download form
$scope.csrfToken = angular.element('meta[name="csrf-token"]')[0].content
## Binding of the export type (global / current)
$scope.export =
type: 'current'
## datePicker parameters for interval beginning
$scope.exportStart =
format: Fablab.uibDateFormat
opened: false # default: datePicker is not shown
minDate: null
maxDate: moment().subtract(1, 'day').toDate()
options:
startingDay: Fablab.weekStartingDay
## datePicker parameters for interval ending
$scope.exportEnd =
format: Fablab.uibDateFormat
opened: false # default: datePicker is not shown
minDate: null
maxDate: moment().subtract(1, 'day').toDate()
options:
startingDay: Fablab.weekStartingDay
##
# Callback to open the datepicker (interval start)
# @param $event {Object} jQuery event object
##
$scope.toggleStartDatePicker = ($event) ->
$scope.exportStart.opened = !$scope.exportStart.opened
##
# Callback to open the datepicker (interval end)
# @param $event {Object} jQuery event object
##
$scope.toggleEndDatePicker = ($event) ->
$scope.exportEnd.opened = !$scope.exportEnd.opened
##
# Callback when exchanging the export type between 'global' and 'current view'
# Adjust the query and the requesting url according to this type.
##
$scope.setRequest = ->
if $scope.export.type == 'global'
$scope.actionUrl = '/stats/global/export'
$scope.query = JSON.stringify(
"query":
"bool":
"must": [
{
"range":
"date":
"gte": moment($scope.dates.start).format()
"lte": moment($scope.dates.end).format()
}
]
)
else
$scope.actionUrl = '/stats/'+index.key+'/export'
$scope.query = JSON.stringify(query)
##
# Callback to close the modal, telling the caller what is exported
##
$scope.exportData = ->
statusQry = {category: 'statistics', type: $scope.export.type, query: $scope.query}
unless $scope.export.type == 'global'
statusQry['type'] = index.key
statusQry['key'] = type.key
Export.status(statusQry).then (res) ->
unless (res.data.exists)
growl.success _t('export_is_running_you_ll_be_notified_when_its_ready')
$uibModalInstance.close(statusQry)
##
# Callback to cancel the export and close the modal
##
$scope.cancel = ->
$uibModalInstance.dismiss('cancel')
]

View File

@ -0,0 +1,724 @@
/* eslint-disable
no-constant-condition,
no-return-assign,
no-undef,
standard/no-callback-literal,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS205: Consider reworking code to avoid use of IIFEs
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
'use strict';
Application.Controllers.controller('StatisticsController', ['$scope', '$state', '$rootScope', '$uibModal', 'es', 'Member', '_t', 'membersPromise', 'statisticsPromise',
function ($scope, $state, $rootScope, $uibModal, es, Member, _t, membersPromise, statisticsPromise) {
/* PRIVATE STATIC CONSTANTS */
// search window size
const RESULTS_PER_PAGE = 20;
// keep search context for (delay in minutes) ...
const ES_SCROLL_TIME = 1;
/* PUBLIC SCOPE */
// ui-view transitions optimization: if true, the stats will never be refreshed
$scope.preventRefresh = false;
// statistics structure in elasticSearch
$scope.statistics = statisticsPromise;
// fablab users list
$scope.members = membersPromise;
// statistics data recovered from elasticSearch
$scope.data = null;
// when did the search was triggered
$scope.searchDate = null;
// id of the elastic search context
$scope.scrollId = null;
// total number of results for the current query
$scope.totalHits = 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;
// Results of custom aggregations for the current type
$scope.customAggs = {};
// default: results are not sorted
$scope.sorting = {
ca: 'none',
date: 'desc'
};
// 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: Fablab.uibDateFormat,
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: Fablab.uibDateFormat,
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: Fablab.uibDateFormat,
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 = function ($event) { toggleDatePicker($event, $scope.datePickerStart); };
/**
* Callback to open the datepicker (interval end)
* @param $event {Object} jQuery event object
*/
$scope.toggleEndDatePicker = function ($event) { toggleDatePicker($event, $scope.datePickerEnd); };
/**
* Callback to open the datepicker (custom filter)
* @param $event {Object} jQuery event object
*/
$scope.toggleCustomDatePicker = function ($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 (from statistic_indices table)
*/
$scope.setActiveTab = function (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';
$scope.sorting.date = 'desc';
buildCustomFiltersList();
return refreshStats();
};
/**
* Returns true if the provided tab must be hidden due to some global or local configuration
* @param tab {Object} elasticsearch statistic structure (from statistic_indices table)
*/
$scope.hiddenTab = function (tab) {
if (tab.table) {
if ((tab.es_type_key === 'subscription') && $rootScope.fablabWithoutPlans) {
return true;
} else if ((tab.es_type_key === 'space') && $rootScope.fablabWithoutSpaces) {
return true;
} else {
return false;
}
} else {
return true;
}
};
/**
* Callback to validate the filters and send a new request to elastic
*/
$scope.validateFilterChange = function () {
$scope.agePicker.show = false;
$scope.customFilter.show = false;
$scope.type.active = $scope.type.selected;
buildCustomFiltersList();
return refreshStats();
};
/**
* Callback to validate the dates range and refresh the data from elastic
*/
$scope.validateDateChange = function () {
$scope.datePicker.show = false;
return 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 = function (date) { return moment(date).format('LL'); };
/**
* Parse the sex and return a user-friendly string
* @param sex {string} 'male' | 'female'
*/
$scope.formatSex = function (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 = function (key) {
let label = '';
angular.forEach($scope.type.active.subtypes, function (subtype) {
if (subtype.key === key) {
return label = subtype.label;
}
});
return 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 = function (filter) {
if (filter && filter.values) {
if (typeof (filter.values[0]) === 'string') {
return filter.values[0];
} else if (typeof (filter.values[0] === 'object')) {
return 'input_select';
}
} else {
return 'input_text';
}
};
/**
* Change the sorting order and refresh the results to match the new order
* @param filter {Object} any filter
*/
$scope.toggleSorting = function (filter) {
switch ($scope.sorting[filter]) {
case 'none': $scope.sorting[filter] = 'asc'; break;
case 'asc': $scope.sorting[filter] = 'desc'; break;
case 'desc': $scope.sorting[filter] = 'none'; break;
}
return refreshStats();
};
/**
* Return the user's name from his given ID
* @param id {number} user ID
*/
$scope.getUserNameFromId = function (id) {
const name = $scope.members[id];
return (name || `ID ${id}`);
};
/**
* Run a scroll query to elasticsearch to append the next packet of results to those displayed.
* If the ES search context has expired when the user ask for more results, we re-run the whole query.
*/
$scope.showMoreResults = function () {
// if all results were retrieved, do nothing
if ($scope.data.length >= $scope.totalHits) {
return;
}
if (moment($scope.searchDate).add(ES_SCROLL_TIME, 'minutes').isBefore(moment())) {
// elastic search context has expired, so we run again the whole query
return refreshStats();
} else {
return es.scroll({
'scroll': ES_SCROLL_TIME + 'm',
'body': { scrollId: $scope.scrollId }
}
, function (error, response) {
if (error) {
return console.error(`Error: something unexpected occurred during elasticSearch scroll query: ${error}`);
} else {
$scope.scrollId = response._scroll_id;
return $scope.data = $scope.data.concat(response.hits.hits);
}
});
}
};
/**
* Open a modal dialog asking the user for details about exporting the statistics tables to an excel file
*/
$scope.exportToExcel = function () {
const options = {
templateUrl: '<%= asset_path "admin/statistics/export.html" %>',
size: 'sm',
controller: 'ExportStatisticsController',
resolve: {
dates () {
return {
start: $scope.datePickerStart.selected,
end: $scope.datePickerEnd.selected
};
},
query () {
const custom = buildCustomFilterQuery();
return buildElasticDataQuery($scope.type.active.key, custom, $scope.agePicker.start, $scope.agePicker.end, moment($scope.datePickerStart.selected), moment($scope.datePickerEnd.selected), $scope.sorting);
},
index () {
return { key: $scope.selectedIndex.es_type_key };
},
type () {
return { key: $scope.type.active.key };
}
}
};
return $uibModal.open(options)
.result['finally'](null).then(function (info) { console.log(info); });
};
/* PRIVATE SCOPE */
/**
* Kind of constructor: these actions will be realized first when the controller is loaded
*/
const initialize = function () {
// 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', function (event, toState, toParams, fromState, fromParams) {
if ((fromState.name === 'app.admin.statistics') && (Object.keys(fromParams).length === 0)) {
return $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
*/
var toggleDatePicker = function ($event, datePicker) {
$event.preventDefault();
$event.stopPropagation();
return datePicker.opened = !datePicker.opened;
};
/**
* Force update the statistics table, querying elasticSearch according to the current config values
*/
var refreshStats = function () {
if ($scope.selectedIndex && !$scope.preventRefresh) {
$scope.data = [];
$scope.sumCA = 0;
$scope.averageAge = 0;
$scope.sumStat = 0;
$scope.customAggs = {};
$scope.totalHits = null;
$scope.searchDate = new Date();
let custom = buildCustomFilterQuery();
return queryElasticStats($scope.selectedIndex.es_type_key, $scope.type.active.key, custom, function (res, err) {
if (err) {
return console.error(`[statisticsController::refreshStats] Unable to refresh due to ${err}`);
} else {
$scope.data = res.hits.hits;
$scope.totalHits = res.hits.total;
$scope.sumCA = res.aggregations.total_ca.value;
$scope.averageAge = Math.round(res.aggregations.average_age.value * 100) / 100;
$scope.sumStat = res.aggregations.total_stat.value;
$scope.scrollId = res._scroll_id;
return (function () {
const result = [];
for (custom of Array.from($scope.type.active.custom_aggregations)) {
result.push($scope.customAggs[custom.field] = res.aggregations[custom.field].value);
}
return result;
})();
}
});
}
};
/**
* 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 {Object}, error {String} (if any)
*/
var queryElasticStats = function (index, type, custom, callback) {
// handle invalid callback
if (typeof (callback) !== 'function') {
console.error('[statisticsController::queryElasticStats] Error: invalid callback provided');
return;
}
// run query
return es.search({
'index': 'stats',
'type': index,
'size': RESULTS_PER_PAGE,
'scroll': ES_SCROLL_TIME + 'm',
'stat-type': type,
'custom-query': custom ? JSON.stringify(Object.assign({ exclude: custom.exclude }, buildElasticCustomCriterion(custom))) : '',
'start-date': moment($scope.datePickerStart.selected).format(),
'end-date': moment($scope.datePickerEnd.selected).format(),
'body': buildElasticDataQuery(type, custom, $scope.agePicker.start, $scope.agePicker.end, moment($scope.datePickerStart.selected), moment($scope.datePickerEnd.selected), $scope.sorting)
}
, function (error, response) {
if (error) {
return callback({}, `Error: something unexpected occurred during elasticSearch query: ${error}`);
} else {
return callback(response);
}
});
};
/**
* 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
*/
var buildElasticDataQuery = function (type, custom, ageMin, ageMax, intervalBegin, intervalEnd, sortings) {
const q = {
'query': {
'bool': {
'must': [
{
'term': {
'type': type
}
},
{
'range': {
'date': {
'gte': intervalBegin.format(),
'lte': intervalEnd.format()
}
}
}
]
}
}
};
// optional date range
if ((typeof ageMin === 'number') && (typeof ageMax === 'number')) {
q.query.bool.must.push({
'range': {
'age': {
'gte': ageMin,
'lte': ageMax
}
}
});
}
// optional criterion
if (custom) {
const criterion = buildElasticCustomCriterion(custom);
if (custom.exclude) {
q.query.bool.must_not = [
{ 'term': criterion.match }
];
} else {
q.query.bool.must.push(criterion);
}
}
if (sortings) {
q['sort'] = buildElasticSortCriteria(sortings);
}
// aggregations (avg age & CA sum)
q['aggs'] = {
'total_ca': {
'sum': {
'field': 'ca'
}
},
'average_age': {
'avg': {
'field': 'age'
}
},
'total_stat': {
'sum': {
'field': 'stat'
}
}
};
return q;
};
/**
* Build the elasticSearch query DSL to match the selected cutom filter
* @param custom {Object} if custom is empty or undefined, an empty string will be returned
* @returns {{match:*}|string}
*/
var buildElasticCustomCriterion = function (custom) {
if (custom) {
const criterion = {
'match': {}
};
switch ($scope.getCustomValueInputType($scope.customFilter.criterion)) {
case 'input_date': criterion.match[custom.key] = moment(custom.value).format('YYYY-MM-DD'); break;
case 'input_select': criterion.match[custom.key] = custom.value.key; break;
case 'input_list': criterion.match[custom.key + '.name'] = custom.value; break;
default: criterion.match[custom.key] = custom.value;
}
return criterion;
} else {
return '';
}
};
/**
* Parse the provided criteria array and return the corresponding elasticSearch syntax
* @param criteria {Array} array of {key_to_sort:order}
*/
var buildElasticSortCriteria = function (criteria) {
const crits = [];
angular.forEach(criteria, function (value, key) {
if ((typeof value !== 'undefined') && (value !== null) && (value !== 'none')) {
const c = {};
c[key] = { 'order': value };
return crits.push(c);
}
});
return 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)
*/
var buildCustomFiltersList = function () {
$scope.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'] }
];
if (!$scope.type.active.simple) {
const f = { key: 'stat', label: $scope.type.active.label, values: ['input_number'] };
$scope.filters.push(f);
}
return angular.forEach($scope.selectedIndex.additional_fields, function (field) {
const filter = { key: field.key, label: field.label, values: [] };
switch (field.data_type) {
case 'index': filter.values.push('input_number'); break;
case 'number': filter.values.push('input_number'); break;
case 'date': filter.values.push('input_date'); break;
case 'list': filter.values.push('input_list'); break;
default: filter.values.push('input_text');
}
return $scope.filters.push(filter);
});
};
/**
* Build and return an object according to the custom filter set by the user, used to request elasticsearch
* @return {Object|null}
*/
var buildCustomFilterQuery = function () {
let custom = null;
if (!angular.isUndefinedOrNull($scope.customFilter.criterion) &&
!angular.isUndefinedOrNull($scope.customFilter.criterion.key) &&
!angular.isUndefinedOrNull($scope.customFilter.value)) {
custom = {};
custom.key = $scope.customFilter.criterion.key;
custom.value = $scope.customFilter.value;
custom.exclude = $scope.customFilter.exclude;
}
return custom;
};
// init the controller (call at the end !)
return initialize();
}
]);
Application.Controllers.controller('ExportStatisticsController', [ '$scope', '$uibModalInstance', 'Export', 'dates', 'query', 'index', 'type', 'CSRF', 'growl', '_t',
function ($scope, $uibModalInstance, Export, dates, query, index, type, CSRF, growl, _t) {
// Retrieve Anti-CSRF tokens from cookies
CSRF.setMetaTags();
// Bindings for date range
$scope.dates = dates;
// Body of the query to export
$scope.query = JSON.stringify(query);
// API URL where the form will be posted
$scope.actionUrl = `/stats/${index.key}/export`;
// Key of the current search' statistic type
$scope.typeKey = type.key;
// Form action on the above URL
$scope.method = 'post';
// Anti-CSRF token to inject into the download form
$scope.csrfToken = angular.element('meta[name="csrf-token"]')[0].content;
// Binding of the export type (global / current)
$scope.export =
{ type: 'current' };
// datePicker parameters for interval beginning
$scope.exportStart = {
format: Fablab.uibDateFormat,
opened: false, // default: datePicker is not shown
minDate: null,
maxDate: moment().subtract(1, 'day').toDate(),
options: {
startingDay: Fablab.weekStartingDay
}
};
// datePicker parameters for interval ending
$scope.exportEnd = {
format: Fablab.uibDateFormat,
opened: false, // default: datePicker is not shown
minDate: null,
maxDate: moment().subtract(1, 'day').toDate(),
options: {
startingDay: Fablab.weekStartingDay
}
};
/**
* Callback to open the datepicker (interval start)
* @param $event {Object} jQuery event object
*/
$scope.toggleStartDatePicker = function ($event) { $scope.exportStart.opened = !$scope.exportStart.opened; };
/**
* Callback to open the datepicker (interval end)
* @param $event {Object} jQuery event object
*/
$scope.toggleEndDatePicker = function ($event) { $scope.exportEnd.opened = !$scope.exportEnd.opened; };
/**
* Callback when exchanging the export type between 'global' and 'current view'
* Adjust the query and the requesting url according to this type.
*/
$scope.setRequest = function () {
if ($scope.export.type === 'global') {
$scope.actionUrl = '/stats/global/export';
return $scope.query = JSON.stringify({
'query': {
'bool': {
'must': [
{
'range': {
'date': {
'gte': moment($scope.dates.start).format(),
'lte': moment($scope.dates.end).format()
}
}
}
]
}
}
});
} else {
$scope.actionUrl = `/stats/${index.key}/export`;
$scope.query = JSON.stringify(query);
}
};
/**
* Callback to close the modal, telling the caller what is exported
*/
$scope.exportData = function () {
const statusQry = { category: 'statistics', type: $scope.export.type, query: $scope.query };
if ($scope.export.type !== 'global') {
statusQry['type'] = index.key;
statusQry['key'] = type.key;
}
Export.status(statusQry).then(function (res) {
if (!res.data.exists) {
return growl.success(_t('export_is_running_you_ll_be_notified_when_its_ready'));
}
});
return $uibModalInstance.close(statusQry);
};
/**
* Callback to cancel the export and close the modal
*/
$scope.cancel = function () { $uibModalInstance.dismiss('cancel'); };
}
]);

View File

@ -1,65 +0,0 @@
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'))
]

View File

@ -0,0 +1,74 @@
/* eslint-disable
handle-callback-err,
no-return-assign,
no-undef,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
Application.Controllers.controller('TagsController', ['$scope', 'tagsPromise', 'Tag', 'growl', '_t', function ($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 = function (rowform, index) {
if ($scope.tags[index].id != null) {
return rowform.$cancel();
} else {
return $scope.tags.splice(index, 1);
}
};
/**
* Creates a new empty entry in the $scope.tags array
*/
$scope.addTag = function () {
$scope.inserted =
{ name: '' };
return $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 = function (data, id) {
if (id != null) {
return Tag.update({ id }, { tag: data }, response => growl.success(_t('changes_successfully_saved'))
, error => growl.error(_t('an_error_occurred_while_saving_changes')));
} else {
return Tag.save({ tag: data }, function (resp) {
growl.success(_t('new_tag_successfully_saved'));
return $scope.tags[$scope.tags.length - 1].id = resp.id;
}
, function (error) {
growl.error(_t('an_error_occurred_while_saving_the_new_tag'));
return $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
*/
return $scope.removeTag = index =>
// TODO add confirmation : les utilisateurs seront déasociés
Tag.delete({ id: $scope.tags[index].id }, function (resp) {
growl.success(_t('tag_successfully_deleted'));
return $scope.tags.splice(index, 1);
}
, error => growl.error(_t('an_error_occurred_and_the_tag_deletion_failed')));
}
]);

View File

@ -1,358 +0,0 @@
'use strict'
### COMMON CODE ###
##
# Provides a set of common callback methods to the $scope parameter. These methods are used
# in the various trainings' admin controllers.
#
# Provides :
# - $scope.submited(content)
# - $scope.fileinputClass(v)
# - $scope.onDisableToggled
#
# Requires :
# - $state (Ui-Router) [ 'app.admin.trainings' ]
# - $scope.training
##
class TrainingsController
constructor: ($scope, $state) ->
##
# For use with ngUpload (https://github.com/twilson63/ngUpload).
# Intended to be the callback when the upload is done: any raised error will be stacked in the
# $scope.alerts array. If everything goes fine, the user is redirected to the trainings list.
# @param content {Object} JSON - The upload's result
##
$scope.submited = (content) ->
if !content.id?
$scope.alerts = []
angular.forEach content, (v, k)->
angular.forEach v, (err)->
$scope.alerts.push
msg: k+': '+err
type: 'danger'
else
$state.go('app.admin.trainings')
##
# Changes the current user's view, redirecting him to the machines list
##
$scope.cancel = ->
$state.go('app.admin.trainings')
##
# Force the 'public_page' attribute to false when the current training is disabled
##
$scope.onDisableToggled = ->
$scope.training.public_page = !$scope.training.disabled
##
# 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 training creation page (admin)
##
Application.Controllers.controller "NewTrainingController", [ '$scope', '$state', 'machinesPromise', 'CSRF'
, ($scope, $state, machinesPromise, CSRF) ->
### PUBLIC SCOPE ###
## Form action on the following URL
$scope.method = 'post'
## API URL where the form will be posted
$scope.actionUrl = '/api/trainings/'
## list of machines
$scope.machines = machinesPromise
### PRIVATE SCOPE ###
##
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
CSRF.setMetaTags()
## Using the TrainingsController
new TrainingsController($scope, $state)
## !!! MUST BE CALLED AT THE END of the controller
initialize()
]
##
# Controller used in the training edition page (admin)
##
Application.Controllers.controller "EditTrainingController", [ '$scope', '$state', '$stateParams', 'trainingPromise', 'machinesPromise', 'CSRF'
, ($scope, $state, $stateParams, trainingPromise, machinesPromise, CSRF) ->
### PUBLIC SCOPE ###
## Form action on the following URL
$scope.method = 'patch'
## API URL where the form will be posted
$scope.actionUrl = '/api/trainings/' + $stateParams.id
## Details of the training to edit (id in URL)
$scope.training = trainingPromise
## list of machines
$scope.machines = machinesPromise
### PRIVATE SCOPE ###
##
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
CSRF.setMetaTags()
## Using the TrainingsController
new TrainingsController($scope, $state)
## !!! MUST BE CALLED AT THE END of the controller
initialize()
]
##
# Controller used in the trainings management page, allowing admins users to see and manage the list of trainings and reservations.
##
Application.Controllers.controller "TrainingsAdminController", ["$scope", "$state", "$uibModal", 'Training', 'trainingsPromise', 'machinesPromise', '_t', 'growl', 'dialogs'
, ($scope, $state, $uibModal, Training, trainingsPromise, machinesPromise, _t, growl, dialogs) ->
### PUBLIC SCOPE ###
## list of trainings
$scope.trainings = trainingsPromise
## simplified list of machines
$scope.machines = machinesPromise
## Training to monitor, binded with drop-down selection
$scope.monitoring =
training: null
## list of training availabilies, grouped by date
$scope.groupedAvailabilities = {}
## default: accordions are not open
$scope.accordions = {}
## Binding for the parseInt function
$scope.parseInt = parseInt
## Default: we show only enabled trainings
$scope.trainingFiltering = 'enabled'
## Available options for filtering trainings by status
$scope.filterDisabled = [
'enabled',
'disabled',
'all',
]
##
# 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')
##
# 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)->
dialogs.confirm
resolve:
object: ->
title: _t('confirmation_required')
msg: _t('do_you_really_want_to_delete_this_training')
, -> # deletion confirmed
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'))
##
# 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')
##
# Callback when the drop-down selection is changed.
# The selected training details will be loaded from the API and rendered into the accordions.
##
$scope.selectTrainingToMonitor = ->
Training.availabilities {id: $scope.monitoring.training.id}, (training) ->
$scope.groupedAvailabilities = groupAvailabilities([training])
# we open current year/month by default
now = moment()
$scope.accordions[training.name] = {}
$scope.accordions[training.name][now.year()] =
isOpenFirst: true
$scope.accordions[training.name][now.year()][now.month()] =
isOpenFirst: true
### PRIVATE SCOPE ###
##
# 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
]

View File

@ -0,0 +1,372 @@
/* eslint-disable
handle-callback-err,
no-return-assign,
no-undef,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
'use strict';
/* COMMON CODE */
/**
* Provides a set of common callback methods to the $scope parameter. These methods are used
* in the various trainings' admin controllers.
*
* Provides :
* - $scope.submited(content)
* - $scope.fileinputClass(v)
* - $scope.onDisableToggled
*
* Requires :
* - $state (Ui-Router) [ 'app.admin.trainings' ]
* - $scope.training
*/
class TrainingsController {
constructor ($scope, $state) {
/*
* For use with ngUpload (https://github.com/twilson63/ngUpload).
* Intended to be the callback when the upload is done: any raised error will be stacked in the
* $scope.alerts array. If everything goes fine, the user is redirected to the trainings list.
* @param content {Object} JSON - The upload's result
*/
$scope.submited = function (content) {
if ((content.id == null)) {
$scope.alerts = [];
return angular.forEach(content, function (v, k) {
angular.forEach(v, function (err) {
$scope.alerts.push({
msg: k + ': ' + err,
type: 'danger'
});
});
});
} else {
return $state.go('app.admin.trainings');
}
};
/**
* Changes the current user's view, redirecting him to the machines list
*/
$scope.cancel = function () { $state.go('app.admin.trainings'); };
/**
* Force the 'public_page' attribute to false when the current training is disabled
*/
$scope.onDisableToggled = function () { $scope.training.public_page = !$scope.training.disabled; };
/**
* 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 = function (v) {
if (v) {
return 'fileinput-exists';
} else {
return 'fileinput-new';
}
};
}
}
/**
* Controller used in the training creation page (admin)
*/
Application.Controllers.controller('NewTrainingController', [ '$scope', '$state', 'machinesPromise', 'CSRF',
function ($scope, $state, machinesPromise, CSRF) {
/* PUBLIC SCOPE */
// Form action on the following URL
$scope.method = 'post';
// API URL where the form will be posted
$scope.actionUrl = '/api/trainings/';
// list of machines
$scope.machines = machinesPromise;
/* PRIVATE SCOPE */
/**
* Kind of constructor: these actions will be realized first when the controller is loaded
*/
const initialize = function () {
CSRF.setMetaTags();
// Using the TrainingsController
return new TrainingsController($scope, $state);
};
// !!! MUST BE CALLED AT THE END of the controller
return initialize();
}
]);
/**
* Controller used in the training edition page (admin)
*/
Application.Controllers.controller('EditTrainingController', [ '$scope', '$state', '$stateParams', 'trainingPromise', 'machinesPromise', 'CSRF',
function ($scope, $state, $stateParams, trainingPromise, machinesPromise, CSRF) {
/* PUBLIC SCOPE */
// Form action on the following URL
$scope.method = 'patch';
// API URL where the form will be posted
$scope.actionUrl = `/api/trainings/${$stateParams.id}`;
// Details of the training to edit (id in URL)
$scope.training = trainingPromise;
// list of machines
$scope.machines = machinesPromise;
/* PRIVATE SCOPE */
/**
* Kind of constructor: these actions will be realized first when the controller is loaded
*/
const initialize = function () {
CSRF.setMetaTags();
// Using the TrainingsController
return new TrainingsController($scope, $state);
};
// !!! MUST BE CALLED AT THE END of the controller
return initialize();
}
]);
/**
* Controller used in the trainings management page, allowing admins users to see and manage the list of trainings and reservations.
*/
Application.Controllers.controller('TrainingsAdminController', ['$scope', '$state', '$uibModal', 'Training', 'trainingsPromise', 'machinesPromise', '_t', 'growl', 'dialogs',
function ($scope, $state, $uibModal, Training, trainingsPromise, machinesPromise, _t, growl, dialogs) {
/* PUBLIC SCOPE */
// list of trainings
let groupAvailabilities;
$scope.trainings = trainingsPromise;
// simplified list of machines
$scope.machines = machinesPromise;
// Training to monitor, binded with drop-down selection
$scope.monitoring =
{ training: null };
// list of training availabilies, grouped by date
$scope.groupedAvailabilities = {};
// default: accordions are not open
$scope.accordions = {};
// Binding for the parseInt function
$scope.parseInt = parseInt;
// Default: we show only enabled trainings
$scope.trainingFiltering = 'enabled';
// Available options for filtering trainings by status
$scope.filterDisabled = [
'enabled',
'disabled',
'all'
];
/**
* 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 = function (training) {
const selected = [];
angular.forEach($scope.machines, function (m) {
if (training.machine_ids.indexOf(m.id) >= 0) {
return selected.push(m.name);
}
});
if (selected.length) { return selected.join(', '); } else { return _t('none'); }
};
/**
* 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 = function (rowform, index) {
if ($scope.trainings[index].id != null) {
return rowform.$cancel();
} else {
return $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 = function (training, availability) {
$uibModal.open({
templateUrl: '<%= asset_path "admin/trainings/validTrainingModal.html" %>',
controller: ['$scope', '$uibModalInstance', function ($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 = function (user) {
const index = $scope.usersToValid.indexOf(user);
if (index > -1) {
return $scope.usersToValid.splice(index, 1);
} else {
return $scope.usersToValid.push(user);
}
};
/**
* Validates the modifications (training validations) and save them to the server
*/
$scope.ok = function () {
const users = $scope.usersToValid.map(function (u) { return u.id; });
return Training.update({ id: training.id }, {
training: {
users
}
}
, function () { // success
angular.forEach($scope.usersToValid, function (u) { u.is_valid = true; });
$scope.usersToValid = [];
return $uibModalInstance.close(training);
});
};
/**
* Cancel the modifications and close the modal window
*/
return $scope.cancel = function () { $uibModalInstance.dismiss('cancel'); };
}
] });
};
/**
* Delete the provided training and, in case of success, 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 = function (index, training) {
dialogs.confirm(
{
resolve: {
object () {
return {
title: _t('confirmation_required'),
msg: _t('do_you_really_want_to_delete_this_training')
};
}
}
},
function () { // deletion confirmed
training.$delete(function () {
$scope.trainings.splice(index, 1);
growl.info(_t('training_successfully_deleted'));
},
function (error) {
growl.warning(_t('unable_to_delete_the_training_because_some_users_alredy_booked_it'));
console.error(error);
});
}
);
};
/**
* Takes a month number and return its localized literal name
* @param number {Number} from 0 to 11
* @returns {String} eg. 'janvier'
*/
$scope.formatMonth = function (number) {
number = parseInt(number);
return 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 = function (day, month, year) {
day = parseInt(day);
month = parseInt(month);
year = parseInt(year);
return moment({ year, month, day }).format('dddd D');
};
/**
* Callback when the drop-down selection is changed.
* The selected training details will be loaded from the API and rendered into the accordions.
*/
$scope.selectTrainingToMonitor = function () {
Training.availabilities({ id: $scope.monitoring.training.id }, function (training) {
$scope.groupedAvailabilities = groupAvailabilities([training]);
// we open current year/month by default
const now = moment();
$scope.accordions[training.name] = {};
$scope.accordions[training.name][now.year()] = { isOpenFirst: true };
$scope.accordions[training.name][now.year()][now.month()] = { isOpenFirst: true };
});
};
/* PRIVATE SCOPE */
/**
* Group the trainings availabilities 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]
*/
return groupAvailabilities = function (trainings) {
const tree = {};
for (let training of Array.from(trainings)) {
tree[training.name] = {};
tree[training.name].training = training;
for (let availability of Array.from(training.availabilities)) {
const 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);
}
}
return tree;
};
}
]);

View File

@ -1,426 +0,0 @@
'use strict'
Application.Controllers.controller 'ApplicationController', ["$rootScope", "$scope", "$window", '$locale', "Session", "AuthService", "Auth", "$uibModal", "$state", 'growl', 'Notification', '$interval', "Setting", '_t', 'Version'
, ($rootScope, $scope, $window, $locale, Session, AuthService, Auth, $uibModal, $state, growl, Notification, $interval, Setting, _t, Version) ->
### PRIVATE STATIC CONSTANTS ###
# User's notifications will get refreshed every 30s
NOTIFICATIONS_CHECK_PERIOD = 30000
### PUBLIC SCOPE ###
## Fab-manager's version
$scope.version =
version: ''
## currency symbol for the current locale (cf. angular-i18n)
$rootScope.currencySymbol = $locale.NUMBER_FORMATS.CURRENCY_SYM;
##
# Set the current user to the provided value and initialize the session
# @param user {Object} Rails/Devise user
##
$scope.setCurrentUser = (user) ->
unless angular.isUndefinedOrNull(user)
$rootScope.currentUser = user
Session.create(user);
getNotifications()
# fab-manager's app-version
if user.role == 'admin'
$scope.version = Version.get()
else
$scope.version = {version: ''}
##
# Login callback
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
# @param callback {function}
##
$scope.login = (e, callback) ->
e.preventDefault() if e
openLoginModal null, null, callback
##
# Logout callback
# @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()
$rootScope.currentUser = null
$rootScope.toCheckNotifications = false
$scope.notifications =
total: 0
unread: 0
$state.go('app.public.home')
, (error) ->
# An error occurred logging out.
##
# Open the modal window allowing the user to create an account.
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
##
$scope.signup = (e) ->
e.preventDefault() if e
$uibModal.open
templateUrl: '<%= asset_path "shared/signupModal.html" %>'
size: 'md'
controller: ['$scope', '$uibModalInstance', 'Group', 'CustomAsset', ($scope, $uibModalInstance, Group, CustomAsset) ->
# default parameters for the date picker in the account creation modal
$scope.datePicker =
format: Fablab.uibDateFormat
opened: false
options:
startingDay: Fablab.weekStartingDay
# callback to open the date picker (account creation modal)
$scope.openDatePicker = ($event) ->
$event.preventDefault()
$event.stopPropagation()
$scope.datePicker.opened = true
# retrieve the groups (standard, student ...)
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
is_allow_newsletter: false
# Errors display
$scope.alerts = []
$scope.closeAlert = (index) ->
$scope.alerts.splice(index, 1)
# callback for form validation
$scope.ok = ->
# try to create the account
$scope.alerts = []
# remove 'organization' attribute
orga = $scope.user.organization
delete $scope.user.organization
# register on server
Auth.register($scope.user).then (user) ->
# creation successful
$uibModalInstance.close(user)
, (error) ->
# creation failed...
# restore organization param
$scope.user.organization = orga
# display errors
angular.forEach error.data.errors, (v, k) ->
angular.forEach v, (err) ->
$scope.alerts.push
msg: k+': '+err
type: 'danger'
]
.result['finally'](null).then (user) ->
# when the account was created succesfully, set the session to the newly created account
$scope.setCurrentUser(user)
##
# Open the modal window allowing the user to change his password.
# @param token {string} security token for password changing. The user should have recieved it by mail
##
$scope.editPassword = (token) ->
$uibModal.open
templateUrl: '<%= asset_path "shared/passwordEditModal.html" %>'
size: 'md'
controller: ['$scope', '$uibModalInstance', '$http', '_t', ($scope, $uibModalInstance, $http, _t) ->
$scope.user =
reset_password_token: token
$scope.alerts = []
$scope.closeAlert = (index) ->
$scope.alerts.splice(index, 1)
$scope.changePassword = ->
$scope.alerts = []
$http.put('/users/password.json', {user: $scope.user}).success (data) ->
$uibModalInstance.close()
.error (data) ->
angular.forEach data.errors, (v, k) ->
angular.forEach v, (err) ->
$scope.alerts.push
msg: k+': '+err
type: 'danger'
]
.result['finally'](null).then (user) ->
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} see https://docs.angularjs.org/guide/expression#-event-
##
$scope.toggleNavSize = (event) ->
if typeof event == 'undefined'
console.error '[ApplicationController::toggleNavSize] Missing event parameter'
return
toggler = $(event.target)
toggler = toggler.closest('[data-toggle^="class"]') unless toggler.data('toggle')
$class = toggler.data()['toggle']
$target = toggler.data('target') or toggler.attr('data-link')
if $class
$tmp = $class.split(':')[1]
$classes = $tmp.split(',') if $tmp
if $target
$targets = $target.split(',')
if $classes and $classes.length
$.each $targets, ( index, value ) ->
if $classes[index].indexOf( '*' ) != -1
patt = new RegExp( '\\s'
+ $classes[index].replace( /\*/g, '[A-Za-z0-9-_]+' ).split( ' ' ).join( '\\s|\\s' )
+ '\\s', 'g' )
$(toggler).each ( i, it ) ->
cn = ' ' + it.className + ' '
while patt.test( cn )
cn = cn.replace( patt, ' ' )
it.className = $.trim( cn )
($targets[index] !='#') and $($targets[index]).toggleClass($classes[index]) or toggler.toggleClass($classes[index])
toggler.toggleClass('active')
return
### 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)
# force users to complete their profile if they are not
if user.need_completion
$state.transitionTo('app.logged.profileCompletion')
, (error) ->
# Authentication failed...
$rootScope.toCheckNotifications = false
# bind to the $stateChangeStart event (AngularJS/UI-Router)
$rootScope.$on '$stateChangeStart', (event, toState, toParams, fromState, fromParams) ->
return unless toState.data
authorizedRoles = toState.data.authorizedRoles
unless AuthService.isAuthorized(authorizedRoles)
event.preventDefault()
if AuthService.isAuthenticated()
# user is not allowed
console.error('[ApplicationController::initialize] user is not allowed')
else
# user is not logged in
openLoginModal(toState, toParams)
# we stop polling notifications when the page is not in foreground
onPageVisible (state) ->
$rootScope.toCheckNotifications = (state is 'visible')
Setting.get { name: 'fablab_name' }, (data)->
$scope.fablabName = data.setting.value
Setting.get { name: 'name_genre' }, (data)->
$scope.nameGenre = data.setting.value
# shorthands
$scope.isAuthenticated = Auth.isAuthenticated
$scope.isAuthorized = AuthService.isAuthorized
$rootScope.login = $scope.login
##
# Retreive once the notifications from the server and display a message popup for each new one.
# Then, periodically check for new notifications.
##
getNotifications = ->
$rootScope.toCheckNotifications = true
unless $rootScope.checkNotificationsIsInit or !$rootScope.currentUser
setTimeout ->
# we request the most recent notifications
Notification.last_unread (notifications) ->
$rootScope.lastCheck = new Date()
$scope.notifications = notifications.totals
toDisplay = []
angular.forEach notifications.notifications, (n) ->
toDisplay.push(n)
if toDisplay.length < notifications.totals.unread
toDisplay.push({message: {description: _t('and_NUMBER_other_notifications', {NUMBER: notifications.totals.unread - toDisplay.length}, "messageformat")}})
angular.forEach toDisplay, (notification) ->
growl.info(notification.message.description)
, 2000
checkNotifications = ->
if $rootScope.toCheckNotifications
Notification.polling({last_poll: $rootScope.lastCheck}).$promise.then (data) ->
$rootScope.lastCheck = new Date()
$scope.notifications = data.totals
angular.forEach data.notifications, (notification) ->
growl.info(notification.message.description)
$interval(checkNotifications, NOTIFICATIONS_CHECK_PERIOD)
$rootScope.checkNotificationsIsInit = true
##
# Open the modal window allowing the user to log in.
##
openLoginModal = (toState, toParams, callback) ->
<% 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', '$uibModalInstance', '_t', ($scope, $uibModalInstance, _t) ->
user = $scope.user = {}
$scope.login = () ->
Auth.login(user).then (user) ->
# Authentification succeeded ...
$uibModalInstance.close(user)
if callback and typeof callback is "function"
callback(user)
, (error) ->
# Authentication failed...
$scope.alerts = []
$scope.alerts.push
msg: _t('wrong_email_or_password')
type: 'danger'
# handle modal behaviors. The provided reason will be used to define the following actions
$scope.dismiss = ->
$uibModalInstance.dismiss('cancel')
$scope.openSignup = (e) ->
e.preventDefault()
$uibModalInstance.dismiss('signup')
$scope.openResetPassword = (e) ->
e.preventDefault()
$uibModalInstance.dismiss('resetPassword')
]
# what to do when the modal is closed
.result['finally'](null).then (user) ->
# authentification succeeded, set the session, gather the notifications and redirect
$scope.setCurrentUser(user)
if toState isnt null and toParams isnt null
$state.go(toState, toParams)
, (reason) ->
# authentification did not ended successfully
if reason is 'signup'
# open signup modal
$scope.signup()
else if reason is 'resetPassword'
# open the 'reset password' modal
$uibModal.open
templateUrl: '<%= asset_path "shared/passwordNewModal.html" %>'
size: 'sm'
controller: ['$scope', '$uibModalInstance', '$http', ($scope, $uibModalInstance, $http) ->
$scope.user = {email: ''}
$scope.sendReset = () ->
$scope.alerts = []
$http.post('/users/password.json', {user: $scope.user}).success ->
$uibModalInstance.close()
.error ->
$scope.alerts.push
msg: _t('your_email_address_is_unknown')
type: 'danger'
]
.result['finally'](null).then ->
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 %>
##
# Detect if the current page (tab/window) is active of put as background.
# When the status changes, the callback is triggered with the new status as parameter
# Inspired by http://stackoverflow.com/questions/1060008/is-there-a-way-to-detect-if-a-browser-window-is-not-currently-active#answer-1060034
##
onPageVisible = (callback) ->
hidden = 'hidden'
onchange = (evt) ->
v = 'visible'
h = 'hidden'
evtMap =
focus: v
focusin: v
pageshow: v
blur: h
focusout: h
pagehide: h
evt = evt or window.event
if evt.type of evtMap
if typeof callback == 'function' then callback(evtMap[evt.type])
else
if typeof callback == 'function' then callback(if @[hidden] then 'hidden' else 'visible')
return
# Standards:
if hidden of document
document.addEventListener 'visibilitychange', onchange
else if (hidden = 'mozHidden') of document
document.addEventListener 'mozvisibilitychange', onchange
else if (hidden = 'webkitHidden') of document
document.addEventListener 'webkitvisibilitychange', onchange
else if (hidden = 'msHidden') of document
document.addEventListener 'msvisibilitychange', onchange
# IE 9 and lower
else if 'onfocusin' of document
document.onfocusin = document.onfocusout = onchange
# All others
else
window.onpageshow = window.onpagehide = window.onfocus = window.onblur = onchange
# set the initial state (but only if browser supports the Page Visibility API)
if document[hidden] != undefined
onchange type: if document[hidden] then 'blur' else 'focus'
## !!! MUST BE CALLED AT THE END of the controller
initialize()
]

View File

@ -0,0 +1,469 @@
/* eslint-disable
handle-callback-err,
no-return-assign,
no-undef,
standard/no-callback-literal,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
Application.Controllers.controller('ApplicationController', ['$rootScope', '$scope', '$window', '$locale', 'Session', 'AuthService', 'Auth', '$uibModal', '$state', 'growl', 'Notification', '$interval', 'Setting', '_t', 'Version',
function ($rootScope, $scope, $window, $locale, Session, AuthService, Auth, $uibModal, $state, growl, Notification, $interval, Setting, _t, Version) {
/* PRIVATE STATIC CONSTANTS */
// User's notifications will get refreshed every 30s
const NOTIFICATIONS_CHECK_PERIOD = 30000;
/* PUBLIC SCOPE */
// Fab-manager's version
$scope.version =
{ version: '' };
// currency symbol for the current locale (cf. angular-i18n)
$rootScope.currencySymbol = $locale.NUMBER_FORMATS.CURRENCY_SYM;
/**
* Set the current user to the provided value and initialize the session
* @param user {Object} Rails/Devise user
*/
$scope.setCurrentUser = function (user) {
if (!angular.isUndefinedOrNull(user)) {
$rootScope.currentUser = user;
Session.create(user);
getNotifications();
// fab-manager's app-version
if (user.role === 'admin') {
return $scope.version = Version.get();
} else {
return $scope.version = { version: '' };
}
}
};
/**
* Login callback
* @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
* @param callback {function}
*/
$scope.login = function (e, callback) {
if (e) { e.preventDefault(); }
return openLoginModal(null, null, callback);
};
/**
* Logout callback
* @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
*/
$scope.logout = function (e) {
e.preventDefault();
return Auth.logout().then(function () {
Session.destroy();
$rootScope.currentUser = null;
$rootScope.toCheckNotifications = false;
$scope.notifications = {
total: 0,
unread: 0
};
return $state.go('app.public.home');
}, function (error) {
console.error(`An error occurred logging out: ${error}`);
});
};
/**
* Open the modal window allowing the user to create an account.
* @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
*/
$scope.signup = function (e) {
if (e) { e.preventDefault(); }
return $uibModal.open({
templateUrl: '<%= asset_path "shared/signupModal.html" %>',
size: 'md',
controller: ['$scope', '$uibModalInstance', 'Group', 'CustomAsset', function ($scope, $uibModalInstance, Group, CustomAsset) {
// default parameters for the date picker in the account creation modal
$scope.datePicker = {
format: Fablab.uibDateFormat,
opened: false,
options: {
startingDay: Fablab.weekStartingDay
}
};
// callback to open the date picker (account creation modal)
$scope.openDatePicker = function ($event) {
$event.preventDefault();
$event.stopPropagation();
return $scope.datePicker.opened = true;
};
// retrieve the groups (standard, student ...)
Group.query(function (groups) {
$scope.groups = groups;
});
// retrieve the CGU
CustomAsset.get({ name: 'cgu-file' }, function (cgu) {
$scope.cgu = cgu.custom_asset;
});
// default user's parameters
$scope.user = {
is_allow_contact: true,
is_allow_newsletter: false
};
// Errors display
$scope.alerts = [];
$scope.closeAlert = function (index) {
$scope.alerts.splice(index, 1);
};
// callback for form validation
$scope.ok = function () {
// try to create the account
$scope.alerts = [];
// remove 'organization' attribute
const orga = $scope.user.organization;
delete $scope.user.organization;
// register on server
return Auth.register($scope.user).then(function (user) {
// creation successful
$uibModalInstance.close(user);
}, function (error) {
// creation failed...
// restore organization param
$scope.user.organization = orga;
// display errors
angular.forEach(error.data.errors, function (v, k) {
angular.forEach(function (v, err) {
$scope.alerts.push({
msg: k + ': ' + err,
type: 'danger'
});
});
});
});
};
}]
}).result['finally'](null).then(function (user) {
// when the account was created successfully, set the session to the newly created account
$scope.setCurrentUser(user);
});
};
/**
* Open the modal window allowing the user to change his password.
* @param token {string} security token for password changing. The user should have recieved it by mail
*/
$scope.editPassword = function (token) {
$uibModal.open({
templateUrl: '<%= asset_path "shared/passwordEditModal.html" %>',
size: 'md',
controller: ['$scope', '$uibModalInstance', '$http', function ($scope, $uibModalInstance, $http) {
$scope.user = { reset_password_token: token };
$scope.alerts = [];
$scope.closeAlert = function (index) {
$scope.alerts.splice(index, 1);
};
return $scope.changePassword = function () {
$scope.alerts = [];
return $http.put('/users/password.json', { user: $scope.user }).success(function () { $uibModalInstance.close(); }).error(function (data) {
angular.forEach(data.errors, function (v, k) {
angular.forEach(function (v, err) {
$scope.alerts.push({
msg: k + ': ' + err,
type: 'danger'
});
});
});
});
};
}]
}).result['finally'](null).then(function () {
growl.success(_t('your_password_was_successfully_changed'));
return Auth.login().then(function (user) {
$scope.setCurrentUser(user);
}, function (error) {
console.error(`Authentication failed: ${error}`);
}
);
});
};
/**
* Compact/Expend the width of the left navigation bar
* @param event {Object} see https://docs.angularjs.org/guide/expression#-event-
*/
$scope.toggleNavSize = function (event) {
let $classes, $targets;
if (typeof event === 'undefined') {
console.error('[ApplicationController::toggleNavSize] Missing event parameter');
return;
}
let toggler = $(event.target);
if (!toggler.data('toggle')) { toggler = toggler.closest('[data-toggle^="class"]'); }
const $class = toggler.data()['toggle'];
const $target = toggler.data('target') || toggler.attr('data-link');
if ($class) {
const $tmp = $class.split(':')[1];
if ($tmp) { $classes = $tmp.split(','); }
}
if ($target) {
$targets = $target.split(',');
}
if ($classes && $classes.length) {
$.each($targets, function (index) {
if ($classes[index].indexOf('*') !== -1) {
const patt = new RegExp('\\s',
+$classes[index].replace(/\*/g, '[A-Za-z0-9-_]+').split(' ').join('\\s|\\s'),
+'\\s', 'g');
$(toggler).each(function (i, it) {
let cn = ` ${it.className} `;
while (patt.test(cn)) {
cn = cn.replace(patt, ' ');
}
return it.className = $.trim(cn);
});
}
return (($targets[index] !== '#') && $($targets[index]).toggleClass($classes[index])) || toggler.toggleClass($classes[index]);
});
}
toggler.toggleClass('active');
};
/* PRIVATE SCOPE */
/**
* Kind of constructor: these actions will be realized first when the controller is loaded
*/
const initialize = function () {
// try to retrieve any currently logged user
Auth.login().then(function (user) {
$scope.setCurrentUser(user);
// force users to complete their profile if they are not
if (user.need_completion) {
return $state.transitionTo('app.logged.profileCompletion');
}
}, function (error) {
console.error(`Authentication failed: ${error}`);
$rootScope.toCheckNotifications = false;
});
// bind to the $stateChangeStart event (AngularJS/UI-Router)
$rootScope.$on('$stateChangeStart', function (event, toState, toParams) {
if (!toState.data) { return; }
const { authorizedRoles } = toState.data;
if (!AuthService.isAuthorized(authorizedRoles)) {
event.preventDefault();
if (AuthService.isAuthenticated()) {
// user is not allowed
console.error('[ApplicationController::initialize] user is not allowed');
} else {
// user is not logged in
openLoginModal(toState, toParams);
}
}
});
// we stop polling notifications when the page is not in foreground
onPageVisible(function (state) { $rootScope.toCheckNotifications = (state === 'visible'); });
Setting.get({ name: 'fablab_name' }, function (data) { $scope.fablabName = data.setting.value; });
Setting.get({ name: 'name_genre' }, function (data) { $scope.nameGenre = data.setting.value; });
// shorthands
$scope.isAuthenticated = Auth.isAuthenticated;
$scope.isAuthorized = AuthService.isAuthorized;
return $rootScope.login = $scope.login;
};
/**
* Retreive once the notifications from the server and display a message popup for each new one.
* Then, periodically check for new notifications.
*/
var getNotifications = function () {
$rootScope.toCheckNotifications = true;
if (!$rootScope.checkNotificationsIsInit && !!$rootScope.currentUser) {
setTimeout(function () {
// we request the most recent notifications
Notification.last_unread(function (notifications) {
$rootScope.lastCheck = new Date();
$scope.notifications = notifications.totals;
const toDisplay = [];
angular.forEach(notifications.notifications, function (n) { toDisplay.push(n); });
if (toDisplay.length < notifications.totals.unread) {
toDisplay.push({ message: { description: _t('and_NUMBER_other_notifications', { NUMBER: notifications.totals.unread - toDisplay.length }, 'messageformat') } });
}
angular.forEach(toDisplay, function (notification) { growl.info(notification.message.description); });
});
}, 2000);
const checkNotifications = function () {
if ($rootScope.toCheckNotifications) {
return Notification.polling({ last_poll: $rootScope.lastCheck }).$promise.then(function (data) {
$rootScope.lastCheck = new Date();
$scope.notifications = data.totals;
angular.forEach(data.notifications, function (notification) { growl.info(notification.message.description); });
});
}
};
$interval(checkNotifications, NOTIFICATIONS_CHECK_PERIOD);
$rootScope.checkNotificationsIsInit = true;
}
};
/**
* Open the modal window allowing the user to log in.
*/
var openLoginModal = function (toState, toParams, callback) {
<% 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 %>
return $uibModal.open({
templateUrl: '<%= asset_path "shared/deviseModal.html" %>',
size: 'sm',
controller: ['$scope', '$uibModalInstance', '_t', function ($scope, $uibModalInstance, _t) {
const user = ($scope.user = {});
$scope.login = function () {
Auth.login(user).then(function (user) {
// Authentication succeeded ...
$uibModalInstance.close(user);
if (callback && (typeof callback === 'function')) {
return callback(user);
}
}
, function (error) {
console.error(`Authentication failed: ${error}`);
$scope.alerts = [];
return $scope.alerts.push({
msg: _t('wrong_email_or_password'),
type: 'danger'
});
});
};
// handle modal behaviors. The provided reason will be used to define the following actions
$scope.dismiss = function () { $uibModalInstance.dismiss('cancel'); };
$scope.openSignup = function (e) {
e.preventDefault();
return $uibModalInstance.dismiss('signup');
};
return $scope.openResetPassword = function (e) {
e.preventDefault();
return $uibModalInstance.dismiss('resetPassword');
};
}]
}).result['finally'](null).then(function (user) {
// what to do when the modal is closed
// authentication succeeded, set the session, gather the notifications and redirect
$scope.setCurrentUser(user);
if ((toState !== null) && (toParams !== null)) {
return $state.go(toState, toParams);
}
}, function (reason) {
// authentication did not ended successfully
if (reason === 'signup') {
// open signup modal
$scope.signup();
} else if (reason === 'resetPassword') {
// open the 'reset password' modal
return $uibModal.open({
templateUrl: '<%= asset_path "shared/passwordNewModal.html" %>',
size: 'sm',
controller: ['$scope', '$uibModalInstance', '$http', function ($scope, $uibModalInstance, $http) {
$scope.user = { email: '' };
return $scope.sendReset = function () {
$scope.alerts = [];
return $http.post('/users/password.json', { user: $scope.user }).success(function () { $uibModalInstance.close(); }).error(function () {
$scope.alerts.push({
msg: _t('your_email_address_is_unknown'),
type: 'danger'
});
});
};
}]
}).result['finally'](null).then(function () { 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 %>
/**
* Detect if the current page (tab/window) is active of put as background.
* When the status changes, the callback is triggered with the new status as parameter
* Inspired by http://stackoverflow.com/questions/1060008/is-there-a-way-to-detect-if-a-browser-window-is-not-currently-active#answer-1060034
*/
var onPageVisible = function (callback) {
let hidden = 'hidden';
const onchange = function (evt) {
const v = 'visible';
const h = 'hidden';
const evtMap = {
focus: v,
focusin: v,
pageshow: v,
blur: h,
focusout: h,
pagehide: h
};
evt = evt || window.event;
if (evt.type in evtMap) {
if (typeof callback === 'function') { callback(evtMap[evt.type]); }
} else {
if (typeof callback === 'function') { callback(this[hidden] ? 'hidden' : 'visible'); }
}
};
// Standards:
if (hidden in document) {
document.addEventListener('visibilitychange', onchange);
} else if ((hidden = 'mozHidden') in document) {
document.addEventListener('mozvisibilitychange', onchange);
} else if ((hidden = 'webkitHidden') in document) {
document.addEventListener('webkitvisibilitychange', onchange);
} else if ((hidden = 'msHidden') in document) {
document.addEventListener('msvisibilitychange', onchange);
// IE 9 and lower
} else if ('onfocusin' in document) {
document.onfocusin = (document.onfocusout = onchange);
// All others
} else {
window.onpageshow = (window.onpagehide = (window.onfocus = (window.onblur = onchange)));
}
// set the initial state (but only if browser supports the Page Visibility API)
if (document[hidden] !== undefined) {
return onchange({ type: document[hidden] ? 'blur' : 'focus' });
}
};
// !!! MUST BE CALLED AT THE END of the controller
return initialize();
}
]);

View File

@ -1,184 +0,0 @@
'use strict'
##
# Controller used in the public calendar global
##
Application.Controllers.controller "CalendarController", ["$scope", "$state", "$aside", "moment", "Availability", 'Slot', 'Setting', 'growl', 'dialogs', 'bookingWindowStart', 'bookingWindowEnd', '_t', 'uiCalendarConfig', 'CalendarConfig', 'trainingsPromise', 'machinesPromise', 'spacesPromise',
($scope, $state, $aside, moment, Availability, Slot, Setting, growl, dialogs, bookingWindowStart, bookingWindowEnd, _t, uiCalendarConfig, CalendarConfig, trainingsPromise, machinesPromise, spacesPromise) ->
### PRIVATE STATIC CONSTANTS ###
currentMachineEvent = null
machinesPromise.forEach((m) -> m.checked = true)
trainingsPromise.forEach((t) -> t.checked = true)
spacesPromise.forEach((s) -> s.checked = true)
## check all formation/machine is select in filter
isSelectAll = (type, scope) ->
scope[type].length == scope[type].filter((t) -> t.checked).length
### PUBLIC SCOPE ###
## List of trainings
$scope.trainings = trainingsPromise.filter (t) -> !t.disabled
## List of machines
$scope.machines = machinesPromise.filter (t) -> !t.disabled
## List of spaces
$scope.spaces = spacesPromise.filter (t) -> !t.disabled
## add availabilities source to event sources
$scope.eventSources = []
## filter availabilities if have change
$scope.filterAvailabilities = (filter, scope) ->
scope ||= $scope
scope.filter = $scope.filter =
trainings: isSelectAll('trainings', scope)
machines: isSelectAll('machines', scope)
spaces: isSelectAll('spaces', scope)
evt: filter.evt
dispo: filter.dispo
$scope.calendarConfig.events = availabilitySourceUrl()
## a variable for formation/machine/event/dispo checkbox is or not checked
$scope.filter =
trainings: isSelectAll('trainings', $scope)
machines: isSelectAll('machines', $scope)
spaces: isSelectAll('spaces', $scope)
evt: true
dispo: true
## toggle to select all formation/machine
$scope.toggleFilter = (type, filter) ->
$scope[type].forEach((t) -> t.checked = filter[type])
$scope.filterAvailabilities(filter, $scope)
$scope.openFilterAside = ->
$aside.open
templateUrl: 'filterAside.html'
placement: 'right'
size: 'md'
backdrop: false
resolve:
trainings: ->
$scope.trainings
machines: ->
$scope.machines
spaces: ->
$scope.spaces
filter: ->
$scope.filter
toggleFilter: ->
$scope.toggleFilter
filterAvailabilities: ->
$scope.filterAvailabilities
controller: ['$scope', '$uibModalInstance', 'trainings', 'machines', 'spaces', 'filter', 'toggleFilter', 'filterAvailabilities', ($scope, $uibModalInstance, trainings, machines, spaces, filter, toggleFilter, filterAvailabilities) ->
$scope.trainings = trainings
$scope.machines = machines
$scope.spaces = spaces
$scope.filter = filter
$scope.toggleFilter = (type, filter) ->
toggleFilter(type, filter)
$scope.filterAvailabilities = (filter) ->
filterAvailabilities(filter, $scope)
$scope.close = (e) ->
$uibModalInstance.dismiss()
e.stopPropagation()
]
### PRIVATE SCOPE ###
calendarEventClickCb = (event, jsEvent, view) ->
## current calendar object
calendar = uiCalendarConfig.calendars.calendar
if event.available_type == 'machines'
currentMachineEvent = event
calendar.fullCalendar('changeView', 'agendaDay')
calendar.fullCalendar('gotoDate', event.start)
else if event.available_type == 'space'
calendar.fullCalendar('changeView', 'agendaDay')
calendar.fullCalendar('gotoDate', event.start)
else if event.available_type == 'event'
$state.go('app.public.events_show', {id: event.event_id})
else if event.available_type == 'training'
$state.go('app.public.training_show', {id: event.training_id})
else
if event.machine_id
$state.go('app.public.machines_show', {id: event.machine_id})
else if event.space_id
$state.go('app.public.space_show', {id: event.space_id})
## agendaDay view: disable slotEventOverlap
## agendaWeek view: enable slotEventOverlap
toggleSlotEventOverlap = (view) ->
# set defaultView, because when we change slotEventOverlap
# ui-calendar will trigger rerender calendar
$scope.calendarConfig.defaultView = view.type
today = if currentMachineEvent then currentMachineEvent.start else moment().utc().startOf('day')
if today > view.intervalStart and today < view.intervalEnd and today != view.intervalStart
$scope.calendarConfig.defaultDate = today
else
$scope.calendarConfig.defaultDate = view.intervalStart
if view.type == 'agendaDay'
$scope.calendarConfig.slotEventOverlap = false
else
$scope.calendarConfig.slotEventOverlap = true
## function is called when calendar view is rendered or changed
viewRenderCb = (view, element) ->
toggleSlotEventOverlap(view)
if view.type == 'agendaDay'
# get availabilties by 1 day for show machine slots
uiCalendarConfig.calendars.calendar.fullCalendar('refetchEvents')
eventRenderCb = (event, element) ->
if event.tags.length > 0
html = ''
for tag in event.tags
html += "<span class='label label-success text-white'>#{tag.name}</span> "
element.find('.fc-title').append("<br/>"+html)
return
getFilter = ->
t = $scope.trainings.filter((t) -> t.checked).map((t) -> t.id)
m = $scope.machines.filter((m) -> m.checked).map((m) -> m.id)
s = $scope.spaces.filter((s) -> s.checked).map((s) -> s.id)
{t: t, m: m, s: s, evt: $scope.filter.evt, dispo: $scope.filter.dispo}
availabilitySourceUrl = ->
"/api/availabilities/public?#{$.param(getFilter())}"
initialize = ->
## fullCalendar (v2) configuration
$scope.calendarConfig = CalendarConfig
events: availabilitySourceUrl()
slotEventOverlap: true
header:
left: 'month agendaWeek agendaDay'
center: 'title'
right: 'today prev,next'
minTime: moment.duration(moment(bookingWindowStart.setting.value).format('HH:mm:ss'))
maxTime: moment.duration(moment(bookingWindowEnd.setting.value).format('HH:mm:ss'))
defaultView: if window.innerWidth <= 480 then 'agendaDay' else 'agendaWeek'
eventClick: (event, jsEvent, view)->
calendarEventClickCb(event, jsEvent, view)
viewRender: (view, element) ->
viewRenderCb(view, element)
eventRender: (event, element, view) ->
eventRenderCb(event, element)
## !!! MUST BE CALLED AT THE END of the controller
initialize()
]

View File

@ -0,0 +1,214 @@
/* eslint-disable
no-return-assign,
no-undef,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
'use strict';
/**
* Controller used in the public calendar global
*/
Application.Controllers.controller('CalendarController', ['$scope', '$state', '$aside', 'moment', 'Availability', 'Slot', 'Setting', 'growl', 'dialogs', 'bookingWindowStart', 'bookingWindowEnd', '_t', 'uiCalendarConfig', 'CalendarConfig', 'trainingsPromise', 'machinesPromise', 'spacesPromise',
function ($scope, $state, $aside, moment, Availability, Slot, Setting, growl, dialogs, bookingWindowStart, bookingWindowEnd, _t, uiCalendarConfig, CalendarConfig, trainingsPromise, machinesPromise, spacesPromise) {
/* PRIVATE STATIC CONSTANTS */
let currentMachineEvent = null;
machinesPromise.forEach(m => m.checked = true);
trainingsPromise.forEach(t => t.checked = true);
spacesPromise.forEach(s => s.checked = true);
// check all formation/machine is select in filter
const isSelectAll = (type, scope) => scope[type].length === scope[type].filter(t => t.checked).length;
/* PUBLIC SCOPE */
// List of trainings
$scope.trainings = trainingsPromise.filter(t => !t.disabled);
// List of machines
$scope.machines = machinesPromise.filter(t => !t.disabled);
// List of spaces
$scope.spaces = spacesPromise.filter(t => !t.disabled);
// add availabilities source to event sources
$scope.eventSources = [];
// filter availabilities if have change
$scope.filterAvailabilities = function (filter, scope) {
if (!scope) { scope = $scope; }
scope.filter = ($scope.filter = {
trainings: isSelectAll('trainings', scope),
machines: isSelectAll('machines', scope),
spaces: isSelectAll('spaces', scope),
evt: filter.evt,
dispo: filter.dispo
});
return $scope.calendarConfig.events = availabilitySourceUrl();
};
// a variable for formation/machine/event/dispo checkbox is or not checked
$scope.filter = {
trainings: isSelectAll('trainings', $scope),
machines: isSelectAll('machines', $scope),
spaces: isSelectAll('spaces', $scope),
evt: true,
dispo: true
};
// toggle to select all formation/machine
$scope.toggleFilter = function (type, filter) {
$scope[type].forEach(t => t.checked = filter[type]);
return $scope.filterAvailabilities(filter, $scope);
};
$scope.openFilterAside = () =>
$aside.open({
templateUrl: 'filterAside.html',
placement: 'right',
size: 'md',
backdrop: false,
resolve: {
trainings () {
return $scope.trainings;
},
machines () {
return $scope.machines;
},
spaces () {
return $scope.spaces;
},
filter () {
return $scope.filter;
},
toggleFilter () {
return $scope.toggleFilter;
},
filterAvailabilities () {
return $scope.filterAvailabilities;
}
},
controller: ['$scope', '$uibModalInstance', 'trainings', 'machines', 'spaces', 'filter', 'toggleFilter', 'filterAvailabilities', function ($scope, $uibModalInstance, trainings, machines, spaces, filter, toggleFilter, filterAvailabilities) {
$scope.trainings = trainings;
$scope.machines = machines;
$scope.spaces = spaces;
$scope.filter = filter;
$scope.toggleFilter = (type, filter) => toggleFilter(type, filter);
$scope.filterAvailabilities = filter => filterAvailabilities(filter, $scope);
return $scope.close = function (e) {
$uibModalInstance.dismiss();
return e.stopPropagation();
};
}
] });
/* PRIVATE SCOPE */
const calendarEventClickCb = function (event, jsEvent, view) {
// current calendar object
const { calendar } = uiCalendarConfig.calendars;
if (event.available_type === 'machines') {
currentMachineEvent = event;
calendar.fullCalendar('changeView', 'agendaDay');
return calendar.fullCalendar('gotoDate', event.start);
} else if (event.available_type === 'space') {
calendar.fullCalendar('changeView', 'agendaDay');
return calendar.fullCalendar('gotoDate', event.start);
} else if (event.available_type === 'event') {
return $state.go('app.public.events_show', { id: event.event_id });
} else if (event.available_type === 'training') {
return $state.go('app.public.training_show', { id: event.training_id });
} else {
if (event.machine_id) {
return $state.go('app.public.machines_show', { id: event.machine_id });
} else if (event.space_id) {
return $state.go('app.public.space_show', { id: event.space_id });
}
}
};
// agendaDay view: disable slotEventOverlap
// agendaWeek view: enable slotEventOverlap
const toggleSlotEventOverlap = function (view) {
// set defaultView, because when we change slotEventOverlap
// ui-calendar will trigger rerender calendar
$scope.calendarConfig.defaultView = view.type;
const today = currentMachineEvent ? currentMachineEvent.start : moment().utc().startOf('day');
if ((today > view.intervalStart) && (today < view.intervalEnd) && (today !== view.intervalStart)) {
$scope.calendarConfig.defaultDate = today;
} else {
$scope.calendarConfig.defaultDate = view.intervalStart;
}
if (view.type === 'agendaDay') {
return $scope.calendarConfig.slotEventOverlap = false;
} else {
return $scope.calendarConfig.slotEventOverlap = true;
}
};
// function is called when calendar view is rendered or changed
const viewRenderCb = function (view, element) {
toggleSlotEventOverlap(view);
if (view.type === 'agendaDay') {
// get availabilties by 1 day for show machine slots
return uiCalendarConfig.calendars.calendar.fullCalendar('refetchEvents');
}
};
const eventRenderCb = function (event, element) {
if (event.tags.length > 0) {
let html = '';
for (let tag of Array.from(event.tags)) {
html += `<span class='label label-success text-white'>${tag.name}</span> `;
}
element.find('.fc-title').append(`<br/>${html}`);
}
};
const getFilter = function () {
const t = $scope.trainings.filter(t => t.checked).map(t => t.id);
const m = $scope.machines.filter(m => m.checked).map(m => m.id);
const s = $scope.spaces.filter(s => s.checked).map(s => s.id);
return { t, m, s, evt: $scope.filter.evt, dispo: $scope.filter.dispo };
};
var availabilitySourceUrl = () => `/api/availabilities/public?${$.param(getFilter())}`;
const initialize = () =>
// fullCalendar (v2) configuration
$scope.calendarConfig = CalendarConfig({
events: availabilitySourceUrl(),
slotEventOverlap: true,
header: {
left: 'month agendaWeek agendaDay',
center: 'title',
right: 'today prev,next'
},
minTime: moment.duration(moment(bookingWindowStart.setting.value).format('HH:mm:ss')),
maxTime: moment.duration(moment(bookingWindowEnd.setting.value).format('HH:mm:ss')),
defaultView: window.innerWidth <= 480 ? 'agendaDay' : 'agendaWeek',
eventClick (event, jsEvent, view) {
return calendarEventClickCb(event, jsEvent, view);
},
viewRender (view, element) {
return viewRenderCb(view, element);
},
eventRender (event, element, view) {
return eventRenderCb(event, element);
}
});
// !!! MUST BE CALLED AT THE END of the controller
return initialize();
}
]);

View File

@ -1,38 +0,0 @@
'use strict'
Application.Controllers.controller "DashboardController", ["$scope", 'memberPromise', 'SocialNetworks', ($scope, memberPromise, SocialNetworks) ->
## Current user's profile
$scope.user = memberPromise
## List of social networks associated with this user and toggle 'show all' state
$scope.social =
showAllLinks: false
networks: SocialNetworks
### PRIVATE SCOPE ###
##
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
$scope.social.networks = filterNetworks()
##
# Filter social network or website that are associated with the profile of the user provided in promise
# and return the filtered networks
# @return {Array}
##
filterNetworks = ->
networks = [];
for network in SocialNetworks
if $scope.user.profile[network] && $scope.user.profile[network].length > 0
networks.push(network);
networks
## !!! MUST BE CALLED AT THE END of the controller
initialize()
]

View File

@ -0,0 +1,51 @@
/* eslint-disable
no-return-assign,
no-undef,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
'use strict';
Application.Controllers.controller('DashboardController', ['$scope', 'memberPromise', 'SocialNetworks', function ($scope, memberPromise, SocialNetworks) {
// Current user's profile
$scope.user = memberPromise;
// List of social networks associated with this user and toggle 'show all' state
$scope.social = {
showAllLinks: false,
networks: SocialNetworks
};
/* PRIVATE SCOPE */
/**
* Kind of constructor: these actions will be realized first when the controller is loaded
*/
const initialize = () => $scope.social.networks = filterNetworks();
/**
* Filter social network or website that are associated with the profile of the user provided in promise
* and return the filtered networks
* @return {Array}
*/
var filterNetworks = function () {
const networks = [];
for (let network of Array.from(SocialNetworks)) {
if ($scope.user.profile[network] && ($scope.user.profile[network].length > 0)) {
networks.push(network);
}
}
return networks;
};
// !!! MUST BE CALLED AT THE END of the controller
return initialize();
}
]);

View File

@ -1,690 +0,0 @@
'use strict'
Application.Controllers.controller "EventsController", ["$scope", "$state", 'Event', 'categoriesPromise', 'themesPromise', 'ageRangesPromise'
, ($scope, $state, Event, categoriesPromise, themesPromise, ageRangesPromise) ->
### PUBLIC SCOPE ###
## The events displayed on the page
$scope.events = []
## The currently displayed page number
$scope.page = 1
## List of categories for the events
$scope.categories = categoriesPromise
## List of events themes
$scope.themes = themesPromise
## List of age ranges
$scope.ageRanges = ageRangesPromise
## Hide or show the 'load more' button
$scope.noMoreResults = false
## Active filters for the events list
$scope.filters =
category_id: null
theme_id: null
age_range_id: null
$scope.monthNames = [<%= t('date.month_names')[1..-1].map { |m| "\"#{m}\"" }.join(', ') %>]
##
# Adds a resultset of events to the bottom of the page, grouped by month
##
$scope.loadMoreEvents = ->
$scope.page += 1
Event.query Object.assign({page: $scope.page}, $scope.filters), (data) ->
$scope.events = $scope.events.concat data
groupEvents($scope.events)
if (!data[0] || data[0].nb_total_events <= $scope.events.length)
$scope.noMoreResults = true
##
# 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 refresh the events list according to the filters set
##
$scope.filterEvents = ->
# reinitialize results datasets
$scope.page = 1
$scope.eventsGroupByMonth = {}
$scope.events = []
$scope.monthOrder = []
$scope.noMoreResults = false
# run a search query
Event.query Object.assign({page: $scope.page}, $scope.filters), (data) ->
$scope.events = data
groupEvents(data)
if (!data[0] || data[0].nb_total_events <= $scope.events.length)
$scope.noMoreResults = true
##
# Test if the provided event occurs on a single day or on many days
# @param event {{start_date:Date, end_date:Date}} Event object as retreived from the API
# @return {boolean} false if the event occurs on many days
##
$scope.onSingleDay = (event) ->
moment(event.start_date).isSame(event.end_date, 'day')
### PRIVATE SCOPE ###
##
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
$scope.filterEvents()
##
# Group the provided events by month/year and concat them with existing results
# Then compute the ordered list of months for the complete resultset.
# Affect the resulting events groups in $scope.eventsGroupByMonth and the ordered month keys in $scope.monthOrder.
# @param {Array} Events retrived from the API
##
groupEvents = (events) ->
if events.length > 0
eventsGroupedByMonth = _.groupBy(events, (obj) ->
_.map ['month_id', 'year'], (key, value) -> obj[key]
)
$scope.eventsGroupByMonth = Object.assign($scope.eventsGroupByMonth, eventsGroupedByMonth)
$scope.monthOrder = Object.keys($scope.eventsGroupByMonth)
## !!! MUST BE CALLED AT THE END of the controller
initialize()
]
Application.Controllers.controller "ShowEventController", ["$scope", "$state", "$stateParams", "Event", '$uibModal', 'Member', 'Reservation', 'Price', 'CustomAsset', 'eventPromise', 'growl', '_t', 'Wallet', 'helpers', 'dialogs', 'priceCategoriesPromise', 'settingsPromise',
($scope, $state, $stateParams, Event, $uibModal, Member, Reservation, Price, CustomAsset, eventPromise, growl, _t, Wallet, helpers, dialogs, priceCategoriesPromise, settingsPromise) ->
### PUBLIC SCOPE ###
## reservations for the currently shown event
$scope.reservations = []
## user to deal with
$scope.ctrl =
member: {}
## parameters for a new reservation
$scope.reserve =
nbPlaces:
normal: []
nbReservePlaces: 0
tickets: {}
toReserve: false
amountTotal : 0
totalNoCoupon: 0
totalSeats: 0
## Discount coupon to apply to the basket, if any
$scope.coupon =
applied: null
## Get the details for the current event (event's id is recovered from the current URL)
$scope.event = eventPromise
## List of price categories for the events
$scope.priceCategories = priceCategoriesPromise
## 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)
## Message displayed to the end user about rules that applies to events reservations
$scope.eventExplicationsAlert = settingsPromise.event_explications_alert
##
# Callback to delete the provided event (admins only)
# @param event {$resource} angular's Event $resource
##
$scope.deleteEvent = (event) ->
dialogs.confirm
resolve:
object: ->
title: _t('confirmation_required')
msg: _t('do_you_really_want_to_delete_this_event')
, ->
# the admin has confirmed, delete
event.$delete ->
$state.go('app.public.events_list')
growl.info(_t('event_successfully_deleted'))
, (error)->
growl.error(_t('unable_to_delete_the_event_because_some_users_alredy_booked_it'))
##
# Callback to call when the number of tickets to book changes in the current booking
##
$scope.changeNbPlaces = ->
# compute the total remaing places
remain = $scope.event.nb_free_places - $scope.reserve.nbReservePlaces
for ticket of $scope.reserve.tickets
remain -= $scope.reserve.tickets[ticket]
# we store the total number of seats booked, this is used to know if the 'pay' button must be shown
$scope.reserve.totalSeats = $scope.event.nb_free_places - remain
# update the availables seats for full price tickets
fullPriceRemains = $scope.reserve.nbReservePlaces + remain
$scope.reserve.nbPlaces.normal = [0..fullPriceRemains]
# update the available seats for other prices tickets
for key of $scope.reserve.nbPlaces
if key != 'normal'
priceRemain = $scope.reserve.tickets[key] + remain
$scope.reserve.nbPlaces[key] = [0..priceRemain]
# recompute the total price
$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
$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
Member.get {id: $scope.ctrl.member.id}, (member) ->
$scope.ctrl.member = 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)
Wallet.getWalletByUser {user_id: $scope.ctrl.member.id}, (wallet) ->
amountToPay = helpers.getAmountToPay($scope.reserve.amountTotal, wallet.amount)
if $scope.currentUser.role isnt 'admin' and amountToPay > 0
payByStripe(reservation)
else
if $scope.currentUser.role is 'admin' or amountToPay is 0
payOnSite(reservation)
else
# otherwise we alert, this error musn't occur when the current user is not admin
growl.error(_t('please_select_a_member_first'))
##
# 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
tickets_attributes: []
# a single slot is used for events
reservation.slots_attributes.push
start_at: $scope.event.start_date
end_at: $scope.event.end_date
availability_id: $scope.event.availability.id
# iterate over reservations per prices
for price_id, seats of $scope.reserve.tickets
reservation.tickets_attributes.push
event_price_category_id: price_id
booked: seats
# set the attempting marker
$scope.attempting = true
# save the reservation to the API
Reservation.save reservation: reservation, (reservation) ->
# reservation successfull
afterPayment(reservation)
$scope.attempting = false
, (response)->
# reservation failed
$scope.alerts = []
$scope.alerts.push
msg: response.data.card[0]
type: 'danger'
# unset the attempting marker
$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}}
# @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.total_booked_seats
$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)->
# remove the reservation from the user's reservations list for this event (occurrence)
$scope.reservations.splice(index, 1)
# add the number of places transfered (to the new date) to the total of free places for this event
$scope.event.nb_free_places = $scope.event.nb_free_places + reservation.total_booked_seats
# remove the number of places transfered from the total of free places of the receiving occurrance
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.total_booked_seats
##
# Checks if the provided reservation is able to be moved (date change)
# @param reservation {{total_booked_seats:number}}
##
$scope.reservationCanModify = (reservation)->
slotStart = moment(reservation.slots[0].start_at)
now = moment()
isAble = false
angular.forEach $scope.event.recurrence_events, (e)->
isAble = true if e.nb_free_places >= reservation.total_booked_seats
return (isAble and $scope.enableBookingMove and slotStart.diff(now, "hours") >= $scope.moveBookingDelay)
##
# 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 mkRequestParams(r, $scope.coupon.applied), (res) ->
$scope.reserve.amountTotal = res.price
$scope.reserve.totalNoCoupon = res.price_without_coupon
else
$scope.reserve.amountTotal = null
##
# Return the URL allowing to share the current project on the Facebook social network
##
$scope.shareOnFacebook = ->
'https://www.facebook.com/share.php?u='+$state.href('app.public.events_show', {id: $scope.event.id}, {absolute: true}).replace('#', '%23')
##
# Return the URL allowing to share the current project on the Twitter social network
##
$scope.shareOnTwitter = ->
'https://twitter.com/intent/tweet?url='+encodeURIComponent($state.href('app.public.events_show', {id: $scope.event.id}, {absolute: true}))+'&text='+encodeURIComponent($scope.event.title)
##
# Return the textual description of the conditions applyable to the given price's category
# @param category_id {number} ID of the price's category
##
$scope.getPriceCategoryConditions = (category_id) ->
for cat in $scope.priceCategories
if cat.id == category_id
return cat.conditions
### PRIVATE SCOPE ###
##
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
# set the controlled user as the current user if the current user is not an admin
if $scope.currentUser
if $scope.currentUser.role isnt 'admin'
$scope.ctrl.member = $scope.currentUser
# initialize the "reserve" object with the event's data
resetEventReserve()
# if non-admin, get the current user's reservations into $scope.reservations
if $scope.currentUser
getReservations($scope.event.id, 'Event', $scope.currentUser.id)
# watch when a coupon is applied to re-compute the total price
$scope.$watch 'coupon.applied', (newValue, oldValue) ->
unless newValue == null and oldValue == null
$scope.computeEventAmount()
##
# 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
# @return {{user_id:number, reservable_id:number, reservable_type:string, slots_attributes:Array<Object>, nb_reserve_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
tickets_attributes: []
reservation.slots_attributes.push
start_at: event.start_date
end_at: event.end_date
availability_id: event.availability.id
offered: event.offered || false
for evt_px_cat in event.prices
booked = reserve.tickets[evt_px_cat.id]
if booked > 0
reservation.tickets_attributes.push
event_price_category_id: evt_px_cat.id
booked: booked
reservation
##
# Format the parameters expected by /api/prices/compute or /api/reservations and return the resulting object
# @param reservation {Object} as returned by mkReservation()
# @param coupon {Object} Coupon as returned from the API
# @return {{reservation:Object, coupon_code:string}}
##
mkRequestParams = (reservation, coupon) ->
params =
reservation: reservation
coupon_code: (coupon.code if coupon)
params
##
# Set the current reservation to the default values. This implies to reservation form to be hidden.
##
resetEventReserve = ->
if $scope.event
$scope.reserve =
nbPlaces:
normal: [0..$scope.event.nb_free_places]
nbReservePlaces: 0
tickets: {}
toReserve: false
amountTotal : 0
totalSeats: 0
for evt_px_cat in $scope.event.prices
$scope.reserve.nbPlaces[evt_px_cat.id] = [0..$scope.event.nb_free_places]
$scope.reserve.tickets[evt_px_cat.id] = 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(mkRequestParams(reservation, $scope.coupon.applied)).$promise
wallet: ->
Wallet.getWalletByUser({user_id: reservation.user_id}).$promise
cgv: ->
CustomAsset.get({name: 'cgv-file'}).$promise
objectToPay: ->
eventToReserve: $scope.event
reserve: $scope.reserve
member: $scope.ctrl.member
coupon: ->
$scope.coupon.applied
controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'cgv', 'Auth', 'Reservation', 'growl', 'wallet', 'helpers', '$filter', 'coupon',
($scope, $uibModalInstance, $state, reservation, price, cgv, Auth, Reservation, growl, wallet, helpers, $filter, coupon) ->
# User's wallet amount
$scope.walletAmount = wallet.amount
# Price
$scope.amount = helpers.getAmountToPay(price.price, wallet.amount)
# CGV
$scope.cgv = cgv.custom_asset
# Reservation
$scope.reservation = reservation
# Used in wallet info template to interpolate some translations
$scope.numberFilter = $filter('number')
# Callback 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 mkRequestParams($scope.reservation, coupon), (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(mkRequestParams(reservation, $scope.coupon.applied)).$promise
wallet: ->
Wallet.getWalletByUser({user_id: reservation.user_id}).$promise
coupon: ->
$scope.coupon.applied
controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'Auth', 'Reservation', 'wallet', 'helpers', '$filter', 'coupon',
($scope, $uibModalInstance, $state, reservation, price, Auth, Reservation, wallet, helpers, $filter, coupon) ->
# User's wallet amount
$scope.walletAmount = wallet.amount
# Price
$scope.price = price.price
# price to pay
$scope.amount = helpers.getAmountToPay(price.price, wallet.amount)
# Reservation
$scope.reservation = reservation
# Used in wallet info template to interpolate some translations
$scope.numberFilter = $filter('number')
# Button label
if $scope.amount > 0
$scope.validButtonName = _t('confirm_payment_of_html', {ROLE:$scope.currentUser.role, AMOUNT:$filter('currency')($scope.amount)}, "messageformat")
else
if price.price > 0 and $scope.walletAmount == 0
$scope.validButtonName = _t('confirm_payment_of_html', {ROLE:$scope.currentUser.role, AMOUNT:$filter('currency')(price.price)}, "messageformat")
else
$scope.validButtonName = _t('confirm')
# Callback to validate the payment
$scope.ok = ->
$scope.attempting = true
Reservation.save mkRequestParams($scope.reservation, coupon), (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.total_booked_seats
resetEventReserve()
$scope.reserveSuccess = true
$scope.coupon.applied = null
$scope.reservations.push reservation
if $scope.currentUser.role == 'admin'
$scope.ctrl.member = null
## !!! MUST BE CALLED AT THE END of the controller
initialize()
]

View File

@ -0,0 +1,768 @@
/* eslint-disable
camelcase,
handle-callback-err,
no-return-assign,
no-undef,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
Application.Controllers.controller('EventsController', ['$scope', '$state', 'Event', 'categoriesPromise', 'themesPromise', 'ageRangesPromise',
function ($scope, $state, Event, categoriesPromise, themesPromise, ageRangesPromise) {
/* PUBLIC SCOPE */
// The events displayed on the page
$scope.events = [];
// The currently displayed page number
$scope.page = 1;
// List of categories for the events
$scope.categories = categoriesPromise;
// List of events themes
$scope.themes = themesPromise;
// List of age ranges
$scope.ageRanges = ageRangesPromise;
// Hide or show the 'load more' button
$scope.noMoreResults = false;
// Active filters for the events list
$scope.filters = {
category_id: null,
theme_id: null,
age_range_id: null
};
$scope.monthNames = [<%= t('date.month_names')[1..-1].map { |m| "\"#{m}\"" }.join(', ') %>];
/**
* Adds a resultset of events to the bottom of the page, grouped by month
*/
$scope.loadMoreEvents = function () {
$scope.page += 1;
return Event.query(Object.assign({ page: $scope.page }, $scope.filters), function (data) {
$scope.events = $scope.events.concat(data);
groupEvents($scope.events);
if (!data[0] || (data[0].nb_total_events <= $scope.events.length)) {
return $scope.noMoreResults = true;
}
});
};
/**
* Callback to redirect the user to the specified event page
* @param event {{id:number}}
*/
$scope.showEvent = function (event) { $state.go('app.public.events_show', { id: event.id }); };
/**
* Callback to refresh the events list according to the filters set
*/
$scope.filterEvents = function () {
// reinitialize results datasets
$scope.page = 1;
$scope.eventsGroupByMonth = {};
$scope.events = [];
$scope.monthOrder = [];
$scope.noMoreResults = false;
// run a search query
return Event.query(Object.assign({ page: $scope.page }, $scope.filters), function (data) {
$scope.events = data;
groupEvents(data);
if (!data[0] || (data[0].nb_total_events <= $scope.events.length)) {
return $scope.noMoreResults = true;
}
});
};
/**
* Test if the provided event occurs on a single day or on many days
* @param event {{start_date:Date, end_date:Date}} Event object as retreived from the API
* @return {boolean} false if the event occurs on many days
*/
$scope.onSingleDay = function (event) { moment(event.start_date).isSame(event.end_date, 'day'); };
/* PRIVATE SCOPE */
/**
* Kind of constructor: these actions will be realized first when the controller is loaded
*/
const initialize = function () {
$scope.filterEvents();
};
/**
* Group the provided events by month/year and concat them with existing results
* Then compute the ordered list of months for the complete resultset.
* Affect the resulting events groups in $scope.eventsGroupByMonth and the ordered month keys in $scope.monthOrder.
* @param events {Array} Events retrieved from the API
*/
const groupEvents = function (events) {
if (events.length > 0) {
const eventsGroupedByMonth = _.groupBy(events, function (obj) {
return _.map(['month_id', 'year'], function (key) {
return obj[key];
});
});
$scope.eventsGroupByMonth = Object.assign($scope.eventsGroupByMonth, eventsGroupedByMonth);
return $scope.monthOrder = Object.keys($scope.eventsGroupByMonth);
}
};
// # !!! MUST BE CALLED AT THE END of the controller
initialize();
}
]);
Application.Controllers.controller('ShowEventController', ['$scope', '$state', '$stateParams', 'Event', '$uibModal', 'Member', 'Reservation', 'Price', 'CustomAsset', 'eventPromise', 'growl', '_t', 'Wallet', 'helpers', 'dialogs', 'priceCategoriesPromise', 'settingsPromise',
function ($scope, $state, $stateParams, Event, $uibModal, Member, Reservation, Price, CustomAsset, eventPromise, growl, _t, Wallet, helpers, dialogs, priceCategoriesPromise, settingsPromise) {
/* PUBLIC SCOPE */
// reservations for the currently shown event
$scope.reservations = [];
// user to deal with
$scope.ctrl =
{ member: {} };
// parameters for a new reservation
$scope.reserve = {
nbPlaces: {
normal: []
},
nbReservePlaces: 0,
tickets: {},
toReserve: false,
amountTotal: 0,
totalNoCoupon: 0,
totalSeats: 0
};
// Discount coupon to apply to the basket, if any
$scope.coupon =
{ applied: null };
// Get the details for the current event (event's id is recovered from the current URL)
$scope.event = eventPromise;
// List of price categories for the events
$scope.priceCategories = priceCategoriesPromise;
// 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);
// Message displayed to the end user about rules that applies to events reservations
$scope.eventExplicationsAlert = settingsPromise.event_explications_alert;
/**
* Callback to delete the provided event (admins only)
* @param event {$resource} angular's Event $resource
*/
$scope.deleteEvent = function (event) {
dialogs.confirm({
resolve: {
object () {
return {
title: _t('confirmation_required'),
msg: _t('do_you_really_want_to_delete_this_event')
};
}
}
}, function () {
// the admin has confirmed, delete
event.$delete(function () {
$state.go('app.public.events_list');
return growl.info(_t('event_successfully_deleted'));
}, function (error) {
console.error(error);
growl.error(_t('unable_to_delete_the_event_because_some_users_alredy_booked_it'));
});
}
);
};
/**
* Callback to call when the number of tickets to book changes in the current booking
*/
$scope.changeNbPlaces = function () {
// compute the total remaning places
let remain = $scope.event.nb_free_places - $scope.reserve.nbReservePlaces;
for (let ticket in $scope.reserve.tickets) {
remain -= $scope.reserve.tickets[ticket];
}
// we store the total number of seats booked, this is used to know if the 'pay' button must be shown
$scope.reserve.totalSeats = $scope.event.nb_free_places - remain;
// update the available seats for full price tickets
const fullPriceRemains = $scope.reserve.nbReservePlaces + remain;
$scope.reserve.nbPlaces.normal = __range__(0, fullPriceRemains, true);
// update the available seats for other prices tickets
for (let key in $scope.reserve.nbPlaces) {
if (key !== 'normal') {
const priceRemain = $scope.reserve.tickets[key] + remain;
$scope.reserve.nbPlaces[key] = __range__(0, priceRemain, true);
}
}
// recompute the total price
return $scope.computeEventAmount();
};
/**
* Callback to reset the current reservation parameters
* @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
*/
$scope.cancelReserve = function (e) {
e.preventDefault();
return resetEventReserve();
};
/**
* Callback to allow the user to set the details for his reservation
*/
$scope.reserveEvent = function () {
if ($scope.event.nb_total_places > 0) {
$scope.reserveSuccess = false;
if (!$scope.isAuthenticated()) {
return $scope.login(null, function (user) {
$scope.reserve.toReserve = !$scope.reserve.toReserve;
if (user.role !== 'admin') {
return $scope.ctrl.member = user;
}
});
} else {
return $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 = function () {
resetEventReserve();
$scope.reserveSuccess = false;
if ($scope.ctrl.member) {
return Member.get({ id: $scope.ctrl.member.id }, function (member) {
$scope.ctrl.member = member;
return getReservations($scope.event.id, 'Event', $scope.ctrl.member.id);
});
}
};
/**
* Callback to trigger the payment process of the current reservation
*/
$scope.payEvent = function () {
// first, we check that a user was selected
if (Object.keys($scope.ctrl.member).length > 0) {
const reservation = mkReservation($scope.ctrl.member, $scope.reserve, $scope.event);
return Wallet.getWalletByUser({ user_id: $scope.ctrl.member.id }, function (wallet) {
const amountToPay = helpers.getAmountToPay($scope.reserve.amountTotal, wallet.amount);
if (($scope.currentUser.role !== 'admin') && (amountToPay > 0)) {
return payByStripe(reservation);
} else {
if (($scope.currentUser.role === 'admin') || (amountToPay === 0)) {
return payOnSite(reservation);
}
}
});
} else {
// otherwise we alert, this error musn't occur when the current user is not admin
return growl.error(_t('please_select_a_member_first'));
}
};
/**
* Callback to validate the booking of a free event
*/
$scope.validReserveEvent = function () {
const reservation = {
user_id: $scope.ctrl.member.id,
reservable_id: $scope.event.id,
reservable_type: 'Event',
slots_attributes: [],
nb_reserve_places: $scope.reserve.nbReservePlaces,
tickets_attributes: []
};
// a single slot is used for events
reservation.slots_attributes.push({
start_at: $scope.event.start_date,
end_at: $scope.event.end_date,
availability_id: $scope.event.availability.id
});
// iterate over reservations per prices
for (let price_id in $scope.reserve.tickets) {
const seats = $scope.reserve.tickets[price_id];
reservation.tickets_attributes.push({
event_price_category_id: price_id,
booked: seats
});
}
// set the attempting marker
$scope.attempting = true;
// save the reservation to the API
return Reservation.save({ reservation }, function (reservation) {
// reservation successfull
afterPayment(reservation);
return $scope.attempting = false;
}
, function (response) {
// reservation failed
$scope.alerts = [];
$scope.alerts.push({
msg: response.data.card[0],
type: 'danger'
});
// unset the attempting marker
return $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}}
* @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
*/
$scope.modifyReservation = function (reservation, e) {
e.preventDefault();
e.stopPropagation();
const index = $scope.reservations.indexOf(reservation);
return $uibModal.open({
templateUrl: '<%= asset_path "events/modify_event_reservation_modal.html" %>',
resolve: {
event () { return $scope.event; },
reservation () { return reservation; }
},
controller: ['$scope', '$uibModalInstance', 'event', 'reservation', 'Reservation', function ($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 of Array.from(event.recurrence_events)) {
if (e.nb_free_places > reservation.total_booked_seats) {
$scope.reservation.reservable_id = e.id;
break;
}
}
// Callback to validate the new reservation's date
$scope.ok = function () {
let eventToPlace = null;
angular.forEach(event.recurrence_events, function (e) {
if (e.id === parseInt($scope.reservation.reservable_id, 10)) {
return 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 }, function (reservation) {
$uibModalInstance.close(reservation);
$scope.attempting = true;
}
, function (response) {
$scope.alerts = [];
angular.forEach(response, function (v, k) {
angular.forEach(v, function (err) {
$scope.alerts.push({ msg: k + ': ' + err, type: 'danger' });
});
});
$scope.attempting = false;
});
};
// Callback to cancel the modification
$scope.cancel = function () { $uibModalInstance.dismiss('cancel'); };
}
] })
.result['finally'](null).then(function (reservation) {
// remove the reservation from the user's reservations list for this event (occurrence)
$scope.reservations.splice(index, 1);
// add the number of places transfered (to the new date) to the total of free places for this event
$scope.event.nb_free_places = $scope.event.nb_free_places + reservation.total_booked_seats;
// remove the number of places transfered from the total of free places of the receiving occurrance
angular.forEach($scope.event.recurrence_events, function (e) {
if (e.id === parseInt(reservation.reservable.id, 10)) {
return e.nb_free_places = e.nb_free_places - reservation.total_booked_seats;
}
});
});
};
/**
* Checks if the provided reservation is able to be moved (date change)
* @param reservation {{total_booked_seats:number}}
*/
$scope.reservationCanModify = function (reservation) {
const slotStart = moment(reservation.slots[0].start_at);
const now = moment();
let isAble = false;
angular.forEach($scope.event.recurrence_events, function (e) {
if (e.nb_free_places >= reservation.total_booked_seats) { return isAble = true; }
});
return (isAble && $scope.enableBookingMove && (slotStart.diff(now, 'hours') >= $scope.moveBookingDelay));
};
/**
* Compute the total amount for the current reservation according to the previously set parameters
* and assign the result in $scope.reserve.amountTotal
*/
$scope.computeEventAmount = function () {
// first we check that a user was selected
if (Object.keys($scope.ctrl.member).length > 0) {
const r = mkReservation($scope.ctrl.member, $scope.reserve, $scope.event);
return Price.compute(mkRequestParams(r, $scope.coupon.applied), function (res) {
$scope.reserve.amountTotal = res.price;
return $scope.reserve.totalNoCoupon = res.price_without_coupon;
});
} else {
return $scope.reserve.amountTotal = null;
}
};
/**
* Return the URL allowing to share the current project on the Facebook social network
*/
$scope.shareOnFacebook = function () { return `https://www.facebook.com/share.php?u=${$state.href('app.public.events_show', { id: $scope.event.id }, { absolute: true }).replace('#', '%23')}`; };
/**
* Return the URL allowing to share the current project on the Twitter social network
*/
$scope.shareOnTwitter = function () { return `https://twitter.com/intent/tweet?url=${encodeURIComponent($state.href('app.public.events_show', { id: $scope.event.id }, { absolute: true }))}&text=${encodeURIComponent($scope.event.title)}`; };
/**
* Return the textual description of the conditions applyable to the given price's category
* @param category_id {number} ID of the price's category
*/
$scope.getPriceCategoryConditions = function (category_id) {
for (let cat of Array.from($scope.priceCategories)) {
if (cat.id === category_id) {
return cat.conditions;
}
}
};
/* PRIVATE SCOPE */
/**
* Kind of constructor: these actions will be realized first when the controller is loaded
*/
const initialize = function () {
// set the controlled user as the current user if the current user is not an admin
if ($scope.currentUser) {
if ($scope.currentUser.role !== 'admin') {
$scope.ctrl.member = $scope.currentUser;
}
}
// initialize the "reserve" object with the event's data
resetEventReserve();
// if non-admin, get the current user's reservations into $scope.reservations
if ($scope.currentUser) {
getReservations($scope.event.id, 'Event', $scope.currentUser.id);
}
// watch when a coupon is applied to re-compute the total price
return $scope.$watch('coupon.applied', function (newValue, oldValue) {
if ((newValue !== null) || (oldValue !== null)) {
return $scope.computeEventAmount();
}
});
};
/**
* 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)
*/
var getReservations = function (reservable_id, reservable_type, user_id) {
Reservation.query({
reservable_id,
reservable_type,
user_id
}).$promise.then(function (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
* @return {{user_id:number, reservable_id:number, reservable_type:string, slots_attributes:Array<Object>, nb_reserve_places:number}}
*/
var mkReservation = function (member, reserve, event) {
const reservation = {
user_id: member.id,
reservable_id: event.id,
reservable_type: 'Event',
slots_attributes: [],
nb_reserve_places: reserve.nbReservePlaces,
tickets_attributes: []
};
reservation.slots_attributes.push({
start_at: event.start_date,
end_at: event.end_date,
availability_id: event.availability.id,
offered: event.offered || false
});
for (let evt_px_cat of Array.from(event.prices)) {
const booked = reserve.tickets[evt_px_cat.id];
if (booked > 0) {
reservation.tickets_attributes.push({
event_price_category_id: evt_px_cat.id,
booked
});
}
}
return reservation;
};
/**
* Format the parameters expected by /api/prices/compute or /api/reservations and return the resulting object
* @param reservation {Object} as returned by mkReservation()
* @param coupon {Object} Coupon as returned from the API
* @return {{reservation:Object, coupon_code:string}}
*/
var mkRequestParams = function (reservation, coupon) {
const params = {
reservation,
coupon_code: ((coupon ? coupon.code : undefined))
};
return params;
};
/**
* Set the current reservation to the default values. This implies to reservation form to be hidden.
*/
var resetEventReserve = function () {
if ($scope.event) {
$scope.reserve = {
nbPlaces: {
normal: __range__(0, $scope.event.nb_free_places, true)
},
nbReservePlaces: 0,
tickets: {},
toReserve: false,
amountTotal: 0,
totalSeats: 0
};
for (let evt_px_cat of Array.from($scope.event.prices)) {
$scope.reserve.nbPlaces[evt_px_cat.id] = __range__(0, $scope.event.nb_free_places, true);
$scope.reserve.tickets[evt_px_cat.id] = 0;
}
return $scope.event.offered = false;
}
};
/**
* Open a modal window which trigger the stripe payment process
* @param reservation {Object} to book
*/
var payByStripe = function (reservation) {
$uibModal.open({
templateUrl: '<%= asset_path "stripe/payment_modal.html" %>',
size: 'md',
resolve: {
reservation () {
return reservation;
},
price () {
return Price.compute(mkRequestParams(reservation, $scope.coupon.applied)).$promise;
},
wallet () {
return Wallet.getWalletByUser({ user_id: reservation.user_id }).$promise;
},
cgv () {
return CustomAsset.get({ name: 'cgv-file' }).$promise;
},
objectToPay () {
return {
eventToReserve: $scope.event,
reserve: $scope.reserve,
member: $scope.ctrl.member
};
},
coupon () {
return $scope.coupon.applied;
}
},
controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'cgv', 'Auth', 'Reservation', 'growl', 'wallet', 'helpers', '$filter', 'coupon',
function ($scope, $uibModalInstance, $state, reservation, price, cgv, Auth, Reservation, growl, wallet, helpers, $filter, coupon) {
// User's wallet amount
$scope.walletAmount = wallet.amount;
// Price
$scope.amount = helpers.getAmountToPay(price.price, wallet.amount);
// CGV
$scope.cgv = cgv.custom_asset;
// Reservation
$scope.reservation = reservation;
// Used in wallet info template to interpolate some translations
$scope.numberFilter = $filter('number');
// Callback for the stripe payment authorization
return $scope.payment = function (status, response) {
if (response.error) {
return growl.error(response.error.message);
} else {
$scope.attempting = true;
$scope.reservation.card_token = response.id;
Reservation.save(mkRequestParams($scope.reservation, coupon), function (reservation) { $uibModalInstance.close(reservation); }
, function (response) {
$scope.alerts = [];
$scope.alerts.push({
msg: response.data.card[0],
type: 'danger'
});
return $scope.attempting = false;
});
}
};
}
]
}).result['finally'](null).then(function (reservation) { afterPayment(reservation); });
};
/**
* Open a modal window which trigger the local payment process
* @param reservation {Object} to book
*/
var payOnSite = function (reservation) {
$uibModal.open({
templateUrl: '<%= asset_path "shared/valid_reservation_modal.html" %>',
size: 'sm',
resolve: {
reservation () {
return reservation;
},
price () {
return Price.compute(mkRequestParams(reservation, $scope.coupon.applied)).$promise;
},
wallet () {
return Wallet.getWalletByUser({ user_id: reservation.user_id }).$promise;
},
coupon () {
return $scope.coupon.applied;
}
},
controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'Auth', 'Reservation', 'wallet', 'helpers', '$filter', 'coupon',
function ($scope, $uibModalInstance, $state, reservation, price, Auth, Reservation, wallet, helpers, $filter, coupon) {
// User's wallet amount
$scope.walletAmount = wallet.amount;
// Price
$scope.price = price.price;
// price to pay
$scope.amount = helpers.getAmountToPay(price.price, wallet.amount);
// Reservation
$scope.reservation = reservation;
// Used in wallet info template to interpolate some translations
$scope.numberFilter = $filter('number');
// Button label
if ($scope.amount > 0) {
$scope.validButtonName = _t('confirm_payment_of_html', { ROLE: $scope.currentUser.role, AMOUNT: $filter('currency')($scope.amount) }, 'messageformat');
} else {
if ((price.price > 0) && ($scope.walletAmount === 0)) {
$scope.validButtonName = _t('confirm_payment_of_html', { ROLE: $scope.currentUser.role, AMOUNT: $filter('currency')(price.price) }, 'messageformat');
} else {
$scope.validButtonName = _t('confirm');
}
}
// Callback to validate the payment
$scope.ok = function () {
$scope.attempting = true;
return Reservation.save(mkRequestParams($scope.reservation, coupon), function (reservation) {
$uibModalInstance.close(reservation);
return $scope.attempting = true;
}
, function (response) {
$scope.alerts = [];
angular.forEach(response, function (v, k) {
angular.forEach(v, function (err) {
$scope.alerts.push({
msg: k + ': ' + err,
type: 'danger'
});
});
});
return $scope.attempting = false;
});
};
// Callback to cancel the payment
return $scope.cancel = function () { $uibModalInstance.dismiss('cancel'); };
}
] })
.result['finally'](null).then(function (reservation) { afterPayment(reservation); });
};
/**
* What to do after the payment was successful
* @param resveration {Object} booked reservation
*/
var afterPayment = function (reservation) {
$scope.event.nb_free_places = $scope.event.nb_free_places - reservation.total_booked_seats;
resetEventReserve();
$scope.reserveSuccess = true;
$scope.coupon.applied = null;
$scope.reservations.push(reservation);
if ($scope.currentUser.role === 'admin') {
return $scope.ctrl.member = null;
}
};
// !!! MUST BE CALLED AT THE END of the controller
return initialize();
}
]);
function __range__ (left, right, inclusive) {
let range = [];
let ascending = left < right;
let end = !inclusive ? right : ascending ? right + 1 : right - 1;
for (let i = left; ascending ? i < end : i > end; ascending ? i++ : i--) {
range.push(i);
}
return range;
}

View File

@ -1,54 +0,0 @@
'use strict'
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.lastMembers = lastMembersPromise
## The last tweets from the Fablab official twitter account
$scope.lastTweets = []
## The last projects published/documented on the plateform
$scope.lastProjects = lastProjectsPromise
## The closest 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')
### PRIVATE SCOPE ###
##
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
# 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)
## !!! MUST BE CALLED AT THE END of the controller
initialize()
]

View File

@ -0,0 +1,62 @@
/* eslint-disable
no-undef,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
'use strict';
Application.Controllers.controller('HomeController', ['$scope', '$stateParams', 'Twitter', 'lastMembersPromise', 'lastProjectsPromise', 'upcomingEventsPromise', 'homeBlogpostPromise', 'twitterNamePromise',
function ($scope, $stateParams, Twitter, lastMembersPromise, lastProjectsPromise, upcomingEventsPromise, homeBlogpostPromise, twitterNamePromise) {
/* PUBLIC SCOPE */
// The last registered members who confirmed their addresses
$scope.lastMembers = lastMembersPromise;
// The last tweets from the Fablab official twitter account
$scope.lastTweets = [];
// The last projects published/documented on the plateform
$scope.lastProjects = lastProjectsPromise;
// The closest 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');
/* PRIVATE SCOPE */
/**
* Kind of constructor: these actions will be realized first when the controller is loaded
*/
const initialize = function () {
// 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) {
return $scope.$parent.editPassword($stateParams.reset_password_token);
}
};
// !!! MUST BE CALLED AT THE END of the controller
return initialize();
}
]);

View File

@ -1,635 +0,0 @@
'use strict'
### COMMON CODE ###
##
# Provides a set of common callback methods to the $scope parameter. These methods are used
# in the various machines' admin controllers.
#
# Provides :
# - $scope.submited(content)
# - $scope.cancel()
# - $scope.fileinputClass(v)
# - $scope.addFile()
# - $scope.deleteFile(file)
#
# Requires :
# - $scope.machine.machine_files_attributes = []
# - $state (Ui-Router) [ 'app.public.machines_list' ]
##
class MachinesController
constructor: ($scope, $state)->
##
# For use with ngUpload (https://github.com/twilson63/ngUpload).
# Intended to be the callback when the upload is done: any raised error will be stacked in the
# $scope.alerts array. If everything goes fine, the user is redirected to the machines list.
# @param content {Object} JSON - The upload's result
##
$scope.submited = (content) ->
if !content.id?
$scope.alerts = []
angular.forEach content, (v, k)->
angular.forEach v, (err)->
$scope.alerts.push
msg: k+': '+err
type: 'danger'
else
$state.go('app.public.machines_list')
##
# Changes the current user's view, redirecting him to the machines list
##
$scope.cancel = ->
$state.go('app.public.machines_list')
##
# For use with 'ng-class', returns the CSS class name for the uploads previews.
# The preview may show a placeholder or the content of the file depending on the upload state.
# @param v {*} any attribute, will be tested for truthiness (see JS evaluation rules)
##
$scope.fileinputClass = (v)->
if v
'fileinput-exists'
else
'fileinput-new'
##
# This will create a single new empty entry into the machine attachements list.
##
$scope.addFile = ->
$scope.machine.machine_files_attributes.push {}
##
# This will remove the given file from the machine attachements list. If the file was previously uploaded
# to the server, it will be marked for deletion on the server. Otherwise, it will be simply truncated from
# the attachements array.
# @param file {Object} the file to delete
##
$scope.deleteFile = (file) ->
index = $scope.machine.machine_files_attributes.indexOf(file)
if file.id?
file._destroy = true
else
$scope.machine.machine_files_attributes.splice(index, 1)
##
# 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.slug})
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
# unless all associated trainings are disabled
else
# if all trainings are disabled, just redirect the user to the reservation calendar
if machine.trainings.map((t) -> t.disabled).reduce(((acc, val) -> acc && val), true)
_this.$state.go('app.logged.machines_reserve', {id: machine.slug})
# otherwise open the information modal
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('machines_list._or_the_')
text += training.name.substr(0,1).toLowerCase() + training.name.substr(1)
text
# modal is closed with validation
$scope.ok = ->
$state.go('app.logged.trainings_reserve', {id: $scope.machine.trainings[0].id})
$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", '_t', 'Machine', '$uibModal', 'machinesPromise', ($scope, $state, _t, Machine, $uibModal, machinesPromise) ->
## 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
## Default: we show only enabled machines
$scope.machineFiltering = 'enabled'
## Available options for filtering machines by status
$scope.filterDisabled = [
'enabled',
'disabled',
'all',
]
]
##
# Controller used in the machine creation page (admin)
##
Application.Controllers.controller "NewMachineController", ["$scope", "$state", 'CSRF',($scope, $state, CSRF) ->
CSRF.setMetaTags()
## API URL where the form will be posted
$scope.actionUrl = "/api/machines/"
## Form action on the above URL
$scope.method = "post"
## default machine parameters
$scope.machine =
machine_files_attributes: []
## Using the MachinesController
new MachinesController($scope, $state)
]
##
# Controller used in the machine edition page (admin)
##
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
## Form action on the above URL
$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 = machinePromise
### 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()
]
##
# Controller used in the machine details page (public)
##
Application.Controllers.controller "ShowMachineController", ['$scope', '$state', '$uibModal', '$stateParams', '_t', 'Machine', 'growl', 'machinePromise', 'dialogs'
, ($scope, $state, $uibModal, $stateParams, _t, Machine, growl, machinePromise, dialogs) ->
## Retrieve the details for the machine id in the URL, if an error occurs redirect the user to the machines list
$scope.machine = machinePromise
##
# Callback to delete the current machine (admins only)
##
$scope.delete = (machine) ->
# check the permissions
if $scope.currentUser.role isnt 'admin'
console.error _t('unauthorized_operation')
else
dialogs.confirm
resolve:
object: ->
title: _t('confirmation_required')
msg: _t('do_you_really_want_to_delete_this_machine')
, -> # deletion confirmed
# 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", '$stateParams', '_t', "moment", 'Auth', '$timeout', 'Member', 'Availability', 'plansPromise', 'groupsPromise', 'machinePromise', 'settingsPromise', 'uiCalendarConfig', 'CalendarConfig',
($scope, $stateParams, _t, moment, Auth, $timeout, Member, Availability, plansPromise, groupsPromise, machinePromise, settingsPromise, uiCalendarConfig, CalendarConfig) ->
### PRIVATE STATIC CONSTANTS ###
# Slot free to be booked
FREE_SLOT_BORDER_COLOR = '<%= AvailabilityHelper::MACHINE_COLOR %>'
# Slot already booked by another user
UNAVAILABLE_SLOT_BORDER_COLOR = '<%= AvailabilityHelper::MACHINE_IS_RESERVED_BY_USER %>'
# Slot already booked by the current user
BOOKED_SLOT_BORDER_COLOR = '<%= AvailabilityHelper::IS_RESERVED_BY_CURRENT_USER %>'
### PUBLIC SCOPE ###
## bind the machine availabilities with full-Calendar events
$scope.eventSources = []
## indicates the state of the current view : calendar or plans information
$scope.plansAreShown = false
## will store the user's plan if he choosed to buy one
$scope.selectedPlan = null
## the moment when the plan selection changed for the last time, used to trigger changes in the cart
$scope.planSelectionTime = null
## mapping of fullCalendar events.
$scope.events =
reserved: [] # Slots that the user wants to book
modifiable: null # Slot that the user wants to change
placable: null # Destination slot for the change
paid: [] # Slots that were just booked by the user (transaction ok)
moved: null # Slots that were just moved by the user (change done) -> {newSlot:* oldSlot: *}
## the moment when the slot selection changed for the last time, used to trigger changes in the cart
$scope.selectionTime = null
## the last clicked event in the calender
$scope.selectedEvent = null
## the application global settings
$scope.settings = settingsPromise
## 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: {}
## current machine to reserve
$scope.machine = machinePromise
## fullCalendar (v2) configuration
$scope.calendarConfig = CalendarConfig
minTime: moment.duration(moment(settingsPromise.booking_window_start).format('HH:mm:ss'))
maxTime: moment.duration(moment(settingsPromise.booking_window_end).format('HH:mm:ss'))
eventClick: (event, jsEvent, view) ->
calendarEventClickCb(event, jsEvent, view)
eventRender: (event, element, view) ->
eventRenderCb(event, element)
## Global config: message to the end user concerning the subscriptions rules
$scope.subscriptionExplicationsAlert = settingsPromise.subscription_explications_alert
## Global config: message to the end user concerning the machine bookings
$scope.machineExplicationsAlert = settingsPromise.machine_explications_alert
##
# Change the last selected slot's appearence to looks like 'added to cart'
##
$scope.markSlotAsAdded = ->
$scope.selectedEvent.backgroundColor = FREE_SLOT_BORDER_COLOR
$scope.selectedEvent.title = _t('i_reserve')
updateCalendar()
##
# Change the last selected slot's appearence to looks like 'never added to cart'
##
$scope.markSlotAsRemoved = (slot) ->
slot.backgroundColor = 'white'
slot.borderColor = FREE_SLOT_BORDER_COLOR
slot.title = ''
slot.isValid = false
slot.id = null
slot.is_reserved = false
slot.can_modify = false
slot.offered = false
updateCalendar()
##
# Callback when a slot was successfully cancelled. Reset the slot style as 'ready to book'
##
$scope.slotCancelled = ->
$scope.markSlotAsRemoved($scope.selectedEvent)
##
# Change the last selected slot's appearence to looks like 'currently looking for a new destination to exchange'
##
$scope.markSlotAsModifying = ->
$scope.selectedEvent.backgroundColor = '#eee'
$scope.selectedEvent.title = _t('i_change')
updateCalendar()
##
# Change the last selected slot's appearence to looks like 'the slot being exchanged will take this place'
##
$scope.changeModifyMachineSlot = ->
if $scope.events.placable
$scope.events.placable.backgroundColor = 'white'
$scope.events.placable.title = ''
if !$scope.events.placable or $scope.events.placable._id != $scope.selectedEvent._id
$scope.selectedEvent.backgroundColor = '#bbb'
$scope.selectedEvent.title = _t('i_shift')
updateCalendar()
##
# When modifying an already booked reservation, callback when the modification was successfully done.
##
$scope.modifyMachineSlot = ->
$scope.events.placable.title = if $scope.currentUser.role isnt 'admin' then _t('i_ve_reserved') else _t('not_available')
$scope.events.placable.backgroundColor = 'white'
$scope.events.placable.borderColor = $scope.events.modifiable.borderColor
$scope.events.placable.id = $scope.events.modifiable.id
$scope.events.placable.is_reserved = true
$scope.events.placable.can_modify = true
$scope.events.modifiable.backgroundColor = 'white'
$scope.events.modifiable.title = ''
$scope.events.modifiable.borderColor = FREE_SLOT_BORDER_COLOR
$scope.events.modifiable.id = null
$scope.events.modifiable.is_reserved = false
$scope.events.modifiable.can_modify = false
updateCalendar()
##
# Cancel the current booking modification, reseting the whole process
##
$scope.cancelModifyMachineSlot = ->
if $scope.events.placable
$scope.events.placable.backgroundColor = 'white'
$scope.events.placable.title = ''
$scope.events.modifiable.title = if $scope.currentUser.role isnt 'admin' then _t('i_ve_reserved') else _t('not_available')
$scope.events.modifiable.backgroundColor = 'white'
updateCalendar()
##
# Callback to deal with the reservations of the user selected in the dropdown list instead of the current user's
# reservations. (admins only)
##
$scope.updateMember = ->
$scope.plansAreShown = false
$scope.selectedPlan = null
Member.get {id: $scope.ctrl.member.id}, (member) ->
$scope.ctrl.member = member
##
# 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)
$scope.planSelectionTime = new Date()
##
# 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) ->
# toggle selected plan
if $scope.selectedPlan != plan
$scope.selectedPlan = plan
else
$scope.selectedPlan = null
$scope.planSelectionTime = new Date()
##
# Once the reservation is booked (payment process successfully completed), change the event style
# in fullCalendar, update the user's subscription and free-credits if needed
# @param reservation {Object}
##
$scope.afterPayment = (reservation)->
angular.forEach $scope.events.reserved, (machineSlot, key) ->
machineSlot.is_reserved = true
machineSlot.can_modify = true
if $scope.currentUser.role isnt 'admin'
machineSlot.title = _t('i_ve_reserved')
machineSlot.borderColor = BOOKED_SLOT_BORDER_COLOR
updateMachineSlot(machineSlot, reservation, $scope.currentUser)
else
machineSlot.title = _t('not_available')
machineSlot.borderColor = UNAVAILABLE_SLOT_BORDER_COLOR
updateMachineSlot(machineSlot, reservation, $scope.ctrl.member)
machineSlot.backgroundColor = 'white'
if $scope.selectedPlan
$scope.ctrl.member.subscribed_plan = angular.copy($scope.selectedPlan)
Auth._currentUser.subscribed_plan = angular.copy($scope.selectedPlan)
$scope.plansAreShown = false
$scope.selectedPlan = null
refetchCalendar()
##
# To use as callback in Array.prototype.filter to get only enabled plans
##
$scope.filterDisabledPlans = (plan) ->
!plan.disabled
### 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
##
# 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) ->
$scope.selectedEvent = event
$scope.selectionTime = new Date()
##
# Triggered when fullCalendar tries to graphicaly render an event block.
# Append the event tag into the block, just after the event title.
# @see http://fullcalendar.io/docs/event_rendering/eventRender/
##
eventRenderCb = (event, element) ->
if $scope.currentUser.role is 'admin' and event.tags.length > 0
html = ''
for tag in event.tags
html += "<span class='label label-success text-white' title='#{tag.name}'>#{tag.name}</span>"
element.find('.fc-time').append(html)
return
##
# After payment, update the id of the newly reserved slot with the id returned by the server.
# This will allow the user to modify the reservation he just booked. 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
##
# Update the calendar's display to render the new attributes of the events
##
updateCalendar = ->
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
##
# Asynchronously fetch the events from the API and refresh the calendar's view with these new events
##
refetchCalendar = ->
$timeout ->
uiCalendarConfig.calendars.calendar.fullCalendar 'refetchEvents'
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
## !!! MUST BE CALLED AT THE END of the controller
initialize()
]

View File

@ -0,0 +1,647 @@
/* eslint-disable
handle-callback-err,
no-return-assign,
no-undef,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
'use strict';
/* COMMON CODE */
/**
* Provides a set of common callback methods to the $scope parameter. These methods are used
* in the various machines' admin controllers.
*
* Provides :
* - $scope.submited(content)
* - $scope.cancel()
* - $scope.fileinputClass(v)
* - $scope.addFile()
* - $scope.deleteFile(file)
*
* Requires :
* - $scope.machine.machine_files_attributes = []
* - $state (Ui-Router) [ 'app.public.machines_list' ]
*/
class MachinesController {
constructor ($scope, $state) {
/**
* For use with ngUpload (https://github.com/twilson63/ngUpload).
* Intended to be the callback when the upload is done: any raised error will be stacked in the
* $scope.alerts array. If everything goes fine, the user is redirected to the machines list.
* @param content {Object} JSON - The upload's result
*/
$scope.submited = function (content) {
if ((content.id == null)) {
$scope.alerts = [];
angular.forEach(content, function (v, k) {
angular.forEach(function (v, err) {
$scope.alerts.push({
msg: k + ': ' + err,
type: 'danger'
});
});
});
} else {
return $state.go('app.public.machines_list');
}
};
/**
* Changes the current user's view, redirecting him to the machines list
*/
$scope.cancel = function () { $state.go('app.public.machines_list'); };
/**
* For use with 'ng-class', returns the CSS class name for the uploads previews.
* The preview may show a placeholder or the content of the file depending on the upload state.
* @param v {*} any attribute, will be tested for truthiness (see JS evaluation rules)
*/
$scope.fileinputClass = function (v) {
if (v) {
return 'fileinput-exists';
} else {
return 'fileinput-new';
}
};
/**
* This will create a single new empty entry into the machine attachements list.
*/
$scope.addFile = function () { $scope.machine.machine_files_attributes.push({}); };
/**
* This will remove the given file from the machine attachements list. If the file was previously uploaded
* to the server, it will be marked for deletion on the server. Otherwise, it will be simply truncated from
* the attachements array.
* @param file {Object} the file to delete
*/
$scope.deleteFile = function (file) {
const index = $scope.machine.machine_files_attributes.indexOf(file);
if (file.id != null) {
return file._destroy = true;
} else {
return $scope.machine.machine_files_attributes.splice(index, 1);
}
};
}
}
/**
* 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-
*/
const _reserveMachine = function (machine, e) {
const _this = this;
e.preventDefault();
e.stopPropagation();
// retrieve the full machine object
return machine = _this.Machine.get({ id: machine.id }, function () {
// 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 || (machine.trainings.length === 0)) {
return _this.$state.go('app.logged.machines_reserve', { id: machine.slug });
} 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) {
return _this.$uibModal.open({
templateUrl: '<%= asset_path "machines/training_reservation_modal.html" %>',
controller: ['$scope', '$uibModalInstance', function ($scope, $uibModalInstance) {
$scope.machine = machine;
return $scope.cancel = function () { $uibModalInstance.dismiss('cancel'); };
}]
});
// ... but does not have booked the training, tell him to register for a training session first
// unless all associated trainings are disabled
} else {
// if all trainings are disabled, just redirect the user to the reservation calendar
if (machine.trainings.map(function (t) { return t.disabled; }).reduce(function (acc, val) { return acc && val; }, true)) {
return _this.$state.go('app.logged.machines_reserve', { id: machine.slug });
// otherwise open the information modal
} else {
return _this.$uibModal.open({
templateUrl: '<%= asset_path "machines/request_training_modal.html" %>',
controller: ['$scope', '$uibModalInstance', '$state', function ($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 = function () {
let text = '';
angular.forEach($scope.machine.trainings, function (training) {
if (text.length > 0) {
text += _this._t('machines_list._or_the_');
}
return text += training.name.substr(0, 1).toLowerCase() + training.name.substr(1);
});
return text;
};
// modal is closed with validation
$scope.ok = function () {
$state.go('app.logged.trainings_reserve', { id: $scope.machine.trainings[0].id });
return $uibModalInstance.close(machine);
};
// modal is closed with escaping
return $scope.cancel = function (e) {
e.preventDefault();
return $uibModalInstance.dismiss('cancel');
};
}
] });
}
}
// if the user is not logged, open the login modal window
} else {
return _this.$scope.login();
}
}
});
};
/**
* Controller used in the public listing page, allowing everyone to see the list of machines
*/
Application.Controllers.controller('MachinesController', ['$scope', '$state', '_t', 'Machine', '$uibModal', 'machinesPromise',
function ($scope, $state, _t, Machine, $uibModal, machinesPromise) {
// Retrieve the list of machines
$scope.machines = machinesPromise;
/**
* Redirect the user to the machine details page
*/
$scope.showMachine = function (machine) { $state.go('app.public.machines_show', { id: machine.slug }); };
/**
* Callback to book a reservation for the current machine
*/
$scope.reserveMachine = _reserveMachine.bind({
$scope,
$state,
_t,
$uibModal,
Machine
});
// Default: we show only enabled machines
$scope.machineFiltering = 'enabled';
// Available options for filtering machines by status
return $scope.filterDisabled = [
'enabled',
'disabled',
'all'
];
}
]);
/**
* Controller used in the machine creation page (admin)
*/
Application.Controllers.controller('NewMachineController', ['$scope', '$state', 'CSRF', function ($scope, $state, CSRF) {
CSRF.setMetaTags();
// API URL where the form will be posted
$scope.actionUrl = '/api/machines/';
// Form action on the above URL
$scope.method = 'post';
// default machine parameters
$scope.machine =
{ machine_files_attributes: [] };
// Using the MachinesController
return new MachinesController($scope, $state);
}
]);
/**
* Controller used in the machine edition page (admin)
*/
Application.Controllers.controller('EditMachineController', ['$scope', '$state', '$stateParams', 'machinePromise', 'CSRF',
function ($scope, $state, $stateParams, machinePromise, CSRF) {
/* PUBLIC SCOPE */
// API URL where the form will be posted
$scope.actionUrl = `/api/machines/${$stateParams.id}`;
// Form action on the above URL
$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 = machinePromise;
/* PRIVATE SCOPE */
/**
* Kind of constructor: these actions will be realized first when the controller is loaded
*/
const initialize = function () {
CSRF.setMetaTags();
// Using the MachinesController
return new MachinesController($scope, $state);
};
// !!! MUST BE CALLED AT THE END of the controller
return initialize();
}
]);
/**
* Controller used in the machine details page (public)
*/
Application.Controllers.controller('ShowMachineController', ['$scope', '$state', '$uibModal', '$stateParams', '_t', 'Machine', 'growl', 'machinePromise', 'dialogs',
function ($scope, $state, $uibModal, $stateParams, _t, Machine, growl, machinePromise, dialogs) {
// Retrieve the details for the machine id in the URL, if an error occurs redirect the user to the machines list
$scope.machine = machinePromise;
/**
* Callback to delete the current machine (admins only)
*/
$scope.delete = function (machine) {
// check the permissions
if ($scope.currentUser.role !== 'admin') {
console.error(_t('unauthorized_operation'));
} else {
dialogs.confirm({
resolve: {
object () {
return {
title: _t('confirmation_required'),
msg: _t('do_you_really_want_to_delete_this_machine')
};
}
}
}
, function () { // deletion confirmed
// delete the machine then redirect to the machines listing
machine.$delete(
function () { $state.go('app.public.machines_list'); },
function (error) { growl.warning(_t('the_machine_cant_be_deleted_because_it_is_already_reserved_by_some_users')); console.error(error); }
);
});
}
};
/**
* Callback to book a reservation for the current machine
*/
return $scope.reserveMachine = _reserveMachine.bind({
$scope,
$state,
_t,
$uibModal,
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', '$stateParams', '_t', 'moment', 'Auth', '$timeout', 'Member', 'Availability', 'plansPromise', 'groupsPromise', 'machinePromise', 'settingsPromise', 'uiCalendarConfig', 'CalendarConfig',
function ($scope, $stateParams, _t, moment, Auth, $timeout, Member, Availability, plansPromise, groupsPromise, machinePromise, settingsPromise, uiCalendarConfig, CalendarConfig) {
/* PRIVATE STATIC CONSTANTS */
// Slot free to be booked
const FREE_SLOT_BORDER_COLOR = '<%= AvailabilityHelper::MACHINE_COLOR %>';
// Slot already booked by another user
const UNAVAILABLE_SLOT_BORDER_COLOR = '<%= AvailabilityHelper::MACHINE_IS_RESERVED_BY_USER %>';
// Slot already booked by the current user
const BOOKED_SLOT_BORDER_COLOR = '<%= AvailabilityHelper::IS_RESERVED_BY_CURRENT_USER %>';
/* PUBLIC SCOPE */
// bind the machine availabilities with full-Calendar events
$scope.eventSources = [];
// indicates the state of the current view : calendar or plans information
$scope.plansAreShown = false;
// will store the user's plan if he choosed to buy one
$scope.selectedPlan = null;
// the moment when the plan selection changed for the last time, used to trigger changes in the cart
$scope.planSelectionTime = null;
// mapping of fullCalendar events.
$scope.events = {
reserved: [], // Slots that the user wants to book
modifiable: null, // Slot that the user wants to change
placable: null, // Destination slot for the change
paid: [], // Slots that were just booked by the user (transaction ok)
moved: null // Slots that were just moved by the user (change done) -> {newSlot:* oldSlot: *}
};
// the moment when the slot selection changed for the last time, used to trigger changes in the cart
$scope.selectionTime = null;
// the last clicked event in the calender
$scope.selectedEvent = null;
// the application global settings
$scope.settings = settingsPromise;
// list of plans, classified by group
$scope.plansClassifiedByGroup = [];
for (let group of Array.from(groupsPromise)) {
const groupObj = { id: group.id, name: group.name, plans: [] };
for (let plan of Array.from(plansPromise)) {
if (plan.group_id === group.id) { groupObj.plans.push(plan); }
}
$scope.plansClassifiedByGroup.push(groupObj);
}
// the user to deal with, ie. the current user for non-admins
$scope.ctrl =
{ member: {} };
// current machine to reserve
$scope.machine = machinePromise;
// fullCalendar (v2) configuration
$scope.calendarConfig = CalendarConfig({
minTime: moment.duration(moment(settingsPromise.booking_window_start).format('HH:mm:ss')),
maxTime: moment.duration(moment(settingsPromise.booking_window_end).format('HH:mm:ss')),
eventClick (event, jsEvent, view) {
return calendarEventClickCb(event, jsEvent, view);
},
eventRender (event, element, view) {
return eventRenderCb(event, element);
}
});
// Global config: message to the end user concerning the subscriptions rules
$scope.subscriptionExplicationsAlert = settingsPromise.subscription_explications_alert;
// Global config: message to the end user concerning the machine bookings
$scope.machineExplicationsAlert = settingsPromise.machine_explications_alert;
/**
* Change the last selected slot's appearence to looks like 'added to cart'
*/
$scope.markSlotAsAdded = function () {
$scope.selectedEvent.backgroundColor = FREE_SLOT_BORDER_COLOR;
$scope.selectedEvent.title = _t('i_reserve');
return updateCalendar();
};
/**
* Change the last selected slot's appearence to looks like 'never added to cart'
*/
$scope.markSlotAsRemoved = function (slot) {
slot.backgroundColor = 'white';
slot.borderColor = FREE_SLOT_BORDER_COLOR;
slot.title = '';
slot.isValid = false;
slot.id = null;
slot.is_reserved = false;
slot.can_modify = false;
slot.offered = false;
return updateCalendar();
};
/**
* Callback when a slot was successfully cancelled. Reset the slot style as 'ready to book'
*/
$scope.slotCancelled = function () { $scope.markSlotAsRemoved($scope.selectedEvent); };
/**
* Change the last selected slot's appearence to looks like 'currently looking for a new destination to exchange'
*/
$scope.markSlotAsModifying = function () {
$scope.selectedEvent.backgroundColor = '#eee';
$scope.selectedEvent.title = _t('i_change');
return updateCalendar();
};
/**
* Change the last selected slot's appearence to looks like 'the slot being exchanged will take this place'
*/
$scope.changeModifyMachineSlot = function () {
if ($scope.events.placable) {
$scope.events.placable.backgroundColor = 'white';
$scope.events.placable.title = '';
}
if (!$scope.events.placable || ($scope.events.placable._id !== $scope.selectedEvent._id)) {
$scope.selectedEvent.backgroundColor = '#bbb';
$scope.selectedEvent.title = _t('i_shift');
}
return updateCalendar();
};
/**
* When modifying an already booked reservation, callback when the modification was successfully done.
*/
$scope.modifyMachineSlot = function () {
$scope.events.placable.title = $scope.currentUser.role !== 'admin' ? _t('i_ve_reserved') : _t('not_available');
$scope.events.placable.backgroundColor = 'white';
$scope.events.placable.borderColor = $scope.events.modifiable.borderColor;
$scope.events.placable.id = $scope.events.modifiable.id;
$scope.events.placable.is_reserved = true;
$scope.events.placable.can_modify = true;
$scope.events.modifiable.backgroundColor = 'white';
$scope.events.modifiable.title = '';
$scope.events.modifiable.borderColor = FREE_SLOT_BORDER_COLOR;
$scope.events.modifiable.id = null;
$scope.events.modifiable.is_reserved = false;
$scope.events.modifiable.can_modify = false;
return updateCalendar();
};
/**
* Cancel the current booking modification, reseting the whole process
*/
$scope.cancelModifyMachineSlot = function () {
if ($scope.events.placable) {
$scope.events.placable.backgroundColor = 'white';
$scope.events.placable.title = '';
}
$scope.events.modifiable.title = $scope.currentUser.role !== 'admin' ? _t('i_ve_reserved') : _t('not_available');
$scope.events.modifiable.backgroundColor = 'white';
return updateCalendar();
};
/**
* Callback to deal with the reservations of the user selected in the dropdown list instead of the current user's
* reservations. (admins only)
*/
$scope.updateMember = function () {
$scope.plansAreShown = false;
$scope.selectedPlan = null;
Member.get({ id: $scope.ctrl.member.id }, function (member) { $scope.ctrl.member = member; });
};
/**
* 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 = function (e) {
e.preventDefault();
$scope.plansAreShown = false;
$scope.selectPlan($scope.selectedPlan);
return $scope.planSelectionTime = new Date();
};
/**
* Switch the user's view from the reservation agenda to the plan subscription
*/
$scope.showPlans = function () { $scope.plansAreShown = true; };
/**
* Add the provided plan to the current shopping cart
* @param plan {Object} the plan to subscribe
*/
$scope.selectPlan = function (plan) {
// toggle selected plan
if ($scope.selectedPlan !== plan) {
$scope.selectedPlan = plan;
} else {
$scope.selectedPlan = null;
}
return $scope.planSelectionTime = new Date();
};
/**
* Once the reservation is booked (payment process successfully completed), change the event style
* in fullCalendar, update the user's subscription and free-credits if needed
* @param reservation {Object}
*/
$scope.afterPayment = function (reservation) {
angular.forEach($scope.events.reserved, function (machineSlot, key) {
machineSlot.is_reserved = true;
machineSlot.can_modify = true;
if ($scope.currentUser.role !== '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);
}
return machineSlot.backgroundColor = 'white';
});
if ($scope.selectedPlan) {
$scope.ctrl.member.subscribed_plan = angular.copy($scope.selectedPlan);
Auth._currentUser.subscribed_plan = angular.copy($scope.selectedPlan);
$scope.plansAreShown = false;
$scope.selectedPlan = null;
}
return refetchCalendar();
};
/**
* To use as callback in Array.prototype.filter to get only enabled plans
*/
$scope.filterDisabledPlans = function (plan) { return !plan.disabled; };
/* PRIVATE SCOPE */
/**
* Kind of constructor: these actions will be realized first when the controller is loaded
*/
const initialize = function () {
Availability.machine({ machineId: $stateParams.id }, function (availabilities) {
$scope.eventSources.push({
events: availabilities,
textColor: 'black'
});
});
if ($scope.currentUser.role !== 'admin') {
return $scope.ctrl.member = $scope.currentUser;
}
};
/**
* 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).
*/
var calendarEventClickCb = function (event, jsEvent, view) {
$scope.selectedEvent = event;
return $scope.selectionTime = new Date();
};
/**
* Triggered when fullCalendar tries to graphicaly render an event block.
* Append the event tag into the block, just after the event title.
* @see http://fullcalendar.io/docs/event_rendering/eventRender/
*/
var eventRenderCb = function (event, element) {
if (($scope.currentUser.role === 'admin') && (event.tags.length > 0)) {
let html = '';
for (let tag of Array.from(event.tags)) {
html += `<span class='label label-success text-white' title='${tag.name}'>${tag.name}</span>`;
}
element.find('.fc-time').append(html);
}
};
/**
* 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
*/
var updateMachineSlot = function (slot, reservation, user) {
angular.forEach(reservation.slots, function (s) {
if (slot.start.isSame(s.start_at)) {
slot.id = s.id;
return slot.user = user;
}
});
};
/**
* Update the calendar's display to render the new attributes of the events
*/
var updateCalendar = function () { uiCalendarConfig.calendars.calendar.fullCalendar('rerenderEvents'); };
/**
* Asynchronously fetch the events from the API and refresh the calendar's view with these new events
*/
var refetchCalendar = function () {
$timeout(function () {
uiCalendarConfig.calendars.calendar.fullCalendar('refetchEvents');
return uiCalendarConfig.calendars.calendar.fullCalendar('rerenderEvents');
});
};
// !!! MUST BE CALLED AT THE END of the controller
return initialize();
}
]);

View File

@ -1,126 +0,0 @@
'use strict'
##
# Navigation controller. List the links availables in the left navigation pane and their icon.
##
Application.Controllers.controller "MainNavController", ["$scope", "$location", "$cookies", ($scope, $location, $cookies) ->
## Common links (public application)
$scope.navLinks = [
{
state: 'app.public.home'
linkText: 'home'
linkIcon: 'home'
}
{
state: 'app.public.machines_list'
linkText: 'reserve_a_machine'
linkIcon: 'cogs'
}
{
state: 'app.public.trainings_list'
linkText: 'trainings_registrations'
linkIcon: 'graduation-cap'
}
{
state: 'app.public.events_list'
linkText: 'events_registrations'
linkIcon: 'tags'
}
{
state: 'app.public.calendar'
linkText: 'public_calendar'
linkIcon: 'calendar'
}
{
state: 'app.public.projects_list'
linkText: 'projects_gallery'
linkIcon: 'th'
}
]
unless Fablab.withoutPlans
$scope.navLinks.push({
state: 'app.public.plans'
linkText: 'subscriptions'
linkIcon: 'credit-card'
})
unless Fablab.withoutSpaces
$scope.navLinks.splice(3, 0, {
state: 'app.public.spaces_list'
linkText: 'reserve_a_space'
linkIcon: 'rocket'
})
Fablab.adminNavLinks = Fablab.adminNavLinks || []
adminNavLinks = [
{
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: '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: 'manage_the_events'
linkIcon: 'tags'
}
{
state: 'app.public.machines_list'
linkText: 'manage_the_machines'
linkIcon: 'cogs'
}
{
state: 'app.admin.project_elements'
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'
}
{
state: 'app.admin.open_api_clients'
linkText: 'open_api_clients'
linkIcon: 'cloud'
}
].concat(Fablab.adminNavLinks)
$scope.adminNavLinks = adminNavLinks
unless Fablab.withoutSpaces
$scope.adminNavLinks.splice(7, 0, {
state: 'app.public.spaces_list'
linkText: 'manage_the_spaces'
linkIcon: 'rocket'
})
]

View File

@ -0,0 +1,138 @@
/* eslint-disable
no-undef,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
'use strict';
/**
* Navigation controller. List the links availables in the left navigation pane and their icon.
*/
Application.Controllers.controller('MainNavController', ['$scope', '$location', '$cookies', function ($scope, $location, $cookies) {
// Common links (public application)
$scope.navLinks = [
{
state: 'app.public.home',
linkText: 'home',
linkIcon: 'home'
},
{
state: 'app.public.machines_list',
linkText: 'reserve_a_machine',
linkIcon: 'cogs'
},
{
state: 'app.public.trainings_list',
linkText: 'trainings_registrations',
linkIcon: 'graduation-cap'
},
{
state: 'app.public.events_list',
linkText: 'events_registrations',
linkIcon: 'tags'
},
{
state: 'app.public.calendar',
linkText: 'public_calendar',
linkIcon: 'calendar'
},
{
state: 'app.public.projects_list',
linkText: 'projects_gallery',
linkIcon: 'th'
}
];
if (!Fablab.withoutPlans) {
$scope.navLinks.push({
state: 'app.public.plans',
linkText: 'subscriptions',
linkIcon: 'credit-card'
});
}
if (!Fablab.withoutSpaces) {
$scope.navLinks.splice(3, 0, {
state: 'app.public.spaces_list',
linkText: 'reserve_a_space',
linkIcon: 'rocket'
});
}
Fablab.adminNavLinks = Fablab.adminNavLinks || [];
const 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: '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: 'manage_the_events',
linkIcon: 'tags'
},
{
state: 'app.public.machines_list',
linkText: 'manage_the_machines',
linkIcon: 'cogs'
},
{
state: 'app.admin.project_elements',
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'
},
{
state: 'app.admin.open_api_clients',
linkText: 'open_api_clients',
linkIcon: 'cloud'
}
].concat(Fablab.adminNavLinks);
$scope.adminNavLinks = adminNavLinks;
if (!Fablab.withoutSpaces) {
return $scope.adminNavLinks.splice(7, 0, {
state: 'app.public.spaces_list',
linkText: 'manage_the_spaces',
linkIcon: 'rocket'
});
}
}
]);

View File

@ -1,297 +0,0 @@
'use strict'
##
# Controller used in the members listing page
##
Application.Controllers.controller "MembersController", ["$scope", 'Member', 'membersPromise', ($scope, Member, membersPromise) ->
### PRIVATE STATIC CONSTANTS ###
# number of invoices loaded each time we click on 'load more...'
MEMBERS_PER_PAGE = 10
### PUBLIC SCOPE ###
## currently displayed page of members
$scope.page = 1
## members list
$scope.members = membersPromise
# true when all members are loaded
$scope.noMoreResults = false
##
# Callback for the 'load more' button.
# Will load the next results of the current search, if any
##
$scope.showNextMembers = ->
$scope.page += 1
Member.query {
requested_attributes:'[profile]',
page: $scope.page,
size: MEMBERS_PER_PAGE
}, (members) ->
$scope.members = $scope.members.concat(members)
if (!members[0] || members[0].maxMembers <= $scope.members.length)
$scope.noMoreResults = true
### PRIVATE SCOPE ###
##
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
if (!membersPromise[0] || membersPromise[0].maxMembers <= $scope.members.length)
$scope.noMoreResults = true
## !!! MUST BE CALLED AT THE END of the controller
initialize()
]
##
# Controller used when editing the current user's profile
##
Application.Controllers.controller "EditProfileController", ["$scope", "$rootScope", "$state", "$window", "Member", "Auth", "Session", "activeProviderPromise", 'growl', 'dialogs', 'CSRF', 'memberPromise', 'groups', '_t'
, ($scope, $rootScope, $state, $window, 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.filter (g) -> !g.disabled
## Form action on the above URL
$scope.method = 'patch'
## Current user's profile
$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 = {}
## Should the passord be modified?
$scope.password =
change: false
## Angular-Bootstrap datepicker configuration for birthday
$scope.datePicker =
format: Fablab.uibDateFormat
opened: false # default: datePicker is not shown
options:
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
$rootScope.currentUser = user
Auth._currentUser.group_id = user.group_id
$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)
##
# 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.currentUser.profile.user_avatar = content.profile.user_avatar
Auth._currentUser.profile.user_avatar = content.profile.user_avatar
$scope.currentUser.name = content.name
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.
# @param v {*} any attribute, will be tested for truthiness (see JS evaluation rules)
##
$scope.fileinputClass = (v)->
if v
'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 =
total: 0
unread: 0
$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()
]
##
# Controller used on the public user's profile page (seeing another user's profile)
##
Application.Controllers.controller "ShowProfileController", ["$scope", 'memberPromise', 'SocialNetworks', ($scope, memberPromise, SocialNetworks) ->
## Selected user's information
$scope.user = memberPromise # DEPENDENCY WITH NAVINUM GAMIFICATION PLUGIN !!!!
## List of social networks associated with this user and toggle 'show all' state
$scope.social =
showAllLinks: false
networks: SocialNetworks
### PRIVATE SCOPE ###
##
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
$scope.social.networks = filterNetworks()
##
# Filter social network or website that are associated with the profile of the user provided in promise
# and return the filtered networks
# @return {Array}
##
filterNetworks = ->
networks = [];
for network in SocialNetworks
if $scope.user.profile[network] && $scope.user.profile[network].length > 0
networks.push(network);
networks
## !!! MUST BE CALLED AT THE END of the controller
initialize()
]

View File

@ -0,0 +1,314 @@
/* eslint-disable
no-return-assign,
no-undef,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
'use strict';
/**
* Controller used in the members listing page
*/
Application.Controllers.controller('MembersController', ['$scope', 'Member', 'membersPromise', function ($scope, Member, membersPromise) {
/* PRIVATE STATIC CONSTANTS */
// number of invoices loaded each time we click on 'load more...'
const MEMBERS_PER_PAGE = 10;
/* PUBLIC SCOPE */
// currently displayed page of members
$scope.page = 1;
// members list
$scope.members = membersPromise;
// true when all members are loaded
$scope.noMoreResults = false;
/**
* Callback for the 'load more' button.
* Will load the next results of the current search, if any
*/
$scope.showNextMembers = function () {
$scope.page += 1;
return Member.query({
requested_attributes: '[profile]',
page: $scope.page,
size: MEMBERS_PER_PAGE
}, function (members) {
$scope.members = $scope.members.concat(members);
if (!members[0] || (members[0].maxMembers <= $scope.members.length)) {
return $scope.noMoreResults = true;
}
});
};
/* PRIVATE SCOPE */
/**
* Kind of constructor: these actions will be realized first when the controller is loaded
*/
const initialize = function () {
if (!membersPromise[0] || (membersPromise[0].maxMembers <= $scope.members.length)) {
return $scope.noMoreResults = true;
}
};
// !!! MUST BE CALLED AT THE END of the controller
return initialize();
}
]);
/**
* Controller used when editing the current user's profile
*/
Application.Controllers.controller('EditProfileController', ['$scope', '$rootScope', '$state', '$window', 'Member', 'Auth', 'Session', 'activeProviderPromise', 'growl', 'dialogs', 'CSRF', 'memberPromise', 'groups', '_t',
function ($scope, $rootScope, $state, $window, 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.filter(g => !g.disabled);
// Form action on the above URL
$scope.method = 'patch';
// Current user's profile
$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 = {};
// Should the passord be modified?
$scope.password =
{ change: false };
// Angular-Bootstrap datepicker configuration for birthday
$scope.datePicker = {
format: Fablab.uibDateFormat,
opened: false, // default: datePicker is not shown
options: {
startingDay: Fablab.weekStartingDay
}
};
/**
* Return the group object, identified by the ID set in $scope.userGroup
*/
$scope.getUserGroup = function () {
for (let group of Array.from($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 } }, function (user) {
$scope.user = user;
$rootScope.currentUser = user;
Auth._currentUser.group_id = user.group_id;
$scope.group.change = false;
return growl.success(_t('your_group_has_been_successfully_changed'));
}
, function (err) {
growl.error(_t('an_unexpected_error_prevented_your_group_from_being_changed'));
return console.error(err);
});
/**
* Callback to diplay the datepicker as a dropdown when clicking on the input field
* @param $event {Object} jQuery event object
*/
$scope.openDatePicker = function ($event) {
$event.preventDefault();
$event.stopPropagation();
return $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 = function (content) {
if ((content.id == null)) {
$scope.alerts = [];
return angular.forEach(content, (v, k) =>
angular.forEach(v, err =>
$scope.alerts.push({
msg: k + ': ' + err,
type: 'danger'
})
)
);
} else {
$scope.currentUser.profile.user_avatar = content.profile.user_avatar;
Auth._currentUser.profile.user_avatar = content.profile.user_avatar;
$scope.currentUser.name = content.name;
Auth._currentUser.name = content.name;
$scope.currentUser = content;
Auth._currentUser = content;
$rootScope.currentUser = content;
return $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 () {
return {
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(function () {
$state.go('app.public.home');
return growl.success(_t('your_user_account_has_been_successfully_deleted_goodbye'));
})
, function (error) {
console.log(error);
return 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.
* @param v {*} any attribute, will be tested for truthiness (see JS evaluation rules)
*/
$scope.fileinputClass = function (v) {
if (v) {
return 'fileinput-exists';
} else {
return '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(function (oldUser) {
Session.destroy();
$rootScope.currentUser = null;
$rootScope.toCheckNotifications = false;
$scope.notifications = {
total: 0,
unread: 0
};
return $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
*/
const initialize = function () {
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
return angular.forEach(activeProviderPromise.mapping, map => $scope.preventField[map] = true);
};
// !!! MUST BE CALLED AT THE END of the controller
return initialize();
}
]);
/**
* Controller used on the public user's profile page (seeing another user's profile)
*/
Application.Controllers.controller('ShowProfileController', ['$scope', 'memberPromise', 'SocialNetworks', function ($scope, memberPromise, SocialNetworks) {
// Selected user's information
$scope.user = memberPromise; // DEPENDENCY WITH NAVINUM GAMIFICATION PLUGIN !!!!
// List of social networks associated with this user and toggle 'show all' state
$scope.social = {
showAllLinks: false,
networks: SocialNetworks
};
/* PRIVATE SCOPE */
/**
* Kind of constructor: these actions will be realized first when the controller is loaded
*/
const initialize = () => $scope.social.networks = filterNetworks();
/**
* Filter social network or website that are associated with the profile of the user provided in promise
* and return the filtered networks
* @return {Array}
*/
var filterNetworks = function () {
const networks = [];
for (let network of Array.from(SocialNetworks)) {
if ($scope.user.profile[network] && ($scope.user.profile[network].length > 0)) {
networks.push(network);
}
}
return networks;
};
// !!! MUST BE CALLED AT THE END of the controller
return initialize();
}
]);

View File

@ -1,112 +0,0 @@
'use strict'
##
# Controller used in notifications page
# inherits $scope.$parent.notifications (global notifications state) from ApplicationController
##
Application.Controllers.controller "NotificationsController", ["$scope", 'Notification', ($scope, Notification) ->
### PRIVATE STATIC CONSTANTS ###
# Number of notifications added to the page when the user clicks on 'load next notifications'
NOTIFICATIONS_PER_PAGE = 15
### PUBLIC SCOPE ###
## Array containg the archived notifications (already read)
$scope.notificationsRead = []
## Array containg the new notifications (not read)
$scope.notificationsUnread = []
## Total number of notifications for the current user
$scope.total = 0
## Total number of unread notifications for the current user
$scope.totalUnread = 0
## By default, the pagination mode is activated to limit the page size
$scope.paginateActive = true
## The currently displayed page number
$scope.page = 1
##
# 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} see https://docs.angularjs.org/guide/expression#-event-
##
$scope.markAsRead = (notification, e) ->
e.preventDefault()
Notification.update {id: notification.id},
id: notification.id
is_read: true
, (updatedNotif) ->
# remove notif from unreads
index = $scope.notificationsUnread.indexOf(notification)
$scope.notificationsUnread.splice(index,1)
# add update notif to read
$scope.notificationsRead.push updatedNotif
# update counters
$scope.$parent.notifications.unread -= 1
$scope.totalUnread -= 1
##
# Mark every unread notifications as read and move them for the unread list to to read array.
##
$scope.markAllAsRead = ->
Notification.update {}
, -> # success
# add notifs to read
angular.forEach $scope.notificationsUnread, (n)->
n.is_read = true
$scope.notificationsRead.push n
# clear unread
$scope.notificationsUnread = []
# update counters
$scope.$parent.notifications.unread = 0
$scope.totalUnread = 0
##
# Request the server to retrieve the next notifications and add them
# to their corresponding notifications list (read or unread).
##
$scope.addMoreNotifications = ->
Notification.query {page: $scope.page}, (notifications) ->
$scope.total = notifications.totals.total
$scope.totalUnread = notifications.totals.unread
angular.forEach notifications.notifications, (notif) ->
if notif.is_read
$scope.notificationsRead.push(notif)
else
$scope.notificationsUnread.push(notif)
$scope.paginateActive = (notifications.totals.total > ($scope.notificationsRead.length + $scope.notificationsUnread.length))
$scope.page += 1
### PRIVATE SCOPE ###
##
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
$scope.addMoreNotifications()
## !!! MUST BE CALLED AT THE END of the controller
initialize()
]

View File

@ -0,0 +1,113 @@
/* eslint-disable
no-return-assign,
no-undef,
no-unused-vars,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
'use strict';
/**
* Controller used in notifications page
* inherits $scope.$parent.notifications (global notifications state) from ApplicationController
*/
Application.Controllers.controller('NotificationsController', ['$scope', 'Notification', function ($scope, Notification) {
/* PUBLIC SCOPE */
// Array containg the archived notifications (already read)
$scope.notificationsRead = [];
// Array containg the new notifications (not read)
$scope.notificationsUnread = [];
// Total number of notifications for the current user
$scope.total = 0;
// Total number of unread notifications for the current user
$scope.totalUnread = 0;
// By default, the pagination mode is activated to limit the page size
$scope.paginateActive = true;
// The currently displayed page number
$scope.page = 1;
/**
* 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} see https://docs.angularjs.org/guide/expression#-event-
*/
$scope.markAsRead = function (notification, e) {
e.preventDefault();
return Notification.update({ id: notification.id }, {
id: notification.id,
is_read: true
}
, function (updatedNotif) {
// remove notif from unreads
const index = $scope.notificationsUnread.indexOf(notification);
$scope.notificationsUnread.splice(index, 1);
// add update notif to read
$scope.notificationsRead.push(updatedNotif);
// update counters
$scope.$parent.notifications.unread -= 1;
return $scope.totalUnread -= 1;
});
};
/**
* Mark every unread notifications as read and move them for the unread list to to read array.
*/
$scope.markAllAsRead = () =>
Notification.update({}
, function () { // success
// add notifs to read
angular.forEach($scope.notificationsUnread, function (n) {
n.is_read = true;
return $scope.notificationsRead.push(n);
});
// clear unread
$scope.notificationsUnread = [];
// update counters
$scope.$parent.notifications.unread = 0;
return $scope.totalUnread = 0;
});
/**
* Request the server to retrieve the next notifications and add them
* to their corresponding notifications list (read or unread).
*/
$scope.addMoreNotifications = function () {
Notification.query({ page: $scope.page }, function (notifications) {
$scope.total = notifications.totals.total;
$scope.totalUnread = notifications.totals.unread;
angular.forEach(notifications.notifications, function (notif) {
if (notif.is_read) {
return $scope.notificationsRead.push(notif);
} else {
return $scope.notificationsUnread.push(notif);
}
});
return $scope.paginateActive = (notifications.totals.total > ($scope.notificationsRead.length + $scope.notificationsUnread.length));
});
return $scope.page += 1;
};
/* PRIVATE SCOPE */
/**
* Kind of constructor: these actions will be realized first when the controller is loaded
*/
const initialize = () => $scope.addMoreNotifications();
// !!! MUST BE CALLED AT THE END of the controller
return initialize();
}
]);

View File

@ -1,344 +0,0 @@
'use strict'
Application.Controllers.controller "PlansIndexController", ["$scope", "$rootScope", "$state", '$uibModal', 'Auth', 'dialogs', 'growl', 'plansPromise', 'groupsPromise', 'Subscription', 'Member', 'subscriptionExplicationsPromise', '_t', 'Wallet', 'helpers'
, ($scope, $rootScope, $state, $uibModal, Auth, dialogs, growl, plansPromise, groupsPromise, Subscription, Member, subscriptionExplicationsPromise, _t, Wallet, helpers) ->
### PUBLIC SCOPE ###
## list of groups
$scope.groups = groupsPromise.filter (g) -> g.slug != 'admins' & !g.disabled
## default : do not show the group changing form
## group ID of the current/selected user
$scope.group =
change: false
id: null
## list of plans, classified by group
$scope.plansClassifiedByGroup = []
for group in $scope.groups
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.paid =
plan: null
## plan to subscribe (shopping cart)
$scope.selectedPlan = null
## Discount coupon to apply to the basket, if any
$scope.coupon =
applied: null
## Storage for the total price (plan price + coupon, if any)
$scope.cart =
total: null
## text that appears in the bottom-right box of the page (subscriptions rules details)
$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.paid.plan = null
$scope.group.change = false
Member.get {id: $scope.ctrl.member.id}, (member) ->
$scope.ctrl.member = member
$scope.group.id = $scope.ctrl.member.group_id
##
# 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
updateCartPrice()
else
$scope.selectedPlan = null
else
$scope.login()
##
# Callback to trigger the payment process of the subscription
##
$scope.openSubscribePlanModal = ->
Wallet.getWalletByUser {user_id: $scope.ctrl.member.id}, (wallet) ->
amountToPay = helpers.getAmountToPay($scope.cart.total, wallet.amount)
if $scope.currentUser.role isnt 'admin' and amountToPay > 0
payByStripe()
else
if $scope.currentUser.role is 'admin' or amountToPay is 0
payOnSite()
##
# Return the group object, identified by the ID set in $scope.group.id
##
$scope.getUserGroup = ->
for group in $scope.groups
if group.id == $scope.group.id
return group
##
# Change the group of the current/selected user to the one set in $scope.group.id
##
$scope.selectGroup = ->
Member.update {id: $scope.ctrl.member.id}, {user: {group_id: $scope.group.id}}, (user) ->
$scope.ctrl.member = user
$scope.group.change = false
if $scope.currentUser.role isnt 'admin'
$rootScope.currentUser = user
Auth._currentUser.group_id = user.group_id
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 and user.profile
if user.profile.gender == "true" then 'male' else 'female'
else 'other'
##
# Test if the provided date is in the future
# @param dateTime {Date}
# @return {boolean}
##
$scope.isInFuture = (dateTime)->
if moment().diff(moment(dateTime)) < 0
true
else
false
##
# To use as callback in Array.prototype.filter to get only enabled plans
##
$scope.filterDisabledPlans = (plan) ->
!plan.disabled
### 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.paid.plan = $scope.currentUser.subscribed_plan
$scope.group.id = $scope.currentUser.group_id
$scope.$on 'devise:new-session', (event, user)->
$scope.ctrl.member = user
# watch when a coupon is applied to re-compute the total price
$scope.$watch 'coupon.applied', (newValue, oldValue) ->
unless newValue == null and oldValue == null
updateCartPrice()
##
# Compute the total amount for the current reservation according to the previously set parameters
# and assign the result in $scope.reserve.amountTotal
##
updateCartPrice = ->
# first we check that a user was selected
if Object.keys($scope.ctrl.member).length > 0
$scope.cart.total = $scope.selectedPlan.amount
# apply the coupon if any
if $scope.coupon.applied
if $scope.coupon.applied.type == 'percent_off'
discount = $scope.cart.total * $scope.coupon.applied.percent_off / 100
else if $scope.coupon.applied.type == 'amount_off'
discount = $scope.coupon.applied.amount_off
$scope.cart.total -= discount
else
$scope.reserve.amountTotal = null
##
# 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
price: -> $scope.cart.total
wallet: ->
Wallet.getWalletByUser({user_id: $scope.ctrl.member.id}).$promise
coupon: -> $scope.coupon.applied
controller: ['$scope', '$uibModalInstance', '$state', 'selectedPlan', 'member', 'price', 'Subscription', 'CustomAsset', 'wallet', 'helpers', '$filter', 'coupon',
($scope, $uibModalInstance, $state, selectedPlan, member, price, Subscription, CustomAsset, wallet, helpers, $filter, coupon) ->
# User's wallet amount
$scope.walletAmount = wallet.amount
# Final price to pay by the user
$scope.amount = helpers.getAmountToPay(price, wallet.amount)
# The plan that the user is about to subscribe
$scope.selectedPlan = selectedPlan
# Used in wallet info template to interpolate some translations
$scope.numberFilter = $filter('number')
# retrieve the CGV
CustomAsset.get {name: 'cgv-file'}, (cgv) ->
$scope.cgv = cgv.custom_asset
##
# Callback for click on the 'proceed' button.
# Handle the stripe's card tokenization process response and save the subscription to the API with the
# card token just created.
##
$scope.payment = (status, response) ->
if response.error
growl.error(response.error.message)
else
$scope.attempting = true
Subscription.save
coupon_code: (coupon.code if coupon)
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.paid.plan = angular.copy($scope.selectedPlan)
$scope.selectedPlan = null
$scope.coupon.applied = 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
price: -> $scope.cart.total
wallet: ->
Wallet.getWalletByUser({user_id: $scope.ctrl.member.id}).$promise
coupon: -> $scope.coupon.applied
controller: ['$scope', '$uibModalInstance', '$state', 'selectedPlan', 'member', 'price', 'Subscription', 'wallet', 'helpers', '$filter', 'coupon',
($scope, $uibModalInstance, $state, selectedPlan, member, price, Subscription, wallet, helpers, $filter, coupon) ->
# user wallet amount
$scope.walletAmount = wallet.amount
# subcription price, coupon subtracted if any
$scope.price = price
# price to pay
$scope.amount = helpers.getAmountToPay($scope.price, wallet.amount)
# Used in wallet info template to interpolate some translations
$scope.numberFilter = $filter('number')
# The plan that the user is about to subscribe
$scope.plan = selectedPlan
# The member who is subscribing a plan
$scope.member = member
# Button label
if $scope.amount > 0
$scope.validButtonName = _t('confirm_payment_of_html', {ROLE:$scope.currentUser.role, AMOUNT:$filter('currency')($scope.amount)}, "messageformat")
else
if price.price > 0 and $scope.walletAmount == 0
$scope.validButtonName = _t('confirm_payment_of_html', {ROLE:$scope.currentUser.role, AMOUNT:$filter('currency')(price.price)}, "messageformat")
else
$scope.validButtonName = _t('confirm')
##
# Callback for the 'proceed' button.
# Save the subscription to the API
##
$scope.ok = ->
$scope.attempting = true
Subscription.save
coupon_code: (coupon.code if coupon)
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
##
# Callback for the 'cancel' button.
# Close the modal box.
##
$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)
$scope.ctrl.member = null
$scope.paid.plan = angular.copy($scope.selectedPlan)
$scope.selectedPlan = null
$scope.coupon.applied = null
## !!! MUST BE CALLED AT THE END of the controller
initialize()
]

View File

@ -0,0 +1,383 @@
/* eslint-disable
no-return-assign,
no-undef,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
'use strict';
Application.Controllers.controller('PlansIndexController', ['$scope', '$rootScope', '$state', '$uibModal', 'Auth', 'dialogs', 'growl', 'plansPromise', 'groupsPromise', 'Subscription', 'Member', 'subscriptionExplicationsPromise', '_t', 'Wallet', 'helpers',
function ($scope, $rootScope, $state, $uibModal, Auth, dialogs, growl, plansPromise, groupsPromise, Subscription, Member, subscriptionExplicationsPromise, _t, Wallet, helpers) {
/* PUBLIC SCOPE */
// list of groups
$scope.groups = groupsPromise.filter(function (g) { return (g.slug !== 'admins') & !g.disabled; });
// default : do not show the group changing form
// group ID of the current/selected user
$scope.group = {
change: false,
id: null
};
// list of plans, classified by group
$scope.plansClassifiedByGroup = [];
for (var group of Array.from($scope.groups)) {
const groupObj = { id: group.id, name: group.name, plans: [] };
for (let plan of Array.from(plansPromise)) {
if (plan.group_id === group.id) { groupObj.plans.push(plan); }
}
$scope.plansClassifiedByGroup.push(groupObj);
}
// user to deal with
$scope.ctrl = {
member: null,
member_id: null
};
// already subscribed plan of the current user
$scope.paid =
{ plan: null };
// plan to subscribe (shopping cart)
$scope.selectedPlan = null;
// Discount coupon to apply to the basket, if any
$scope.coupon =
{ applied: null };
// Storage for the total price (plan price + coupon, if any)
$scope.cart =
{ total: null };
// text that appears in the bottom-right box of the page (subscriptions rules details)
$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 = function () {
$scope.selectedPlan = null;
$scope.paid.plan = null;
$scope.group.change = false;
return Member.get({ id: $scope.ctrl.member.id }, function (member) {
$scope.ctrl.member = member;
return $scope.group.id = $scope.ctrl.member.group_id;
});
};
/**
* Add the provided plan to the shopping basket
* @param plan {Object} The plan to subscribe to
*/
$scope.selectPlan = function (plan) {
if ($scope.isAuthenticated()) {
if ($scope.selectedPlan !== plan) {
$scope.selectedPlan = plan;
updateCartPrice();
} else {
$scope.selectedPlan = null;
}
} else {
$scope.login();
}
};
/**
* Callback to trigger the payment process of the subscription
*/
$scope.openSubscribePlanModal = function () {
Wallet.getWalletByUser({ user_id: $scope.ctrl.member.id }, function (wallet) {
const amountToPay = helpers.getAmountToPay($scope.cart.total, wallet.amount);
if (($scope.currentUser.role !== 'admin') && (amountToPay > 0)) {
return payByStripe();
} else {
if (($scope.currentUser.role === 'admin') || (amountToPay === 0)) {
return payOnSite();
}
}
});
};
/**
* Return the group object, identified by the ID set in $scope.group.id
*/
$scope.getUserGroup = function () {
for (group of Array.from($scope.groups)) {
if (group.id === $scope.group.id) {
return group;
}
}
};
/**
* Change the group of the current/selected user to the one set in $scope.group.id
*/
$scope.selectGroup = function () {
Member.update({ id: $scope.ctrl.member.id }, { user: { group_id: $scope.group.id } }, function (user) {
$scope.ctrl.member = user;
$scope.group.change = false;
$scope.selectedPlan = null;
if ($scope.currentUser.role !== 'admin') {
$rootScope.currentUser = user;
Auth._currentUser.group_id = user.group_id;
growl.success(_t('your_group_was_successfully_changed'));
} else {
growl.success(_t('the_user_s_group_was_successfully_changed'));
}
}
, function (err) {
if ($scope.currentUser.role !== '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 = function (user) {
if (user && user.profile) {
if (user.profile.gender === 'true') { return 'male'; } else { return 'female'; }
} else { return 'other'; }
};
/**
* Test if the provided date is in the future
* @param dateTime {Date}
* @return {boolean}
*/
$scope.isInFuture = function (dateTime) {
return (moment().diff(moment(dateTime)) < 0);
};
/**
* To use as callback in Array.prototype.filter to get only enabled plans
*/
$scope.filterDisabledPlans = function (plan) { return !plan.disabled; };
/* PRIVATE SCOPE */
/**
* Kind of constructor: these actions will be realized first when the controller is loaded
*/
const initialize = function () {
if ($scope.currentUser) {
if ($scope.currentUser.role !== 'admin') {
$scope.ctrl.member = $scope.currentUser;
$scope.paid.plan = $scope.currentUser.subscribed_plan;
$scope.group.id = $scope.currentUser.group_id;
}
}
$scope.$on('devise:new-session', function (event, user) { $scope.ctrl.member = user; });
// watch when a coupon is applied to re-compute the total price
return $scope.$watch('coupon.applied', function (newValue, oldValue) {
if ((newValue !== null) || (oldValue !== null)) {
return updateCartPrice();
}
});
};
/**
* Compute the total amount for the current reservation according to the previously set parameters
* and assign the result in $scope.reserve.amountTotal
*/
var updateCartPrice = function () {
// first we check that a user was selected
if (Object.keys($scope.ctrl.member).length > 0) {
$scope.cart.total = $scope.selectedPlan.amount;
// apply the coupon if any
if ($scope.coupon.applied) {
let discount;
if ($scope.coupon.applied.type === 'percent_off') {
discount = ($scope.cart.total * $scope.coupon.applied.percent_off) / 100;
} else if ($scope.coupon.applied.type === 'amount_off') {
discount = $scope.coupon.applied.amount_off;
}
return $scope.cart.total -= discount;
}
} else {
return $scope.reserve.amountTotal = null;
}
};
/**
* Open a modal window which trigger the stripe payment process
*/
var payByStripe = function () {
$uibModal.open({
templateUrl: '<%= asset_path "stripe/payment_modal.html" %>',
size: 'md',
resolve: {
selectedPlan () { return $scope.selectedPlan; },
member () { return $scope.ctrl.member; },
price () { return $scope.cart.total; },
wallet () {
return Wallet.getWalletByUser({ user_id: $scope.ctrl.member.id }).$promise;
},
coupon () { return $scope.coupon.applied; }
},
controller: ['$scope', '$uibModalInstance', '$state', 'selectedPlan', 'member', 'price', 'Subscription', 'CustomAsset', 'wallet', 'helpers', '$filter', 'coupon',
function ($scope, $uibModalInstance, $state, selectedPlan, member, price, Subscription, CustomAsset, wallet, helpers, $filter, coupon) {
// User's wallet amount
$scope.walletAmount = wallet.amount;
// Final price to pay by the user
$scope.amount = helpers.getAmountToPay(price, wallet.amount);
// The plan that the user is about to subscribe
$scope.selectedPlan = selectedPlan;
// Used in wallet info template to interpolate some translations
$scope.numberFilter = $filter('number');
// retrieve the CGV
CustomAsset.get({ name: 'cgv-file' }, function (cgv) { $scope.cgv = cgv.custom_asset; });
/**
* Callback for click on the 'proceed' button.
* Handle the stripe's card tokenization process response and save the subscription to the API with the
* card token just created.
*/
$scope.payment = function (status, response) {
if (response.error) {
growl.error(response.error.message);
} else {
$scope.attempting = true;
Subscription.save({
coupon_code: ((coupon ? coupon.code : undefined)),
subscription: {
plan_id: selectedPlan.id,
user_id: member.id,
card_token: response.id
}
}
, function (data) { // success
$uibModalInstance.close(data);
}
, function (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(function (subscription) {
$scope.ctrl.member.subscribed_plan = angular.copy($scope.selectedPlan);
Auth._currentUser.subscribed_plan = angular.copy($scope.selectedPlan);
$scope.paid.plan = angular.copy($scope.selectedPlan);
$scope.selectedPlan = null;
$scope.coupon.applied = null;
});
};
/**
* Open a modal window which trigger the local payment process
*/
var payOnSite = function () {
$uibModal.open({
templateUrl: '<%= asset_path "plans/payment_modal.html" %>',
size: 'sm',
resolve: {
selectedPlan () { return $scope.selectedPlan; },
member () { return $scope.ctrl.member; },
price () { return $scope.cart.total; },
wallet () {
return Wallet.getWalletByUser({ user_id: $scope.ctrl.member.id }).$promise;
},
coupon () { return $scope.coupon.applied; }
},
controller: ['$scope', '$uibModalInstance', '$state', 'selectedPlan', 'member', 'price', 'Subscription', 'wallet', 'helpers', '$filter', 'coupon',
function ($scope, $uibModalInstance, $state, selectedPlan, member, price, Subscription, wallet, helpers, $filter, coupon) {
// user wallet amount
$scope.walletAmount = wallet.amount;
// subcription price, coupon subtracted if any
$scope.price = price;
// price to pay
$scope.amount = helpers.getAmountToPay($scope.price, wallet.amount);
// Used in wallet info template to interpolate some translations
$scope.numberFilter = $filter('number');
// The plan that the user is about to subscribe
$scope.plan = selectedPlan;
// The member who is subscribing a plan
$scope.member = member;
// Button label
if ($scope.amount > 0) {
$scope.validButtonName = _t('confirm_payment_of_html', { ROLE: $scope.currentUser.role, AMOUNT: $filter('currency')($scope.amount) }, 'messageformat');
} else {
if ((price.price > 0) && ($scope.walletAmount === 0)) {
$scope.validButtonName = _t('confirm_payment_of_html', { ROLE: $scope.currentUser.role, AMOUNT: $filter('currency')(price.price) }, 'messageformat');
} else {
$scope.validButtonName = _t('confirm');
}
}
/**
* Callback for the 'proceed' button.
* Save the subscription to the API
*/
$scope.ok = function () {
$scope.attempting = true;
Subscription.save({
coupon_code: ((coupon ? coupon.code : undefined)),
subscription: {
plan_id: selectedPlan.id,
user_id: member.id
}
}
, function (data) { // success
$uibModalInstance.close(data);
}
, function (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;
}
);
};
/**
* Callback for the 'cancel' button.
* Close the modal box.
*/
$scope.cancel = function () { $uibModalInstance.dismiss('cancel'); };
}
]
}).result['finally'](null).then(function (reservation) {
$scope.ctrl.member.subscribed_plan = angular.copy($scope.selectedPlan);
Auth._currentUser.subscribed_plan = angular.copy($scope.selectedPlan);
$scope.ctrl.member = null;
$scope.paid.plan = angular.copy($scope.selectedPlan);
$scope.selectedPlan = null;
return $scope.coupon.applied = null;
});
};
// !!! MUST BE CALLED AT THE END of the controller
return initialize();
}
]);

View File

@ -1,203 +0,0 @@
'use strict'
Application.Controllers.controller "CompleteProfileController", ["$scope", "$rootScope", "$state", "$window", "_t", "growl", "CSRF", "Auth", "Member", "settingsPromise", "activeProviderPromise", "groupsPromise", "cguFile", "memberPromise", "Session", "dialogs", "AuthProvider"
, ($scope, $rootScope, $state, $window, _t, growl, CSRF, Auth, Member, settingsPromise, activeProviderPromise, groupsPromise, cguFile, memberPromise, Session, dialogs, AuthProvider) ->
### 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
## information from the current SSO provider
$scope.activeProvider = activeProviderPromise
## list of user's groups (student/standard/...)
$scope.groups = groupsPromise
## current user, contains information 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: Fablab.uibDateFormat
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
##
# Test if the user's mail is marked as duplicate
# @return {boolean}
##
$scope.hasDuplicate = ->
email = memberPromise.email
if email
return !(email.match(/^<([^>]+)>.{20}-duplicate$/) == null)
##
# Ask for email confirmation and send the SSO merging token again
# @param $event {Object} jQuery event object
##
$scope.resendCode = (event) ->
event.preventDefault()
event.stopPropagation()
dialogs.confirm
templateUrl: '<%= asset_path "profile/resend_code_modal.html" %>'
resolve:
object: ->
email: memberPromise.email
, (email) ->
# Request the server to send an auth-migration email to the current user
AuthProvider.send_code {email: email}, (res) ->
growl.info(_t('code_successfully_sent_again'))
, (err) ->
growl.error(err.data.error)
##
# 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 =
total: 0
unread: 0
$window.location.href = activeProviderPromise.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()
# 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()
]

View File

@ -0,0 +1,219 @@
/* eslint-disable
no-return-assign,
no-undef,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
'use strict';
Application.Controllers.controller('CompleteProfileController', ['$scope', '$rootScope', '$state', '$window', '_t', 'growl', 'CSRF', 'Auth', 'Member', 'settingsPromise', 'activeProviderPromise', 'groupsPromise', 'cguFile', 'memberPromise', 'Session', 'dialogs', 'AuthProvider',
function ($scope, $rootScope, $state, $window, _t, growl, CSRF, Auth, Member, settingsPromise, activeProviderPromise, groupsPromise, cguFile, memberPromise, Session, dialogs, AuthProvider) {
/* 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;
// information from the current SSO provider
$scope.activeProvider = activeProviderPromise;
// list of user's groups (student/standard/...)
$scope.groups = groupsPromise;
// current user, contains information 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: Fablab.uibDateFormat,
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 = function ($event) {
$event.preventDefault();
$event.stopPropagation();
return $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 = function (content) {
if ((content.id == null)) {
$scope.alerts = [];
angular.forEach(content, function (v, k) {
angular.forEach(v, function (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;
return $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 = function (v) {
if (v) {
return 'fileinput-exists';
} else {
return 'fileinput-new';
}
};
/**
* Merge the current user into the account with the given auth_token
*/
$scope.registerAuthToken = function () {
Member.merge({ id: $rootScope.currentUser.id }, { user: { auth_token: $scope.user.auth_token } }, function (user) {
$scope.user = user;
Auth._currentUser = user;
$rootScope.currentUser = user;
$state.go('app.public.home');
}
, function (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 = function () {
const { email } = memberPromise;
if (email) {
const duplicate = email.match(/^<([^>]+)>.{20}-duplicate$/);
if (duplicate) {
return duplicate[1];
}
}
return email;
};
/**
* Test if the user's mail is marked as duplicate
* @return {boolean}
*/
$scope.hasDuplicate = function () {
const { email } = memberPromise;
if (email) {
return !(email.match(/^<([^>]+)>.{20}-duplicate$/) === null);
}
};
/**
* Ask for email confirmation and send the SSO merging token again
* @param event {Object} jQuery event object
*/
$scope.resendCode = function (event) {
event.preventDefault();
event.stopPropagation();
dialogs.confirm(
{
templateUrl: '<%= asset_path "profile/resend_code_modal.html" %>',
resolve: {
object () {
return { email: memberPromise.email };
}
}
},
function (email) {
// Request the server to send an auth-migration email to the current user
AuthProvider.send_code({ email },
function (res) { growl.info(_t('code_successfully_sent_again')); },
function (err) { growl.error(err.data.error); }
);
}
);
};
/**
* Disconnect and re-connect the user to the SSO to force the synchronisation of the profile's data
*/
$scope.syncProfile = function () {
Auth.logout().then(function (oldUser) {
Session.destroy();
$rootScope.currentUser = null;
$rootScope.toCheckNotifications = false;
$scope.notifications = {
total: 0,
unread: 0
};
$window.location.href = activeProviderPromise.link_to_sso_connect;
});
};
/* PRIVATE SCOPE */
/**
* Kind of constructor: these actions will be realized first when the controller is loaded
*/
const initialize = function () {
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, function (map) { $scope.preventField[map] = true; });
};
// !!! MUST BE CALLED AT THE END of the controller
return initialize();
}
]);

View File

@ -1,543 +0,0 @@
'use strict'
### COMMON CODE ###
##
# Provides a set of common properties and methods to the $scope parameter. They are used
# in the various projects' admin controllers.
#
# Provides :
# - $scope.totalSteps
# - $scope.machines = [{Machine}]
# - $scope.components = [{Component}]
# - $scope.themes = [{Theme}]
# - $scope.licences = [{Licence}]
# - $scope.allowedExtensions = [{String}]
# - $scope.submited(content)
# - $scope.cancel()
# - $scope.addFile()
# - $scope.deleteFile(file)
# - $scope.addStep()
# - $scope.deleteStep(step)
# - $scope.changeStepIndex(step, newIdx)
#
# Requires :
# - $scope.project.project_caos_attributes = []
# - $scope.project.project_steps_attributes = []
# - $state (Ui-Router) [ 'app.public.projects_show', 'app.public.projects_list' ]
##
class ProjectsController
constructor: ($scope, $state, Project, Machine, Member, Component, Theme, Licence, $document, Diacritics, dialogs, allowedExtensions, _t)->
## Retrieve the list of machines from the server
Machine.query().$promise.then (data)->
$scope.machines = data.map (d) ->
id: d.id
name: d.name
## Retrieve the list of components from the server
Component.query().$promise.then (data)->
$scope.components = data.map (d) ->
id: d.id
name: d.name
## Retrieve the list of themes from the server
Theme.query().$promise.then (data)->
$scope.themes = data.map (d) ->
id: d.id
name: d.name
## Retrieve the list of licences from the server
Licence.query().$promise.then (data)->
$scope.licences = data.map (d) ->
id: d.id
name: d.name
## Total number of documentation steps for the current project
$scope.totalSteps = $scope.project.project_steps_attributes.length
## List of extensions allowed for CAD attachements upload
$scope.allowedExtensions = allowedExtensions
##
# 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 project 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'
# using https://github.com/oblador/angular-scroll
$('section[ui-view=main]').scrollTop(0, 200)
return
else
$state.go('app.public.projects_show', {id: content.slug})
##
# For use with 'ng-class', returns the CSS class name for the uploads previews.
# The preview may show a placeholder or the content of the file depending on the upload state.
# @param v {*} any attribute, will be tested for truthiness (see JS evaluation rules)
##
$scope.fileinputClass = (v)->
if v
'fileinput-exists'
else
'fileinput-new'
##
# This will create a single new empty entry into the project's CAO attachements list.
##
$scope.addFile = ->
$scope.project.project_caos_attributes.push {}
##
# This will remove the given file from the project's CAO attachements list. If the file was previously uploaded
# to the server, it will be marked for deletion on the server. Otherwise, it will be simply truncated from
# the CAO attachements array.
# @param file {Object} the file to delete
##
$scope.deleteFile = (file) ->
index = $scope.project.project_caos_attributes.indexOf(file)
if file.id?
file._destroy = true
else
$scope.project.project_caos_attributes.splice(index, 1)
##
# This will create a single new empty entry into the project's steps list.
##
$scope.addStep = ->
$scope.totalSteps += 1
$scope.project.project_steps_attributes.push { step_nb: $scope.totalSteps, project_step_images_attributes: [] }
##
# This will remove the given step from the project's steps list. If the step was previously saved
# on the server, it will be marked for deletion for the next saving. Otherwise, it will be simply truncated from
# the steps array.
# @param file {Object} the file to delete
##
$scope.deleteStep = (step) ->
dialogs.confirm
resolve:
object: ->
title: _t('confirmation_required')
msg: _t('do_you_really_want_to_delete_this_step')
, -> # deletion confirmed
index = $scope.project.project_steps_attributes.indexOf(step)
if step.id?
step._destroy = true
else
$scope.project.project_steps_attributes.splice(index, 1)
# update the new total number of steps
$scope.totalSteps -= 1
# reindex the remaning steps
for s in $scope.project.project_steps_attributes
if s.step_nb > step.step_nb
s.step_nb -= 1
##
# Change the step_nb property of the given step to the new value provided. The step that was previously at this
# index will be assigned to the old position of the provided step.
# @param event {Object} see https://docs.angularjs.org/guide/expression#-event-
# @param step {Object} the project's step to reindex
# @param newIdx {number} the new index to assign to the step
##
$scope.changeStepIndex = (event, step, newIdx) ->
event.preventDefault() if event
for s in $scope.project.project_steps_attributes
if s.step_nb == newIdx
s.step_nb = step.step_nb
step.step_nb = newIdx
break
false
$scope.autoCompleteName = (nameLookup) ->
unless nameLookup
return
asciiName = Diacritics.remove(nameLookup)
Member.search { query: asciiName }, (users) ->
$scope.matchingMembers = users
, (error)->
console.error(error)
##
# This will create a single new empty entry into the project's step image list.
##
$scope.addProjectStepImage = (step)->
step.project_step_images_attributes.push {}
##
# This will remove the given image from the project's step image list.
# @param step {Object} the project step has images
# @param image {Object} the image to delete
##
$scope.deleteProjectStepImage = (step, image) ->
index = step.project_step_images_attributes.indexOf(image)
if image.id?
image._destroy = true
else
step.project_step_images_attributes.splice(index, 1)
##
# Controller used on projects listing page
##
Application.Controllers.controller "ProjectsController", ["$scope", "$state", 'Project', 'machinesPromise', 'themesPromise', 'componentsPromise', 'paginationService', 'OpenlabProject', '$window', 'growl', '_t', '$location', '$timeout'
, ($scope, $state, Project, machinesPromise, themesPromise, componentsPromise, paginationService, OpenlabProject, $window, growl, _t, $location, $timeout) ->
### PRIVATE STATIC CONSTANTS ###
# Number of projects added to the page when the user clicks on 'load more projects'
PROJECTS_PER_PAGE = 16
### PUBLIC SCOPE ###
## Fab-manager's instance ID in the openLab network
$scope.openlabAppId = Fablab.openlabAppId
## Is openLab enabled on the instance?
$scope.openlab =
projectsActive: Fablab.openlabProjectsActive
searchOverWholeNetwork: false
## default search parameters
$scope.search =
q: ($location.$$search.q || "")
from: ($location.$$search.from || undefined)
machine_id: (parseInt($location.$$search.machine_id) || undefined)
component_id: (parseInt($location.$$search.component_id) || undefined)
theme_id: (parseInt($location.$$search.theme_id) || undefined)
## list of projects to display
$scope.projects = []
## list of machines / used for filtering
$scope.machines = machinesPromise
## list of themes / used for filtering
$scope.themes = themesPromise
## list of components / used for filtering
$scope.components = componentsPromise
$scope.searchOverWholeNetworkChanged = ->
setTimeout ->
$scope.resetFiltersAndTriggerSearch()
, 150
$scope.loadMore = ->
if $scope.openlab.searchOverWholeNetwork is true
$scope.projectsPagination.loadMore(q: $scope.search.q)
else
$scope.projectsPagination.loadMore(search: $scope.search)
$scope.resetFiltersAndTriggerSearch = ->
$scope.search.q = ""
$scope.search.from = undefined
$scope.search.machine_id = undefined
$scope.search.component_id = undefined
$scope.search.theme_id = undefined
$scope.setUrlQueryParams($scope.search)
$scope.triggerSearch()
$scope.triggerSearch = ->
currentPage = parseInt($location.$$search.page) || 1
if $scope.openlab.searchOverWholeNetwork is true
updateUrlParam('whole_network', 't')
$scope.projectsPagination = new paginationService.Instance(OpenlabProject, currentPage, PROJECTS_PER_PAGE, null, { }, loadMoreOpenlabCallback)
OpenlabProject.query { q: $scope.search.q, page: currentPage, per_page: PROJECTS_PER_PAGE }, (projectsPromise)->
if projectsPromise.errors?
growl.error(_t('openlab_search_not_available_at_the_moment'))
$scope.openlab.searchOverWholeNetwork = false
$scope.triggerSearch()
else
$scope.projectsPagination.totalCount = projectsPromise.meta.total
$scope.projects = normalizeProjectsAttrs(projectsPromise.projects)
else
updateUrlParam('whole_network', 'f')
$scope.projectsPagination = new paginationService.Instance(Project, currentPage, PROJECTS_PER_PAGE, null, { }, loadMoreCallback, 'search')
Project.search { search: $scope.search, page: currentPage, per_page: PROJECTS_PER_PAGE }, (projectsPromise)->
$scope.projectsPagination.totalCount = projectsPromise.meta.total
$scope.projects = projectsPromise.projects
##
# Callback to switch the user's view to the detailled project page
# @param project {{slug:string}} The project to display
##
$scope.showProject = (project) ->
if ($scope.openlab.searchOverWholeNetwork is true) and (project.app_id isnt Fablab.openlabAppId)
$window.open(project.project_url, '_blank')
return true
else
$state.go('app.public.projects_show', {id: project.slug})
##
# function to set all url query search parameters from search object
##
$scope.setUrlQueryParams = (search)->
updateUrlParam('page', 1)
updateUrlParam('q', search.q)
updateUrlParam('from', search.from)
updateUrlParam('theme_id', search.theme_id)
updateUrlParam('component_id', search.component_id)
updateUrlParam('machine_id', search.machine_id)
### PRIVATE SCOPE ###
##
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
if $location.$$search.whole_network is 'f'
$scope.openlab.searchOverWholeNetwork = false
else
$scope.openlab.searchOverWholeNetwork = $scope.openlab.projectsActive || false
$scope.triggerSearch()
##
# function to update url query param, little hack to turn off reloadOnSearch and re-enable it after setting the params
# params example: 'q' , 'presse-purée'
##
updateUrlParam = (name, value) ->
$state.current.reloadOnSearch = false
$location.search(name, value)
$timeout ->
$state.current.reloadOnSearch = undefined
loadMoreCallback = (projectsPromise)->
$scope.projects = $scope.projects.concat(projectsPromise.projects)
updateUrlParam('page', $scope.projectsPagination.currentPage)
loadMoreOpenlabCallback = (projectsPromise)->
$scope.projects = $scope.projects.concat(normalizeProjectsAttrs(projectsPromise.projects))
updateUrlParam('page', $scope.projectsPagination.currentPage)
normalizeProjectsAttrs = (projects)->
projects.map((project)->
project.project_image = project.image_url
return project
)
## !!! MUST BE CALLED AT THE END of the controller
initialize()
]
##
# Controller used in the project creation page
##
Application.Controllers.controller "NewProjectController", ["$scope", "$state", 'Project', 'Machine', 'Member', 'Component', 'Theme', 'Licence', '$document', 'CSRF', 'Diacritics', 'dialogs', 'allowedExtensions', '_t'
, ($scope, $state, Project, Machine, Member, Component, Theme, Licence, $document, CSRF, Diacritics, dialogs, allowedExtensions, _t) ->
CSRF.setMetaTags()
## API URL where the form will be posted
$scope.actionUrl = "/api/projects/"
## Form action on the above URL
$scope.method = 'post'
## Default project parameters
$scope.project =
project_steps_attributes: []
project_caos_attributes: []
$scope.matchingMembers = []
## Using the ProjectsController
new ProjectsController($scope, $state, Project, Machine, Member, Component, Theme, Licence, $document, Diacritics, dialogs, allowedExtensions, _t)
]
##
# Controller used in the project edition page
##
Application.Controllers.controller "EditProjectController", ["$scope", "$state", '$stateParams', 'Project', 'Machine', 'Member', 'Component', 'Theme', 'Licence', '$document', 'CSRF', 'projectPromise', 'Diacritics', 'dialogs', 'allowedExtensions', '_t'
, ($scope, $state, $stateParams, Project, Machine, Member, Component, Theme, Licence, $document, CSRF, projectPromise, Diacritics, dialogs, allowedExtensions, _t) ->
CSRF.setMetaTags()
## API URL where the form will be posted
$scope.actionUrl = "/api/projects/" + $stateParams.id
## Form action on the above URL
$scope.method = 'put'
## Retrieve the project's details, if an error occured, redirect the user to the projects list page
$scope.project = projectPromise
$scope.matchingMembers = $scope.project.project_users.map (u) ->
id: u.id
name: u.full_name
## Using the ProjectsController
new ProjectsController($scope, $state, Project, Machine, Member, Component, Theme, Licence, $document, Diacritics, dialogs, allowedExtensions, _t)
]
##
# Controller used in the public project's details page
##
Application.Controllers.controller "ShowProjectController", ["$scope", "$state", "projectPromise", '$location', '$uibModal', 'dialogs', '_t'
, ($scope, $state, projectPromise, $location, $uibModal, dialogs, _t) ->
### PUBLIC SCOPE ###
## Store the project's details
$scope.project = projectPromise
$scope.projectUrl = $location.absUrl()
$scope.disqusShortname = Fablab.disqusShortname
##
# Test if the provided user has the edition rights on the current project
# @param [user] {{id:number}} (optional) the user to check rights
# @returns boolean
##
$scope.projectEditableBy = (user) ->
return false if not user?
return true if $scope.project.author_id == user.id
canEdit = false
angular.forEach $scope.project.project_users, (u)->
canEdit = true if u.id == user.id and u.is_valid
return canEdit
##
# 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
##
# 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
##
$scope.deleteProject = ->
# check the permissions
if $scope.currentUser.role is 'admin' or $scope.projectDeletableBy($scope.currentUser)
# delete the project then refresh the projects list
dialogs.confirm
resolve:
object: ->
title: _t('confirmation_required')
msg: _t('do_you_really_want_to_delete_this_project')
, -> # cancel confirmed
$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'))
]
##
# Return the URL allowing to share the current project on the Facebook social network
##
$scope.shareOnFacebook = ->
'https://www.facebook.com/share.php?u='+$state.href('app.public.projects_show', {id: $scope.project.slug}, {absolute: true}).replace('#', '%23')
##
# Return the URL allowing to share the current project on the Twitter social network
##
$scope.shareOnTwitter = ->
'https://twitter.com/intent/tweet?url='+encodeURIComponent($state.href('app.public.projects_show', {id: $scope.project.slug}, {absolute: true}))+'&text='+encodeURIComponent($scope.project.name)
]

View File

@ -0,0 +1,585 @@
/* eslint-disable
handle-callback-err,
no-return-assign,
no-undef,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS205: Consider reworking code to avoid use of IIFEs
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
'use strict';
/* COMMON CODE */
/**
* Provides a set of common properties and methods to the $scope parameter. They are used
* in the various projects' admin controllers.
*
* Provides :
* - $scope.totalSteps
* - $scope.machines = [{Machine}]
* - $scope.components = [{Component}]
* - $scope.themes = [{Theme}]
* - $scope.licences = [{Licence}]
* - $scope.allowedExtensions = [{String}]
* - $scope.submited(content)
* - $scope.cancel()
* - $scope.addFile()
* - $scope.deleteFile(file)
* - $scope.addStep()
* - $scope.deleteStep(step)
* - $scope.changeStepIndex(step, newIdx)
*
* Requires :
* - $scope.project.project_caos_attributes = []
* - $scope.project.project_steps_attributes = []
* - $state (Ui-Router) [ 'app.public.projects_show', 'app.public.projects_list' ]
*/
class ProjectsController {
constructor ($scope, $state, Project, Machine, Member, Component, Theme, Licence, $document, Diacritics, dialogs, allowedExtensions, _t) {
// Retrieve the list of machines from the server
Machine.query().$promise.then(function (data) {
$scope.machines = data.map(function (d) {
return ({
id: d.id,
name: d.name
});
});
});
// Retrieve the list of components from the server
Component.query().$promise.then(function (data) {
$scope.components = data.map(function (d) {
return ({
id: d.id,
name: d.name
});
});
});
// Retrieve the list of themes from the server
Theme.query().$promise.then(function (data) {
$scope.themes = data.map(function (d) {
return ({
id: d.id,
name: d.name
});
});
});
// Retrieve the list of licences from the server
Licence.query().$promise.then(function (data) {
$scope.licences = data.map(function (d) {
return ({
id: d.id,
name: d.name
});
});
});
// Total number of documentation steps for the current project
$scope.totalSteps = $scope.project.project_steps_attributes.length;
// List of extensions allowed for CAD attachements upload
$scope.allowedExtensions = allowedExtensions;
/**
* 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 project page.
* @param content {Object} JSON - The upload's result
*/
$scope.submited = function (content) {
if ((content.id == null)) {
$scope.alerts = [];
angular.forEach(content, function (v, k) {
angular.forEach(v, function (err) {
$scope.alerts.push({
msg: k + ': ' + err,
type: 'danger'
});
});
});
// using https://github.com/oblador/angular-scroll
$('section[ui-view=main]').scrollTop(0, 200);
} else {
return $state.go('app.public.projects_show', { id: content.slug });
}
};
/**
* 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 = function (v) {
if (v) {
return 'fileinput-exists';
} else {
return 'fileinput-new';
}
};
/**
* This will create a single new empty entry into the project's CAO attachements list.
*/
$scope.addFile = function () { $scope.project.project_caos_attributes.push({}); };
/**
* This will remove the given file from the project's CAO attachements list. If the file was previously uploaded
* to the server, it will be marked for deletion on the server. Otherwise, it will be simply truncated from
* the CAO attachements array.
* @param file {Object} the file to delete
*/
$scope.deleteFile = function (file) {
const index = $scope.project.project_caos_attributes.indexOf(file);
if (file.id != null) {
return file._destroy = true;
} else {
return $scope.project.project_caos_attributes.splice(index, 1);
}
};
/**
* This will create a single new empty entry into the project's steps list.
*/
$scope.addStep = function () {
$scope.totalSteps += 1;
return $scope.project.project_steps_attributes.push({ step_nb: $scope.totalSteps, project_step_images_attributes: [] });
};
/**
* This will remove the given step from the project's steps list. If the step was previously saved
* on the server, it will be marked for deletion for the next saving. Otherwise, it will be simply truncated from
* the steps array.
* @param step {Object} the step to delete
*/
$scope.deleteStep = function (step) {
dialogs.confirm({
resolve: {
object () {
return {
title: _t('confirmation_required'),
msg: _t('do_you_really_want_to_delete_this_step')
};
}
}
}
, function () { // deletion confirmed
const index = $scope.project.project_steps_attributes.indexOf(step);
if (step.id != null) {
step._destroy = true;
} else {
$scope.project.project_steps_attributes.splice(index, 1);
}
// update the new total number of steps
$scope.totalSteps -= 1;
// reindex the remaining steps
return (function () {
const result = [];
for (let s of Array.from($scope.project.project_steps_attributes)) {
if (s.step_nb > step.step_nb) {
result.push(s.step_nb -= 1);
} else {
result.push(undefined);
}
}
return result;
})();
});
};
/**
* Change the step_nb property of the given step to the new value provided. The step that was previously at this
* index will be assigned to the old position of the provided step.
* @param event {Object} see https://docs.angularjs.org/guide/expression#-event-
* @param step {Object} the project's step to reindex
* @param newIdx {number} the new index to assign to the step
*/
$scope.changeStepIndex = function (event, step, newIdx) {
if (event) { event.preventDefault(); }
for (let s of Array.from($scope.project.project_steps_attributes)) {
if (s.step_nb === newIdx) {
s.step_nb = step.step_nb;
step.step_nb = newIdx;
break;
}
}
return false;
};
$scope.autoCompleteName = function (nameLookup) {
if (!nameLookup) {
return;
}
const asciiName = Diacritics.remove(nameLookup);
Member.search(
{ query: asciiName },
function (users) { $scope.matchingMembers = users; },
function (error) { console.error(error); }
);
};
/**
* This will create a single new empty entry into the project's step image list.
*/
$scope.addProjectStepImage = function (step) { step.project_step_images_attributes.push({}); };
/**
* This will remove the given image from the project's step image list.
* @param step {Object} the project step has images
* @param image {Object} the image to delete
*/
$scope.deleteProjectStepImage = function (step, image) {
const index = step.project_step_images_attributes.indexOf(image);
if (image.id != null) {
return image._destroy = true;
} else {
return step.project_step_images_attributes.splice(index, 1);
}
};
}
}
/**
* Controller used on projects listing page
*/
Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'Project', 'machinesPromise', 'themesPromise', 'componentsPromise', 'paginationService', 'OpenlabProject', '$window', 'growl', '_t', '$location', '$timeout',
function ($scope, $state, Project, machinesPromise, themesPromise, componentsPromise, paginationService, OpenlabProject, $window, growl, _t, $location, $timeout) {
/* PRIVATE STATIC CONSTANTS */
// Number of projects added to the page when the user clicks on 'load more projects'
const PROJECTS_PER_PAGE = 16;
/* PUBLIC SCOPE */
// Fab-manager's instance ID in the openLab network
$scope.openlabAppId = Fablab.openlabAppId;
// Is openLab enabled on the instance?
$scope.openlab = {
projectsActive: Fablab.openlabProjectsActive,
searchOverWholeNetwork: false
};
// default search parameters
$scope.search = {
q: ($location.$$search.q || ''),
from: ($location.$$search.from || undefined),
machine_id: (parseInt($location.$$search.machine_id) || undefined),
component_id: (parseInt($location.$$search.component_id) || undefined),
theme_id: (parseInt($location.$$search.theme_id) || undefined)
};
// list of projects to display
$scope.projects = [];
// list of machines / used for filtering
$scope.machines = machinesPromise;
// list of themes / used for filtering
$scope.themes = themesPromise;
// list of components / used for filtering
$scope.components = componentsPromise;
$scope.searchOverWholeNetworkChanged = function () {
setTimeout(
function () { $scope.resetFiltersAndTriggerSearch(); },
150
);
};
$scope.loadMore = function () {
if ($scope.openlab.searchOverWholeNetwork === true) {
return $scope.projectsPagination.loadMore({ q: $scope.search.q });
} else {
return $scope.projectsPagination.loadMore({ search: $scope.search });
}
};
$scope.resetFiltersAndTriggerSearch = function () {
$scope.search.q = '';
$scope.search.from = undefined;
$scope.search.machine_id = undefined;
$scope.search.component_id = undefined;
$scope.search.theme_id = undefined;
$scope.setUrlQueryParams($scope.search);
return $scope.triggerSearch();
};
$scope.triggerSearch = function () {
const currentPage = parseInt($location.$$search.page) || 1;
if ($scope.openlab.searchOverWholeNetwork === true) {
updateUrlParam('whole_network', 't');
$scope.projectsPagination = new paginationService.Instance(OpenlabProject, currentPage, PROJECTS_PER_PAGE, null, { }, loadMoreOpenlabCallback);
return OpenlabProject.query({ q: $scope.search.q, page: currentPage, per_page: PROJECTS_PER_PAGE }, function (projectsPromise) {
if (projectsPromise.errors != null) {
growl.error(_t('openlab_search_not_available_at_the_moment'));
$scope.openlab.searchOverWholeNetwork = false;
return $scope.triggerSearch();
} else {
$scope.projectsPagination.totalCount = projectsPromise.meta.total;
return $scope.projects = normalizeProjectsAttrs(projectsPromise.projects);
}
});
} else {
updateUrlParam('whole_network', 'f');
$scope.projectsPagination = new paginationService.Instance(Project, currentPage, PROJECTS_PER_PAGE, null, { }, loadMoreCallback, 'search');
return Project.search({ search: $scope.search, page: currentPage, per_page: PROJECTS_PER_PAGE }, function (projectsPromise) {
$scope.projectsPagination.totalCount = projectsPromise.meta.total;
return $scope.projects = projectsPromise.projects;
});
}
};
/**
* Callback to switch the user's view to the detailled project page
* @param project {{slug:string}} The project to display
*/
$scope.showProject = function (project) {
if (($scope.openlab.searchOverWholeNetwork === true) && (project.app_id !== Fablab.openlabAppId)) {
$window.open(project.project_url, '_blank');
return true;
} else {
return $state.go('app.public.projects_show', { id: project.slug });
}
};
/**
* function to set all url query search parameters from search object
*/
$scope.setUrlQueryParams = function (search) {
updateUrlParam('page', 1);
updateUrlParam('q', search.q);
updateUrlParam('from', search.from);
updateUrlParam('theme_id', search.theme_id);
updateUrlParam('component_id', search.component_id);
return updateUrlParam('machine_id', search.machine_id);
};
/* PRIVATE SCOPE */
/**
* Kind of constructor: these actions will be realized first when the controller is loaded
*/
const initialize = function () {
if ($location.$$search.whole_network === 'f') {
$scope.openlab.searchOverWholeNetwork = false;
} else {
$scope.openlab.searchOverWholeNetwork = $scope.openlab.projectsActive || false;
}
return $scope.triggerSearch();
};
/**
* function to update url query param, little hack to turn off reloadOnSearch and re-enable it after setting the params
* params example: 'q' , 'presse-purée'
*/
var updateUrlParam = function (name, value) {
$state.current.reloadOnSearch = false;
$location.search(name, value);
return $timeout(function () { $state.current.reloadOnSearch = undefined; });
};
var loadMoreCallback = function (projectsPromise) {
$scope.projects = $scope.projects.concat(projectsPromise.projects);
return updateUrlParam('page', $scope.projectsPagination.currentPage);
};
var loadMoreOpenlabCallback = function (projectsPromise) {
$scope.projects = $scope.projects.concat(normalizeProjectsAttrs(projectsPromise.projects));
return updateUrlParam('page', $scope.projectsPagination.currentPage);
};
var normalizeProjectsAttrs = function (projects) {
projects.map(function (project) {
project.project_image = project.image_url;
return project;
});
};
// !!! MUST BE CALLED AT THE END of the controller
return initialize();
}
]);
/**
* Controller used in the project creation page
*/
Application.Controllers.controller('NewProjectController', ['$scope', '$state', 'Project', 'Machine', 'Member', 'Component', 'Theme', 'Licence', '$document', 'CSRF', 'Diacritics', 'dialogs', 'allowedExtensions', '_t',
function ($scope, $state, Project, Machine, Member, Component, Theme, Licence, $document, CSRF, Diacritics, dialogs, allowedExtensions, _t) {
CSRF.setMetaTags();
// API URL where the form will be posted
$scope.actionUrl = '/api/projects/';
// Form action on the above URL
$scope.method = 'post';
// Default project parameters
$scope.project = {
project_steps_attributes: [],
project_caos_attributes: []
};
$scope.matchingMembers = [];
// Using the ProjectsController
return new ProjectsController($scope, $state, Project, Machine, Member, Component, Theme, Licence, $document, Diacritics, dialogs, allowedExtensions, _t);
}
]);
/**
* Controller used in the project edition page
*/
Application.Controllers.controller('EditProjectController', ['$scope', '$state', '$stateParams', 'Project', 'Machine', 'Member', 'Component', 'Theme', 'Licence', '$document', 'CSRF', 'projectPromise', 'Diacritics', 'dialogs', 'allowedExtensions', '_t',
function ($scope, $state, $stateParams, Project, Machine, Member, Component, Theme, Licence, $document, CSRF, projectPromise, Diacritics, dialogs, allowedExtensions, _t) {
CSRF.setMetaTags();
// API URL where the form will be posted
$scope.actionUrl = `/api/projects/${$stateParams.id}`;
// Form action on the above URL
$scope.method = 'put';
// Retrieve the project's details, if an error occured, redirect the user to the projects list page
$scope.project = projectPromise;
$scope.matchingMembers = $scope.project.project_users.map(function (u) {
return ({
id: u.id,
name: u.full_name
});
});
// Using the ProjectsController
return new ProjectsController($scope, $state, Project, Machine, Member, Component, Theme, Licence, $document, Diacritics, dialogs, allowedExtensions, _t);
}
]);
/**
* Controller used in the public project's details page
*/
Application.Controllers.controller('ShowProjectController', ['$scope', '$state', 'projectPromise', '$location', '$uibModal', 'dialogs', '_t',
function ($scope, $state, projectPromise, $location, $uibModal, dialogs, _t) {
/* PUBLIC SCOPE */
// Store the project's details
$scope.project = projectPromise;
$scope.projectUrl = $location.absUrl();
$scope.disqusShortname = Fablab.disqusShortname;
/**
* Test if the provided user has the edition rights on the current project
* @param [user] {{id:number}} (optional) the user to check rights
* @returns boolean
*/
$scope.projectEditableBy = function (user) {
if ((user == null)) { return false; }
if ($scope.project.author_id === user.id) { return true; }
let canEdit = false;
angular.forEach($scope.project.project_users, function (u) {
if ((u.id === user.id) && u.is_valid) { return canEdit = true; }
});
return canEdit;
};
/**
* 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 = function (user) {
if ((user == null)) { return false; }
if ($scope.project.author_id === user.id) { return true; }
};
/**
* 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
*/
$scope.deleteProject = function () {
// check the permissions
if (($scope.currentUser.role === 'admin') || $scope.projectDeletableBy($scope.currentUser)) {
// delete the project then refresh the projects list
return dialogs.confirm({
resolve: {
object () {
return {
title: _t('confirmation_required'),
msg: _t('do_you_really_want_to_delete_this_project')
};
}
}
}
, function () { // cancel confirmed
$scope.project.$delete(function () { $state.go('app.public.projects_list', {}, { reload: true }); });
});
} else {
return 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 = function (e) {
if (e) { e.preventDefault(); }
$uibModal.open({
templateUrl: '<%= asset_path "shared/signalAbuseModal.html" %>',
size: 'md',
resolve: {
project () { return $scope.project; }
},
controller: ['$scope', '$uibModalInstance', '_t', 'growl', 'Abuse', 'project', function ($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 = function () { $uibModalInstance.dismiss('cancel'); };
// callback for form validation
return $scope.ok = function () {
Abuse.save(
{},
{ abuse: $scope.signaler },
function (res) {
// creation successful
growl.success(_t('your_report_was_successful_thanks'));
return $uibModalInstance.close(res);
}
, function (error) {
// creation failed...
growl.error(_t('an_error_occured_while_sending_your_report'));
}
);
};
}]
});
};
/**
* Return the URL allowing to share the current project on the Facebook social network
*/
$scope.shareOnFacebook = function () { return `https://www.facebook.com/share.php?u=${$state.href('app.public.projects_show', { id: $scope.project.slug }, { absolute: true }).replace('#', '%23')}`; };
/**
* Return the URL allowing to share the current project on the Twitter social network
*/
$scope.shareOnTwitter = function () { return `https://twitter.com/intent/tweet?url=${encodeURIComponent($state.href('app.public.projects_show', { id: $scope.project.slug }, { absolute: true }))}&text=${encodeURIComponent($scope.project.name)}`; };
}
]);

View File

@ -1,537 +0,0 @@
### COMMON CODE ###
##
# Provides a set of common callback methods to the $scope parameter. These methods are used
# in the various spaces' admin controllers.
#
# Provides :
# - $scope.submited(content)
# - $scope.cancel()
# - $scope.fileinputClass(v)
# - $scope.addFile()
# - $scope.deleteFile(file)
#
# Requires :
# - $scope.space.space_files_attributes = []
# - $state (Ui-Router) [ 'app.public.spaces_list' ]
##
class SpacesController
constructor: ($scope, $state) ->
##
# For use with ngUpload (https://github.com/twilson63/ngUpload).
# Intended to be the callback when the upload is done: any raised error will be stacked in the
# $scope.alerts array. If everything goes fine, the user is redirected to the spaces list.
# @param content {Object} JSON - The upload's result
##
$scope.submited = (content) ->
if !content.id?
$scope.alerts = []
angular.forEach content, (v, k)->
angular.forEach v, (err)->
$scope.alerts.push
msg: k+': '+err
type: 'danger'
else
$state.go('app.public.spaces_list')
##
# Changes the current user's view, redirecting him to the spaces list
##
$scope.cancel = ->
$state.go('app.public.spaces_list')
##
# For use with 'ng-class', returns the CSS class name for the uploads previews.
# The preview may show a placeholder or the content of the file depending on the upload state.
# @param v {*} any attribute, will be tested for truthiness (see JS evaluation rules)
##
$scope.fileinputClass = (v)->
if v
'fileinput-exists'
else
'fileinput-new'
##
# This will create a single new empty entry into the space attachements list.
##
$scope.addFile = ->
$scope.space.space_files_attributes.push {}
##
# This will remove the given file from the space attachements list. If the file was previously uploaded
# to the server, it will be marked for deletion on the server. Otherwise, it will be simply truncated from
# the attachements array.
# @param file {Object} the file to delete
##
$scope.deleteFile = (file) ->
index = $scope.space.space_files_attributes.indexOf(file)
if file.id?
file._destroy = true
else
$scope.space.space_files_attributes.splice(index, 1)
##
# Controller used in the public listing page, allowing everyone to see the list of spaces
##
Application.Controllers.controller 'SpacesController', ['$scope', '$state', 'spacesPromise', ($scope, $state, spacesPromise) ->
## Retrieve the list of spaces
$scope.spaces = spacesPromise
##
# Redirect the user to the space details page
##
$scope.showSpace = (space) ->
$state.go('app.public.space_show', { id: space.slug })
##
# Callback to book a reservation for the current space
##
$scope.reserveSpace = (space) ->
$state.go('app.logged.space_reserve', { id: space.slug })
## Default: we show only enabled spaces
$scope.spaceFiltering = 'enabled'
## Available options for filtering spaces by status
$scope.filterDisabled = [
'enabled',
'disabled',
'all',
]
]
##
# Controller used in the space creation page (admin)
##
Application.Controllers.controller 'NewSpaceController', ['$scope', '$state', 'CSRF',($scope, $state, CSRF) ->
CSRF.setMetaTags()
## API URL where the form will be posted
$scope.actionUrl = "/api/spaces/"
## Form action on the above URL
$scope.method = "post"
## default space parameters
$scope.space =
space_files_attributes: []
## Using the SpacesController
new SpacesController($scope, $state)
]
##
# Controller used in the space edition page (admin)
##
Application.Controllers.controller 'EditSpaceController', ['$scope', '$state', '$stateParams', 'spacePromise', 'CSRF',($scope, $state, $stateParams, spacePromise, CSRF) ->
CSRF.setMetaTags()
## API URL where the form will be posted
$scope.actionUrl = "/api/spaces/" + $stateParams.id
## Form action on the above URL
$scope.method = "put"
## space to modify
$scope.space = spacePromise
## Using the SpacesController
new SpacesController($scope, $state)
]
Application.Controllers.controller 'ShowSpaceController', ['$scope', '$state', 'spacePromise', '_t', 'dialogs', 'growl', ($scope, $state, spacePromise, _t, dialogs, growl) ->
## Details of the space witch id/slug is provided in the URL
$scope.space = spacePromise
##
# Callback to book a reservation for the current space
# @param event {Object} see https://docs.angularjs.org/guide/expression#-event-
##
$scope.reserveSpace = (event) ->
event.preventDefault()
$state.go('app.logged.space_reserve', { id: $scope.space.slug })
##
# Callback to book a reservation for the current space
# @param event {Object} see https://docs.angularjs.org/guide/expression#-event-
##
$scope.deleteSpace = (event) ->
event.preventDefault()
# check the permissions
if $scope.currentUser.role isnt 'admin'
console.error _t('space_show.unauthorized_operation')
else
dialogs.confirm
resolve:
object: ->
title: _t('space_show.confirmation_required')
msg: _t('space_show.do_you_really_want_to_delete_this_space')
, -> # deletion confirmed
# delete the machine then redirect to the machines listing
$scope.space.$delete ->
$state.go('app.public.spaces_list')
, (error)->
growl.warning(_t('space_show.the_space_cant_be_deleted_because_it_is_already_reserved_by_some_users'))
]
##
# Controller used in the spaces reservation agenda page.
# This controller is very similar to the machine reservation controller with one major difference: here, there is many places
# per slots.
##
Application.Controllers.controller "ReserveSpaceController", ["$scope", '$stateParams', 'Auth', '$timeout', 'Availability', 'Member', 'availabilitySpacesPromise', 'plansPromise', 'groupsPromise', 'settingsPromise', 'spacePromise', '_t', 'uiCalendarConfig', 'CalendarConfig'
($scope, $stateParams, Auth, $timeout, Availability, Member, availabilitySpacesPromise, plansPromise, groupsPromise, settingsPromise, spacePromise, _t, uiCalendarConfig, CalendarConfig) ->
### PRIVATE STATIC CONSTANTS ###
# Color of the selected event backgound
SELECTED_EVENT_BG_COLOR = '#ffdd00'
# Slot free to be booked
FREE_SLOT_BORDER_COLOR = '<%= AvailabilityHelper::SPACE_COLOR %>'
# Slot with reservation from current user
RESERVED_SLOT_BORDER_COLOR = '<%= AvailabilityHelper::IS_RESERVED_BY_CURRENT_USER %>'
### PUBLIC SCOPE ###
## bind the spaces availabilities with full-Calendar events
$scope.eventSources = [ { events: availabilitySpacesPromise, textColor: 'black' } ]
## the user to deal with, ie. the current user for non-admins
$scope.ctrl =
member: {}
## list of plans, classified by group
$scope.plansClassifiedByGroup = []
for group in groupsPromise
groupObj = { id: group.id, name: group.name, plans: [] }
for plan in plansPromise
groupObj.plans.push(plan) if plan.group_id == group.id
$scope.plansClassifiedByGroup.push(groupObj)
## mapping of fullCalendar events.
$scope.events =
reserved: [] # Slots that the user wants to book
modifiable: null # Slot that the user wants to change
placable: null # Destination slot for the change
paid: [] # Slots that were just booked by the user (transaction ok)
moved: null # Slots that were just moved by the user (change done) -> {newSlot:* oldSlot: *}
## the moment when the slot selection changed for the last time, used to trigger changes in the cart
$scope.selectionTime = null
## the last clicked event in the calender
$scope.selectedEvent = null
## indicates the state of the current view : calendar or plans information
$scope.plansAreShown = false
## will store the user's plan if he choosed to buy one
$scope.selectedPlan = null
## the moment when the plan selection changed for the last time, used to trigger changes in the cart
$scope.planSelectionTime = null
## Selected space
$scope.space = spacePromise
## fullCalendar (v2) configuration
$scope.calendarConfig = CalendarConfig
minTime: moment.duration(moment(settingsPromise.booking_window_start).format('HH:mm:ss'))
maxTime: moment.duration(moment(settingsPromise.booking_window_end).format('HH:mm:ss'))
eventClick: (event, jsEvent, view) ->
calendarEventClickCb(event, jsEvent, view)
eventRender: (event, element, view) ->
eventRenderCb(event, element, view)
## Application global settings
$scope.settings = settingsPromise
## Global config: message to the end user concerning the subscriptions rules
$scope.subscriptionExplicationsAlert = settingsPromise.subscription_explications_alert
## Global config: message to the end user concerning the space reservation
$scope.spaceExplicationsAlert = settingsPromise.space_explications_alert
##
# Change the last selected slot's appearence to looks like 'added to cart'
##
$scope.markSlotAsAdded = ->
$scope.selectedEvent.backgroundColor = SELECTED_EVENT_BG_COLOR
updateCalendar()
##
# Change the last selected slot's appearence to looks like 'never added to cart'
##
$scope.markSlotAsRemoved = (slot) ->
slot.backgroundColor = 'white'
slot.title = ''
slot.borderColor = FREE_SLOT_BORDER_COLOR
slot.id = null
slot.isValid = false
slot.is_reserved = false
slot.can_modify = false
slot.offered = false
slot.is_completed = false if slot.is_completed
updateCalendar()
##
# Callback when a slot was successfully cancelled. Reset the slot style as 'ready to book'
##
$scope.slotCancelled = ->
$scope.markSlotAsRemoved($scope.selectedEvent)
##
# Change the last selected slot's appearence to looks like 'currently looking for a new destination to exchange'
##
$scope.markSlotAsModifying = ->
$scope.selectedEvent.backgroundColor = '#eee'
$scope.selectedEvent.title = _t('space_reserve.i_change')
updateCalendar()
##
# Change the last selected slot's appearence to looks like 'the slot being exchanged will take this place'
##
$scope.changeModifyTrainingSlot = ->
if $scope.events.placable
$scope.events.placable.backgroundColor = 'white'
$scope.events.placable.title = ''
if !$scope.events.placable or $scope.events.placable._id != $scope.selectedEvent._id
$scope.selectedEvent.backgroundColor = '#bbb'
$scope.selectedEvent.title = _t('space_reserve.i_shift')
updateCalendar()
##
# When modifying an already booked reservation, callback when the modification was successfully done.
##
$scope.modifyTrainingSlot = ->
$scope.events.placable.title = _t('space_reserve.i_ve_reserved')
$scope.events.placable.backgroundColor = 'white'
$scope.events.placable.borderColor = $scope.events.modifiable.borderColor
$scope.events.placable.id = $scope.events.modifiable.id
$scope.events.placable.is_reserved = true
$scope.events.placable.can_modify = true
$scope.events.modifiable.backgroundColor = 'white'
$scope.events.modifiable.title = ''
$scope.events.modifiable.borderColor = FREE_SLOT_BORDER_COLOR
$scope.events.modifiable.id = null
$scope.events.modifiable.is_reserved = false
$scope.events.modifiable.can_modify = false
$scope.events.modifiable.is_completed = false if $scope.events.modifiable.is_completed
updateCalendar()
##
# Cancel the current booking modification, reseting the whole process
##
$scope.cancelModifyTrainingSlot = ->
if $scope.events.placable
$scope.events.placable.backgroundColor = 'white'
$scope.events.placable.title = ''
$scope.events.modifiable.title = _t('space_reserve.i_ve_reserved')
$scope.events.modifiable.backgroundColor = 'white'
updateCalendar()
##
# Callback to deal with the reservations of the user selected in the dropdown list instead of the current user's
# reservations. (admins only)
##
$scope.updateMember = ->
if $scope.ctrl.member
Member.get {id: $scope.ctrl.member.id}, (member) ->
$scope.ctrl.member = member
Availability.spaces {spaceId: $scope.space.id, member_id: $scope.ctrl.member.id}, (spaces) ->
uiCalendarConfig.calendars.calendar.fullCalendar 'removeEvents'
$scope.eventSources.splice(0, 1,
events: spaces
textColor: 'black'
)
# as the events are re-fetched for the new user, we must re-init the cart
$scope.events.reserved = []
$scope.selectedPlan = null
$scope.plansAreShown = false
##
# Add the provided plan to the current shopping cart
# @param plan {Object} the plan to subscribe
##
$scope.selectPlan = (plan) ->
# toggle selected plan
if $scope.selectedPlan != plan
$scope.selectedPlan = plan
else
$scope.selectedPlan = null
$scope.planSelectionTime = new Date()
##
# Changes the user current view from the plan subsription screen to the machine reservation agenda
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
##
$scope.doNotSubscribePlan = (e)->
e.preventDefault()
$scope.plansAreShown = false
$scope.selectedPlan = null
$scope.planSelectionTime = new Date()
##
# Switch the user's view from the reservation agenda to the plan subscription
##
$scope.showPlans = ->
$scope.plansAreShown = true
##
# Once the reservation is booked (payment process successfully completed), change the event style
# in fullCalendar, update the user's subscription and free-credits if needed
# @param reservation {Object}
##
$scope.afterPayment = (reservation)->
angular.forEach $scope.events.paid, (spaceSlot, key) ->
spaceSlot.is_reserved = true
spaceSlot.can_modify = true
spaceSlot.title = _t('space_reserve.i_ve_reserved')
spaceSlot.backgroundColor = 'white'
spaceSlot.borderColor = RESERVED_SLOT_BORDER_COLOR
updateSpaceSlotId(spaceSlot, reservation)
if $scope.selectedPlan
$scope.ctrl.member.subscribed_plan = angular.copy($scope.selectedPlan)
Auth._currentUser.subscribed_plan = angular.copy($scope.selectedPlan)
$scope.plansAreShown = false
$scope.selectedPlan = null
$scope.ctrl.member.training_credits = angular.copy(reservation.user.training_credits)
$scope.ctrl.member.machine_credits = angular.copy(reservation.user.machine_credits)
Auth._currentUser.training_credits = angular.copy(reservation.user.training_credits)
Auth._currentUser.machine_credits = angular.copy(reservation.user.machine_credits)
refetchCalendar()
##
# To use as callback in Array.prototype.filter to get only enabled plans
##
$scope.filterDisabledPlans = (plan) ->
!plan.disabled
### PRIVATE SCOPE ###
##
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
if $scope.currentUser.role isnt 'admin'
Member.get id: $scope.currentUser.id, (member) ->
$scope.ctrl.member = member
##
# Triggered when the user clicks on a reservation slot in the agenda.
# Defines the behavior to adopt depending on the slot status (already booked, free, ready to be reserved ...),
# the user's subscription (current or about to be took) and the time (the user cannot modify a booked reservation
# if it's too late).
# @see http://fullcalendar.io/docs/mouse/eventClick/
##
calendarEventClickCb = (event, jsEvent, view) ->
$scope.selectedEvent = event
if $stateParams.id is 'all'
$scope.training = event.training
$scope.selectionTime = new Date()
##
# Triggered when fullCalendar tries to graphicaly render an event block.
# Append the event tag into the block, just after the event title.
# @see http://fullcalendar.io/docs/event_rendering/eventRender/
##
eventRenderCb = (event, element, view)->
if $scope.currentUser.role is 'admin' and event.tags.length > 0
html = ''
for tag in event.tags
html += "<span class='label label-success text-white' title='#{tag.name}'>#{tag.name}</span>"
element.find('.fc-time').append(html)
return
##
# After payment, update the id of the newly reserved slot with the id returned by the server.
# This will allow the user to modify the reservation he just booked.
# @param slot {Object}
# @param reservation {Object}
##
updateSpaceSlotId = (slot, reservation)->
angular.forEach reservation.slots, (s)->
if slot.start_at == slot.start_at
slot.id = s.id
##
# Update the calendar's display to render the new attributes of the events
##
updateCalendar = ->
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
##
# Asynchronously fetch the events from the API and refresh the calendar's view with these new events
##
refetchCalendar = ->
$timeout ->
uiCalendarConfig.calendars.calendar.fullCalendar 'refetchEvents'
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
## !!! MUST BE CALLED AT THE END of the controller
initialize()
]

View File

@ -0,0 +1,553 @@
/* eslint-disable
handle-callback-err,
no-return-assign,
no-self-compare,
no-undef,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/* COMMON CODE */
/**
* Provides a set of common callback methods to the $scope parameter. These methods are used
* in the various spaces' admin controllers.
*
* Provides :
* - $scope.submited(content)
* - $scope.cancel()
* - $scope.fileinputClass(v)
* - $scope.addFile()
* - $scope.deleteFile(file)
*
* Requires :
* - $scope.space.space_files_attributes = []
* - $state (Ui-Router) [ 'app.public.spaces_list' ]
*/
class SpacesController {
constructor ($scope, $state) {
/*
* For use with ngUpload (https://github.com/twilson63/ngUpload).
* Intended to be the callback when the upload is done: any raised error will be stacked in the
* $scope.alerts array. If everything goes fine, the user is redirected to the spaces list.
* @param content {Object} JSON - The upload's result
*/
$scope.submited = function (content) {
if ((content.id == null)) {
$scope.alerts = [];
angular.forEach(content, function (v, k) {
angular.forEach(v, function (err) {
$scope.alerts.push({
msg: k + ': ' + err,
type: 'danger'
});
});
});
} else {
$state.go('app.public.spaces_list');
}
};
/**
* Changes the current user's view, redirecting him to the spaces list
*/
$scope.cancel = function () { $state.go('app.public.spaces_list'); };
/**
* For use with 'ng-class', returns the CSS class name for the uploads previews.
* The preview may show a placeholder or the content of the file depending on the upload state.
* @param v {*} any attribute, will be tested for truthiness (see JS evaluation rules)
*/
$scope.fileinputClass = function (v) {
if (v) {
return 'fileinput-exists';
} else {
return 'fileinput-new';
}
};
/**
* This will create a single new empty entry into the space attachments list.
*/
$scope.addFile = function () { $scope.space.space_files_attributes.push({}); };
/**
* This will remove the given file from the space attachments list. If the file was previously uploaded
* to the server, it will be marked for deletion on the server. Otherwise, it will be simply truncated from
* the attachments array.
* @param file {Object} the file to delete
*/
$scope.deleteFile = function (file) {
const index = $scope.space.space_files_attributes.indexOf(file);
if (file.id != null) {
return file._destroy = true;
} else {
return $scope.space.space_files_attributes.splice(index, 1);
}
};
}
}
/**
* Controller used in the public listing page, allowing everyone to see the list of spaces
*/
Application.Controllers.controller('SpacesController', ['$scope', '$state', 'spacesPromise', function ($scope, $state, spacesPromise) {
// Retrieve the list of spaces
$scope.spaces = spacesPromise;
/**
* Redirect the user to the space details page
*/
$scope.showSpace = function (space) { $state.go('app.public.space_show', { id: space.slug }); };
/**
* Callback to book a reservation for the current space
*/
$scope.reserveSpace = function (space) { $state.go('app.logged.space_reserve', { id: space.slug }); };
// Default: we show only enabled spaces
$scope.spaceFiltering = 'enabled';
// Available options for filtering spaces by status
$scope.filterDisabled = [
'enabled',
'disabled',
'all'
];
}]);
/**
* Controller used in the space creation page (admin)
*/
Application.Controllers.controller('NewSpaceController', ['$scope', '$state', 'CSRF', function ($scope, $state, CSRF) {
CSRF.setMetaTags();
// API URL where the form will be posted
$scope.actionUrl = '/api/spaces/';
// Form action on the above URL
$scope.method = 'post';
// default space parameters
$scope.space =
{ space_files_attributes: [] };
// Using the SpacesController
return new SpacesController($scope, $state);
}]);
/**
* Controller used in the space edition page (admin)
*/
Application.Controllers.controller('EditSpaceController', ['$scope', '$state', '$stateParams', 'spacePromise', 'CSRF',
function ($scope, $state, $stateParams, spacePromise, CSRF) {
CSRF.setMetaTags();
// API URL where the form will be posted
$scope.actionUrl = `/api/spaces/${$stateParams.id}`;
// Form action on the above URL
$scope.method = 'put';
// space to modify
$scope.space = spacePromise;
// Using the SpacesController
return new SpacesController($scope, $state);
}]);
Application.Controllers.controller('ShowSpaceController', ['$scope', '$state', 'spacePromise', '_t', 'dialogs', 'growl',
function ($scope, $state, spacePromise, _t, dialogs, growl) {
// Details of the space witch id/slug is provided in the URL
$scope.space = spacePromise;
/**
* Callback to book a reservation for the current space
* @param event {Object} see https://docs.angularjs.org/guide/expression#-event-
*/
$scope.reserveSpace = function (event) {
event.preventDefault();
return $state.go('app.logged.space_reserve', { id: $scope.space.slug });
};
/**
* Callback to book a reservation for the current space
* @param event {Object} see https://docs.angularjs.org/guide/expression#-event-
*/
$scope.deleteSpace = function (event) {
event.preventDefault();
// check the permissions
if ($scope.currentUser.role !== 'admin') {
return console.error(_t('space_show.unauthorized_operation'));
} else {
return dialogs.confirm({
resolve: {
object () {
return {
title: _t('space_show.confirmation_required'),
msg: _t('space_show.do_you_really_want_to_delete_this_space')
};
}
}
}
, function () { // deletion confirmed
// delete the machine then redirect to the machines listing
$scope.space.$delete(
function () {
$state.go('app.public.spaces_list');
},
function (error) {
growl.warning(_t('space_show.the_space_cant_be_deleted_because_it_is_already_reserved_by_some_users'));
console.error(error);
}
);
});
}
};
}]);
/**
* Controller used in the spaces reservation agenda page.
* This controller is very similar to the machine reservation controller with one major difference: here, there is many places
* per slots.
*/
Application.Controllers.controller('ReserveSpaceController', ['$scope', '$stateParams', 'Auth', '$timeout', 'Availability', 'Member', 'availabilitySpacesPromise', 'plansPromise', 'groupsPromise', 'settingsPromise', 'spacePromise', '_t', 'uiCalendarConfig', 'CalendarConfig',
function ($scope, $stateParams, Auth, $timeout, Availability, Member, availabilitySpacesPromise, plansPromise, groupsPromise, settingsPromise, spacePromise, _t, uiCalendarConfig, CalendarConfig) {
/* PRIVATE STATIC CONSTANTS */
// Color of the selected event backgound
const SELECTED_EVENT_BG_COLOR = '#ffdd00';
// Slot free to be booked
const FREE_SLOT_BORDER_COLOR = '<%= AvailabilityHelper::SPACE_COLOR %>';
// Slot with reservation from current user
const RESERVED_SLOT_BORDER_COLOR = '<%= AvailabilityHelper::IS_RESERVED_BY_CURRENT_USER %>';
/* PUBLIC SCOPE */
// bind the spaces availabilities with full-Calendar events
$scope.eventSources = [ { events: availabilitySpacesPromise, textColor: 'black' } ];
// the user to deal with, ie. the current user for non-admins
$scope.ctrl =
{ member: {} };
// list of plans, classified by group
$scope.plansClassifiedByGroup = [];
for (let group of Array.from(groupsPromise)) {
const groupObj = { id: group.id, name: group.name, plans: [] };
for (let plan of Array.from(plansPromise)) {
if (plan.group_id === group.id) { groupObj.plans.push(plan); }
}
$scope.plansClassifiedByGroup.push(groupObj);
}
// mapping of fullCalendar events.
$scope.events = {
reserved: [], // Slots that the user wants to book
modifiable: null, // Slot that the user wants to change
placable: null, // Destination slot for the change
paid: [], // Slots that were just booked by the user (transaction ok)
moved: null // Slots that were just moved by the user (change done) -> {newSlot:* oldSlot: *}
};
// the moment when the slot selection changed for the last time, used to trigger changes in the cart
$scope.selectionTime = null;
// the last clicked event in the calender
$scope.selectedEvent = null;
// indicates the state of the current view : calendar or plans information
$scope.plansAreShown = false;
// will store the user's plan if he choosed to buy one
$scope.selectedPlan = null;
// the moment when the plan selection changed for the last time, used to trigger changes in the cart
$scope.planSelectionTime = null;
// Selected space
$scope.space = spacePromise;
// fullCalendar (v2) configuration
$scope.calendarConfig = CalendarConfig({
minTime: moment.duration(moment(settingsPromise.booking_window_start).format('HH:mm:ss')),
maxTime: moment.duration(moment(settingsPromise.booking_window_end).format('HH:mm:ss')),
eventClick (event, jsEvent, view) {
return calendarEventClickCb(event, jsEvent, view);
},
eventRender (event, element, view) {
return eventRenderCb(event, element, view);
}
});
// Application global settings
$scope.settings = settingsPromise;
// Global config: message to the end user concerning the subscriptions rules
$scope.subscriptionExplicationsAlert = settingsPromise.subscription_explications_alert;
// Global config: message to the end user concerning the space reservation
$scope.spaceExplicationsAlert = settingsPromise.space_explications_alert;
/**
* Change the last selected slot's appearence to looks like 'added to cart'
*/
$scope.markSlotAsAdded = function () {
$scope.selectedEvent.backgroundColor = SELECTED_EVENT_BG_COLOR;
return updateCalendar();
};
/**
* Change the last selected slot's appearence to looks like 'never added to cart'
*/
$scope.markSlotAsRemoved = function (slot) {
slot.backgroundColor = 'white';
slot.title = '';
slot.borderColor = FREE_SLOT_BORDER_COLOR;
slot.id = null;
slot.isValid = false;
slot.is_reserved = false;
slot.can_modify = false;
slot.offered = false;
if (slot.is_completed) { slot.is_completed = false; }
return updateCalendar();
};
/**
* Callback when a slot was successfully cancelled. Reset the slot style as 'ready to book'
*/
$scope.slotCancelled = function () { $scope.markSlotAsRemoved($scope.selectedEvent); };
/**
* Change the last selected slot's appearence to looks like 'currently looking for a new destination to exchange'
*/
$scope.markSlotAsModifying = function () {
$scope.selectedEvent.backgroundColor = '#eee';
$scope.selectedEvent.title = _t('space_reserve.i_change');
return updateCalendar();
};
/**
* Change the last selected slot's appearence to looks like 'the slot being exchanged will take this place'
*/
$scope.changeModifyTrainingSlot = function () {
if ($scope.events.placable) {
$scope.events.placable.backgroundColor = 'white';
$scope.events.placable.title = '';
}
if (!$scope.events.placable || ($scope.events.placable._id !== $scope.selectedEvent._id)) {
$scope.selectedEvent.backgroundColor = '#bbb';
$scope.selectedEvent.title = _t('space_reserve.i_shift');
}
return updateCalendar();
};
/**
* When modifying an already booked reservation, callback when the modification was successfully done.
*/
$scope.modifyTrainingSlot = function () {
$scope.events.placable.title = _t('space_reserve.i_ve_reserved');
$scope.events.placable.backgroundColor = 'white';
$scope.events.placable.borderColor = $scope.events.modifiable.borderColor;
$scope.events.placable.id = $scope.events.modifiable.id;
$scope.events.placable.is_reserved = true;
$scope.events.placable.can_modify = true;
$scope.events.modifiable.backgroundColor = 'white';
$scope.events.modifiable.title = '';
$scope.events.modifiable.borderColor = FREE_SLOT_BORDER_COLOR;
$scope.events.modifiable.id = null;
$scope.events.modifiable.is_reserved = false;
$scope.events.modifiable.can_modify = false;
if ($scope.events.modifiable.is_completed) { $scope.events.modifiable.is_completed = false; }
return updateCalendar();
};
/**
* Cancel the current booking modification, reseting the whole process
*/
$scope.cancelModifyTrainingSlot = function () {
if ($scope.events.placable) {
$scope.events.placable.backgroundColor = 'white';
$scope.events.placable.title = '';
}
$scope.events.modifiable.title = _t('space_reserve.i_ve_reserved');
$scope.events.modifiable.backgroundColor = 'white';
return updateCalendar();
};
/**
* Callback to deal with the reservations of the user selected in the dropdown list instead of the current user's
* reservations. (admins only)
*/
$scope.updateMember = function () {
if ($scope.ctrl.member) {
Member.get({ id: $scope.ctrl.member.id }, function (member) {
$scope.ctrl.member = member;
return Availability.spaces({ spaceId: $scope.space.id, member_id: $scope.ctrl.member.id }, function (spaces) {
uiCalendarConfig.calendars.calendar.fullCalendar('removeEvents');
return $scope.eventSources.splice(0, 1, {
events: spaces,
textColor: 'black'
}
);
});
});
}
// as the events are re-fetched for the new user, we must re-init the cart
$scope.events.reserved = [];
$scope.selectedPlan = null;
return $scope.plansAreShown = false;
};
/**
* Add the provided plan to the current shopping cart
* @param plan {Object} the plan to subscribe
*/
$scope.selectPlan = function (plan) {
// toggle selected plan
if ($scope.selectedPlan !== plan) {
$scope.selectedPlan = plan;
} else {
$scope.selectedPlan = null;
}
return $scope.planSelectionTime = new Date();
};
/**
* Changes the user current view from the plan subsription screen to the machine reservation agenda
* @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
*/
$scope.doNotSubscribePlan = function (e) {
e.preventDefault();
$scope.plansAreShown = false;
$scope.selectedPlan = null;
return $scope.planSelectionTime = new Date();
};
/**
* Switch the user's view from the reservation agenda to the plan subscription
*/
$scope.showPlans = function () { $scope.plansAreShown = true; };
/**
* Once the reservation is booked (payment process successfully completed), change the event style
* in fullCalendar, update the user's subscription and free-credits if needed
* @param reservation {Object}
*/
$scope.afterPayment = function (reservation) {
angular.forEach($scope.events.paid, function (spaceSlot, key) {
spaceSlot.is_reserved = true;
spaceSlot.can_modify = true;
spaceSlot.title = _t('space_reserve.i_ve_reserved');
spaceSlot.backgroundColor = 'white';
spaceSlot.borderColor = RESERVED_SLOT_BORDER_COLOR;
return updateSpaceSlotId(spaceSlot, reservation);
});
if ($scope.selectedPlan) {
$scope.ctrl.member.subscribed_plan = angular.copy($scope.selectedPlan);
Auth._currentUser.subscribed_plan = angular.copy($scope.selectedPlan);
$scope.plansAreShown = false;
$scope.selectedPlan = null;
}
$scope.ctrl.member.training_credits = angular.copy(reservation.user.training_credits);
$scope.ctrl.member.machine_credits = angular.copy(reservation.user.machine_credits);
Auth._currentUser.training_credits = angular.copy(reservation.user.training_credits);
Auth._currentUser.machine_credits = angular.copy(reservation.user.machine_credits);
return refetchCalendar();
};
/**
* To use as callback in Array.prototype.filter to get only enabled plans
*/
$scope.filterDisabledPlans = function (plan) { return !plan.disabled; };
/* PRIVATE SCOPE */
/**
* Kind of constructor: these actions will be realized first when the controller is loaded
*/
const initialize = function () {
if ($scope.currentUser.role !== 'admin') {
return Member.get({ id: $scope.currentUser.id }, function (member) { $scope.ctrl.member = member; });
}
};
/**
* Triggered when the user clicks on a reservation slot in the agenda.
* Defines the behavior to adopt depending on the slot status (already booked, free, ready to be reserved ...),
* the user's subscription (current or about to be took) and the time (the user cannot modify a booked reservation
* if it's too late).
* @see http://fullcalendar.io/docs/mouse/eventClick/
*/
var calendarEventClickCb = function (event, jsEvent, view) {
$scope.selectedEvent = event;
if ($stateParams.id === 'all') {
$scope.training = event.training;
}
return $scope.selectionTime = new Date();
};
/**
* Triggered when fullCalendar tries to graphicaly render an event block.
* Append the event tag into the block, just after the event title.
* @see http://fullcalendar.io/docs/event_rendering/eventRender/
*/
var eventRenderCb = function (event, element, view) {
if (($scope.currentUser.role === 'admin') && (event.tags.length > 0)) {
let html = '';
for (let tag of Array.from(event.tags)) {
html += `<span class='label label-success text-white' title='${tag.name}'>${tag.name}</span>`;
}
element.find('.fc-time').append(html);
}
};
/**
* 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}
*/
var updateSpaceSlotId = function (slot, reservation) {
angular.forEach(reservation.slots, function (s) {
if (slot.start_at === slot.start_at) {
return slot.id = s.id;
}
});
};
/**
* Update the calendar's display to render the new attributes of the events
*/
var updateCalendar = function () { uiCalendarConfig.calendars.calendar.fullCalendar('rerenderEvents'); };
/**
* Asynchronously fetch the events from the API and refresh the calendar's view with these new events
*/
var refetchCalendar = function () {
$timeout(function () {
uiCalendarConfig.calendars.calendar.fullCalendar('refetchEvents');
return uiCalendarConfig.calendars.calendar.fullCalendar('rerenderEvents');
});
};
// !!! MUST BE CALLED AT THE END of the controller
return initialize();
}
]);

View File

@ -1,426 +0,0 @@
'use strict'
##
# Public listing of the trainings
##
Application.Controllers.controller "TrainingsController", ['$scope', '$state', 'trainingsPromise', ($scope, $state, trainingsPromise) ->
## List of trainings
$scope.trainings = trainingsPromise
##
# Callback for the 'reserve' button
##
$scope.reserveTraining = (training, event) ->
$state.go('app.logged.trainings_reserve', {id: training.slug})
##
# Callback for the 'show' button
##
$scope.showTraining = (training) ->
$state.go('app.public.training_show', {id: training.slug})
]
##
# Public view of a specific training
##
Application.Controllers.controller "ShowTrainingController", ['$scope', '$state', 'trainingPromise', 'growl', '_t', 'dialogs', ($scope, $state, trainingPromise, growl, _t, dialogs) ->
## Current training
$scope.training = trainingPromise
##
# Callback to delete the current training (admins only)
##
$scope.delete = (training) ->
# check the permissions
if $scope.currentUser.role isnt 'admin'
console.error _t('unauthorized_operation')
else
dialogs.confirm
resolve:
object: ->
title: _t('confirmation_required')
msg: _t('do_you_really_want_to_delete_this_training')
, -> # deletion confirmed
# delete the training then redirect to the trainings listing
training.$delete ->
$state.go('app.public.trainings_list')
, (error)->
growl.warning(_t('the_training_cant_be_deleted_because_it_is_already_reserved_by_some_users'))
##
# Callback for the 'reserve' button
##
$scope.reserveTraining = (training, event) ->
$state.go('app.logged.trainings_reserve', {id: training.id})
##
# Revert view to the full list of trainings ("<-" button)
##
$scope.cancel = (event) ->
$state.go('app.public.trainings_list')
]
##
# 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", '$stateParams', 'Auth', '$timeout', 'Availability', 'Member', 'availabilityTrainingsPromise', 'plansPromise', 'groupsPromise', 'settingsPromise', 'trainingPromise', '_t', 'uiCalendarConfig', 'CalendarConfig'
($scope, $stateParams, Auth, $timeout, Availability, Member, availabilityTrainingsPromise, plansPromise, groupsPromise, settingsPromise, trainingPromise, _t, uiCalendarConfig, CalendarConfig) ->
### PRIVATE STATIC CONSTANTS ###
# Color of the selected event backgound
SELECTED_EVENT_BG_COLOR = '#ffdd00'
# Slot free to be booked
FREE_SLOT_BORDER_COLOR = '<%= AvailabilityHelper::TRAINING_COLOR %>'
### PUBLIC SCOPE ###
## 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: {}
## list of plans, classified by group
$scope.plansClassifiedByGroup = []
for group in groupsPromise
groupObj = { id: group.id, name: group.name, plans: [] }
for plan in plansPromise
groupObj.plans.push(plan) if plan.group_id == group.id
$scope.plansClassifiedByGroup.push(groupObj)
## mapping of fullCalendar events.
$scope.events =
reserved: [] # Slots that the user wants to book
modifiable: null # Slot that the user wants to change
placable: null # Destination slot for the change
paid: [] # Slots that were just booked by the user (transaction ok)
moved: null # Slots that were just moved by the user (change done) -> {newSlot:* oldSlot: *}
## the moment when the slot selection changed for the last time, used to trigger changes in the cart
$scope.selectionTime = null
## the last clicked event in the calender
$scope.selectedEvent = null
## indicates the state of the current view : calendar or plans information
$scope.plansAreShown = false
## will store the user's plan if he choosed to buy one
$scope.selectedPlan = null
## the moment when the plan selection changed for the last time, used to trigger changes in the cart
$scope.planSelectionTime = null
## Selected training
$scope.training = trainingPromise
## 'all' OR training's slug
$scope.mode = $stateParams.id
## fullCalendar (v2) configuration
$scope.calendarConfig = CalendarConfig
minTime: moment.duration(moment(settingsPromise.booking_window_start).format('HH:mm:ss'))
maxTime: moment.duration(moment(settingsPromise.booking_window_end).format('HH:mm:ss'))
eventClick: (event, jsEvent, view) ->
calendarEventClickCb(event, jsEvent, view)
eventRender: (event, element, view) ->
eventRenderCb(event, element, view)
## Application global settings
$scope.settings = settingsPromise
## Global config: message to the end user concerning the subscriptions rules
$scope.subscriptionExplicationsAlert = settingsPromise.subscription_explications_alert
## Global config: message to the end user concerning the training reservation
$scope.trainingExplicationsAlert = settingsPromise.training_explications_alert
## Global config: message to the end user giving advice about the training reservation
$scope.trainingInformationMessage = settingsPromise.training_information_message
##
# Change the last selected slot's appearence to looks like 'added to cart'
##
$scope.markSlotAsAdded = ->
$scope.selectedEvent.backgroundColor = SELECTED_EVENT_BG_COLOR
updateCalendar()
##
# Change the last selected slot's appearence to looks like 'never added to cart'
##
$scope.markSlotAsRemoved = (slot) ->
slot.backgroundColor = 'white'
slot.title = slot.training.name
slot.borderColor = FREE_SLOT_BORDER_COLOR
slot.id = null
slot.isValid = false
slot.is_reserved = false
slot.can_modify = false
slot.offered = false
slot.is_completed = false if slot.is_completed
updateCalendar()
##
# Callback when a slot was successfully cancelled. Reset the slot style as 'ready to book'
##
$scope.slotCancelled = ->
$scope.markSlotAsRemoved($scope.selectedEvent)
##
# Change the last selected slot's appearence to looks like 'currently looking for a new destination to exchange'
##
$scope.markSlotAsModifying = ->
$scope.selectedEvent.backgroundColor = '#eee'
$scope.selectedEvent.title = $scope.selectedEvent.training.name + ' - ' + _t('i_change')
updateCalendar()
##
# Change the last selected slot's appearence to looks like 'the slot being exchanged will take this place'
##
$scope.changeModifyTrainingSlot = ->
if $scope.events.placable
$scope.events.placable.backgroundColor = 'white'
$scope.events.placable.title = $scope.events.placable.training.name
if !$scope.events.placable or $scope.events.placable._id != $scope.selectedEvent._id
$scope.selectedEvent.backgroundColor = '#bbb'
$scope.selectedEvent.title = $scope.selectedEvent.training.name + ' - ' + _t('i_shift')
updateCalendar()
##
# When modifying an already booked reservation, callback when the modification was successfully done.
##
$scope.modifyTrainingSlot = ->
$scope.events.placable.title = if $scope.currentUser.role isnt 'admin' then $scope.events.placable.training.name + " - " + _t('i_ve_reserved') else $scope.events.placable.training.name
$scope.events.placable.backgroundColor = 'white'
$scope.events.placable.borderColor = $scope.events.modifiable.borderColor
$scope.events.placable.id = $scope.events.modifiable.id
$scope.events.placable.is_reserved = true
$scope.events.placable.can_modify = true
$scope.events.modifiable.backgroundColor = 'white'
$scope.events.modifiable.title = $scope.events.modifiable.training.name
$scope.events.modifiable.borderColor = FREE_SLOT_BORDER_COLOR
$scope.events.modifiable.id = null
$scope.events.modifiable.is_reserved = false
$scope.events.modifiable.can_modify = false
$scope.events.modifiable.is_completed = false if $scope.events.modifiable.is_completed
updateCalendar()
##
# Cancel the current booking modification, reseting the whole process
##
$scope.cancelModifyTrainingSlot = ->
if $scope.events.placable
$scope.events.placable.backgroundColor = 'white'
$scope.events.placable.title = $scope.events.placable.training.name
$scope.events.modifiable.title = if $scope.currentUser.role isnt 'admin' then $scope.events.modifiable.training.name + " - " + _t('i_ve_reserved') else $scope.events.modifiable.training.name
$scope.events.modifiable.backgroundColor = 'white'
updateCalendar()
##
# Callback to deal with the reservations of the user selected in the dropdown list instead of the current user's
# reservations. (admins only)
##
$scope.updateMember = ->
if $scope.ctrl.member
Member.get {id: $scope.ctrl.member.id}, (member) ->
$scope.ctrl.member = member
id = if $stateParams.id is 'all' then $stateParams.id else $scope.training.id
Availability.trainings {trainingId: id, member_id: $scope.ctrl.member.id}, (trainings) ->
uiCalendarConfig.calendars.calendar.fullCalendar 'removeEvents'
$scope.eventSources.splice(0, 1,
events: trainings
textColor: 'black'
)
# as the events are re-fetched for the new user, we must re-init the cart
$scope.events.reserved = []
$scope.selectedPlan = null
$scope.plansAreShown = false
##
# Add the provided plan to the current shopping cart
# @param plan {Object} the plan to subscribe
##
$scope.selectPlan = (plan) ->
# toggle selected plan
if $scope.selectedPlan != plan
$scope.selectedPlan = plan
else
$scope.selectedPlan = null
$scope.planSelectionTime = new Date()
##
# Changes the user current view from the plan subsription screen to the machine reservation agenda
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
##
$scope.doNotSubscribePlan = (e)->
e.preventDefault()
$scope.plansAreShown = false
$scope.selectedPlan = null
$scope.planSelectionTime = new Date()
##
# Switch the user's view from the reservation agenda to the plan subscription
##
$scope.showPlans = ->
$scope.plansAreShown = true
##
# Once the reservation is booked (payment process successfully completed), change the event style
# in fullCalendar, update the user's subscription and free-credits if needed
# @param reservation {Object}
##
$scope.afterPayment = (reservation)->
$scope.events.paid[0].backgroundColor = 'white'
$scope.events.paid[0].is_reserved = true
$scope.events.paid[0].can_modify = true
updateTrainingSlotId($scope.events.paid[0], reservation)
$scope.events.paid[0].borderColor = '#b2e774'
$scope.events.paid[0].title = $scope.events.paid[0].training.name + " - " + _t('i_ve_reserved')
if $scope.selectedPlan
$scope.ctrl.member.subscribed_plan = angular.copy($scope.selectedPlan)
Auth._currentUser.subscribed_plan = angular.copy($scope.selectedPlan)
$scope.plansAreShown = false
$scope.selectedPlan = null
$scope.ctrl.member.training_credits = angular.copy(reservation.user.training_credits)
$scope.ctrl.member.machine_credits = angular.copy(reservation.user.machine_credits)
Auth._currentUser.training_credits = angular.copy(reservation.user.training_credits)
Auth._currentUser.machine_credits = angular.copy(reservation.user.machine_credits)
refetchCalendar()
##
# To use as callback in Array.prototype.filter to get only enabled plans
##
$scope.filterDisabledPlans = (plan) ->
!plan.disabled
### PRIVATE SCOPE ###
##
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
if $scope.currentUser.role isnt 'admin'
Member.get id: $scope.currentUser.id, (member) ->
$scope.ctrl.member = member
##
# Triggered when the user clicks on a reservation slot in the agenda.
# Defines the behavior to adopt depending on the slot status (already booked, free, ready to be reserved ...),
# the user's subscription (current or about to be took) and the time (the user cannot modify a booked reservation
# if it's too late).
# @see http://fullcalendar.io/docs/mouse/eventClick/
##
calendarEventClickCb = (event, jsEvent, view) ->
$scope.selectedEvent = event
if $stateParams.id is 'all'
$scope.training = event.training
$scope.selectionTime = new Date()
##
# Triggered when fullCalendar tries to graphicaly render an event block.
# Append the event tag into the block, just after the event title.
# @see http://fullcalendar.io/docs/event_rendering/eventRender/
##
eventRenderCb = (event, element, view)->
if $scope.currentUser.role is 'admin' and event.tags.length > 0
html = ''
for tag in event.tags
html += "<span class='label label-success text-white' title='#{tag.name}'>#{tag.name}</span>"
element.find('.fc-time').append(html)
return
##
# After payment, update the id of the newly reserved slot with the id returned by the server.
# This will allow the user to modify the reservation he just booked.
# @param slot {Object}
# @param reservation {Object}
##
updateTrainingSlotId = (slot, reservation)->
angular.forEach reservation.slots, (s)->
if slot.start_at == slot.start_at
slot.id = s.id
##
# Update the calendar's display to render the new attributes of the events
##
updateCalendar = ->
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
##
# Asynchronously fetch the events from the API and refresh the calendar's view with these new events
##
refetchCalendar = ->
$timeout ->
uiCalendarConfig.calendars.calendar.fullCalendar 'refetchEvents'
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
## !!! MUST BE CALLED AT THE END of the controller
initialize()
]

View File

@ -0,0 +1,427 @@
/* eslint-disable
handle-callback-err,
no-return-assign,
no-self-compare,
no-undef,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
'use strict';
/**
* Public listing of the trainings
*/
Application.Controllers.controller('TrainingsController', ['$scope', '$state', 'trainingsPromise',
function ($scope, $state, trainingsPromise) {
// List of trainings
$scope.trainings = trainingsPromise;
/**
* Callback for the 'reserve' button
*/
$scope.reserveTraining = function (training, event) { $state.go('app.logged.trainings_reserve', { id: training.slug }); };
/**
* Callback for the 'show' button
*/
$scope.showTraining = function (training) { $state.go('app.public.training_show', { id: training.slug }); };
}]);
/**
* Public view of a specific training
*/
Application.Controllers.controller('ShowTrainingController', ['$scope', '$state', 'trainingPromise', 'growl', '_t', 'dialogs',
function ($scope, $state, trainingPromise, growl, _t, dialogs) {
// Current training
$scope.training = trainingPromise;
/**
* Callback to delete the current training (admins only)
*/
$scope.delete = function (training) {
// check the permissions
if ($scope.currentUser.role !== 'admin') {
console.error(_t('unauthorized_operation'));
} else {
dialogs.confirm(
{
resolve: {
object () {
return {
title: _t('confirmation_required'),
msg: _t('do_you_really_want_to_delete_this_training')
};
}
}
},
function () { // deletion confirmed
// delete the training then redirect to the trainings listing
training.$delete(
function () { $state.go('app.public.trainings_list'); },
function (error) {
growl.warning(_t('the_training_cant_be_deleted_because_it_is_already_reserved_by_some_users'));
console.error(error);
}
);
}
);
}
};
/**
* Callback for the 'reserve' button
*/
$scope.reserveTraining = function (training, event) { $state.go('app.logged.trainings_reserve', { id: training.id }); };
/**
* Revert view to the full list of trainings ("<-" button)
*/
$scope.cancel = function (event) { $state.go('app.public.trainings_list'); };
}]);
/**
* 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', '$stateParams', 'Auth', '$timeout', 'Availability', 'Member', 'availabilityTrainingsPromise', 'plansPromise', 'groupsPromise', 'settingsPromise', 'trainingPromise', '_t', 'uiCalendarConfig', 'CalendarConfig',
function ($scope, $stateParams, Auth, $timeout, Availability, Member, availabilityTrainingsPromise, plansPromise, groupsPromise, settingsPromise, trainingPromise, _t, uiCalendarConfig, CalendarConfig) {
/* PRIVATE STATIC CONSTANTS */
// Color of the selected event backgound
const SELECTED_EVENT_BG_COLOR = '#ffdd00';
// Slot free to be booked
const FREE_SLOT_BORDER_COLOR = '<%= AvailabilityHelper::TRAINING_COLOR %>';
/* PUBLIC SCOPE */
// 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: {} };
// list of plans, classified by group
$scope.plansClassifiedByGroup = [];
for (let group of Array.from(groupsPromise)) {
const groupObj = { id: group.id, name: group.name, plans: [] };
for (let plan of Array.from(plansPromise)) {
if (plan.group_id === group.id) { groupObj.plans.push(plan); }
}
$scope.plansClassifiedByGroup.push(groupObj);
}
// mapping of fullCalendar events.
$scope.events = {
reserved: [], // Slots that the user wants to book
modifiable: null, // Slot that the user wants to change
placable: null, // Destination slot for the change
paid: [], // Slots that were just booked by the user (transaction ok)
moved: null // Slots that were just moved by the user (change done) -> {newSlot:* oldSlot: *}
};
// the moment when the slot selection changed for the last time, used to trigger changes in the cart
$scope.selectionTime = null;
// the last clicked event in the calender
$scope.selectedEvent = null;
// indicates the state of the current view : calendar or plans information
$scope.plansAreShown = false;
// will store the user's plan if he choosed to buy one
$scope.selectedPlan = null;
// the moment when the plan selection changed for the last time, used to trigger changes in the cart
$scope.planSelectionTime = null;
// Selected training
$scope.training = trainingPromise;
// 'all' OR training's slug
$scope.mode = $stateParams.id;
// fullCalendar (v2) configuration
$scope.calendarConfig = CalendarConfig({
minTime: moment.duration(moment(settingsPromise.booking_window_start).format('HH:mm:ss')),
maxTime: moment.duration(moment(settingsPromise.booking_window_end).format('HH:mm:ss')),
eventClick (event, jsEvent, view) {
return calendarEventClickCb(event, jsEvent, view);
},
eventRender (event, element, view) {
return eventRenderCb(event, element, view);
}
});
// Application global settings
$scope.settings = settingsPromise;
// Global config: message to the end user concerning the subscriptions rules
$scope.subscriptionExplicationsAlert = settingsPromise.subscription_explications_alert;
// Global config: message to the end user concerning the training reservation
$scope.trainingExplicationsAlert = settingsPromise.training_explications_alert;
// Global config: message to the end user giving advice about the training reservation
$scope.trainingInformationMessage = settingsPromise.training_information_message;
/**
* Change the last selected slot's appearence to looks like 'added to cart'
*/
$scope.markSlotAsAdded = function () {
$scope.selectedEvent.backgroundColor = SELECTED_EVENT_BG_COLOR;
return updateCalendar();
};
/**
* Change the last selected slot's appearence to looks like 'never added to cart'
*/
$scope.markSlotAsRemoved = function (slot) {
slot.backgroundColor = 'white';
slot.title = slot.training.name;
slot.borderColor = FREE_SLOT_BORDER_COLOR;
slot.id = null;
slot.isValid = false;
slot.is_reserved = false;
slot.can_modify = false;
slot.offered = false;
if (slot.is_completed) { slot.is_completed = false; }
return updateCalendar();
};
/**
* Callback when a slot was successfully cancelled. Reset the slot style as 'ready to book'
*/
$scope.slotCancelled = function () { $scope.markSlotAsRemoved($scope.selectedEvent); };
/**
* Change the last selected slot's appearence to looks like 'currently looking for a new destination to exchange'
*/
$scope.markSlotAsModifying = function () {
$scope.selectedEvent.backgroundColor = '#eee';
$scope.selectedEvent.title = $scope.selectedEvent.training.name + ' - ' + _t('i_change');
return updateCalendar();
};
/**
* Change the last selected slot's appearence to looks like 'the slot being exchanged will take this place'
*/
$scope.changeModifyTrainingSlot = function () {
if ($scope.events.placable) {
$scope.events.placable.backgroundColor = 'white';
$scope.events.placable.title = $scope.events.placable.training.name;
}
if (!$scope.events.placable || ($scope.events.placable._id !== $scope.selectedEvent._id)) {
$scope.selectedEvent.backgroundColor = '#bbb';
$scope.selectedEvent.title = $scope.selectedEvent.training.name + ' - ' + _t('i_shift');
}
return updateCalendar();
};
/**
* When modifying an already booked reservation, callback when the modification was successfully done.
*/
$scope.modifyTrainingSlot = function () {
$scope.events.placable.title = $scope.currentUser.role !== 'admin' ? $scope.events.placable.training.name + ' - ' + _t('i_ve_reserved') : $scope.events.placable.training.name;
$scope.events.placable.backgroundColor = 'white';
$scope.events.placable.borderColor = $scope.events.modifiable.borderColor;
$scope.events.placable.id = $scope.events.modifiable.id;
$scope.events.placable.is_reserved = true;
$scope.events.placable.can_modify = true;
$scope.events.modifiable.backgroundColor = 'white';
$scope.events.modifiable.title = $scope.events.modifiable.training.name;
$scope.events.modifiable.borderColor = FREE_SLOT_BORDER_COLOR;
$scope.events.modifiable.id = null;
$scope.events.modifiable.is_reserved = false;
$scope.events.modifiable.can_modify = false;
if ($scope.events.modifiable.is_completed) { $scope.events.modifiable.is_completed = false; }
return updateCalendar();
};
/**
* Cancel the current booking modification, reseting the whole process
*/
$scope.cancelModifyTrainingSlot = function () {
if ($scope.events.placable) {
$scope.events.placable.backgroundColor = 'white';
$scope.events.placable.title = $scope.events.placable.training.name;
}
$scope.events.modifiable.title = $scope.currentUser.role !== 'admin' ? $scope.events.modifiable.training.name + ' - ' + _t('i_ve_reserved') : $scope.events.modifiable.training.name;
$scope.events.modifiable.backgroundColor = 'white';
return updateCalendar();
};
/**
* Callback to deal with the reservations of the user selected in the dropdown list instead of the current user's
* reservations. (admins only)
*/
$scope.updateMember = function () {
if ($scope.ctrl.member) {
Member.get({ id: $scope.ctrl.member.id }, function (member) {
$scope.ctrl.member = member;
const id = $stateParams.id === 'all' ? $stateParams.id : $scope.training.id;
return Availability.trainings({ trainingId: id, member_id: $scope.ctrl.member.id }, function (trainings) {
uiCalendarConfig.calendars.calendar.fullCalendar('removeEvents');
return $scope.eventSources.splice(0, 1, {
events: trainings,
textColor: 'black'
}
);
});
});
}
// as the events are re-fetched for the new user, we must re-init the cart
$scope.events.reserved = [];
$scope.selectedPlan = null;
return $scope.plansAreShown = false;
};
/**
* Add the provided plan to the current shopping cart
* @param plan {Object} the plan to subscribe
*/
$scope.selectPlan = function (plan) {
// toggle selected plan
if ($scope.selectedPlan !== plan) {
$scope.selectedPlan = plan;
} else {
$scope.selectedPlan = null;
}
return $scope.planSelectionTime = new Date();
};
/**
* Changes the user current view from the plan subsription screen to the machine reservation agenda
* @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
*/
$scope.doNotSubscribePlan = function (e) {
e.preventDefault();
$scope.plansAreShown = false;
$scope.selectedPlan = null;
return $scope.planSelectionTime = new Date();
};
/**
* Switch the user's view from the reservation agenda to the plan subscription
*/
$scope.showPlans = function () { $scope.plansAreShown = true; };
/**
* Once the reservation is booked (payment process successfully completed), change the event style
* in fullCalendar, update the user's subscription and free-credits if needed
* @param reservation {Object}
*/
$scope.afterPayment = function (reservation) {
$scope.events.paid[0].backgroundColor = 'white';
$scope.events.paid[0].is_reserved = true;
$scope.events.paid[0].can_modify = true;
updateTrainingSlotId($scope.events.paid[0], reservation);
$scope.events.paid[0].borderColor = '#b2e774';
$scope.events.paid[0].title = $scope.events.paid[0].training.name + ' - ' + _t('i_ve_reserved');
if ($scope.selectedPlan) {
$scope.ctrl.member.subscribed_plan = angular.copy($scope.selectedPlan);
Auth._currentUser.subscribed_plan = angular.copy($scope.selectedPlan);
$scope.plansAreShown = false;
$scope.selectedPlan = null;
}
$scope.ctrl.member.training_credits = angular.copy(reservation.user.training_credits);
$scope.ctrl.member.machine_credits = angular.copy(reservation.user.machine_credits);
Auth._currentUser.training_credits = angular.copy(reservation.user.training_credits);
Auth._currentUser.machine_credits = angular.copy(reservation.user.machine_credits);
return refetchCalendar();
};
/**
* To use as callback in Array.prototype.filter to get only enabled plans
*/
$scope.filterDisabledPlans = function (plan) { return !plan.disabled; };
/* PRIVATE SCOPE */
/**
* Kind of constructor: these actions will be realized first when the controller is loaded
*/
const initialize = function () {
if ($scope.currentUser.role !== 'admin') {
return Member.get({ id: $scope.currentUser.id }, function (member) { $scope.ctrl.member = member; });
}
};
/**
* Triggered when the user clicks on a reservation slot in the agenda.
* Defines the behavior to adopt depending on the slot status (already booked, free, ready to be reserved ...),
* the user's subscription (current or about to be took) and the time (the user cannot modify a booked reservation
* if it's too late).
* @see http://fullcalendar.io/docs/mouse/eventClick/
*/
var calendarEventClickCb = function (event, jsEvent, view) {
$scope.selectedEvent = event;
if ($stateParams.id === 'all') {
$scope.training = event.training;
}
return $scope.selectionTime = new Date();
};
/**
* Triggered when fullCalendar tries to graphicaly render an event block.
* Append the event tag into the block, just after the event title.
* @see http://fullcalendar.io/docs/event_rendering/eventRender/
*/
var eventRenderCb = function (event, element, view) {
if (($scope.currentUser.role === 'admin') && (event.tags.length > 0)) {
let html = '';
for (let tag of Array.from(event.tags)) {
html += `<span class='label label-success text-white' title='${tag.name}'>${tag.name}</span>`;
}
element.find('.fc-time').append(html);
}
};
/**
* 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}
*/
var updateTrainingSlotId = function (slot, reservation) {
angular.forEach(reservation.slots, function (s) {
if (slot.start_at === slot.start_at) {
return slot.id = s.id;
}
});
};
/**
* Update the calendar's display to render the new attributes of the events
*/
var updateCalendar = function () { uiCalendarConfig.calendars.calendar.fullCalendar('rerenderEvents'); };
/**
* Asynchronously fetch the events from the API and refresh the calendar's view with these new events
*/
var refetchCalendar = function () {
$timeout(function () {
uiCalendarConfig.calendars.calendar.fullCalendar('refetchEvents');
return uiCalendarConfig.calendars.calendar.fullCalendar('rerenderEvents');
});
};
// !!! MUST BE CALLED AT THE END of the controller
return initialize();
}
]);

View File

@ -1,12 +0,0 @@
'use strict'
Application.Controllers.controller "WalletController", ['$scope', 'walletPromise', 'transactionsPromise', ($scope, walletPromise, transactionsPromise)->
### PUBLIC SCOPE ###
## current user wallet
$scope.wallet = walletPromise
## current wallet transactions
$scope.transactions = transactionsPromise
]

View File

@ -0,0 +1,24 @@
/* eslint-disable
no-return-assign,
no-undef,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
'use strict';
Application.Controllers.controller('WalletController', ['$scope', 'walletPromise', 'transactionsPromise',
function ($scope, walletPromise, transactionsPromise) {
/* PUBLIC SCOPE */
// current user wallet
$scope.wallet = walletPromise;
// current wallet transactions
return $scope.transactions = transactionsPromise;
}
]);

View File

@ -1,22 +1,22 @@
'use strict';
Application.Directives.directive('bsJasnyFileinput', [function(){
Application.Directives.directive('bsJasnyFileinput', [function () {
return {
require : ['ngModel'],
link : function($scope, elm, attrs, requiredCtrls){
require: ['ngModel'],
link: function ($scope, elm, attrs, requiredCtrls) {
var ngModelCtrl = requiredCtrls[0];
var fileinput = elm.parents('[data-provides=fileinput]');
var filetypeRegex = attrs.bsJasnyFileinput;
fileinput.on('clear.bs.fileinput', function(e){
if(ngModelCtrl){
fileinput.on('clear.bs.fileinput', function (e) {
if (ngModelCtrl) {
ngModelCtrl.$setViewValue(null);
ngModelCtrl.$setPristine();
$scope.$apply();
}
});
fileinput.on('change.bs.fileinput', function(e, files){
if(ngModelCtrl){
if(files){
fileinput.on('change.bs.fileinput', function (e, files) {
if (ngModelCtrl) {
if (files) {
ngModelCtrl.$setViewValue(files.result);
} else {
ngModelCtrl.$setPristine();
@ -24,14 +24,11 @@ Application.Directives.directive('bsJasnyFileinput', [function(){
// TODO: ne marche pas pour filetype
if (filetypeRegex) {
if(files && typeof files.type !== "undefined" && files.type.match(new RegExp(filetypeRegex)))
ngModelCtrl.$setValidity('filetype', true);
else
ngModelCtrl.$setValidity('filetype', false);
if (files && typeof files.type !== 'undefined' && files.type.match(new RegExp(filetypeRegex))) { ngModelCtrl.$setValidity('filetype', true); } else { ngModelCtrl.$setValidity('filetype', false); }
};
}
$scope.$apply();
});
}
}
};
}]);

View File

@ -1,569 +0,0 @@
Application.Directives.directive 'cart', [ '$rootScope', '$uibModal', 'dialogs', 'growl', 'Auth', 'Price', 'Wallet', 'CustomAsset', 'Slot', 'helpers', '_t'
, ($rootScope, $uibModal, dialogs, growl, Auth, Price, Wallet, CustomAsset, Slot, helpers, _t) ->
{
restrict: 'E'
scope:
slot: '='
slotSelectionTime: '='
events: '='
user: '='
modePlans: '='
plan: '='
planSelectionTime: '='
settings: '='
onSlotAddedToCart: '='
onSlotRemovedFromCart: '='
onSlotStartToModify: '='
onSlotModifyDestination: '='
onSlotModifySuccess: '='
onSlotModifyCancel: '='
onSlotModifyUnselect: '='
onSlotCancelSuccess: '='
afterPayment: '='
reservableId: '@'
reservableType: '@'
reservableName: '@'
limitToOneSlot: '@'
templateUrl: '<%= asset_path "shared/_cart.html" %>'
link: ($scope, element, attributes) ->
## will store the user's plan if he choosed to buy one
$scope.selectedPlan = null
## total amount of the bill to pay
$scope.amountTotal = 0
## total amount of the elements in the cart, without considering any coupon
$scope.totalNoCoupon = 0
## Discount coupon to apply to the basket, if any
$scope.coupon =
applied: null
## Global config: is the user authorized to change his bookings slots?
$scope.enableBookingMove = ($scope.settings.booking_move_enable == "true")
## Global config: delay in hours before a booking while changing the booking slot is forbidden
$scope.moveBookingDelay = parseInt($scope.settings.booking_move_delay)
## Global config: is the user authorized to cancel his bookings?
$scope.enableBookingCancel = ($scope.settings.booking_cancel_enable == "true")
## Global config: delay in hours before a booking while the cancellation is forbidden
$scope.cancelBookingDelay = parseInt($scope.settings.booking_cancel_delay)
##
# Add the provided slot to the shopping cart (state transition from free to 'about to be reserved')
# and increment the total amount of the cart if needed.
# @param slot {Object} fullCalendar event object
##
$scope.validateSlot = (slot)->
slot.isValid = true
updateCartPrice()
##
# Remove the provided slot from the shopping cart (state transition from 'about to be reserved' to free)
# and decrement the total amount of the cart if needed.
# @param slot {Object} fullCalendar event object
# @param index {number} index of the slot in the reservation array
# @param [event] {Object} see https://docs.angularjs.org/guide/expression#-event-
##
$scope.removeSlot = (slot, index, event)->
event.preventDefault() if event
$scope.events.reserved.splice(index, 1)
# if is was the last slot, we remove any plan from the cart
if $scope.events.reserved.length == 0
$scope.selectedPlan = null
$scope.plan = null
$scope.modePlans = false
$scope.onSlotRemovedFromCart(slot) if typeof $scope.onSlotRemovedFromCart == 'function'
updateCartPrice()
##
# Checks that every selected slots were added to the shopping cart. Ie. will return false if
# any checked slot was not validated by the user.
##
$scope.isSlotsValid = ->
isValid = true
angular.forEach $scope.events.reserved, (m)->
isValid = false if !m.isValid
isValid
##
# Switch the user's view from the reservation agenda to the plan subscription
##
$scope.showPlans = ->
# first, we ensure that a user was selected (admin) or logged (member)
if Object.keys($scope.user).length > 0
$scope.modePlans = true
else
# otherwise we alert, this error musn't occur when the current user hasn't the admin role
growl.error(_t('cart.please_select_a_member_first'))
##
# Validates the shopping chart and redirect the user to the payment step
##
$scope.payCart = ->
# first, we check that a user was selected
if Object.keys($scope.user).length > 0
reservation = mkReservation($scope.user, $scope.events.reserved, $scope.selectedPlan)
Wallet.getWalletByUser {user_id: $scope.user.id}, (wallet) ->
amountToPay = helpers.getAmountToPay($scope.amountTotal, wallet.amount)
if not $scope.isAdmin() and amountToPay > 0
payByStripe(reservation)
else
if $scope.isAdmin() or amountToPay is 0
payOnSite(reservation)
else
# otherwise we alert, this error musn't occur when the current user is not admin
growl.error(_t('cart.please_select_a_member_first'))
##
# When modifying an already booked reservation, confirm the modification.
##
$scope.modifySlot = ->
Slot.update {id: $scope.events.modifiable.id},
slot:
start_at: $scope.events.placable.start
end_at: $scope.events.placable.end
availability_id: $scope.events.placable.availability_id
, -> # success
# -> run the callback
$scope.onSlotModifySuccess() if typeof $scope.onSlotModifySuccess == 'function'
# -> set the events as successfully moved (to display a summary)
$scope.events.moved =
newSlot: $scope.events.placable
oldSlot: $scope.events.modifiable
# -> reset the 'moving' status
$scope.events.placable = null
$scope.events.modifiable = null
, (err) -> # failure
growl.error(_t('cart.unable_to_change_the_reservation'))
console.error(err)
##
# Cancel the current booking modification, reseting the whole process
# @param event {Object} see https://docs.angularjs.org/guide/expression#-event-
##
$scope.cancelModifySlot = (event) ->
event.preventDefault() if event
$scope.onSlotModifyCancel() if typeof $scope.onSlotModifyCancel == 'function'
$scope.events.placable = null
$scope.events.modifiable = null
##
# When modifying an already booked reservation, cancel the choice of the new slot
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
##
$scope.removeSlotToPlace = (e)->
e.preventDefault()
$scope.onSlotModifyUnselect() if typeof $scope.onSlotModifyUnselect == 'function'
$scope.events.placable = null
##
# Checks if $scope.events.modifiable and $scope.events.placable have tag incompatibilities
# @returns {boolean} true in case of incompatibility
##
$scope.tagMissmatch = ->
return false if $scope.events.placable.tag_ids.length == 0
for tag in $scope.events.modifiable.tags
if tag.id not in $scope.events.placable.tag_ids
return true
false
##
# Check if the currently logged user has teh 'admin' role?
# @returns {boolean}
##
$scope.isAdmin = ->
$rootScope.currentUser and $rootScope.currentUser.role is 'admin'
### PRIVATE SCOPE ###
##
# Kind of constructor: these actions will be realized first when the directive is loaded
##
initialize = ->
# What the binded slot
$scope.$watch 'slotSelectionTime', (newValue, oldValue) ->
if newValue != oldValue
slotSelectionChanged()
$scope.$watch 'user', (newValue, oldValue) ->
if newValue != oldValue
resetCartState()
updateCartPrice()
$scope.$watch 'planSelectionTime', (newValue, oldValue) ->
if newValue != oldValue
planSelectionChanged()
# watch when a coupon is applied to re-compute the total price
$scope.$watch 'coupon.applied', (newValue, oldValue) ->
unless newValue == null and oldValue == null
updateCartPrice()
##
# Callback triggered when the selected slot changed
##
slotSelectionChanged = ->
if $scope.slot
if not $scope.slot.is_reserved and not $scope.events.modifiable and not $scope.slot.is_completed
# slot is not reserved and we are not currently modifying a slot
# -> can be added to cart or removed if already present
index = $scope.events.reserved.indexOf($scope.slot)
if index == -1
if $scope.limitToOneSlot is 'true' and $scope.events.reserved[0]
# if we limit the number of slots in the cart to 1, and there is already
# a slot in the cart, we remove it before adding the new one
$scope.removeSlot($scope.events.reserved[0], 0)
# slot is not in the cart, so we add it
$scope.events.reserved.push $scope.slot
$scope.onSlotAddedToCart() if typeof $scope.onSlotAddedToCart == 'function'
else
# slot is in the cart, remove it
$scope.removeSlot($scope.slot, index)
# in every cases, because a new reservation has started, we reset the cart content
resetCartState()
# finally, we update the prices
updateCartPrice()
else if !$scope.slot.is_reserved and !$scope.slot.is_completed and $scope.events.modifiable
# slot is not reserved but we are currently modifying a slot
# -> we request the calender to change the rendering
$scope.onSlotModifyUnselect() if typeof $scope.onSlotModifyUnselect == 'function'
# -> then, we re-affect the destination slot
if !$scope.events.placable or $scope.events.placable._id != $scope.slot._id
$scope.events.placable = $scope.slot
else
$scope.events.placable = null
else if $scope.slot.is_reserved and $scope.events.modifiable and $scope.slot.is_reserved._id == $scope.events.modifiable._id
# slot is reserved and currently modified
# -> we cancel the modification
$scope.cancelModifySlot()
else if $scope.slot.is_reserved and (slotCanBeModified($scope.slot) or slotCanBeCanceled($scope.slot)) and !$scope.events.modifiable and $scope.events.reserved.length == 0
# slot is reserved and is ok to be modified or cancelled
# but we are not currently running a modification or having any slots in the cart
# -> first the affect the modification/cancellation rights attributes to the current slot
resetCartState()
$scope.slot.movable = slotCanBeModified($scope.slot)
$scope.slot.cancelable = slotCanBeCanceled($scope.slot)
# -> then, we open a dialog to ask to the user to choose an action
dialogs.confirm
templateUrl: '<%= asset_path "shared/confirm_modify_slot_modal.html" %>'
resolve:
object: -> $scope.slot
, (type) ->
# the user has choosen an action, so we proceed
if type == 'move'
$scope.onSlotStartToModify() if typeof $scope.onSlotStartToModify == 'function'
$scope.events.modifiable = $scope.slot
else if type == 'cancel'
dialogs.confirm
resolve:
object: ->
title: _t('cart.confirmation_required')
msg: _t('cart.do_you_really_want_to_cancel_this_reservation')
, -> # cancel confirmed
Slot.cancel {id: $scope.slot.id}, -> # successfully canceled
growl.success _t('cart.reservation_was_cancelled_successfully')
$scope.onSlotCancelSuccess() if typeof $scope.onSlotCancelSuccess == 'function'
, -> # error while canceling
growl.error _t('cart.cancellation_failed')
##
# Reset the parameters that may lead to a wrong price but leave the content (events added to cart)
##
resetCartState = ->
$scope.selectedPlan = null
$scope.coupon.applied = null
$scope.events.moved = null
$scope.events.paid = []
$scope.events.modifiable = null
$scope.events.placable = null
##
# Determines if the provided booked slot is able to be modified by the user.
# @param slot {Object} fullCalendar event object
##
slotCanBeModified = (slot)->
return true if $scope.isAdmin()
slotStart = moment(slot.start)
now = moment()
if slot.can_modify and $scope.enableBookingMove and slotStart.diff(now, "hours") >= $scope.moveBookingDelay
return true
else
return false
##
# Determines if the provided booked slot is able to be canceled by the user.
# @param slot {Object} fullCalendar event object
##
slotCanBeCanceled = (slot) ->
return true if $scope.isAdmin()
slotStart = moment(slot.start)
now = moment()
if slot.can_modify and $scope.enableBookingCancel and slotStart.diff(now, "hours") >= $scope.cancelBookingDelay
return true
else
return false
##
# Callback triggered when the selected slot changed
##
planSelectionChanged = ->
if Auth.isAuthenticated()
if $scope.selectedPlan != $scope.plan
$scope.selectedPlan = $scope.plan
else
$scope.selectedPlan = null
updateCartPrice()
else
$rootScope.login null, ->
$scope.selectedPlan = $scope.plan
updateCartPrice()
##
# Update the total price of the current selection/reservation
##
updateCartPrice = ->
if Object.keys($scope.user).length > 0
r = mkReservation($scope.user, $scope.events.reserved, $scope.selectedPlan)
Price.compute mkRequestParams(r, $scope.coupon.applied), (res) ->
$scope.amountTotal = res.price
$scope.totalNoCoupon = res.price_without_coupon
setSlotsDetails(res.details)
else
# otherwise we alert, this error musn't occur when the current user is not admin
growl.warning(_t('cart.please_select_a_member_first'))
$scope.amountTotal = null
setSlotsDetails = (details) ->
angular.forEach $scope.events.reserved, (slot) ->
angular.forEach details.slots, (s) ->
if moment(s.start_at).isSame(slot.start)
slot.promo = s.promo
slot.price = s.price
##
# Format the parameters expected by /api/prices/compute or /api/reservations and return the resulting object
# @param reservation {Object} as returned by mkReservation()
# @param coupon {Object} Coupon as returned from the API
# @return {{reservation:Object, coupon_code:string}}
##
mkRequestParams = (reservation, coupon) ->
params =
reservation: reservation
coupon_code: (coupon.code if coupon)
params
##
# Create an hash map implementing the Reservation specs
# @param member {Object} User as retreived from the API: current user / selected user if current is admin
# @param slots {Array<Object>} Array of fullCalendar events: slots selected on the calendar
# @param [plan] {Object} Plan as retrived from the API: plan to buy with the current reservation
# @return {{user_id:Number, reservable_id:Number, reservable_type:String, slots_attributes:Array<Object>, plan_id:Number|null}}
##
mkReservation = (member, slots, plan = null) ->
reservation =
user_id: member.id
reservable_id: $scope.reservableId
reservable_type: $scope.reservableType
slots_attributes: []
plan_id: (plan.id if plan)
angular.forEach slots, (slot, key) ->
reservation.slots_attributes.push
start_at: slot.start
end_at: slot.end
availability_id: slot.availability_id
offered: slot.offered || false
reservation
##
# Open a modal window that allows the user to process a credit card payment for his current shopping cart.
##
payByStripe = (reservation) ->
$uibModal.open
templateUrl: '<%= asset_path "stripe/payment_modal.html" %>'
size: 'md'
resolve:
reservation: ->
reservation
price: ->
Price.compute(mkRequestParams(reservation, $scope.coupon.applied)).$promise
wallet: ->
Wallet.getWalletByUser({user_id: reservation.user_id}).$promise
cgv: ->
CustomAsset.get({name: 'cgv-file'}).$promise
coupon: ->
$scope.coupon.applied
controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'cgv', 'Auth', 'Reservation', 'wallet', 'helpers', '$filter', 'coupon',
($scope, $uibModalInstance, $state, reservation, price, cgv, Auth, Reservation, wallet, helpers, $filter, coupon) ->
# user wallet amount
$scope.walletAmount = wallet.amount
# Price
$scope.amount = helpers.getAmountToPay(price.price, wallet.amount)
# CGV
$scope.cgv = cgv.custom_asset
# Reservation
$scope.reservation = reservation
# Used in wallet info template to interpolate some translations
$scope.numberFilter = $filter('number')
##
# Callback to process the payment with Stripe, triggered on button click
##
$scope.payment = (status, response) ->
if response.error
growl.error(response.error.message)
else
$scope.attempting = true
$scope.reservation.card_token = response.id
Reservation.save mkRequestParams($scope.reservation, coupon), (reservation) ->
$uibModalInstance.close(reservation)
, (response)->
$scope.alerts = []
if response.status == 500
$scope.alerts.push
msg: response.statusText
type: 'danger'
else
if response.data.card and response.data.card.join('').length > 0
$scope.alerts.push
msg: response.data.card.join('. ')
type: 'danger'
else if response.data.payment and response.data.payment.join('').length > 0
$scope.alerts.push
msg: response.data.payment.join('. ')
type: 'danger'
$scope.attempting = false
]
.result['finally'](null).then (reservation)->
afterPayment(reservation)
##
# Open a modal window that allows the user to process a local payment for his current shopping cart (admin only).
##
payOnSite = (reservation) ->
$uibModal.open
templateUrl: '<%= asset_path "shared/valid_reservation_modal.html" %>'
size: 'sm'
resolve:
reservation: ->
reservation
price: ->
Price.compute(mkRequestParams(reservation, $scope.coupon.applied)).$promise
wallet: ->
Wallet.getWalletByUser({user_id: reservation.user_id}).$promise
coupon: ->
$scope.coupon.applied
controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'Auth', 'Reservation', 'wallet', 'helpers', '$filter', 'coupon',
($scope, $uibModalInstance, $state, reservation, price, Auth, Reservation, wallet, helpers, $filter, coupon) ->
# user wallet amount
$scope.walletAmount = wallet.amount
# Global price (total of all items)
$scope.price = price.price
# Price to pay (wallet deducted)
$scope.amount = helpers.getAmountToPay(price.price, wallet.amount)
# Reservation
$scope.reservation = reservation
# Used in wallet info template to interpolate some translations
$scope.numberFilter = $filter('number')
# Button label
if $scope.amount > 0
$scope.validButtonName = _t('cart.confirm_payment_of_html', {ROLE:$rootScope.currentUser.role, AMOUNT:$filter('currency')($scope.amount)}, "messageformat")
else
if price.price > 0 and $scope.walletAmount == 0
$scope.validButtonName = _t('cart.confirm_payment_of_html', {ROLE:$rootScope.currentUser.role, AMOUNT:$filter('currency')(price.price)}, "messageformat")
else
$scope.validButtonName = _t('confirm')
##
# Callback to process the local payment, triggered on button click
##
$scope.ok = ->
$scope.attempting = true
Reservation.save mkRequestParams($scope.reservation, coupon), (reservation) ->
$uibModalInstance.close(reservation)
$scope.attempting = true
, (response)->
$scope.alerts = []
$scope.alerts.push({msg: _t('cart.a_problem_occured_during_the_payment_process_please_try_again_later'), type: 'danger' })
$scope.attempting = false
$scope.cancel = ->
$uibModalInstance.dismiss('cancel')
]
.result['finally'](null).then (reservation)->
afterPayment(reservation)
##
# Actions to run after the payment was successfull
##
afterPayment = (reservation) ->
# we set the cart content as 'paid' to display a summary of the transaction
$scope.events.paid = $scope.events.reserved
# we call the external callback if present
$scope.afterPayment(reservation) if typeof $scope.afterPayment == 'function'
# we reset the coupon and the cart content and we unselect the slot
$scope.events.reserved = []
$scope.coupon.applied = null
$scope.slot = null
$scope.selectedPlan = null
## !!! MUST BE CALLED AT THE END of the directive
initialize()
}
]

View File

@ -0,0 +1,634 @@
/* eslint-disable
no-return-assign,
no-undef,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs', 'growl', 'Auth', 'Price', 'Wallet', 'CustomAsset', 'Slot', 'helpers', '_t',
function ($rootScope, $uibModal, dialogs, growl, Auth, Price, Wallet, CustomAsset, Slot, helpers, _t) {
return ({
restrict: 'E',
scope: {
slot: '=',
slotSelectionTime: '=',
events: '=',
user: '=',
modePlans: '=',
plan: '=',
planSelectionTime: '=',
settings: '=',
onSlotAddedToCart: '=',
onSlotRemovedFromCart: '=',
onSlotStartToModify: '=',
onSlotModifyDestination: '=',
onSlotModifySuccess: '=',
onSlotModifyCancel: '=',
onSlotModifyUnselect: '=',
onSlotCancelSuccess: '=',
afterPayment: '=',
reservableId: '@',
reservableType: '@',
reservableName: '@',
limitToOneSlot: '@'
},
templateUrl: '<%= asset_path "shared/_cart.html" %>',
link ($scope, element, attributes) {
// will store the user's plan if he choosed to buy one
$scope.selectedPlan = null;
// total amount of the bill to pay
$scope.amountTotal = 0;
// total amount of the elements in the cart, without considering any coupon
$scope.totalNoCoupon = 0;
// Discount coupon to apply to the basket, if any
$scope.coupon = { applied: null };
// Global config: is the user authorized to change his bookings slots?
$scope.enableBookingMove = ($scope.settings.booking_move_enable === 'true');
// Global config: delay in hours before a booking while changing the booking slot is forbidden
$scope.moveBookingDelay = parseInt($scope.settings.booking_move_delay);
// Global config: is the user authorized to cancel his bookings?
$scope.enableBookingCancel = ($scope.settings.booking_cancel_enable === 'true');
// Global config: delay in hours before a booking while the cancellation is forbidden
$scope.cancelBookingDelay = parseInt($scope.settings.booking_cancel_delay);
/**
* Add the provided slot to the shopping cart (state transition from free to 'about to be reserved')
* and increment the total amount of the cart if needed.
* @param slot {Object} fullCalendar event object
*/
$scope.validateSlot = function (slot) {
slot.isValid = true;
return updateCartPrice();
};
/**
* Remove the provided slot from the shopping cart (state transition from 'about to be reserved' to free)
* and decrement the total amount of the cart if needed.
* @param slot {Object} fullCalendar event object
* @param index {number} index of the slot in the reservation array
* @param [event] {Object} see https://docs.angularjs.org/guide/expression#-event-
*/
$scope.removeSlot = function (slot, index, event) {
if (event) { event.preventDefault(); }
$scope.events.reserved.splice(index, 1);
// if is was the last slot, we remove any plan from the cart
if ($scope.events.reserved.length === 0) {
$scope.selectedPlan = null;
$scope.plan = null;
$scope.modePlans = false;
}
if (typeof $scope.onSlotRemovedFromCart === 'function') { $scope.onSlotRemovedFromCart(slot); }
return updateCartPrice();
};
/**
* Checks that every selected slots were added to the shopping cart. Ie. will return false if
* any checked slot was not validated by the user.
*/
$scope.isSlotsValid = function () {
let isValid = true;
angular.forEach($scope.events.reserved, function (m) {
if (!m.isValid) { return isValid = false; }
});
return isValid;
};
/**
* Switch the user's view from the reservation agenda to the plan subscription
*/
$scope.showPlans = function () {
// first, we ensure that a user was selected (admin) or logged (member)
if (Object.keys($scope.user).length > 0) {
return $scope.modePlans = true;
} else {
// otherwise we alert, this error musn't occur when the current user hasn't the admin role
return growl.error(_t('cart.please_select_a_member_first'));
}
};
/**
* Validates the shopping chart and redirect the user to the payment step
*/
$scope.payCart = function () {
// first, we check that a user was selected
if (Object.keys($scope.user).length > 0) {
const reservation = mkReservation($scope.user, $scope.events.reserved, $scope.selectedPlan);
return Wallet.getWalletByUser({ user_id: $scope.user.id }, function (wallet) {
const amountToPay = helpers.getAmountToPay($scope.amountTotal, wallet.amount);
if (!$scope.isAdmin() && (amountToPay > 0)) {
return payByStripe(reservation);
} else {
if ($scope.isAdmin() || (amountToPay === 0)) {
return payOnSite(reservation);
}
}
});
} else {
// otherwise we alert, this error musn't occur when the current user is not admin
return growl.error(_t('cart.please_select_a_member_first'));
}
};
/**
* When modifying an already booked reservation, confirm the modification.
*/
$scope.modifySlot = function () {
Slot.update({ id: $scope.events.modifiable.id }, {
slot: {
start_at: $scope.events.placable.start,
end_at: $scope.events.placable.end,
availability_id: $scope.events.placable.availability_id
}
}
, function () { // success
// -> run the callback
if (typeof $scope.onSlotModifySuccess === 'function') { $scope.onSlotModifySuccess(); }
// -> set the events as successfully moved (to display a summary)
$scope.events.moved = {
newSlot: $scope.events.placable,
oldSlot: $scope.events.modifiable
};
// -> reset the 'moving' status
$scope.events.placable = null;
return $scope.events.modifiable = null;
}
, function (err) { // failure
growl.error(_t('cart.unable_to_change_the_reservation'));
return console.error(err);
});
};
/**
* Cancel the current booking modification, reseting the whole process
* @param event {Object} see https://docs.angularjs.org/guide/expression#-event-
*/
$scope.cancelModifySlot = function (event) {
if (event) { event.preventDefault(); }
if (typeof $scope.onSlotModifyCancel === 'function') { $scope.onSlotModifyCancel(); }
$scope.events.placable = null;
return $scope.events.modifiable = null;
};
/**
* When modifying an already booked reservation, cancel the choice of the new slot
* @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
*/
$scope.removeSlotToPlace = function (e) {
e.preventDefault();
if (typeof $scope.onSlotModifyUnselect === 'function') { $scope.onSlotModifyUnselect(); }
return $scope.events.placable = null;
};
/**
* Checks if $scope.events.modifiable and $scope.events.placable have tag incompatibilities
* @returns {boolean} true in case of incompatibility
*/
$scope.tagMissmatch = function () {
if ($scope.events.placable.tag_ids.length === 0) { return false; }
for (let tag of Array.from($scope.events.modifiable.tags)) {
if (!Array.from($scope.events.placable.tag_ids).includes(tag.id)) {
return true;
}
}
return false;
};
/**
* Check if the currently logged user has teh 'admin' role?
* @returns {boolean}
*/
$scope.isAdmin = function () { return $rootScope.currentUser && ($rootScope.currentUser.role === 'admin'); };
/* PRIVATE SCOPE */
/**
* Kind of constructor: these actions will be realized first when the directive is loaded
*/
const initialize = function () {
// What the bound slot
$scope.$watch('slotSelectionTime', function (newValue, oldValue) {
if (newValue !== oldValue) {
return slotSelectionChanged();
}
});
$scope.$watch('user', function (newValue, oldValue) {
if (newValue !== oldValue) {
resetCartState();
return updateCartPrice();
}
});
$scope.$watch('planSelectionTime', function (newValue, oldValue) {
if (newValue !== oldValue) {
return planSelectionChanged();
}
});
// watch when a coupon is applied to re-compute the total price
return $scope.$watch('coupon.applied', function (newValue, oldValue) {
if ((newValue !== null) || (oldValue !== null)) {
return updateCartPrice();
}
});
};
/**
* Callback triggered when the selected slot changed
*/
var slotSelectionChanged = function () {
if ($scope.slot) {
if (!$scope.slot.is_reserved && !$scope.events.modifiable && !$scope.slot.is_completed) {
// slot is not reserved and we are not currently modifying a slot
// -> can be added to cart or removed if already present
const index = $scope.events.reserved.indexOf($scope.slot);
if (index === -1) {
if (($scope.limitToOneSlot === 'true') && $scope.events.reserved[0]) {
// if we limit the number of slots in the cart to 1, and there is already
// a slot in the cart, we remove it before adding the new one
$scope.removeSlot($scope.events.reserved[0], 0);
}
// slot is not in the cart, so we add it
$scope.events.reserved.push($scope.slot);
if (typeof $scope.onSlotAddedToCart === 'function') { $scope.onSlotAddedToCart(); }
} else {
// slot is in the cart, remove it
$scope.removeSlot($scope.slot, index);
}
// in every cases, because a new reservation has started, we reset the cart content
resetCartState();
// finally, we update the prices
return updateCartPrice();
} else if (!$scope.slot.is_reserved && !$scope.slot.is_completed && $scope.events.modifiable) {
// slot is not reserved but we are currently modifying a slot
// -> we request the calender to change the rendering
if (typeof $scope.onSlotModifyUnselect === 'function') { $scope.onSlotModifyUnselect(); }
// -> then, we re-affect the destination slot
if (!$scope.events.placable || ($scope.events.placable._id !== $scope.slot._id)) {
return $scope.events.placable = $scope.slot;
} else {
return $scope.events.placable = null;
}
} else if ($scope.slot.is_reserved && $scope.events.modifiable && ($scope.slot.is_reserved._id === $scope.events.modifiable._id)) {
// slot is reserved and currently modified
// -> we cancel the modification
return $scope.cancelModifySlot();
} else if ($scope.slot.is_reserved && (slotCanBeModified($scope.slot) || slotCanBeCanceled($scope.slot)) && !$scope.events.modifiable && ($scope.events.reserved.length === 0)) {
// slot is reserved and is ok to be modified or cancelled
// but we are not currently running a modification or having any slots in the cart
// -> first the affect the modification/cancellation rights attributes to the current slot
resetCartState();
$scope.slot.movable = slotCanBeModified($scope.slot);
$scope.slot.cancelable = slotCanBeCanceled($scope.slot);
// -> then, we open a dialog to ask to the user to choose an action
return dialogs.confirm({
templateUrl: '<%= asset_path "shared/confirm_modify_slot_modal.html" %>',
resolve: {
object () { return $scope.slot; }
}
}
, function (type) {
// the user has choosen an action, so we proceed
if (type === 'move') {
if (typeof $scope.onSlotStartToModify === 'function') { $scope.onSlotStartToModify(); }
return $scope.events.modifiable = $scope.slot;
} else if (type === 'cancel') {
return dialogs.confirm(
{
resolve: {
object () {
return {
title: _t('cart.confirmation_required'),
msg: _t('cart.do_you_really_want_to_cancel_this_reservation')
};
}
}
},
function () { // cancel confirmed
Slot.cancel({ id: $scope.slot.id }, function () { // successfully canceled
growl.success(_t('cart.reservation_was_cancelled_successfully'));
if (typeof $scope.onSlotCancelSuccess === 'function') { return $scope.onSlotCancelSuccess(); }
}
, function () { // error while canceling
growl.error(_t('cart.cancellation_failed'));
});
}
);
}
});
}
}
};
/**
* Reset the parameters that may lead to a wrong price but leave the content (events added to cart)
*/
var resetCartState = function () {
$scope.selectedPlan = null;
$scope.coupon.applied = null;
$scope.events.moved = null;
$scope.events.paid = [];
$scope.events.modifiable = null;
return $scope.events.placable = null;
};
/**
* Determines if the provided booked slot is able to be modified by the user.
* @param slot {Object} fullCalendar event object
*/
var slotCanBeModified = function (slot) {
if ($scope.isAdmin()) { return true; }
const slotStart = moment(slot.start);
const now = moment();
if (slot.can_modify && $scope.enableBookingMove && (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
*/
var slotCanBeCanceled = function (slot) {
if ($scope.isAdmin()) { return true; }
const slotStart = moment(slot.start);
const now = moment();
if (slot.can_modify && $scope.enableBookingCancel && (slotStart.diff(now, 'hours') >= $scope.cancelBookingDelay)) {
return true;
} else {
return false;
}
};
/**
* Callback triggered when the selected slot changed
*/
var planSelectionChanged = function () {
if (Auth.isAuthenticated()) {
if ($scope.selectedPlan !== $scope.plan) {
$scope.selectedPlan = $scope.plan;
} else {
$scope.selectedPlan = null;
}
return updateCartPrice();
} else {
return $rootScope.login(null, function () {
$scope.selectedPlan = $scope.plan;
return updateCartPrice();
});
}
};
/**
* Update the total price of the current selection/reservation
*/
var updateCartPrice = function () {
if (Object.keys($scope.user).length > 0) {
const r = mkReservation($scope.user, $scope.events.reserved, $scope.selectedPlan);
return Price.compute(mkRequestParams(r, $scope.coupon.applied), function (res) {
$scope.amountTotal = res.price;
$scope.totalNoCoupon = res.price_without_coupon;
return setSlotsDetails(res.details);
});
} else {
// otherwise we alert, this error musn't occur when the current user is not admin
growl.warning(_t('cart.please_select_a_member_first'));
return $scope.amountTotal = null;
}
};
var setSlotsDetails = function (details) {
angular.forEach($scope.events.reserved, function (slot) {
angular.forEach(details.slots, function (s) {
if (moment(s.start_at).isSame(slot.start)) {
slot.promo = s.promo;
return slot.price = s.price;
}
});
});
};
/**
* Format the parameters expected by /api/prices/compute or /api/reservations and return the resulting object
* @param reservation {Object} as returned by mkReservation()
* @param coupon {Object} Coupon as returned from the API
* @return {{reservation:Object, coupon_code:string}}
*/
var mkRequestParams = function (reservation, coupon) {
return {
reservation,
coupon_code: ((coupon ? coupon.code : undefined))
};
};
/**
* Create an hash map implementing the Reservation specs
* @param member {Object} User as retrieved from the API: current user / selected user if current is admin
* @param slots {Array<Object>} Array of fullCalendar events: slots selected on the calendar
* @param [plan] {Object} Plan as retrieved from the API: plan to buy with the current reservation
* @return {{user_id:Number, reservable_id:Number, reservable_type:String, slots_attributes:Array<Object>, plan_id:Number|null}}
*/
var mkReservation = function (member, slots, plan) {
const reservation = {
user_id: member.id,
reservable_id: $scope.reservableId,
reservable_type: $scope.reservableType,
slots_attributes: [],
plan_id: ((plan ? plan.id : undefined))
};
angular.forEach(slots, function (slot, key) {
reservation.slots_attributes.push({
start_at: slot.start,
end_at: slot.end,
availability_id: slot.availability_id,
offered: slot.offered || false
});
});
return reservation;
};
/**
* Open a modal window that allows the user to process a credit card payment for his current shopping cart.
*/
var payByStripe = function (reservation) {
$uibModal.open({
templateUrl: '<%= asset_path "stripe/payment_modal.html" %>',
size: 'md',
resolve: {
reservation () {
return reservation;
},
price () {
return Price.compute(mkRequestParams(reservation, $scope.coupon.applied)).$promise;
},
wallet () {
return Wallet.getWalletByUser({ user_id: reservation.user_id }).$promise;
},
cgv () {
return CustomAsset.get({ name: 'cgv-file' }).$promise;
},
coupon () {
return $scope.coupon.applied;
}
},
controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'cgv', 'Auth', 'Reservation', 'wallet', 'helpers', '$filter', 'coupon',
function ($scope, $uibModalInstance, $state, reservation, price, cgv, Auth, Reservation, wallet, helpers, $filter, coupon) {
// user wallet amount
$scope.walletAmount = wallet.amount;
// Price
$scope.amount = helpers.getAmountToPay(price.price, wallet.amount);
// CGV
$scope.cgv = cgv.custom_asset;
// Reservation
$scope.reservation = reservation;
// Used in wallet info template to interpolate some translations
$scope.numberFilter = $filter('number');
/**
* Callback to process the payment with Stripe, triggered on button click
*/
$scope.payment = function (status, response) {
if (response.error) {
growl.error(response.error.message);
} else {
$scope.attempting = true;
$scope.reservation.card_token = response.id;
Reservation.save(
mkRequestParams($scope.reservation, coupon),
function (reservation) { $uibModalInstance.close(reservation); },
function (response) {
$scope.alerts = [];
if (response.status === 500) {
$scope.alerts.push({
msg: response.statusText,
type: 'danger'
});
} else {
if (response.data.card && (response.data.card.join('').length > 0)) {
$scope.alerts.push({
msg: response.data.card.join('. '),
type: 'danger'
});
} else if (response.data.payment && (response.data.payment.join('').length > 0)) {
$scope.alerts.push({
msg: response.data.payment.join('. '),
type: 'danger'
});
}
}
return $scope.attempting = false;
}
);
}
};
}
]
}).result['finally'](null).then(function (reservation) { afterPayment(reservation); });
};
/**
* Open a modal window that allows the user to process a local payment for his current shopping cart (admin only).
*/
var payOnSite = function (reservation) {
$uibModal.open({
templateUrl: '<%= asset_path "shared/valid_reservation_modal.html" %>',
size: 'sm',
resolve: {
reservation () {
return reservation;
},
price () {
return Price.compute(mkRequestParams(reservation, $scope.coupon.applied)).$promise;
},
wallet () {
return Wallet.getWalletByUser({ user_id: reservation.user_id }).$promise;
},
coupon () {
return $scope.coupon.applied;
}
},
controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'Auth', 'Reservation', 'wallet', 'helpers', '$filter', 'coupon',
function ($scope, $uibModalInstance, $state, reservation, price, Auth, Reservation, wallet, helpers, $filter, coupon) {
// user wallet amount
$scope.walletAmount = wallet.amount;
// Global price (total of all items)
$scope.price = price.price;
// Price to pay (wallet deducted)
$scope.amount = helpers.getAmountToPay(price.price, wallet.amount);
// Reservation
$scope.reservation = reservation;
// Used in wallet info template to interpolate some translations
$scope.numberFilter = $filter('number');
// Button label
if ($scope.amount > 0) {
$scope.validButtonName = _t('cart.confirm_payment_of_html', { ROLE: $rootScope.currentUser.role, AMOUNT: $filter('currency')($scope.amount) }, 'messageformat');
} else {
if ((price.price > 0) && ($scope.walletAmount === 0)) {
$scope.validButtonName = _t('cart.confirm_payment_of_html', { ROLE: $rootScope.currentUser.role, AMOUNT: $filter('currency')(price.price) }, 'messageformat');
} else {
$scope.validButtonName = _t('confirm');
}
}
/**
* Callback to process the local payment, triggered on button click
*/
$scope.ok = function () {
$scope.attempting = true;
return Reservation.save(mkRequestParams($scope.reservation, coupon), function (reservation) {
$uibModalInstance.close(reservation);
return $scope.attempting = true;
}
, function (response) {
$scope.alerts = [];
$scope.alerts.push({ msg: _t('cart.a_problem_occured_during_the_payment_process_please_try_again_later'), type: 'danger' });
return $scope.attempting = false;
});
};
$scope.cancel = function () { $uibModalInstance.dismiss('cancel'); };
}
]
}).result['finally'](null).then(function (reservation) { afterPayment(reservation); });
};
/**
* Actions to run after the payment was successfull
*/
var afterPayment = function (reservation) {
// we set the cart content as 'paid' to display a summary of the transaction
$scope.events.paid = $scope.events.reserved;
// we call the external callback if present
if (typeof $scope.afterPayment === 'function') { $scope.afterPayment(reservation); }
// we reset the coupon and the cart content and we unselect the slot
$scope.events.reserved = [];
$scope.coupon.applied = null;
$scope.slot = null;
return $scope.selectedPlan = null;
};
// !!! MUST BE CALLED AT THE END of the directive
return initialize();
}
});
}
]);

View File

@ -1,20 +0,0 @@
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)
}
]

View File

@ -0,0 +1,38 @@
/* eslint-disable
no-undef,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
Application.Directives.directive('confirmationNeeded', [() =>
({
priority: 1,
terminal: true,
link (scope, element, attrs) {
const msg = attrs.confirmationNeeded || 'Are you sure?';
const clickAction = attrs.ngClick;
return element.bind('click', function () {
if (attrs.confirmationNeededIf != null) {
const confirmNeededIf = scope.$eval(attrs.confirmationNeededIf);
if (confirmNeededIf === true) {
if (window.confirm(msg)) {
return scope.$eval(clickAction);
}
} else {
return scope.$eval(clickAction);
}
} else {
if (window.confirm(msg)) {
return scope.$eval(clickAction);
}
}
});
}
})
]);

View File

@ -1,59 +0,0 @@
Application.Directives.directive 'coupon', [ '$rootScope', 'Coupon', '_t', ($rootScope, Coupon, _t) ->
{
restrict: 'E'
scope:
show: '='
coupon: '='
total: '='
userId: '@'
templateUrl: '<%= asset_path "shared/_coupon.html" %>'
link: ($scope, element, attributes) ->
# Whether code input is shown or not (ie. the link 'I have a coupon' is shown)
$scope.code =
input: false
# Available status are: 'pending', 'valid', 'invalid'
$scope.status = 'pending'
# Binding for the code inputed (see the attached template)
$scope.couponCode = null
# Code validation messages
$scope.messages = []
# Re-compute if the code can be applied when the total of the cart changes
$scope.$watch 'total', (newValue, oldValue) ->
if newValue and newValue != oldValue and $scope.couponCode
$scope.validateCode()
##
# Callback to validate the code
##
$scope.validateCode = ->
$scope.messages = []
if $scope.couponCode == ''
$scope.status = 'pending'
$scope.coupon = null
else
Coupon.validate {code: $scope.couponCode, user_id: $scope.userId, amount: $scope.total}, (res) ->
$scope.status = 'valid'
$scope.coupon = res
if res.type == 'percent_off'
$scope.messages.push(type: 'success', message: _t('the_coupon_has_been_applied_you_get_PERCENT_discount', {PERCENT: res.percent_off}))
else
$scope.messages.push(type: 'success', message: _t('the_coupon_has_been_applied_you_get_AMOUNT_CURRENCY', {AMOUNT: res.amount_off, CURRENCY: $rootScope.currencySymbol}))
, (err) ->
$scope.status = 'invalid'
$scope.coupon = null
$scope.messages.push(type: 'danger', message: _t('unable_to_apply_the_coupon_because_'+err.data.status))
##
# Callback to remove the message at provided index from the displayed list
##
$scope.closeMessage = (index) ->
$scope.messages.splice(index, 1);
}
]

View File

@ -0,0 +1,75 @@
/* eslint-disable
no-return-assign,
no-undef,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
Application.Directives.directive('coupon', [ '$rootScope', 'Coupon', '_t', function ($rootScope, Coupon, _t) {
return ({
restrict: 'E',
scope: {
show: '=',
coupon: '=',
total: '=',
userId: '@'
},
templateUrl: '<%= asset_path "shared/_coupon.html" %>',
link ($scope, element, attributes) {
// Whether code input is shown or not (ie. the link 'I have a coupon' is shown)
$scope.code =
{ input: false };
// Available status are: 'pending', 'valid', 'invalid'
$scope.status = 'pending';
// Binding for the code inputed (see the attached template)
$scope.couponCode = null;
// Code validation messages
$scope.messages = [];
// Re-compute if the code can be applied when the total of the cart changes
$scope.$watch('total', function (newValue, oldValue) {
if (newValue && (newValue !== oldValue) && $scope.couponCode) {
return $scope.validateCode();
}
});
/**
* Callback to validate the code
*/
$scope.validateCode = function () {
$scope.messages = [];
if ($scope.couponCode === '') {
$scope.status = 'pending';
return $scope.coupon = null;
} else {
return Coupon.validate({ code: $scope.couponCode, user_id: $scope.userId, amount: $scope.total }, function (res) {
$scope.status = 'valid';
$scope.coupon = res;
if (res.type === 'percent_off') {
return $scope.messages.push({ type: 'success', message: _t('the_coupon_has_been_applied_you_get_PERCENT_discount', { PERCENT: res.percent_off }) });
} else {
return $scope.messages.push({ type: 'success', message: _t('the_coupon_has_been_applied_you_get_AMOUNT_CURRENCY', { AMOUNT: res.amount_off, CURRENCY: $rootScope.currencySymbol }) });
}
}
, function (err) {
$scope.status = 'invalid';
$scope.coupon = null;
return $scope.messages.push({ type: 'danger', message: _t(`unable_to_apply_the_coupon_because_${err.data.status}`) });
});
}
};
/**
* Callback to remove the message at provided index from the displayed list
*/
$scope.closeMessage = function (index) { $scope.messages.splice(index, 1); };
}
});
}]);

View File

@ -1,105 +0,0 @@
'use strict'
Application.Directives.directive 'fileread', [ ->
{
scope:
fileread: "="
link: (scope, element, attributes) ->
element.bind "change", (changeEvent) ->
scope.$apply ->
scope.fileread = changeEvent.target.files[0]
}
]
# This `bsHolder` angular directive is a workaround for
# an incompatability between angular and the holder.js
# image placeholder library.
#
# To use, simply define `bs-holder` on any element
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
}
]
Application.Directives.directive 'match', [ ->
{
require: 'ngModel'
restrict: 'A'
scope:
match: '='
link: (scope, elem, attrs, ctrl) ->
scope.$watch ->
(ctrl.$pristine && angular.isUndefined(ctrl.$modelValue)) || scope.match == ctrl.$modelValue
, (currentValue) ->
ctrl.$setValidity('match', currentValue)
}
]
Application.Directives.directive 'publishProject', [ ->
{
restrict: 'A'
link: (scope, elem, attrs, ctrl) ->
elem.bind 'click', ($event)->
if ($event)
$event.preventDefault()
$event.stopPropagation()
return if (elem.attr('disabled'))
input = angular.element('<input name="project[state]" type="hidden" value="published">')
form = angular.element('form')
form.append(input)
form.triggerHandler('submit')
form[0].submit()
}
]
Application.Directives.directive "disableAnimation", ($animate) ->
restrict: "A"
link: (scope, elem, attrs) ->
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)
}
]

View File

@ -0,0 +1,131 @@
/* eslint-disable
no-return-assign,
no-undef,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
'use strict';
Application.Directives.directive('fileread', [ () =>
({
scope: {
fileread: '='
},
link (scope, element, attributes) {
return element.bind('change', changeEvent =>
scope.$apply(() => scope.fileread = changeEvent.target.files[0])
);
}
})
]);
// This `bsHolder` angular directive is a workaround for
// an incompatability between angular and the holder.js
// image placeholder library.
//
// To use, simply define `bs-holder` on any element
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]);
}
})
]);
Application.Directives.directive('match', [ () =>
({
require: 'ngModel',
restrict: 'A',
scope: {
match: '='
},
link (scope, elem, attrs, ctrl) {
return scope.$watch(() => (ctrl.$pristine && angular.isUndefined(ctrl.$modelValue)) || (scope.match === ctrl.$modelValue)
, currentValue => ctrl.$setValidity('match', currentValue));
}
})
]);
Application.Directives.directive('publishProject', [ () =>
({
restrict: 'A',
link (scope, elem, attrs, ctrl) {
return elem.bind('click', function ($event) {
if ($event) {
$event.preventDefault();
$event.stopPropagation();
}
if (elem.attr('disabled')) { return; }
const input = angular.element('<input name="project[state]" type="hidden" value="published">');
const form = angular.element('form');
form.append(input);
form.triggerHandler('submit');
return form[0].submit();
});
}
})
]);
Application.Directives.directive('disableAnimation', ['$animate', ($animate) =>
({
restrict: 'A',
link (scope, elem, attrs) {
return attrs.$observe('disableAnimation', value => $animate.enabled(!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) {
if (!ctrl) { return; }
// Do a copy of the controller
const ctrlCopy = {};
angular.copy(ctrl, ctrlCopy);
// Get the form's parent
const parent = elm.parent().controller('form');
// Remove parent link to the controller
parent.$removeControl(ctrl);
// Replace form controller with a "isolated form"
const isolatedFormCtrl = {
$setValidity (validationToken, isValid, control) {
ctrlCopy.$setValidity(validationToken, isValid, control);
return parent.$setValidity(validationToken, true, ctrl);
},
$setDirty () {
elm.removeClass('ng-pristine').addClass('ng-dirty');
ctrl.$dirty = true;
return ctrl.$pristine = false;
}
};
return angular.extend(ctrl, isolatedFormCtrl);
}
})
]);

View File

@ -1,10 +0,0 @@
Application.Directives.directive 'fabUserAvatar', [ ->
{
restrict: 'E'
scope:
userAvatar: "=ngModel"
avatarClass: '@'
templateUrl: '<%= asset_path "shared/_user_avatar.html" %>'
}
]

View File

@ -0,0 +1,20 @@
/* eslint-disable
no-undef,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
Application.Directives.directive('fabUserAvatar', [ function () {
return ({
restrict: 'E',
scope: {
userAvatar: '=ngModel',
avatarClass: '@'
},
templateUrl: '<%= asset_path "shared/_user_avatar.html" %>'
});
}]);

View File

@ -1,34 +0,0 @@
'use strict'
##
# This directive will allow to select a member.
# Please surround it with a ng-if directive to prevent it from being used by a non-admin user.
# The resulting member will be set into the parent $scope (=> $scope.ctrl.member).
# The directive takes an optional parameter "subscription" as a "boolean string" that will filter the user
# which have a valid running subscription or not.
# Usage: <select-member [subscription="false|true"]></select-member>
##
Application.Directives.directive 'selectMember', [ 'Diacritics', 'Member', (Diacritics, Member) ->
{
restrict: 'E'
templateUrl: '<%= asset_path "shared/_member_select.html" %>'
link: (scope, element, attributes) ->
scope.autoCompleteName = (nameLookup) ->
unless nameLookup
return
scope.isLoadingMembers = true
asciiName = Diacritics.remove(nameLookup)
q = { query: asciiName }
if attributes.subscription
q['subscription'] = attributes.subscription
Member.search q, (users) ->
scope.matchingMembers = users
scope.isLoadingMembers = false
, (error)->
console.error(error)
}
]

View File

@ -0,0 +1,48 @@
/* eslint-disable
no-return-assign,
no-undef,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
'use strict';
/**
* This directive will allow to select a member.
* Please surround it with a ng-if directive to prevent it from being used by a non-admin user.
* The resulting member will be set into the parent $scope (=> $scope.ctrl.member).
* The directive takes an optional parameter "subscription" as a "boolean string" that will filter the user
* which have a valid running subscription or not.
* Usage: <select-member [subscription="false|true"]></select-member>
*/
Application.Directives.directive('selectMember', [ 'Diacritics', 'Member', function (Diacritics, Member) {
return ({
restrict: 'E',
templateUrl: '<%= asset_path "shared/_member_select.html" %>',
link (scope, element, attributes) {
return scope.autoCompleteName = function (nameLookup) {
if (!nameLookup) {
return;
}
scope.isLoadingMembers = true;
const asciiName = Diacritics.remove(nameLookup);
const q = { query: asciiName };
if (attributes.subscription) {
q['subscription'] = attributes.subscription;
}
return Member.search(q, function (users) {
scope.matchingMembers = users;
return scope.isLoadingMembers = false;
}
, function (error) { console.error(error); });
};
}
});
}]);

View File

@ -1,23 +0,0 @@
Application.Directives.directive 'socialLink', [ ->
{
restrict: 'E'
scope:
network: '@?'
user: '='
templateUrl: '<%= asset_path "shared/_social_link.html" %>'
link: (scope, element, attributes) ->
if scope.network == 'dailymotion'
scope.image = "<%= asset_path('social/dailymotion.png') %>"
scope.altText = 'd'
else if scope.network == 'echosciences'
scope.image = "<%= asset_path('social/echosciences.png') %>"
scope.altText = 'E)'
else
if scope.network == 'website'
scope.faClass = 'fa-globe'
else
scope.faClass = 'fa-'+scope.network.replace('_', '-')
}
]

View File

@ -0,0 +1,36 @@
/* eslint-disable
no-return-assign,
no-undef,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
Application.Directives.directive('socialLink', [ function () {
return ({
restrict: 'E',
scope: {
network: '@?',
user: '='
},
templateUrl: '<%= asset_path "shared/_social_link.html" %>',
link (scope, element, attributes) {
if (scope.network === 'dailymotion') {
scope.image = "<%= asset_path('social/dailymotion.png') %>";
return scope.altText = 'd';
} else if (scope.network === 'echosciences') {
scope.image = "<%= asset_path('social/echosciences.png') %>";
return scope.altText = 'E)';
} else {
if (scope.network === 'website') {
return scope.faClass = 'fa-globe';
} else {
return scope.faClass = `fa-${scope.network.replace('_', '-')}`;
}
}
}
});
}]);

View File

@ -3,22 +3,21 @@
// https://github.com/gtramontina/stripe-angular
Application.Directives.directive('stripeForm', ['$window',
function($window) {
function ($window) {
var directive = { restrict: 'A' };
directive.link = function(scope, element, attributes) {
directive.link = function (scope, element, attributes) {
var form = angular.element(element);
form.bind('submit', function() {
form.bind('submit', function () {
var button = form.find('button');
button.prop('disabled', true);
$window.Stripe.createToken(form[0], function() {
$window.Stripe.createToken(form[0], function () {
var args = arguments;
scope.$apply(function() {
scope.$apply(function () {
scope[attributes.stripeForm].apply(scope, args);
});
//button.prop('disabled', false);
// button.prop('disabled', false);
});
});
};
return directive;
}]);

View File

@ -1,34 +0,0 @@
'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
}
]

View File

@ -0,0 +1,55 @@
/* eslint-disable
no-return-assign,
no-undef,
no-useless-escape,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
'use strict';
Application.Directives.directive('url', [ function () {
const URL_REGEXP = /^(https?:\/\/)([\da-z\.-]+)\.([-a-z0-9\.]{2,30})([\/\w \.-]*)*\/?$/;
return {
require: 'ngModel',
link (scope, element, attributes, ctrl) {
return ctrl.$validators.url = function (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', [ function () {
const ENDPOINT_REGEXP = /^\/?([-._~:?#\[\]@!$&'()*+,;=%\w]+\/?)*$/;
return {
require: 'ngModel',
link (scope, element, attributes, ctrl) {
return ctrl.$validators.endpoint = function (modelValue, viewValue) {
if (ctrl.$isEmpty(modelValue)) {
return true;
}
if (ENDPOINT_REGEXP.test(viewValue)) {
return true;
}
// otherwise, this is invalid
return false;
};
}
};
}
]);

View File

@ -1,279 +0,0 @@
'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.Filters.filter "machineFilter", [ ->
(elements, selectedMachine) ->
if !angular.isUndefined(elements) and !angular.isUndefined(selectedMachine) and elements? and selectedMachine?
filteredElements = []
angular.forEach elements, (element)->
if element.machine_ids.indexOf(selectedMachine) != -1
filteredElements.push(element)
filteredElements
else
elements
]
Application.Filters.filter "projectMemberFilter", [ "Auth", (Auth)->
(projects, selectedMember) ->
if !angular.isUndefined(projects) and angular.isDefined(selectedMember) and projects? and selectedMember? and selectedMember != ""
filteredProject = []
# Mes projets
if selectedMember == '0'
angular.forEach projects, (project)->
if project.author_id == Auth._currentUser.id
filteredProject.push(project)
# les projets auxquels je collabore
else
angular.forEach projects, (project)->
if project.user_ids.indexOf(Auth._currentUser.id) != -1
filteredProject.push(project)
filteredProject
else
projects
]
Application.Filters.filter "themeFilter", [ ->
(projects, selectedTheme) ->
if !angular.isUndefined(projects) and !angular.isUndefined(selectedTheme) and projects? and selectedTheme?
filteredProjects = []
angular.forEach projects, (project)->
if project.theme_ids.indexOf(selectedTheme) != -1
filteredProjects.push(project)
filteredProjects
else
projects
]
Application.Filters.filter "componentFilter", [ ->
(projects, selectedComponent) ->
if !angular.isUndefined(projects) and !angular.isUndefined(selectedComponent) and projects? and selectedComponent?
filteredProjects = []
angular.forEach projects, (project)->
if project.component_ids.indexOf(selectedComponent) != -1
filteredProjects.push(project)
filteredProjects
else
projects
]
Application.Filters.filter "projectsByAuthor", [ ->
(projects, authorId) ->
if !angular.isUndefined(projects) and angular.isDefined(authorId) and projects? and authorId? and authorId != ""
filteredProject = []
angular.forEach projects, (project)->
if project.author_id == authorId
filteredProject.push(project)
filteredProject
else
projects
]
Application.Filters.filter "projectsCollabored", [ ->
(projects, memberId) ->
if !angular.isUndefined(projects) and angular.isDefined(memberId) and projects? and memberId? and memberId != ""
filteredProject = []
angular.forEach projects, (project)->
if project.user_ids.indexOf(memberId) != -1
filteredProject.push(project)
filteredProject
else
projects
]
# depend on humanize.js lib in /vendor
Application.Filters.filter "humanize", [ ->
(element, param) ->
Humanize.truncate(element, param, null)
]
##
# This filter will convert ASCII carriage-return character to the HTML break-line tag
##
Application.Filters.filter "breakFilter", [ ->
(text) ->
if text?
text.replace(/\n+/g, '<br />')
]
##
# This filter will take a HTML text as input and will return it without the html tags
##
Application.Filters.filter "simpleText", [ ->
(text) ->
if text?
text = text.replace(/<br\s*\/?>/g, '\n')
text.replace(/<\/?\w+[^>]*>/g, '')
else
""
]
Application.Filters.filter "toTrusted", [ "$sce", ($sce) ->
(text) ->
$sce.trustAsHtml text
]
Application.Filters.filter "planIntervalFilter", [ ->
(interval, intervalCount) ->
moment.duration(intervalCount, interval).humanize()
]
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 = []
angular.forEach elements, (element)->
element.start_at = element.availability.start_at if angular.isUndefined(element.start_at)
element.end_at = element.availability.end_at if angular.isUndefined(element.end_at)
switch selectedScope
when "future"
if new Date(element.end_at) >= new Date
filteredElements.push(element)
when "future_asc"
if new Date(element.end_at) >= new Date
filteredElements.push(element)
when "passed"
if new Date(element.end_at) <= new Date
filteredElements.push(element)
else
return []
switch selectedScope
when "future_asc"
filteredElements.reverse()
else
filteredElements
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})
]
Application.Filters.filter 'toIsoDate', [ ->
(date) ->
return date unless (date instanceof Date || moment.isMoment(date))
moment(date).format('YYYY-MM-DD')
]
Application.Filters.filter 'booleanFormat', [ '_t', (_t) ->
(boolean) ->
if boolean or boolean == 'true'
_t('yes')
else
_t('no')
]
Application.Filters.filter 'booleanFormat', [ '_t', (_t) ->
(boolean) ->
if (typeof boolean == 'boolean' and boolean) or (typeof boolean == 'string' and boolean == 'true')
_t('yes')
else
_t('no')
]
Application.Filters.filter 'maxCount', [ '_t', (_t) ->
(max) ->
if typeof max == 'undefined' or max == null or (typeof max == 'number' and max == 0)
_t('unlimited')
else
max
]
Application.Filters.filter 'filterDisabled', [ ->
(list, filter) ->
if angular.isArray(list)
list.filter (e) ->
switch filter
when 'disabled' then e.disabled
when 'enabled' then !e.disabled
else true
else
list
]

View File

@ -0,0 +1,389 @@
/* eslint-disable
no-undef,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS202: Simplify dynamic range loops
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
'use strict';
Application.Filters.filter('array', [ () =>
function (arrayLength) {
if (arrayLength) {
arrayLength = Math.ceil(arrayLength);
const arr = new Array(arrayLength);
for (let i = 0, end = arrayLength, asc = end >= 0; asc ? i < end : i > end; asc ? i++ : i--) {
arr[i] = i;
}
return arr;
}
}
]);
// filter for projects and trainings
Application.Filters.filter('machineFilter', [ () =>
function (elements, selectedMachine) {
if (!angular.isUndefined(elements) && !angular.isUndefined(selectedMachine) && (elements != null) && (selectedMachine != null)) {
const filteredElements = [];
angular.forEach(elements, function (element) {
if (element.machine_ids.indexOf(selectedMachine) !== -1) {
return filteredElements.push(element);
}
});
return filteredElements;
} else {
return elements;
}
}
]);
Application.Filters.filter('projectMemberFilter', [ 'Auth', Auth =>
function (projects, selectedMember) {
if (!angular.isUndefined(projects) && angular.isDefined(selectedMember) && (projects != null) && (selectedMember != null) && (selectedMember !== '')) {
const filteredProject = [];
// Mes projets
if (selectedMember === '0') {
angular.forEach(projects, function (project) {
if (project.author_id === Auth._currentUser.id) {
return filteredProject.push(project);
}
});
// les projets auxquels je collabore
} else {
angular.forEach(projects, function (project) {
if (project.user_ids.indexOf(Auth._currentUser.id) !== -1) {
return filteredProject.push(project);
}
});
}
return filteredProject;
} else {
return projects;
}
}
]);
Application.Filters.filter('themeFilter', [ () =>
function (projects, selectedTheme) {
if (!angular.isUndefined(projects) && !angular.isUndefined(selectedTheme) && (projects != null) && (selectedTheme != null)) {
const filteredProjects = [];
angular.forEach(projects, function (project) {
if (project.theme_ids.indexOf(selectedTheme) !== -1) {
return filteredProjects.push(project);
}
});
return filteredProjects;
} else {
return projects;
}
}
]);
Application.Filters.filter('componentFilter', [ () =>
function (projects, selectedComponent) {
if (!angular.isUndefined(projects) && !angular.isUndefined(selectedComponent) && (projects != null) && (selectedComponent != null)) {
const filteredProjects = [];
angular.forEach(projects, function (project) {
if (project.component_ids.indexOf(selectedComponent) !== -1) {
return filteredProjects.push(project);
}
});
return filteredProjects;
} else {
return projects;
}
}
]);
Application.Filters.filter('projectsByAuthor', [ () =>
function (projects, authorId) {
if (!angular.isUndefined(projects) && angular.isDefined(authorId) && (projects != null) && (authorId != null) && (authorId !== '')) {
const filteredProject = [];
angular.forEach(projects, function (project) {
if (project.author_id === authorId) {
return filteredProject.push(project);
}
});
return filteredProject;
} else {
return projects;
}
}
]);
Application.Filters.filter('projectsCollabored', [ () =>
function (projects, memberId) {
if (!angular.isUndefined(projects) && angular.isDefined(memberId) && (projects != null) && (memberId != null) && (memberId !== '')) {
const filteredProject = [];
angular.forEach(projects, function (project) {
if (project.user_ids.indexOf(memberId) !== -1) {
return filteredProject.push(project);
}
});
return filteredProject;
} else {
return projects;
}
}
]);
// depend on humanize.js lib in /vendor
Application.Filters.filter('humanize', [ () =>
(element, param) => Humanize.truncate(element, param, null)
]);
/**
* This filter will convert ASCII carriage-return character to the HTML break-line tag
*/
Application.Filters.filter('breakFilter', [ () =>
function (text) {
if (text != null) {
return text.replace(/\n+/g, '<br />');
}
}
]);
/**
* This filter will take a HTML text as input and will return it without the html tags
*/
Application.Filters.filter('simpleText', [ () =>
function (text) {
if (text != null) {
text = text.replace(/<br\s*\/?>/g, '\n');
return text.replace(/<\/?\w+[^>]*>/g, '');
} else {
return '';
}
}
]);
Application.Filters.filter('toTrusted', [ '$sce', $sce =>
text => $sce.trustAsHtml(text)
]);
Application.Filters.filter('planIntervalFilter', [ () =>
(interval, intervalCount) => moment.duration(intervalCount, interval).humanize()
]);
Application.Filters.filter('humanReadablePlanName', ['$filter', $filter =>
function (plan, groups, short) {
if (plan != null) {
let result = plan.base_name;
if (groups != null) {
for (let group of Array.from(groups)) {
if (group.id === plan.group_id) {
if (short != null) {
result += ` - ${group.slug}`;
} else {
result += ` - ${group.name}`;
}
}
}
}
result += ` - ${$filter('planIntervalFilter')(plan.interval, plan.interval_count)}`;
return result;
}
}
]);
Application.Filters.filter('trainingReservationsFilter', [ () =>
function (elements, selectedScope) {
if (!angular.isUndefined(elements) && !angular.isUndefined(selectedScope) && (elements != null) && (selectedScope != null)) {
const filteredElements = [];
angular.forEach(elements, function (element) {
switch (selectedScope) {
case 'future':
if (new Date(element.start_at) > new Date()) {
return filteredElements.push(element);
}
break;
case 'passed':
if ((new Date(element.start_at) <= new Date()) && !element.is_valid) {
return filteredElements.push(element);
}
break;
case 'valided':
if ((new Date(element.start_at) <= new Date()) && element.is_valid) {
return filteredElements.push(element);
}
break;
default:
return [];
}
});
return filteredElements;
} else {
return elements;
}
}
]);
Application.Filters.filter('eventsReservationsFilter', [ () =>
function (elements, selectedScope) {
if (!angular.isUndefined(elements) && !angular.isUndefined(selectedScope) && (elements != null) && (selectedScope != null) && (selectedScope !== '')) {
const filteredElements = [];
angular.forEach(elements, function (element) {
if (angular.isUndefined(element.start_at)) { element.start_at = element.availability.start_at; }
if (angular.isUndefined(element.end_at)) { element.end_at = element.availability.end_at; }
switch (selectedScope) {
case 'future':
if (new Date(element.end_at) >= new Date()) {
return filteredElements.push(element);
}
break;
case 'future_asc':
if (new Date(element.end_at) >= new Date()) {
return filteredElements.push(element);
}
break;
case 'passed':
if (new Date(element.end_at) <= new Date()) {
return filteredElements.push(element);
}
break;
default:
return [];
}
});
switch (selectedScope) {
case 'future_asc':
return filteredElements.reverse();
default:
return filteredElements;
}
} else {
return elements;
}
}
]);
Application.Filters.filter('groupFilter', [ () =>
function (elements, member) {
if (!angular.isUndefined(elements) && !angular.isUndefined(member) && (elements != null) && (member != null)) {
const filteredElements = [];
angular.forEach(elements, function (element) {
if (member.group_id === element.id) {
return filteredElements.push(element);
}
});
return filteredElements;
} else {
return 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', [ () =>
function (items) {
if (!angular.isArray(items)) {
return items;
}
return items.slice().reverse();
}
]);
Application.Filters.filter('toArray', [ () =>
function (obj) {
if (!(obj instanceof Object)) { return obj; }
return _.map(obj, function (val, key) {
if (angular.isObject(val)) {
return Object.defineProperty(val, '$key', { __proto__: null, value: key });
}
});
}
]);
Application.Filters.filter('toIsoDate', [ () =>
function (date) {
if (!(date instanceof Date) && !moment.isMoment(date)) { return date; }
return moment(date).format('YYYY-MM-DD');
}
]);
Application.Filters.filter('booleanFormat', [ '_t', _t =>
function (boolean) {
if (boolean || (boolean === 'true')) {
return _t('yes');
} else {
return _t('no');
}
}
]);
Application.Filters.filter('booleanFormat', [ '_t', _t =>
function (boolean) {
if (((typeof boolean === 'boolean') && boolean) || ((typeof boolean === 'string') && (boolean === 'true'))) {
return _t('yes');
} else {
return _t('no');
}
}
]);
Application.Filters.filter('maxCount', [ '_t', _t =>
function (max) {
if ((typeof max === 'undefined') || (max === null) || ((typeof max === 'number') && (max === 0))) {
return _t('unlimited');
} else {
return max;
}
}
]);
Application.Filters.filter('filterDisabled', [ () =>
function (list, filter) {
if (angular.isArray(list)) {
return list.filter(function (e) {
switch (filter) {
case 'disabled': return e.disabled;
case 'enabled': return !e.disabled;
default: return true;
}
});
} else {
return list;
}
}
]);

View File

@ -1,1197 +0,0 @@
angular.module('application.router', ['ui.router']).
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': {}
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" %>'
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
]
# 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
]
# 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: 'DashboardController'
resolve:
translations: [ 'Translations', (Translations) ->
Translations.query(['app.logged.dashboard.profile', 'app.shared.public_profile']).$promise
]
.state 'app.logged.dashboard.settings',
url: '/settings'
views:
'main@':
templateUrl: '<%= asset_path "dashboard/settings.html" %>'
controller: 'EditProfileController'
resolve:
groups: ['Group', (Group)->
Group.query().$promise
]
activeProviderPromise: ['AuthProvider', (AuthProvider) ->
AuthProvider.active().$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query(['app.logged.dashboard.settings', '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
]
.state 'app.logged.dashboard.wallet',
url: '/wallet'
views:
'main@':
templateUrl: '<%= asset_path "dashboard/wallet.html" %>'
controller: 'WalletController'
resolve:
walletPromise: ['Wallet', 'currentUser', (Wallet, currentUser)->
Wallet.getWalletByUser(user_id: currentUser.id).$promise
]
transactionsPromise: ['Wallet', 'walletPromise', (Wallet, walletPromise)->
Wallet.transactions(id: walletPromise.id).$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query(['app.shared.wallet']).$promise
]
# 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', 'app.shared.public_profile']).$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]', page: 1, size: 10}).$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query('app.logged.members').$promise
]
# projects
.state 'app.public.projects_list',
url: '/projects?q&page&theme_id&component_id&machine_id&from&whole_network'
views:
'main@':
templateUrl: '<%= asset_path "projects/index.html.erb" %>'
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:
allowedExtensions: ['Project', (Project)->
Project.allowedExtensions().$promise
]
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
]
allowedExtensions: ['Project', (Project)->
Project.allowedExtensions().$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query(['app.logged.projects_edit', 'app.shared.project']).$promise
]
# machines
.state 'app.public.machines_list',
url: '/machines'
views:
'main@':
templateUrl: '<%= asset_path "machines/index.html.erb" %>'
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().$promise
]
groupsPromise: ['Group', (Group)->
Group.query().$promise
]
machinePromise: ['Machine', '$stateParams', (Machine, $stateParams)->
Machine.get(id: $stateParams.id).$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',
'app.shared.wallet', 'app.shared.coupon_input', 'app.shared.cart']).$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
]
# spaces
.state 'app.public.spaces_list',
url: '/spaces'
abstract: Fablab.withoutSpaces
views:
'main@':
templateUrl: '<%= asset_path "spaces/index.html" %>'
controller: 'SpacesController'
resolve:
spacesPromise: ['Space', (Space)->
Space.query().$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query(['app.public.spaces_list']).$promise
]
.state 'app.admin.space_new',
url: '/spaces/new'
abstract: Fablab.withoutSpaces
views:
'main@':
templateUrl: '<%= asset_path "spaces/new.html" %>'
controller: 'NewSpaceController'
resolve:
translations: [ 'Translations', (Translations) ->
Translations.query(['app.admin.space_new', 'app.shared.space']).$promise
]
.state 'app.public.space_show',
url: '/spaces/:id'
abstract: Fablab.withoutSpaces
views:
'main@':
templateUrl: '<%= asset_path "spaces/show.html" %>'
controller: 'ShowSpaceController'
resolve:
spacePromise: ['Space', '$stateParams', (Space, $stateParams)->
Space.get(id: $stateParams.id).$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query(['app.public.space_show']).$promise
]
.state 'app.admin.space_edit',
url: '/spaces/:id/edit'
abstract: Fablab.withoutSpaces
views:
'main@':
templateUrl: '<%= asset_path "spaces/edit.html" %>'
controller: 'EditSpaceController'
resolve:
spacePromise: ['Space', '$stateParams', (Space, $stateParams)->
Space.get(id: $stateParams.id).$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query(['app.admin.space_edit', 'app.shared.space']).$promise
]
.state 'app.logged.space_reserve',
url: '/spaces/:id/reserve'
abstract: Fablab.withoutSpaces
views:
'main@':
templateUrl: '<%= asset_path "spaces/reserve.html" %>'
controller: 'ReserveSpaceController'
resolve:
spacePromise: ['Space', '$stateParams', (Space, $stateParams)->
Space.get(id: $stateParams.id).$promise
]
availabilitySpacesPromise: ['Availability', '$stateParams', (Availability, $stateParams)->
Availability.spaces({spaceId: $stateParams.id}).$promise
]
plansPromise: ['Plan', (Plan)->
Plan.query().$promise
]
groupsPromise: ['Group', (Group)->
Group.query().$promise
]
settingsPromise: ['Setting', (Setting)->
Setting.query(names: "['booking_window_start',
'booking_window_end',
'booking_move_enable',
'booking_move_delay',
'booking_cancel_enable',
'booking_cancel_delay',
'subscription_explications_alert',
'space_explications_alert']").$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query(['app.logged.space_reserve', 'app.shared.plan_subscribe', 'app.shared.member_select',
'app.shared.stripe', 'app.shared.valid_reservation_modal', 'app.shared.confirm_modify_slot_modal',
'app.shared.wallet', 'app.shared.coupon_input', 'app.shared.cart']).$promise
]
# trainings
.state 'app.public.trainings_list',
url: '/trainings'
views:
'main@':
templateUrl: '<%= asset_path "trainings/index.html.erb" %>'
controller: 'TrainingsController'
resolve:
trainingsPromise: ['Training', (Training)->
Training.query({ public_page: true }).$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query(['app.public.trainings_list']).$promise
]
.state 'app.public.training_show',
url: '/trainings/:id'
views:
'main@':
templateUrl: '<%= asset_path "trainings/show.html" %>'
controller: 'ShowTrainingController'
resolve:
trainingPromise: ['Training', '$stateParams', (Training, $stateParams)->
Training.get({id: $stateParams.id}).$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query(['app.public.training_show']).$promise
]
.state 'app.logged.trainings_reserve',
url: '/trainings/:id/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().$promise
]
groupsPromise: ['Group', (Group)->
Group.query().$promise
]
availabilityTrainingsPromise: ['Availability', '$stateParams', (Availability, $stateParams)->
Availability.trainings({trainingId: $stateParams.id}).$promise
]
trainingPromise: ['Training', '$stateParams', (Training, $stateParams)->
Training.get({id: $stateParams.id}).$promise unless $stateParams.id == 'all'
]
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',
'app.shared.wallet', 'app.shared.coupon_input', 'app.shared.cart']).$promise
]
# notifications
.state 'app.logged.notifications',
url: '/notifications'
views:
'main@':
templateUrl: '<%= asset_path "notifications/index.html.erb" %>'
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.erb" %>'
controller: 'PlansIndexController'
resolve:
subscriptionExplicationsPromise: ['Setting', (Setting)->
Setting.get(name: 'subscription_explications_alert').$promise
]
plansPromise: ['Plan', (Plan)->
Plan.query().$promise
]
groupsPromise: ['Group', (Group)->
Group.query().$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query(['app.public.plans', 'app.shared.member_select', 'app.shared.stripe', 'app.shared.wallet',
'app.shared.coupon_input']).$promise
]
# events
.state 'app.public.events_list',
url: '/events'
views:
'main@':
templateUrl: '<%= asset_path "events/index.html.erb" %>'
controller: 'EventsController'
resolve:
categoriesPromise: ['Category', (Category) ->
Category.query().$promise
]
themesPromise: ['EventTheme', (EventTheme) ->
EventTheme.query().$promise
]
ageRangesPromise: ['AgeRange', (AgeRange) ->
AgeRange.query().$promise
]
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
]
priceCategoriesPromise: ['PriceCategory', (PriceCategory) ->
PriceCategory.query().$promise
]
settingsPromise: ['Setting', (Setting)->
Setting.query(names: "['booking_move_enable', 'booking_move_delay', 'event_explications_alert']").$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query(['app.public.events_show', 'app.shared.member_select', 'app.shared.stripe',
'app.shared.valid_reservation_modal', 'app.shared.wallet', 'app.shared.coupon_input']).$promise
]
# global calendar (trainings, machines and events)
.state 'app.public.calendar',
url: '/calendar'
views:
'main@':
templateUrl: '<%= asset_path "calendar/calendar.html" %>'
controller: 'CalendarController'
resolve:
bookingWindowStart: ['Setting', (Setting)->
Setting.get(name: 'booking_window_start').$promise
]
bookingWindowEnd: ['Setting', (Setting)->
Setting.get(name: 'booking_window_end').$promise
]
trainingsPromise: ['Training', (Training)->
Training.query().$promise
]
machinesPromise: ['Machine', (Machine)->
Machine.query().$promise
]
spacesPromise: ['Space', (Space) ->
Space.query().$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query(['app.public.calendar']).$promise
]
# --- namespace /admin/... ---
# calendar
.state 'app.admin.calendar',
url: '/admin/calendar'
views:
'main@':
templateUrl: '<%= asset_path "admin/calendar/calendar.html" %>'
controller: 'AdminCalendarController'
resolve:
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.erb" %>'
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.erb" %>'
controller: 'TrainingsAdminController'
resolve:
trainingsPromise: ['Training', (Training)->
Training.query().$promise
]
machinesPromise: ['Machine', (Machine)->
Machine.query().$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query(['app.admin.trainings', 'app.shared.trainings']).$promise
]
.state 'app.admin.trainings_new',
url: '/admin/trainings/new'
views:
'main@':
templateUrl: '<%= asset_path "admin/trainings/new.html" %>'
controller: 'NewTrainingController'
resolve:
machinesPromise: ['Machine', (Machine)->
Machine.query().$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query(['app.admin.trainings_new', 'app.shared.trainings']).$promise
]
.state 'app.admin.trainings_edit',
url: '/admin/trainings/:id/edit'
views:
'main@':
templateUrl: '<%= asset_path "admin/trainings/edit.html" %>'
controller: 'EditTrainingController'
resolve:
trainingPromise: ['Training', '$stateParams', (Training, $stateParams)->
Training.get(id: $stateParams.id).$promise
]
machinesPromise: ['Machine', (Machine)->
Machine.query().$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query('app.shared.trainings').$promise
]
# events
.state 'app.admin.events',
url: '/admin/events'
views:
'main@':
templateUrl: '<%= asset_path "admin/events/index.html.erb" %>'
controller: 'AdminEventsController'
resolve:
eventsPromise: ['Event', (Event)->
Event.query(page: 1).$promise
]
categoriesPromise: ['Category', (Category) ->
Category.query().$promise
]
themesPromise: ['EventTheme', (EventTheme) ->
EventTheme.query().$promise
]
ageRangesPromise: ['AgeRange', (AgeRange) ->
AgeRange.query().$promise
]
priceCategoriesPromise: ['PriceCategory', (PriceCategory) ->
PriceCategory.query().$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:
categoriesPromise: ['Category', (Category) ->
Category.query().$promise
]
themesPromise: ['EventTheme', (EventTheme) ->
EventTheme.query().$promise
]
ageRangesPromise: ['AgeRange', (AgeRange) ->
AgeRange.query().$promise
]
priceCategoriesPromise: ['PriceCategory', (PriceCategory) ->
PriceCategory.query().$promise
]
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
]
categoriesPromise: ['Category', (Category) ->
Category.query().$promise
]
themesPromise: ['EventTheme', (EventTheme) ->
EventTheme.query().$promise
]
ageRangesPromise: ['AgeRange', (AgeRange) ->
AgeRange.query().$promise
]
priceCategoriesPromise: ['PriceCategory', (PriceCategory) ->
PriceCategory.query().$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.erb" %>'
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', 'app.shared.member_select', 'app.shared.coupon']).$promise
]
trainingsPromise: ['Training', (Training) ->
Training.query().$promise
]
machineCreditsPromise: ['Credit', (Credit) ->
Credit.query({creditable_type: 'Machine'}).$promise
]
machinesPromise: ['Machine', (Machine) ->
Machine.query().$promise
]
trainingCreditsPromise: ['Credit', (Credit) ->
Credit.query({creditable_type: 'Training'}).$promise
]
couponsPromise: ['Coupon', (Coupon) ->
Coupon.query().$promise
]
spacesPromise: ['Space', (Space) ->
Space.query().$promise
]
spacesPricesPromise: ['Price', (Price)->
Price.query(priceable_type: 'Space', plan_id: 'null').$promise
]
spacesCreditsPromise: ['Credit', (Credit) ->
Credit.query({creditable_type: 'Space'}).$promise
]
# plans
.state 'app.admin.plans',
abstract: true
resolve:
prices: ['Pricing', (Pricing) ->
Pricing.query().$promise
]
groups: ['Group', (Group) ->
Group.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:
spaces: ['Space', (Space) ->
Space.query().$promise
]
machines: ['Machine', (Machine) ->
Machine.query().$promise
]
plans: ['Plan', (Plan) ->
Plan.query().$promise
]
planPromise: ['Plan', '$stateParams', (Plan, $stateParams) ->
Plan.get({id: $stateParams.id}).$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query(['app.admin.plans.edit', 'app.shared.plan']).$promise
]
# coupons
.state 'app.admin.coupons_new',
url: '/admin/coupons/new'
views:
'main@':
templateUrl: '<%= asset_path "admin/coupons/new.html" %>'
controller: 'NewCouponController'
resolve:
translations: [ 'Translations', (Translations) ->
Translations.query(['app.admin.coupons_new', 'app.shared.coupon']).$promise
]
.state 'app.admin.coupons_edit',
url: '/admin/coupons/:id/edit'
views:
'main@':
templateUrl: '<%= asset_path "admin/coupons/edit.html" %>'
controller: 'EditCouponController'
resolve:
couponPromise: ['Coupon', '$stateParams', (Coupon, $stateParams) ->
Coupon.get({id: $stateParams.id}).$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query(['app.admin.coupons_edit', 'app.shared.coupon']).$promise
]
# invoices
.state 'app.admin.invoices',
url: '/admin/invoices'
views:
'main@':
templateUrl: '<%= asset_path "admin/invoices/index.html.erb" %>'
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
]
invoices: [ 'Invoice', (Invoice) ->
Invoice.list({
query: { number: '', customer: '', date: null, order_by: '-reference', page: 1, size: 20 }
}).$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query('app.admin.invoices').$promise
]
# members
.state 'app.admin.members',
url: '/admin/members'
views:
'main@':
templateUrl: '<%= asset_path "admin/members/index.html.erb" %>'
controller: 'AdminMembersController'
'groups@app.admin.members':
templateUrl: '<%= asset_path "admin/groups/index.html.erb" %>'
controller: 'GroupsController'
'tags@app.admin.members':
templateUrl: '<%= asset_path "admin/tags/index.html.erb" %>'
controller: 'TagsController'
'authentification@app.admin.members':
templateUrl: '<%= asset_path "admin/authentications/index.html.erb" %>'
controller: 'AuthentificationController'
resolve:
membersPromise: ['Member', (Member)->
Member.list({ query: { search: '', order_by: 'id', page: 1, size: 20 } }).$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
]
activeProviderPromise: ['AuthProvider', (AuthProvider) ->
AuthProvider.active().$promise
]
walletPromise: ['Wallet', '$stateParams', (Wallet, $stateParams)->
Wallet.getWalletByUser(user_id: $stateParams.id).$promise
]
transactionsPromise: ['Wallet', 'walletPromise', (Wallet, walletPromise)->
Wallet.transactions(id: walletPromise.id).$promise
]
tagsPromise: ['Tag', (Tag)->
Tag.query().$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query(['app.admin.members_edit', 'app.shared.user', 'app.shared.user_admin', 'app.shared.wallet']).$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
]
# 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.erb" %>'
controller: 'StatisticsController'
resolve:
membersPromise: ['Member', (Member) ->
Member.mapping().$promise
]
statisticsPromise: ['Statistics', (Statistics)->
Statistics.query().$promise
]
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.erb" %>'
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_explications_alert',
'space_explications_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',
'reminder_enable',
'reminder_delay',
'visibility_yearly',
'visibility_others',
'display_name_enable',
'machines_sort_by'
]").$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
]
profileImageFile: ['CustomAsset', (CustomAsset) ->
CustomAsset.get({name: 'profile-image-file'}).$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query('app.admin.settings').$promise
]
# OpenAPI Clients
.state 'app.admin.open_api_clients',
url: '/open_api_clients'
views:
'main@':
templateUrl: '<%= asset_path "admin/open_api_clients/index.html.erb" %>'
controller: 'OpenAPIClientsController'
resolve:
clientsPromise: ['OpenAPIClient', (OpenAPIClient)->
OpenAPIClient.query().$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query('app.admin.open_api_clients').$promise
]
]

View File

@ -0,0 +1,1086 @@
/* eslint-disable
no-return-assign,
no-undef,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
angular.module('application.router', ['ui.router'])
.config(['$stateProvider', '$urlRouterProvider', '$locationProvider', function ($stateProvider, $urlRouterProvider, $locationProvider) {
$locationProvider.hashPrefix('!');
$urlRouterProvider.otherwise('/');
// abstract root parents states
// these states controls the access rights to the various routes inherited from them
return $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', function (CustomAsset) { return CustomAsset.get({ name: 'logo-file' }).$promise; }],
logoBlackFile: ['CustomAsset', function (CustomAsset) { return CustomAsset.get({ name: 'logo-black-file' }).$promise; }],
commonTranslations: ['Translations', function (Translations) { return Translations.query(['app.public.common', 'app.shared.buttons', 'app.shared.elements']).$promise; }]
},
onEnter: ['$rootScope', 'logoFile', 'logoBlackFile', function ($rootScope, logoFile, logoBlackFile) {
// Application logo
$rootScope.logo = logoFile.custom_asset;
return $rootScope.logoBlack = logoBlackFile.custom_asset;
}]
})
.state('app.public', {
abstract: true
})
.state('app.logged', {
abstract: true,
data: {
authorizedRoles: ['member', 'admin']
},
resolve: {
currentUser: ['Auth', function (Auth) { return Auth.currentUser(); }]
},
onEnter: ['$state', '$timeout', 'currentUser', '$rootScope', function ($state, $timeout, currentUser, $rootScope) {
$rootScope.currentUser = currentUser;
}]
})
.state('app.admin', {
abstract: true,
data: {
authorizedRoles: ['admin']
},
resolve: {
currentUser: ['Auth', function (Auth) { return Auth.currentUser(); }]
},
onEnter: ['$state', '$timeout', 'currentUser', '$rootScope', function ($state, $timeout, currentUser, $rootScope) {
$rootScope.currentUser = currentUser;
}]
})
// main pages
.state('app.public.about', {
url: '/about',
views: {
'content@': {
templateUrl: '<%= asset_path "shared/about.html" %>',
controller: 'AboutController'
}
},
resolve: {
translations: ['Translations', function (Translations) { return 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', function (Member) { return Member.lastSubscribed({ limit: 4 }).$promise; }],
lastProjectsPromise: ['Project', function (Project) { return Project.lastPublished().$promise; }],
upcomingEventsPromise: ['Event', function (Event) { return Event.upcoming({ limit: 3 }).$promise; }],
homeBlogpostPromise: ['Setting', function (Setting) { return Setting.get({ name: 'home_blogpost' }).$promise; }],
twitterNamePromise: ['Setting', function (Setting) { return Setting.get({ name: 'twitter_name' }).$promise; }],
translations: ['Translations', function (Translations) { return Translations.query('app.public.home').$promise; }]
}
})
// 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', function (Setting) { return Setting.query({ names: "['fablab_name', 'name_genre']" }).$promise; }],
activeProviderPromise: ['AuthProvider', function (AuthProvider) { return AuthProvider.active().$promise; }],
groupsPromise: ['Group', function (Group) { return Group.query().$promise; }],
cguFile: ['CustomAsset', function (CustomAsset) { return CustomAsset.get({ name: 'cgu-file' }).$promise; }],
memberPromise: ['Member', 'currentUser', function (Member, currentUser) { return Member.get({ id: currentUser.id }).$promise; }],
translations: ['Translations', function (Translations) { return Translations.query(['app.logged.profileCompletion', 'app.shared.user']).$promise; }]
}
})
// dashboard
.state('app.logged.dashboard', {
abstract: true,
url: '/dashboard',
resolve: {
memberPromise: ['Member', 'currentUser', function (Member, currentUser) { return Member.get({ id: currentUser.id }).$promise; }]
}
})
.state('app.logged.dashboard.profile', {
url: '/profile',
views: {
'main@': {
templateUrl: '<%= asset_path "dashboard/profile.html" %>',
controller: 'DashboardController'
}
},
resolve: {
translations: ['Translations', function (Translations) { return Translations.query(['app.logged.dashboard.profile', 'app.shared.public_profile']).$promise; }]
}
})
.state('app.logged.dashboard.settings', {
url: '/settings',
views: {
'main@': {
templateUrl: '<%= asset_path "dashboard/settings.html" %>',
controller: 'EditProfileController'
}
},
resolve: {
groups: ['Group', function (Group) { return Group.query().$promise; }],
activeProviderPromise: ['AuthProvider', function (AuthProvider) { return AuthProvider.active().$promise; }],
translations: ['Translations', function (Translations) { return Translations.query(['app.logged.dashboard.settings', 'app.shared.user']).$promise; }]
}
})
.state('app.logged.dashboard.projects', {
url: '/projects',
views: {
'main@': {
templateUrl: '<%= asset_path "dashboard/projects.html" %>',
controller: 'DashboardController'
}
},
resolve: {
translations: ['Translations', function (Translations) { return 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', function (Translations) { return 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', function (Translations) { return 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', function (Translations) { return Translations.query('app.logged.dashboard.invoices').$promise; }]
}
})
.state('app.logged.dashboard.wallet', {
url: '/wallet',
views: {
'main@': {
templateUrl: '<%= asset_path "dashboard/wallet.html" %>',
controller: 'WalletController'
}
},
resolve: {
walletPromise: ['Wallet', 'currentUser', function (Wallet, currentUser) { return Wallet.getWalletByUser({ user_id: currentUser.id }).$promise; }],
transactionsPromise: ['Wallet', 'walletPromise', function (Wallet, walletPromise) { return Wallet.transactions({ id: walletPromise.id }).$promise; }],
translations: ['Translations', function (Translations) { return Translations.query(['app.shared.wallet']).$promise; }]
}
})
// members
.state('app.logged.members_show', {
url: '/members/:id',
views: {
'main@': {
templateUrl: '<%= asset_path "members/show.html" %>',
controller: 'ShowProfileController'
}
},
resolve: {
memberPromise: ['$stateParams', 'Member', function ($stateParams, Member) { return Member.get({ id: $stateParams.id }).$promise; }],
translations: ['Translations', function (Translations) { return Translations.query(['app.logged.members_show', 'app.shared.public_profile']).$promise; }]
}
})
.state('app.logged.members', {
url: '/members',
views: {
'main@': {
templateUrl: '<%= asset_path "members/index.html" %>',
controller: 'MembersController'
}
},
resolve: {
membersPromise: ['Member', function (Member) { return Member.query({ requested_attributes: '[profile]', page: 1, size: 10 }).$promise; }],
translations: ['Translations', function (Translations) { return Translations.query('app.logged.members').$promise; }]
}
})
// projects
.state('app.public.projects_list', {
url: '/projects?q&page&theme_id&component_id&machine_id&from&whole_network',
views: {
'main@': {
templateUrl: '<%= asset_path "projects/index.html.erb" %>',
controller: 'ProjectsController'
}
},
resolve: {
themesPromise: ['Theme', function (Theme) { return Theme.query().$promise; }],
componentsPromise: ['Component', function (Component) { return Component.query().$promise; }],
machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }],
translations: ['Translations', function (Translations) { return 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: {
allowedExtensions: ['Project', function (Project) { return Project.allowedExtensions().$promise; }],
translations: ['Translations', function (Translations) { return 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', function ($stateParams, Project) { return Project.get({ id: $stateParams.id }).$promise; }],
translations: ['Translations', function (Translations) { return 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', function ($stateParams, Project) { return Project.get({ id: $stateParams.id }).$promise; }],
allowedExtensions: ['Project', function (Project) { return Project.allowedExtensions().$promise; }],
translations: ['Translations', function (Translations) { return Translations.query(['app.logged.projects_edit', 'app.shared.project']).$promise; }]
}
})
// machines
.state('app.public.machines_list', {
url: '/machines',
views: {
'main@': {
templateUrl: '<%= asset_path "machines/index.html.erb" %>',
controller: 'MachinesController'
}
},
resolve: {
machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }],
translations: ['Translations', function (Translations) { return 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', function (Translations) { return 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', function (Machine, $stateParams) { return Machine.get({ id: $stateParams.id }).$promise; }],
translations: ['Translations', function (Translations) { return 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', function (Plan) { return Plan.query().$promise; }],
groupsPromise: ['Group', function (Group) { return Group.query().$promise; }],
machinePromise: ['Machine', '$stateParams', function (Machine, $stateParams) { return Machine.get({ id: $stateParams.id }).$promise; }],
settingsPromise: ['Setting', function (Setting) {
return 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', function (Translations) {
return Translations.query(['app.logged.machines_reserve', 'app.shared.plan_subscribe', 'app.shared.member_select',
'app.shared.stripe', 'app.shared.valid_reservation_modal', 'app.shared.confirm_modify_slot_modal',
'app.shared.wallet', 'app.shared.coupon_input', 'app.shared.cart']).$promise;
}]
}
})
.state('app.admin.machines_edit', {
url: '/machines/:id/edit',
views: {
'main@': {
templateUrl: '<%= asset_path "machines/edit.html" %>',
controller: 'EditMachineController'
}
},
resolve: {
machinePromise: ['Machine', '$stateParams', function (Machine, $stateParams) { return Machine.get({ id: $stateParams.id }).$promise; }],
translations: ['Translations', function (Translations) { return Translations.query(['app.admin.machines_edit', 'app.shared.machine']).$promise; }]
}
})
// spaces
.state('app.public.spaces_list', {
url: '/spaces',
abstract: Fablab.withoutSpaces,
views: {
'main@': {
templateUrl: '<%= asset_path "spaces/index.html" %>',
controller: 'SpacesController'
}
},
resolve: {
spacesPromise: ['Space', function (Space) { return Space.query().$promise; }],
translations: ['Translations', function (Translations) { return Translations.query(['app.public.spaces_list']).$promise; }]
}
})
.state('app.admin.space_new', {
url: '/spaces/new',
abstract: Fablab.withoutSpaces,
views: {
'main@': {
templateUrl: '<%= asset_path "spaces/new.html" %>',
controller: 'NewSpaceController'
}
},
resolve: {
translations: ['Translations', function (Translations) { return Translations.query(['app.admin.space_new', 'app.shared.space']).$promise; }]
}
})
.state('app.public.space_show', {
url: '/spaces/:id',
abstract: Fablab.withoutSpaces,
views: {
'main@': {
templateUrl: '<%= asset_path "spaces/show.html" %>',
controller: 'ShowSpaceController'
}
},
resolve: {
spacePromise: ['Space', '$stateParams', function (Space, $stateParams) { return Space.get({ id: $stateParams.id }).$promise; }],
translations: ['Translations', function (Translations) { return Translations.query(['app.public.space_show']).$promise; }]
}
})
.state('app.admin.space_edit', {
url: '/spaces/:id/edit',
abstract: Fablab.withoutSpaces,
views: {
'main@': {
templateUrl: '<%= asset_path "spaces/edit.html" %>',
controller: 'EditSpaceController'
}
},
resolve: {
spacePromise: ['Space', '$stateParams', function (Space, $stateParams) { return Space.get({ id: $stateParams.id }).$promise; }],
translations: ['Translations', function (Translations) { return Translations.query(['app.admin.space_edit', 'app.shared.space']).$promise; }]
}
})
.state('app.logged.space_reserve', {
url: '/spaces/:id/reserve',
abstract: Fablab.withoutSpaces,
views: {
'main@': {
templateUrl: '<%= asset_path "spaces/reserve.html" %>',
controller: 'ReserveSpaceController'
}
},
resolve: {
spacePromise: ['Space', '$stateParams', function (Space, $stateParams) { return Space.get({ id: $stateParams.id }).$promise; }],
availabilitySpacesPromise: ['Availability', '$stateParams', function (Availability, $stateParams) { return Availability.spaces({ spaceId: $stateParams.id }).$promise; }],
plansPromise: ['Plan', function (Plan) { return Plan.query().$promise; }],
groupsPromise: ['Group', function (Group) { return Group.query().$promise; }],
settingsPromise: ['Setting', function (Setting) {
return Setting.query({
names: `['booking_window_start', \
'booking_window_end', \
'booking_move_enable', \
'booking_move_delay', \
'booking_cancel_enable', \
'booking_cancel_delay', \
'subscription_explications_alert', \
'space_explications_alert']` }).$promise;
}],
translations: ['Translations', function (Translations) {
return Translations.query(['app.logged.space_reserve', 'app.shared.plan_subscribe', 'app.shared.member_select',
'app.shared.stripe', 'app.shared.valid_reservation_modal', 'app.shared.confirm_modify_slot_modal',
'app.shared.wallet', 'app.shared.coupon_input', 'app.shared.cart']).$promise;
}]
}
})
// trainings
.state('app.public.trainings_list', {
url: '/trainings',
views: {
'main@': {
templateUrl: '<%= asset_path "trainings/index.html.erb" %>',
controller: 'TrainingsController'
}
},
resolve: {
trainingsPromise: ['Training', function (Training) { return Training.query({ public_page: true }).$promise; }],
translations: ['Translations', function (Translations) { return Translations.query(['app.public.trainings_list']).$promise; }]
}
})
.state('app.public.training_show', {
url: '/trainings/:id',
views: {
'main@': {
templateUrl: '<%= asset_path "trainings/show.html" %>',
controller: 'ShowTrainingController'
}
},
resolve: {
trainingPromise: ['Training', '$stateParams', function (Training, $stateParams) { return Training.get({ id: $stateParams.id }).$promise; }],
translations: ['Translations', function (Translations) { return Translations.query(['app.public.training_show']).$promise; }]
}
})
.state('app.logged.trainings_reserve', {
url: '/trainings/:id/reserve',
views: {
'main@': {
templateUrl: '<%= asset_path "trainings/reserve.html" %>',
controller: 'ReserveTrainingController'
}
},
resolve: {
explicationAlertPromise: ['Setting', function (Setting) { return Setting.get({ name: 'training_explications_alert' }).$promise; }],
plansPromise: ['Plan', function (Plan) { return Plan.query().$promise; }],
groupsPromise: ['Group', function (Group) { return Group.query().$promise; }],
availabilityTrainingsPromise: ['Availability', '$stateParams', function (Availability, $stateParams) { return Availability.trainings({ trainingId: $stateParams.id }).$promise; }],
trainingPromise: ['Training', '$stateParams', function (Training, $stateParams) {
if ($stateParams.id !== 'all') { return Training.get({ id: $stateParams.id }).$promise; }
}],
settingsPromise: ['Setting', function (Setting) {
return 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', function (Translations) {
return Translations.query(['app.logged.trainings_reserve', 'app.shared.plan_subscribe', 'app.shared.member_select',
'app.shared.stripe', 'app.shared.valid_reservation_modal', 'app.shared.confirm_modify_slot_modal',
'app.shared.wallet', 'app.shared.coupon_input', 'app.shared.cart']).$promise;
}]
}
})
// notifications
.state('app.logged.notifications', {
url: '/notifications',
views: {
'main@': {
templateUrl: '<%= asset_path "notifications/index.html.erb" %>',
controller: 'NotificationsController'
}
},
resolve: {
translations: ['Translations', function (Translations) { return Translations.query('app.logged.notifications').$promise; }]
}
})
// pricing
.state('app.public.plans', {
url: '/plans',
abstract: Fablab.withoutPlans,
views: {
'main@': {
templateUrl: '<%= asset_path "plans/index.html.erb" %>',
controller: 'PlansIndexController'
}
},
resolve: {
subscriptionExplicationsPromise: ['Setting', function (Setting) { return Setting.get({ name: 'subscription_explications_alert' }).$promise; }],
plansPromise: ['Plan', function (Plan) { return Plan.query().$promise; }],
groupsPromise: ['Group', function (Group) { return Group.query().$promise; }],
translations: ['Translations', function (Translations) {
return Translations.query(['app.public.plans', 'app.shared.member_select', 'app.shared.stripe', 'app.shared.wallet',
'app.shared.coupon_input']).$promise;
}]
}
})
// events
.state('app.public.events_list', {
url: '/events',
views: {
'main@': {
templateUrl: '<%= asset_path "events/index.html.erb" %>',
controller: 'EventsController'
}
},
resolve: {
categoriesPromise: ['Category', function (Category) { return Category.query().$promise; }],
themesPromise: ['EventTheme', function (EventTheme) { return EventTheme.query().$promise; }],
ageRangesPromise: ['AgeRange', function (AgeRange) { return AgeRange.query().$promise; }],
translations: ['Translations', function (Translations) { return 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', function (Event, $stateParams) { return Event.get({ id: $stateParams.id }).$promise; }],
priceCategoriesPromise: ['PriceCategory', function (PriceCategory) { return PriceCategory.query().$promise; }],
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['booking_move_enable', 'booking_move_delay', 'event_explications_alert']" }).$promise; }],
translations: ['Translations', function (Translations) {
return Translations.query(['app.public.events_show', 'app.shared.member_select', 'app.shared.stripe',
'app.shared.valid_reservation_modal', 'app.shared.wallet', 'app.shared.coupon_input']).$promise;
}]
}
})
// global calendar (trainings, machines and events)
.state('app.public.calendar', {
url: '/calendar',
views: {
'main@': {
templateUrl: '<%= asset_path "calendar/calendar.html" %>',
controller: 'CalendarController'
}
},
resolve: {
bookingWindowStart: ['Setting', function (Setting) { return Setting.get({ name: 'booking_window_start' }).$promise; }],
bookingWindowEnd: ['Setting', function (Setting) { return Setting.get({ name: 'booking_window_end' }).$promise; }],
trainingsPromise: ['Training', function (Training) { return Training.query().$promise; }],
machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }],
spacesPromise: ['Space', function (Space) { return Space.query().$promise; }],
translations: ['Translations', function (Translations) { return Translations.query(['app.public.calendar']).$promise; }]
}
})
// --- namespace /admin/... ---
// calendar
.state('app.admin.calendar', {
url: '/admin/calendar',
views: {
'main@': {
templateUrl: '<%= asset_path "admin/calendar/calendar.html" %>',
controller: 'AdminCalendarController'
}
},
resolve: {
bookingWindowStart: ['Setting', function (Setting) { return Setting.get({ name: 'booking_window_start' }).$promise; }],
bookingWindowEnd: ['Setting', function (Setting) { return Setting.get({ name: 'booking_window_end' }).$promise; }],
machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }],
translations: ['Translations', function (Translations) { return 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.erb" %>',
controller: 'ProjectElementsController'
}
},
resolve: {
componentsPromise: ['Component', function (Component) { return Component.query().$promise; }],
licencesPromise: ['Licence', function (Licence) { return Licence.query().$promise; }],
themesPromise: ['Theme', function (Theme) { return Theme.query().$promise; }],
translations: ['Translations', function (Translations) { return Translations.query('app.admin.project_elements').$promise; }]
}
})
// trainings
.state('app.admin.trainings', {
url: '/admin/trainings',
views: {
'main@': {
templateUrl: '<%= asset_path "admin/trainings/index.html.erb" %>',
controller: 'TrainingsAdminController'
}
},
resolve: {
trainingsPromise: ['Training', function (Training) { return Training.query().$promise; }],
machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }],
translations: ['Translations', function (Translations) { return Translations.query(['app.admin.trainings', 'app.shared.trainings']).$promise; }]
}
})
.state('app.admin.trainings_new', {
url: '/admin/trainings/new',
views: {
'main@': {
templateUrl: '<%= asset_path "admin/trainings/new.html" %>',
controller: 'NewTrainingController'
}
},
resolve: {
machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }],
translations: ['Translations', function (Translations) { return Translations.query(['app.admin.trainings_new', 'app.shared.trainings']).$promise; }]
}
})
.state('app.admin.trainings_edit', {
url: '/admin/trainings/:id/edit',
views: {
'main@': {
templateUrl: '<%= asset_path "admin/trainings/edit.html" %>',
controller: 'EditTrainingController'
}
},
resolve: {
trainingPromise: ['Training', '$stateParams', function (Training, $stateParams) { return Training.get({ id: $stateParams.id }).$promise; }],
machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }],
translations: ['Translations', function (Translations) { return Translations.query('app.shared.trainings').$promise; }]
}
})
// events
.state('app.admin.events', {
url: '/admin/events',
views: {
'main@': {
templateUrl: '<%= asset_path "admin/events/index.html.erb" %>',
controller: 'AdminEventsController'
}
},
resolve: {
eventsPromise: ['Event', function (Event) { return Event.query({ page: 1 }).$promise; }],
categoriesPromise: ['Category', function (Category) { return Category.query().$promise; }],
themesPromise: ['EventTheme', function (EventTheme) { return EventTheme.query().$promise; }],
ageRangesPromise: ['AgeRange', function (AgeRange) { return AgeRange.query().$promise; }],
priceCategoriesPromise: ['PriceCategory', function (PriceCategory) { return PriceCategory.query().$promise; }],
translations: ['Translations', function (Translations) { return 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: {
categoriesPromise: ['Category', function (Category) { return Category.query().$promise; }],
themesPromise: ['EventTheme', function (EventTheme) { return EventTheme.query().$promise; }],
ageRangesPromise: ['AgeRange', function (AgeRange) { return AgeRange.query().$promise; }],
priceCategoriesPromise: ['PriceCategory', function (PriceCategory) { return PriceCategory.query().$promise; }],
translations: ['Translations', function (Translations) { return 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', function (Event, $stateParams) { return Event.get({ id: $stateParams.id }).$promise; }],
categoriesPromise: ['Category', function (Category) { return Category.query().$promise; }],
themesPromise: ['EventTheme', function (EventTheme) { return EventTheme.query().$promise; }],
ageRangesPromise: ['AgeRange', function (AgeRange) { return AgeRange.query().$promise; }],
priceCategoriesPromise: ['PriceCategory', function (PriceCategory) { return PriceCategory.query().$promise; }],
translations: ['Translations', function (Translations) { return 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', function (Event, $stateParams) { return Event.get({ id: $stateParams.id }).$promise; }],
reservationsPromise: ['Reservation', '$stateParams', function (Reservation, $stateParams) { return Reservation.query({ reservable_id: $stateParams.id, reservable_type: 'Event' }).$promise; }],
translations: ['Translations', function (Translations) { return Translations.query('app.admin.event_reservations').$promise; }]
}
})
// pricing
.state('app.admin.pricing', {
url: '/admin/pricing',
views: {
'main@': {
templateUrl: '<%= asset_path "admin/pricing/index.html.erb" %>',
controller: 'EditPricingController'
}
},
resolve: {
plans: ['Plan', function (Plan) { return Plan.query().$promise; }],
groups: ['Group', function (Group) { return Group.query().$promise; }],
machinesPricesPromise: ['Price', function (Price) { return Price.query({ priceable_type: 'Machine', plan_id: 'null' }).$promise; }],
trainingsPricingsPromise: ['TrainingsPricing', function (TrainingsPricing) { return TrainingsPricing.query().$promise; }],
translations: ['Translations', function (Translations) { return Translations.query(['app.admin.pricing', 'app.shared.member_select', 'app.shared.coupon']).$promise; }],
trainingsPromise: ['Training', function (Training) { return Training.query().$promise; }],
machineCreditsPromise: ['Credit', function (Credit) { return Credit.query({ creditable_type: 'Machine' }).$promise; }],
machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }],
trainingCreditsPromise: ['Credit', function (Credit) { return Credit.query({ creditable_type: 'Training' }).$promise; }],
couponsPromise: ['Coupon', function (Coupon) { return Coupon.query().$promise; }],
spacesPromise: ['Space', function (Space) { return Space.query().$promise; }],
spacesPricesPromise: ['Price', function (Price) { return Price.query({ priceable_type: 'Space', plan_id: 'null' }).$promise; }],
spacesCreditsPromise: ['Credit', function (Credit) { return Credit.query({ creditable_type: 'Space' }).$promise; }]
}
})
// plans
.state('app.admin.plans', {
abstract: true,
resolve: {
prices: ['Pricing', function (Pricing) { return Pricing.query().$promise; }],
groups: ['Group', function (Group) { return Group.query().$promise; }],
partners: ['User', function (User) { return 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', function (Translations) { return 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: {
spaces: ['Space', function (Space) { return Space.query().$promise; }],
machines: ['Machine', function (Machine) { return Machine.query().$promise; }],
plans: ['Plan', function (Plan) { return Plan.query().$promise; }],
planPromise: ['Plan', '$stateParams', function (Plan, $stateParams) { return Plan.get({ id: $stateParams.id }).$promise; }],
translations: ['Translations', function (Translations) { return Translations.query(['app.admin.plans.edit', 'app.shared.plan']).$promise; }]
}
})
// coupons
.state('app.admin.coupons_new', {
url: '/admin/coupons/new',
views: {
'main@': {
templateUrl: '<%= asset_path "admin/coupons/new.html" %>',
controller: 'NewCouponController'
}
},
resolve: {
translations: ['Translations', function (Translations) { return Translations.query(['app.admin.coupons_new', 'app.shared.coupon']).$promise; }]
}
})
.state('app.admin.coupons_edit', {
url: '/admin/coupons/:id/edit',
views: {
'main@': {
templateUrl: '<%= asset_path "admin/coupons/edit.html" %>',
controller: 'EditCouponController'
}
},
resolve: {
couponPromise: ['Coupon', '$stateParams', function (Coupon, $stateParams) { return Coupon.get({ id: $stateParams.id }).$promise; }],
translations: ['Translations', function (Translations) { return Translations.query(['app.admin.coupons_edit', 'app.shared.coupon']).$promise; }]
}
})
// invoices
.state('app.admin.invoices', {
url: '/admin/invoices',
views: {
'main@': {
templateUrl: '<%= asset_path "admin/invoices/index.html.erb" %>',
controller: 'InvoicesController'
}
},
resolve: {
settings: ['Setting', function (Setting) {
return 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;
}],
invoices: [ 'Invoice', function (Invoice) {
return Invoice.list({
query: { number: '', customer: '', date: null, order_by: '-reference', page: 1, size: 20 }
}).$promise;
}],
translations: ['Translations', function (Translations) { return Translations.query('app.admin.invoices').$promise; }]
}
})
// members
.state('app.admin.members', {
url: '/admin/members',
views: {
'main@': {
templateUrl: '<%= asset_path "admin/members/index.html.erb" %>',
controller: 'AdminMembersController'
},
'groups@app.admin.members': {
templateUrl: '<%= asset_path "admin/groups/index.html.erb" %>',
controller: 'GroupsController'
},
'tags@app.admin.members': {
templateUrl: '<%= asset_path "admin/tags/index.html.erb" %>',
controller: 'TagsController'
},
'authentification@app.admin.members': {
templateUrl: '<%= asset_path "admin/authentications/index.html.erb" %>',
controller: 'AuthentificationController'
}
},
resolve: {
membersPromise: ['Member', function (Member) { return Member.list({ query: { search: '', order_by: 'id', page: 1, size: 20 } }).$promise; }],
adminsPromise: ['Admin', function (Admin) { return Admin.query().$promise; }],
groupsPromise: ['Group', function (Group) { return Group.query().$promise; }],
tagsPromise: ['Tag', function (Tag) { return Tag.query().$promise; }],
authProvidersPromise: ['AuthProvider', function (AuthProvider) { return AuthProvider.query().$promise; }],
translations: ['Translations', function (Translations) { return 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', function (Translations) { return 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', function (Member, $stateParams) { return Member.get({ id: $stateParams.id }).$promise; }],
activeProviderPromise: ['AuthProvider', function (AuthProvider) { return AuthProvider.active().$promise; }],
walletPromise: ['Wallet', '$stateParams', function (Wallet, $stateParams) { return Wallet.getWalletByUser({ user_id: $stateParams.id }).$promise; }],
transactionsPromise: ['Wallet', 'walletPromise', function (Wallet, walletPromise) { return Wallet.transactions({ id: walletPromise.id }).$promise; }],
tagsPromise: ['Tag', function (Tag) { return Tag.query().$promise; }],
translations: ['Translations', function (Translations) { return Translations.query(['app.admin.members_edit', 'app.shared.user', 'app.shared.user_admin', 'app.shared.wallet']).$promise; }]
}
})
.state('app.admin.admins_new', {
url: '/admin/admins/new',
views: {
'main@': {
templateUrl: '<%= asset_path "admin/admins/new.html" %>',
controller: 'NewAdminController'
}
},
resolve: {
translations: ['Translations', function (Translations) { return Translations.query('app.admin.admins_new').$promise; }]
}
})
// 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', function (AuthProvider) { return AuthProvider.mapping_fields().$promise; }],
authProvidersPromise: ['AuthProvider', function (AuthProvider) { return AuthProvider.query().$promise; }],
translations: ['Translations', function (Translations) { return 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', function (AuthProvider, $stateParams) { return AuthProvider.get({ id: $stateParams.id }).$promise; }],
mappingFieldsPromise: ['AuthProvider', function (AuthProvider) { return AuthProvider.mapping_fields().$promise; }],
translations: ['Translations', function (Translations) { return 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.erb" %>',
controller: 'StatisticsController'
}
},
resolve: {
membersPromise: ['Member', function (Member) { return Member.mapping().$promise; }],
statisticsPromise: ['Statistics', function (Statistics) { return Statistics.query().$promise; }],
translations: ['Translations', function (Translations) { return 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', function (Translations) { return Translations.query('app.admin.stats_graphs').$promise; }]
}
})
// configurations
.state('app.admin.settings', {
url: '/admin/settings',
views: {
'main@': {
templateUrl: '<%= asset_path "admin/settings/index.html.erb" %>',
controller: 'SettingsController'
}
},
resolve: {
settingsPromise: ['Setting', function (Setting) {
return 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_explications_alert', \
'space_explications_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', \
'reminder_enable', \
'reminder_delay', \
'visibility_yearly', \
'visibility_others', \
'display_name_enable', \
'machines_sort_by']` }).$promise;
}],
cguFile: ['CustomAsset', function (CustomAsset) { return CustomAsset.get({ name: 'cgu-file' }).$promise; }],
cgvFile: ['CustomAsset', function (CustomAsset) { return CustomAsset.get({ name: 'cgv-file' }).$promise; }],
faviconFile: ['CustomAsset', function (CustomAsset) { return CustomAsset.get({ name: 'favicon-file' }).$promise; }],
profileImageFile: ['CustomAsset', function (CustomAsset) { return CustomAsset.get({ name: 'profile-image-file' }).$promise; }],
translations: ['Translations', function (Translations) { return Translations.query('app.admin.settings').$promise; }]
}
})
// OpenAPI Clients
.state('app.admin.open_api_clients', {
url: '/open_api_clients',
views: {
'main@': {
templateUrl: '<%= asset_path "admin/open_api_clients/index.html.erb" %>',
controller: 'OpenAPIClientsController'
}
},
resolve: {
clientsPromise: ['OpenAPIClient', function (OpenAPIClient) { return OpenAPIClient.query().$promise; }],
translations: ['Translations', function (Translations) { return Translations.query('app.admin.open_api_clients').$promise; }]
}
});
}
]);

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