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:
commit
727b205815
3
.eslintignore
Normal file
3
.eslintignore
Normal file
@ -0,0 +1,3 @@
|
||||
node_modules/**
|
||||
vendor/**
|
||||
|
12
.eslintrc
Normal file
12
.eslintrc
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "standard",
|
||||
"rules": {
|
||||
"semi": ["error", "always"]
|
||||
},
|
||||
"globals": {
|
||||
"Application": true,
|
||||
"angular": true,
|
||||
"Fablab": true
|
||||
}
|
||||
}
|
||||
|
@ -1 +0,0 @@
|
||||
2.6.7
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -45,3 +45,7 @@
|
||||
|
||||
# Plugins are versioned is their own repository
|
||||
/plugins/*
|
||||
|
||||
# Ignore node.js dev dependencies
|
||||
/node_modules
|
||||
npm-debug.log
|
||||
|
2
.rubocop.yml
Normal file
2
.rubocop.yml
Normal file
@ -0,0 +1,2 @@
|
||||
Metrics/LineLength:
|
||||
Max: 120
|
@ -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
41
Gemfile
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
};
|
@ -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
|
||||
]
|
29
app/assets/javascripts/controllers/about.js
Normal file
29
app/assets/javascripts/controllers/about.js
Normal 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);
|
||||
}
|
||||
]);
|
2
app/assets/javascripts/controllers/admin/admins.js
Normal file
2
app/assets/javascripts/controllers/admin/admins.js
Normal file
@ -0,0 +1,2 @@
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Sanity-check the conversion and remove this comment.
|
@ -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)
|
||||
]
|
342
app/assets/javascripts/controllers/admin/authentications.js.erb
Normal file
342
app/assets/javascripts/controllers/admin/authentications.js.erb
Normal 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);
|
||||
}
|
||||
]);
|
@ -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 </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()
|
||||
]
|
546
app/assets/javascripts/controllers/admin/calendar.js.erb
Normal file
546
app/assets/javascripts/controllers/admin/calendar.js.erb
Normal 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 </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();
|
||||
}
|
||||
]);
|
@ -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()
|
||||
]
|
130
app/assets/javascripts/controllers/admin/coupons.js
Normal file
130
app/assets/javascripts/controllers/admin/coupons.js
Normal 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();
|
||||
}
|
||||
]);
|
@ -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()
|
||||
]
|
536
app/assets/javascripts/controllers/admin/events.js.erb
Normal file
536
app/assets/javascripts/controllers/admin/events.js.erb
Normal 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();
|
||||
}
|
||||
]);
|
@ -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()
|
||||
]
|
750
app/assets/javascripts/controllers/admin/graphs.js
Normal file
750
app/assets/javascripts/controllers/admin/graphs.js
Normal 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();
|
||||
}
|
||||
]);
|
@ -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'))
|
||||
|
||||
|
||||
]
|
100
app/assets/javascripts/controllers/admin/groups.js
Normal file
100
app/assets/javascripts/controllers/admin/groups.js
Normal 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')));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
]);
|
@ -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()
|
||||
]
|
602
app/assets/javascripts/controllers/admin/invoices.js.erb
Normal file
602
app/assets/javascripts/controllers/admin/invoices.js.erb
Normal 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();
|
||||
}
|
||||
]);
|
@ -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'
|
||||
|
||||
|
||||
]
|
661
app/assets/javascripts/controllers/admin/members.js.erb
Normal file
661
app/assets/javascripts/controllers/admin/members.js.erb
Normal 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'; }
|
||||
};
|
||||
}
|
||||
|
||||
]);
|
@ -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'))
|
||||
|
||||
|
||||
]
|
96
app/assets/javascripts/controllers/admin/open_api_clients.js
Normal file
96
app/assets/javascripts/controllers/admin/open_api_clients.js
Normal 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'));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
]);
|
@ -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()
|
||||
]
|
286
app/assets/javascripts/controllers/admin/plans.js.erb
Normal file
286
app/assets/javascripts/controllers/admin/plans.js.erb
Normal 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);
|
||||
}
|
||||
]);
|
@ -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')
|
||||
]
|
32
app/assets/javascripts/controllers/admin/price_category.js
Normal file
32
app/assets/javascripts/controllers/admin/price_category.js
Normal 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');
|
||||
}
|
||||
]);
|
@ -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()
|
||||
]
|
647
app/assets/javascripts/controllers/admin/pricing.js.erb
Normal file
647
app/assets/javascripts/controllers/admin/pricing.js.erb
Normal 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();
|
||||
}
|
||||
]);
|
@ -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)
|
||||
]
|
160
app/assets/javascripts/controllers/admin/project_elements.js
Normal file
160
app/assets/javascripts/controllers/admin/project_elements.js
Normal 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);
|
||||
}
|
||||
};
|
||||
}
|
||||
]);
|
@ -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()
|
||||
|
||||
]
|
256
app/assets/javascripts/controllers/admin/settings.js
Normal file
256
app/assets/javascripts/controllers/admin/settings.js
Normal 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();
|
||||
}
|
||||
|
||||
]);
|
@ -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')
|
||||
]
|
724
app/assets/javascripts/controllers/admin/statistics.js.erb
Normal file
724
app/assets/javascripts/controllers/admin/statistics.js.erb
Normal 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'); };
|
||||
}
|
||||
]);
|
@ -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'))
|
||||
|
||||
|
||||
]
|
74
app/assets/javascripts/controllers/admin/tags.js
Normal file
74
app/assets/javascripts/controllers/admin/tags.js
Normal 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')));
|
||||
}
|
||||
|
||||
]);
|
@ -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
|
||||
|
||||
]
|
372
app/assets/javascripts/controllers/admin/trainings.js.erb
Normal file
372
app/assets/javascripts/controllers/admin/trainings.js.erb
Normal 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;
|
||||
};
|
||||
}
|
||||
|
||||
]);
|
@ -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()
|
||||
]
|
469
app/assets/javascripts/controllers/application.js.erb
Normal file
469
app/assets/javascripts/controllers/application.js.erb
Normal 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();
|
||||
}
|
||||
]);
|
@ -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()
|
||||
]
|
214
app/assets/javascripts/controllers/calendar.js
Normal file
214
app/assets/javascripts/controllers/calendar.js
Normal 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();
|
||||
}
|
||||
]);
|
@ -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()
|
||||
|
||||
|
||||
]
|
51
app/assets/javascripts/controllers/dashboard.js
Normal file
51
app/assets/javascripts/controllers/dashboard.js
Normal 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();
|
||||
}
|
||||
|
||||
]);
|
@ -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()
|
||||
]
|
768
app/assets/javascripts/controllers/events.js.erb
Normal file
768
app/assets/javascripts/controllers/events.js.erb
Normal 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;
|
||||
}
|
@ -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()
|
||||
]
|
62
app/assets/javascripts/controllers/home.js
Normal file
62
app/assets/javascripts/controllers/home.js
Normal 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();
|
||||
}
|
||||
]);
|
@ -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()
|
||||
]
|
647
app/assets/javascripts/controllers/machines.js.erb
Normal file
647
app/assets/javascripts/controllers/machines.js.erb
Normal 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();
|
||||
}
|
||||
]);
|
@ -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'
|
||||
})
|
||||
]
|
138
app/assets/javascripts/controllers/main_nav.js
Normal file
138
app/assets/javascripts/controllers/main_nav.js
Normal 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'
|
||||
});
|
||||
}
|
||||
}
|
||||
]);
|
@ -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()
|
||||
|
||||
]
|
314
app/assets/javascripts/controllers/members.js
Normal file
314
app/assets/javascripts/controllers/members.js
Normal 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();
|
||||
}
|
||||
|
||||
]);
|
@ -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()
|
||||
]
|
113
app/assets/javascripts/controllers/notifications.js
Normal file
113
app/assets/javascripts/controllers/notifications.js
Normal 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();
|
||||
}
|
||||
]);
|
@ -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()
|
||||
]
|
383
app/assets/javascripts/controllers/plans.js.erb
Normal file
383
app/assets/javascripts/controllers/plans.js.erb
Normal 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();
|
||||
}
|
||||
]);
|
@ -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()
|
||||
|
||||
]
|
219
app/assets/javascripts/controllers/profile.js.erb
Normal file
219
app/assets/javascripts/controllers/profile.js.erb
Normal 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();
|
||||
}
|
||||
|
||||
]);
|
@ -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)
|
||||
]
|
585
app/assets/javascripts/controllers/projects.js.erb
Normal file
585
app/assets/javascripts/controllers/projects.js.erb
Normal 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)}`; };
|
||||
}
|
||||
]);
|
@ -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()
|
||||
|
||||
]
|
553
app/assets/javascripts/controllers/spaces.js.erb
Normal file
553
app/assets/javascripts/controllers/spaces.js.erb
Normal 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();
|
||||
}
|
||||
|
||||
]);
|
@ -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()
|
||||
|
||||
]
|
427
app/assets/javascripts/controllers/trainings.js.erb
Normal file
427
app/assets/javascripts/controllers/trainings.js.erb
Normal 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();
|
||||
}
|
||||
|
||||
]);
|
@ -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
|
||||
]
|
24
app/assets/javascripts/controllers/wallet.js
Normal file
24
app/assets/javascripts/controllers/wallet.js
Normal 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;
|
||||
}
|
||||
]);
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
@ -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()
|
||||
}
|
||||
]
|
||||
|
||||
|
634
app/assets/javascripts/directives/cart.js.erb
Normal file
634
app/assets/javascripts/directives/cart.js.erb
Normal 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
]);
|
@ -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)
|
||||
}
|
||||
]
|
38
app/assets/javascripts/directives/confirmation_needed.js
Normal file
38
app/assets/javascripts/directives/confirmation_needed.js
Normal 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
]);
|
@ -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);
|
||||
}
|
||||
]
|
||||
|
||||
|
75
app/assets/javascripts/directives/coupon.js.erb
Normal file
75
app/assets/javascripts/directives/coupon.js.erb
Normal 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); };
|
||||
}
|
||||
});
|
||||
}]);
|
@ -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)
|
||||
|
||||
}
|
||||
]
|
131
app/assets/javascripts/directives/directives.js
Normal file
131
app/assets/javascripts/directives/directives.js
Normal 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);
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
]);
|
@ -1,10 +0,0 @@
|
||||
Application.Directives.directive 'fabUserAvatar', [ ->
|
||||
{
|
||||
restrict: 'E'
|
||||
scope:
|
||||
userAvatar: "=ngModel"
|
||||
avatarClass: '@'
|
||||
templateUrl: '<%= asset_path "shared/_user_avatar.html" %>'
|
||||
}
|
||||
]
|
||||
|
20
app/assets/javascripts/directives/fab_user_avatar.js.erb
Normal file
20
app/assets/javascripts/directives/fab_user_avatar.js.erb
Normal 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" %>'
|
||||
});
|
||||
}]);
|
@ -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)
|
||||
|
||||
}
|
||||
]
|
||||
|
48
app/assets/javascripts/directives/selectMember.js.erb
Normal file
48
app/assets/javascripts/directives/selectMember.js.erb
Normal 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); });
|
||||
};
|
||||
}
|
||||
|
||||
});
|
||||
}]);
|
@ -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('_', '-')
|
||||
}
|
||||
]
|
||||
|
||||
|
36
app/assets/javascripts/directives/socialLink.js.erb
Normal file
36
app/assets/javascripts/directives/socialLink.js.erb
Normal 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('_', '-')}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}]);
|
@ -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;
|
||||
|
||||
}]);
|
||||
|
@ -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
|
||||
}
|
||||
]
|
55
app/assets/javascripts/directives/validators.js
Normal file
55
app/assets/javascripts/directives/validators.js
Normal 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;
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
]);
|
@ -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
|
||||
]
|
389
app/assets/javascripts/filters/filters.js
Normal file
389
app/assets/javascripts/filters/filters.js
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
]);
|
@ -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
|
||||
]
|
||||
|
||||
]
|
1086
app/assets/javascripts/router.js.erb
Normal file
1086
app/assets/javascripts/router.js.erb
Normal 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
Loading…
x
Reference in New Issue
Block a user