release 2.0 rc
33
.dockerignore
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# Ignore bundler config.
|
||||||
|
.bundle
|
||||||
|
vendor/cache
|
||||||
|
|
||||||
|
config/database.yml
|
||||||
|
config/application.yml
|
||||||
|
|
||||||
|
# Ignore the default SQLite database.
|
||||||
|
db/*.sqlite3
|
||||||
|
db/*.sqlite3-journal
|
||||||
|
|
||||||
|
# Ignore all logfiles and tempfiles.
|
||||||
|
log
|
||||||
|
tmp
|
||||||
|
|
||||||
|
public/uploads
|
||||||
|
public/assets
|
||||||
|
|
||||||
|
*.DS_Store
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# PDF invoices
|
||||||
|
invoices
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
.vagrant
|
||||||
|
Vagrantfile
|
||||||
|
|
||||||
|
.git*
|
||||||
|
|
||||||
|
Dockerfile
|
||||||
|
docker-compose*
|
19
.gitignore
vendored
@ -13,17 +13,24 @@
|
|||||||
/db/*.sqlite3-journal
|
/db/*.sqlite3-journal
|
||||||
|
|
||||||
# Ignore all logfiles and tempfiles.
|
# Ignore all logfiles and tempfiles.
|
||||||
/log/*
|
/log/*.log
|
||||||
!/log/.keep
|
|
||||||
/tmp
|
/tmp
|
||||||
|
|
||||||
# uploads and public assets
|
|
||||||
/public/uploads
|
/public/uploads
|
||||||
/public/assets
|
/public/assets
|
||||||
|
|
||||||
# MacOS and IDE files
|
# Ignore application configuration
|
||||||
|
/config/application.yml
|
||||||
|
|
||||||
|
*.DS_Store
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
|
# PDF invoices
|
||||||
|
/invoices/*
|
||||||
|
|
||||||
|
/config/database.yml
|
||||||
|
/config/application.yml
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
# machine specific database config
|
.vagrant
|
||||||
/config/database.yml
|
|
||||||
|
@ -1 +1 @@
|
|||||||
fabmanager
|
fablab
|
||||||
|
@ -1 +1 @@
|
|||||||
ruby-2.2.1
|
ruby-2.2.3
|
||||||
|
132
CONTRIBUTING.md
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
# Contributing to FabManager
|
||||||
|
|
||||||
|
♥ [FabManager](http://www.fab-manager.com) and want to get involved?
|
||||||
|
Thanks! There are plenty of ways you can help!
|
||||||
|
|
||||||
|
Please take a moment to review this document in order to make the contribution process easy and effective for everyone
|
||||||
|
involved.
|
||||||
|
|
||||||
|
Following these guidelines helps to communicate that you respect the time of the developers managing and developing
|
||||||
|
this open source project. In return, they should reciprocate that respect in addressing your issue or assessing
|
||||||
|
patches and features.
|
||||||
|
|
||||||
|
|
||||||
|
## Using the issue tracker
|
||||||
|
|
||||||
|
The [issue tracker](https://github.com/LaCasemate/fab-manager/issues) is the preferred channel for [bug reports](#bugs),
|
||||||
|
[features requests](#features) and [submitting pull requests](#pull-requests), but please respect the following
|
||||||
|
restrictions:
|
||||||
|
|
||||||
|
* Please **do not** use the issue tracker for personal support requests (use [the forum](http://www.fab-manager.com/forum)).
|
||||||
|
|
||||||
|
* Please **do not** derail or troll issues. Keep the discussion on topic and respect the opinions of others.
|
||||||
|
|
||||||
|
* Please **do not** open issues or pull requests regarding the code in plugins or third parties software, (open them
|
||||||
|
in their respective repositories).
|
||||||
|
|
||||||
|
|
||||||
|
<a name="bugs"></a>
|
||||||
|
## Bug reports
|
||||||
|
|
||||||
|
A bug is a _demonstrable problem_ that is caused by the code in the repository. Good bug reports are extremely
|
||||||
|
helpful - thank you!
|
||||||
|
|
||||||
|
Guidelines for bug reports:
|
||||||
|
|
||||||
|
1. **Use the GitHub issue search** — check if the issue has already been reported.
|
||||||
|
|
||||||
|
2. **Check if the issue has been fixed** — try to reproduce it using the latest `master` or development
|
||||||
|
branch in the repository.
|
||||||
|
|
||||||
|
3. **Isolate the problem** — ideally create a [reduced test case](https://css-tricks.com/reduced-test-cases/)
|
||||||
|
and a live example.
|
||||||
|
|
||||||
|
A good bug report shouldn't leave others needing to chase you up for more information. Please try to be as detailed
|
||||||
|
as possible in your report. What is your environment? What steps will reproduce the issue? What browser(s) and OS
|
||||||
|
experience the problem? What would you expect to be the outcome? All these details will help people to fix any potential
|
||||||
|
bugs.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
> Short and descriptive example bug report title
|
||||||
|
>
|
||||||
|
> A summary of the issue and the browser/OS environment in which it occurs. If suitable, include the steps required
|
||||||
|
> to reproduce the bug.
|
||||||
|
>
|
||||||
|
> 1. This is the first step
|
||||||
|
> 2. This is the second step
|
||||||
|
> 3. Further steps, etc.
|
||||||
|
>
|
||||||
|
> `<url>` - a link to the reduced test case
|
||||||
|
>
|
||||||
|
> Any other information you want to share that is relevant to the issue being reported. This might include the lines of
|
||||||
|
> code that you have identified as causing the bug, and potential solutions (and your opinions on their merits).
|
||||||
|
|
||||||
|
|
||||||
|
<a name="features"></a>
|
||||||
|
## Feature requests
|
||||||
|
|
||||||
|
Feature requests are welcome. But take a moment to find out whether your idea fits with the scope and aims of the
|
||||||
|
project. It's up to *you* to make a strong case to convince the project's developers of the merits of this feature.
|
||||||
|
Please provide as much detail and context as possible.
|
||||||
|
|
||||||
|
|
||||||
|
<a name="pull-requests"></a>
|
||||||
|
## Pull requests
|
||||||
|
|
||||||
|
Good pull requests - patches, improvements, new features - are a fantastic help. They should remain focused in scope
|
||||||
|
and avoid containing unrelated commits.
|
||||||
|
|
||||||
|
**Please ask first** before embarking on any significant pull request (e.g. implementing features, refactoring code,
|
||||||
|
porting to a different language), otherwise you risk spending a lot of time working on something that the project's
|
||||||
|
developers might not want to merge into the project.
|
||||||
|
|
||||||
|
Please adhere to the coding conventions used throughout a project (indentation, accurate comments, etc.) and any other
|
||||||
|
requirements (such as test coverage).
|
||||||
|
|
||||||
|
Adhering to the following process is the best way to get your work included in the project:
|
||||||
|
|
||||||
|
1. [Fork](https://help.github.com/articles/fork-a-repo/) the project, clone your fork, and configure the remotes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone your fork of the repo into the current directory
|
||||||
|
git clone https://github.com/<your-username>/fab-manager.git
|
||||||
|
# Navigate to the newly cloned directory
|
||||||
|
cd fab-manager
|
||||||
|
# Assign the original repo to a remote called "upstream"
|
||||||
|
git remote add upstream https://github.com/LaCasemate/fab-manager.git
|
||||||
|
```
|
||||||
|
|
||||||
|
2. If you cloned a while ago, get the latest changes from upstream:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout master
|
||||||
|
git pull upstream master
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Create a new topic branch (off the main project development branch) to contain your feature, change, or fix:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout -b <topic-branch-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Commit your changes in logical chunks. Please adhere to these [git commit message guidelines](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)
|
||||||
|
or your code is unlikely be merged into the main project. Use Git's [interactive rebase](https://help.github.com/articles/about-git-rebase/)
|
||||||
|
feature to tidy up your commits before making them public.
|
||||||
|
|
||||||
|
5. Locally merge (or rebase) the upstream development branch into your topic branch:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git pull [--rebase] upstream master
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Push your topic branch up to your fork:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git push origin <topic-branch-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/) with a clear title and description.
|
||||||
|
|
||||||
|
**IMPORTANT**: By submitting a patch, you agree to allow the project owners to license your work under the terms of
|
||||||
|
the [GNU Affero General Public License](LICENSE.md).
|
71
Dockerfile
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
FROM ruby:2.2
|
||||||
|
MAINTAINER peng@sleede.com
|
||||||
|
|
||||||
|
# cf: nginx Dockerfile : https://github.com/nginxinc/docker-nginx
|
||||||
|
RUN apt-key adv --keyserver hkp://pgp.mit.edu:80 --recv-keys 573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62
|
||||||
|
RUN echo "deb http://nginx.org/packages/mainline/debian/ jessie nginx" >> /etc/apt/sources.list
|
||||||
|
|
||||||
|
ENV NGINX_VERSION 1.9.7-1~jessie
|
||||||
|
|
||||||
|
# Install apt based dependencies required to run Rails as
|
||||||
|
# well as RubyGems. As the Ruby image itself is based on a
|
||||||
|
# Debian image, we use apt-get to install those.
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y \
|
||||||
|
nginx=${NGINX_VERSION} \
|
||||||
|
nodejs \
|
||||||
|
supervisor
|
||||||
|
|
||||||
|
# throw errors if Gemfile has been modified since Gemfile.lock
|
||||||
|
RUN bundle config --global frozen 1
|
||||||
|
|
||||||
|
# Run Bundle in a cache efficient way
|
||||||
|
WORKDIR /tmp
|
||||||
|
COPY Gemfile /tmp/
|
||||||
|
COPY Gemfile.lock /tmp/
|
||||||
|
RUN bundle install
|
||||||
|
|
||||||
|
# Clean up APT when done.
|
||||||
|
#RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
|
|
||||||
|
|
||||||
|
# Nginx
|
||||||
|
# Remove the default site
|
||||||
|
RUN rm /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
# forward request and error logs to docker log collector
|
||||||
|
RUN ln -sf /dev/stdout /var/log/nginx/access.log
|
||||||
|
RUN ln -sf /dev/stderr /var/log/nginx/error.log
|
||||||
|
|
||||||
|
|
||||||
|
# Web app
|
||||||
|
RUN mkdir -p /usr/src/app
|
||||||
|
RUN mkdir -p /usr/src/app/config
|
||||||
|
RUN mkdir -p /usr/src/app/invoices
|
||||||
|
RUN mkdir -p /usr/src/app/log
|
||||||
|
RUN mkdir -p /usr/src/app/public/uploads
|
||||||
|
RUN mkdir -p /usr/src/app/public/assets
|
||||||
|
RUN mkdir -p /usr/src/app/tmp/sockets
|
||||||
|
RUN mkdir -p /usr/src/app/tmp/pids
|
||||||
|
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
COPY docker/database.yml /usr/src/app/config/database.yml
|
||||||
|
|
||||||
|
COPY . /usr/src/app
|
||||||
|
|
||||||
|
# Volumes
|
||||||
|
VOLUME /usr/src/app/invoices
|
||||||
|
VOLUME /usr/src/app/public/uploads
|
||||||
|
VOLUME /usr/src/app/public/assets
|
||||||
|
VOLUME /var/log/supervisor
|
||||||
|
|
||||||
|
# Expose port 80 and ssl 443 to the Docker host, so we can access it
|
||||||
|
# from the outside.
|
||||||
|
EXPOSE 80 443
|
||||||
|
|
||||||
|
# The main command to run when the container starts. Also
|
||||||
|
# tell the Rails dev server to bind to all interfaces by
|
||||||
|
# default.
|
||||||
|
COPY docker/supervisor.conf /etc/supervisor/conf.d/fablab.conf
|
||||||
|
CMD ["/usr/bin/supervisord"]
|
45
Gemfile
@ -1,7 +1,7 @@
|
|||||||
source 'https://rubygems.org'
|
source 'https://rubygems.org'
|
||||||
|
|
||||||
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
|
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
|
||||||
gem 'rails', '4.2.1'
|
gem 'rails', '4.2.5'
|
||||||
# Use SCSS for stylesheets
|
# Use SCSS for stylesheets
|
||||||
gem 'sass-rails', '5.0.1'
|
gem 'sass-rails', '5.0.1'
|
||||||
gem 'compass-rails', '2.0.4'
|
gem 'compass-rails', '2.0.4'
|
||||||
@ -11,21 +11,22 @@ gem 'uglifier', '>= 1.3.0'
|
|||||||
# Use CoffeeScript for .js.coffee assets and views
|
# Use CoffeeScript for .js.coffee assets and views
|
||||||
gem 'coffee-rails', '~> 4.1.0'
|
gem 'coffee-rails', '~> 4.1.0'
|
||||||
# See https://github.com/sstephenson/execjs#readme for more supported runtimes
|
# See https://github.com/sstephenson/execjs#readme for more supported runtimes
|
||||||
gem 'therubyracer', platforms: :ruby
|
gem 'therubyracer', '= 0.12.0', platforms: :ruby
|
||||||
|
|
||||||
# Use jquery as the JavaScript library
|
# Use jquery as the JavaScript library
|
||||||
gem 'jquery-rails'
|
gem 'jquery-rails'
|
||||||
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
|
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
|
||||||
gem 'jbuilder', '~> 2.0'
|
gem 'jbuilder', '~> 2.0'
|
||||||
# bundle exec rake doc:rails generates the API under doc/api.
|
# bundle exec rake doc:rails generates the API under doc/api.
|
||||||
gem 'sdoc', '~> 0.4.0', group: :doc
|
gem 'sdoc', '~> 0.4.0', group: :doc #TODO remove unused ?
|
||||||
|
|
||||||
gem 'forgery'
|
gem 'forgery'
|
||||||
gem 'responders', '~> 2.0'
|
gem 'responders', '~> 2.0'
|
||||||
|
|
||||||
group :development, :test do
|
group :development, :test do
|
||||||
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
|
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
|
||||||
gem 'byebug'
|
# comment over to use visual debugger (eg. RubyMine), uncomment to use manual debugging
|
||||||
|
# gem 'byebug'
|
||||||
|
|
||||||
# Access an IRB console on exception pages or by using <%= console %> in views
|
# Access an IRB console on exception pages or by using <%= console %> in views
|
||||||
gem 'web-console', '~> 2.0'
|
gem 'web-console', '~> 2.0'
|
||||||
@ -38,6 +39,8 @@ group :development, :test do
|
|||||||
gem 'spring-commands-rspec'
|
gem 'spring-commands-rspec'
|
||||||
|
|
||||||
gem 'guard-rspec', require: false
|
gem 'guard-rspec', require: false
|
||||||
|
|
||||||
|
gem 'railroady'
|
||||||
end
|
end
|
||||||
|
|
||||||
group :development do
|
group :development do
|
||||||
@ -51,6 +54,9 @@ group :development do
|
|||||||
gem 'capistrano'
|
gem 'capistrano'
|
||||||
gem 'rvm-capistrano', require: false
|
gem 'rvm-capistrano', require: false
|
||||||
gem 'capistrano-sidekiq', require: false
|
gem 'capistrano-sidekiq', require: false
|
||||||
|
gem 'capistrano-maintenance', '0.0.5', require: false
|
||||||
|
|
||||||
|
gem 'active_record_query_trace'
|
||||||
end
|
end
|
||||||
|
|
||||||
group :test do
|
group :test do
|
||||||
@ -70,6 +76,9 @@ gem 'pg'
|
|||||||
gem 'devise'
|
gem 'devise'
|
||||||
gem 'devise-async'
|
gem 'devise-async'
|
||||||
|
|
||||||
|
gem 'omniauth'
|
||||||
|
gem 'omniauth-oauth2'
|
||||||
|
|
||||||
gem 'rolify'
|
gem 'rolify'
|
||||||
|
|
||||||
gem 'kaminari'
|
gem 'kaminari'
|
||||||
@ -79,7 +88,8 @@ gem 'figaro'
|
|||||||
gem 'bootstrap-sass'
|
gem 'bootstrap-sass'
|
||||||
gem 'font-awesome-rails'
|
gem 'font-awesome-rails'
|
||||||
|
|
||||||
gem 'angularjs-rails'
|
#using bower instead
|
||||||
|
#gem 'angularjs-rails'
|
||||||
|
|
||||||
# Image processing ruby wrapper for ImageMagick
|
# Image processing ruby wrapper for ImageMagick
|
||||||
gem 'mini_magick'
|
gem 'mini_magick'
|
||||||
@ -101,13 +111,32 @@ gem 'sinatra', require: false
|
|||||||
# Recurring jobs for Sidekiq
|
# Recurring jobs for Sidekiq
|
||||||
gem 'sidekiq-cron'
|
gem 'sidekiq-cron'
|
||||||
|
|
||||||
|
gem 'stripe', '1.30.2'
|
||||||
|
|
||||||
gem 'recurrence'
|
gem 'recurrence'
|
||||||
|
|
||||||
# Fork de la gem avec support Attachments
|
gem 'newrelic_rpm'
|
||||||
gem 'mandrill_dm', github: 'AbleTech/mandrill_dm'
|
|
||||||
|
|
||||||
gem 'disqus_api'
|
# PDF
|
||||||
|
gem 'prawn'
|
||||||
|
gem 'prawn-table'
|
||||||
|
|
||||||
|
gem 'elasticsearch-rails'
|
||||||
|
gem 'elasticsearch-model'
|
||||||
|
gem 'elasticsearch-persistence'
|
||||||
|
|
||||||
gem 'notify_with'
|
gem 'notify_with'
|
||||||
|
|
||||||
gem 'pundit'
|
gem 'pundit'
|
||||||
|
|
||||||
|
gem 'oj'
|
||||||
|
|
||||||
|
gem 'actionpack-page_caching'
|
||||||
|
gem 'rails-observers'
|
||||||
|
|
||||||
|
gem 'chroma'
|
||||||
|
|
||||||
|
|
||||||
|
gem 'protected_attributes'
|
||||||
|
|
||||||
|
gem 'message_format'
|
237
Gemfile.lock
@ -1,56 +1,55 @@
|
|||||||
GIT
|
|
||||||
remote: git://github.com/AbleTech/mandrill_dm.git
|
|
||||||
revision: 2bbb35dd81887bb915f606699d63a723b450711d
|
|
||||||
specs:
|
|
||||||
mandrill_dm (1.1.0)
|
|
||||||
mandrill-api (~> 1.0.51)
|
|
||||||
|
|
||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
aasm (4.1.0)
|
aasm (4.1.0)
|
||||||
actionmailer (4.2.1)
|
actionmailer (4.2.5)
|
||||||
actionpack (= 4.2.1)
|
actionpack (= 4.2.5)
|
||||||
actionview (= 4.2.1)
|
actionview (= 4.2.5)
|
||||||
activejob (= 4.2.1)
|
activejob (= 4.2.5)
|
||||||
mail (~> 2.5, >= 2.5.4)
|
mail (~> 2.5, >= 2.5.4)
|
||||||
rails-dom-testing (~> 1.0, >= 1.0.5)
|
rails-dom-testing (~> 1.0, >= 1.0.5)
|
||||||
actionpack (4.2.1)
|
actionpack (4.2.5)
|
||||||
actionview (= 4.2.1)
|
actionview (= 4.2.5)
|
||||||
activesupport (= 4.2.1)
|
activesupport (= 4.2.5)
|
||||||
rack (~> 1.6)
|
rack (~> 1.6)
|
||||||
rack-test (~> 0.6.2)
|
rack-test (~> 0.6.2)
|
||||||
rails-dom-testing (~> 1.0, >= 1.0.5)
|
rails-dom-testing (~> 1.0, >= 1.0.5)
|
||||||
rails-html-sanitizer (~> 1.0, >= 1.0.1)
|
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
||||||
actionview (4.2.1)
|
actionpack-page_caching (1.0.2)
|
||||||
activesupport (= 4.2.1)
|
actionpack (>= 4.0.0, < 5)
|
||||||
|
actionview (4.2.5)
|
||||||
|
activesupport (= 4.2.5)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubis (~> 2.7.0)
|
erubis (~> 2.7.0)
|
||||||
rails-dom-testing (~> 1.0, >= 1.0.5)
|
rails-dom-testing (~> 1.0, >= 1.0.5)
|
||||||
rails-html-sanitizer (~> 1.0, >= 1.0.1)
|
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
||||||
activejob (4.2.1)
|
active_record_query_trace (1.4)
|
||||||
activesupport (= 4.2.1)
|
activejob (4.2.5)
|
||||||
|
activesupport (= 4.2.5)
|
||||||
globalid (>= 0.3.0)
|
globalid (>= 0.3.0)
|
||||||
activemodel (4.2.1)
|
activemodel (4.2.5)
|
||||||
activesupport (= 4.2.1)
|
activesupport (= 4.2.5)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
activerecord (4.2.1)
|
activerecord (4.2.5)
|
||||||
activemodel (= 4.2.1)
|
activemodel (= 4.2.5)
|
||||||
activesupport (= 4.2.1)
|
activesupport (= 4.2.5)
|
||||||
arel (~> 6.0)
|
arel (~> 6.0)
|
||||||
activesupport (4.2.1)
|
activesupport (4.2.5)
|
||||||
i18n (~> 0.7)
|
i18n (~> 0.7)
|
||||||
json (~> 1.7, >= 1.7.7)
|
json (~> 1.7, >= 1.7.7)
|
||||||
minitest (~> 5.1)
|
minitest (~> 5.1)
|
||||||
thread_safe (~> 0.3, >= 0.3.4)
|
thread_safe (~> 0.3, >= 0.3.4)
|
||||||
tzinfo (~> 1.1)
|
tzinfo (~> 1.1)
|
||||||
addressable (2.3.8)
|
addressable (2.3.8)
|
||||||
angularjs-rails (1.3.15)
|
arel (6.0.3)
|
||||||
arel (6.0.0)
|
|
||||||
autoprefixer-rails (5.1.8)
|
autoprefixer-rails (5.1.8)
|
||||||
execjs
|
execjs
|
||||||
json
|
json
|
||||||
awesome_print (1.6.1)
|
awesome_print (1.6.1)
|
||||||
|
axiom-types (0.1.1)
|
||||||
|
descendants_tracker (~> 0.0.4)
|
||||||
|
ice_nine (~> 0.11.0)
|
||||||
|
thread_safe (~> 0.3, >= 0.3.1)
|
||||||
bcrypt (3.1.10)
|
bcrypt (3.1.10)
|
||||||
binding_of_caller (0.7.2)
|
binding_of_caller (0.7.2)
|
||||||
debug_inspector (>= 0.0.1)
|
debug_inspector (>= 0.0.1)
|
||||||
@ -59,14 +58,15 @@ GEM
|
|||||||
sass (>= 3.2.19)
|
sass (>= 3.2.19)
|
||||||
buftok (0.2.0)
|
buftok (0.2.0)
|
||||||
builder (3.2.2)
|
builder (3.2.2)
|
||||||
byebug (4.0.4)
|
camertron-eprun (1.1.0)
|
||||||
columnize (= 0.9.0)
|
|
||||||
capistrano (2.15.5)
|
capistrano (2.15.5)
|
||||||
highline
|
highline
|
||||||
net-scp (>= 1.0.0)
|
net-scp (>= 1.0.0)
|
||||||
net-sftp (>= 2.0.0)
|
net-sftp (>= 2.0.0)
|
||||||
net-ssh (>= 2.0.14)
|
net-ssh (>= 2.0.14)
|
||||||
net-ssh-gateway (>= 1.1.0)
|
net-ssh-gateway (>= 1.1.0)
|
||||||
|
capistrano-maintenance (0.0.5)
|
||||||
|
capistrano (~> 2.0)
|
||||||
capistrano-sidekiq (0.5.2)
|
capistrano-sidekiq (0.5.2)
|
||||||
capistrano
|
capistrano
|
||||||
sidekiq
|
sidekiq
|
||||||
@ -77,8 +77,12 @@ GEM
|
|||||||
mime-types (>= 1.16)
|
mime-types (>= 1.16)
|
||||||
celluloid (0.16.0)
|
celluloid (0.16.0)
|
||||||
timers (~> 4.0.0)
|
timers (~> 4.0.0)
|
||||||
|
chroma (0.0.1)
|
||||||
chunky_png (1.3.4)
|
chunky_png (1.3.4)
|
||||||
|
cldr-plurals-runtime-rb (1.0.1)
|
||||||
coderay (1.1.0)
|
coderay (1.1.0)
|
||||||
|
coercible (1.0.0)
|
||||||
|
descendants_tracker (~> 0.0.1)
|
||||||
coffee-rails (4.1.0)
|
coffee-rails (4.1.0)
|
||||||
coffee-script (>= 2.2.0)
|
coffee-script (>= 2.2.0)
|
||||||
railties (>= 4.0.0, < 5.0)
|
railties (>= 4.0.0, < 5.0)
|
||||||
@ -86,7 +90,6 @@ GEM
|
|||||||
coffee-script-source
|
coffee-script-source
|
||||||
execjs
|
execjs
|
||||||
coffee-script-source (1.9.1)
|
coffee-script-source (1.9.1)
|
||||||
columnize (0.9.0)
|
|
||||||
compass (1.0.3)
|
compass (1.0.3)
|
||||||
chunky_png (~> 1.2)
|
chunky_png (~> 1.2)
|
||||||
compass-core (~> 1.0.2)
|
compass-core (~> 1.0.2)
|
||||||
@ -103,9 +106,11 @@ GEM
|
|||||||
compass (~> 1.0.0)
|
compass (~> 1.0.0)
|
||||||
sass-rails (<= 5.0.1)
|
sass-rails (<= 5.0.1)
|
||||||
sprockets (< 2.13)
|
sprockets (< 2.13)
|
||||||
connection_pool (2.1.3)
|
connection_pool (2.2.0)
|
||||||
database_cleaner (1.4.1)
|
database_cleaner (1.4.1)
|
||||||
debug_inspector (0.0.2)
|
debug_inspector (0.0.2)
|
||||||
|
descendants_tracker (0.0.4)
|
||||||
|
thread_safe (~> 0.3, >= 0.3.1)
|
||||||
devise (3.4.1)
|
devise (3.4.1)
|
||||||
bcrypt (~> 3.0)
|
bcrypt (~> 3.0)
|
||||||
orm_adapter (~> 0.1)
|
orm_adapter (~> 0.1)
|
||||||
@ -116,13 +121,30 @@ GEM
|
|||||||
devise-async (0.9.0)
|
devise-async (0.9.0)
|
||||||
devise (~> 3.2)
|
devise (~> 3.2)
|
||||||
diff-lcs (1.2.5)
|
diff-lcs (1.2.5)
|
||||||
disqus_api (0.0.4)
|
domain_name (0.5.25)
|
||||||
activesupport (>= 3.0.0)
|
unf (>= 0.0.5, < 1.0.0)
|
||||||
faraday (>= 0.8)
|
elasticsearch (1.0.12)
|
||||||
faraday_middleware (>= 0.9)
|
elasticsearch-api (= 1.0.12)
|
||||||
|
elasticsearch-transport (= 1.0.12)
|
||||||
|
elasticsearch-api (1.0.12)
|
||||||
|
multi_json
|
||||||
|
elasticsearch-model (0.1.7)
|
||||||
|
activesupport (> 3)
|
||||||
|
elasticsearch (> 0.4)
|
||||||
|
hashie
|
||||||
|
elasticsearch-persistence (0.1.7)
|
||||||
|
activemodel (> 3)
|
||||||
|
activesupport (> 3)
|
||||||
|
elasticsearch (> 0.4)
|
||||||
|
elasticsearch-model (>= 0.1)
|
||||||
|
hashie
|
||||||
|
virtus
|
||||||
|
elasticsearch-rails (0.1.7)
|
||||||
|
elasticsearch-transport (1.0.12)
|
||||||
|
faraday
|
||||||
|
multi_json
|
||||||
equalizer (0.0.11)
|
equalizer (0.0.11)
|
||||||
erubis (2.7.0)
|
erubis (2.7.0)
|
||||||
excon (0.45.1)
|
|
||||||
execjs (2.4.0)
|
execjs (2.4.0)
|
||||||
factory_girl (4.5.0)
|
factory_girl (4.5.0)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
@ -133,8 +155,6 @@ GEM
|
|||||||
i18n (~> 0.5)
|
i18n (~> 0.5)
|
||||||
faraday (0.9.1)
|
faraday (0.9.1)
|
||||||
multipart-post (>= 1.2, < 3)
|
multipart-post (>= 1.2, < 3)
|
||||||
faraday_middleware (0.9.1)
|
|
||||||
faraday (>= 0.7.4, < 0.10)
|
|
||||||
ffi (1.9.8)
|
ffi (1.9.8)
|
||||||
figaro (1.1.0)
|
figaro (1.1.0)
|
||||||
thor (~> 0.14)
|
thor (~> 0.14)
|
||||||
@ -146,7 +166,7 @@ GEM
|
|||||||
formatador (0.2.5)
|
formatador (0.2.5)
|
||||||
friendly_id (5.1.0)
|
friendly_id (5.1.0)
|
||||||
activerecord (>= 4.0.0)
|
activerecord (>= 4.0.0)
|
||||||
globalid (0.3.3)
|
globalid (0.3.6)
|
||||||
activesupport (>= 4.1.0)
|
activesupport (>= 4.1.0)
|
||||||
guard (2.12.5)
|
guard (2.12.5)
|
||||||
formatador (>= 0.2.4)
|
formatador (>= 0.2.4)
|
||||||
@ -162,13 +182,17 @@ GEM
|
|||||||
guard (~> 2.1)
|
guard (~> 2.1)
|
||||||
guard-compat (~> 1.1)
|
guard-compat (~> 1.1)
|
||||||
rspec (>= 2.99.0, < 4.0)
|
rspec (>= 2.99.0, < 4.0)
|
||||||
|
hashie (3.4.2)
|
||||||
highline (1.7.1)
|
highline (1.7.1)
|
||||||
hike (1.2.3)
|
hike (1.2.3)
|
||||||
hitimes (1.2.2)
|
hitimes (1.2.2)
|
||||||
http (0.6.4)
|
http (0.6.4)
|
||||||
http_parser.rb (~> 0.6.0)
|
http_parser.rb (~> 0.6.0)
|
||||||
|
http-cookie (1.0.2)
|
||||||
|
domain_name (~> 0.5)
|
||||||
http_parser.rb (0.6.0)
|
http_parser.rb (0.6.0)
|
||||||
i18n (0.7.0)
|
i18n (0.7.0)
|
||||||
|
ice_nine (0.11.1)
|
||||||
jbuilder (2.2.12)
|
jbuilder (2.2.12)
|
||||||
activesupport (>= 3.0.0, < 5)
|
activesupport (>= 3.0.0, < 5)
|
||||||
multi_json (~> 1.2)
|
multi_json (~> 1.2)
|
||||||
@ -176,7 +200,8 @@ GEM
|
|||||||
rails-dom-testing (~> 1.0)
|
rails-dom-testing (~> 1.0)
|
||||||
railties (>= 4.2.0)
|
railties (>= 4.2.0)
|
||||||
thor (>= 0.14, < 2.0)
|
thor (>= 0.14, < 2.0)
|
||||||
json (1.8.2)
|
json (1.8.3)
|
||||||
|
jwt (1.5.1)
|
||||||
kaminari (0.16.3)
|
kaminari (0.16.3)
|
||||||
actionpack (>= 3.0.0)
|
actionpack (>= 3.0.0)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
@ -185,27 +210,27 @@ GEM
|
|||||||
addressable (~> 2.3)
|
addressable (~> 2.3)
|
||||||
letter_opener (1.3.0)
|
letter_opener (1.3.0)
|
||||||
launchy (~> 2.2)
|
launchy (~> 2.2)
|
||||||
libv8 (3.16.14.7)
|
libv8 (3.16.14.11)
|
||||||
listen (2.10.0)
|
listen (2.10.0)
|
||||||
celluloid (~> 0.16.0)
|
celluloid (~> 0.16.0)
|
||||||
rb-fsevent (>= 0.9.3)
|
rb-fsevent (>= 0.9.3)
|
||||||
rb-inotify (>= 0.9)
|
rb-inotify (>= 0.9)
|
||||||
loofah (2.0.1)
|
loofah (2.0.3)
|
||||||
nokogiri (>= 1.5.9)
|
nokogiri (>= 1.5.9)
|
||||||
lumberjack (1.0.9)
|
lumberjack (1.0.9)
|
||||||
mail (2.6.3)
|
mail (2.6.3)
|
||||||
mime-types (>= 1.16, < 3)
|
mime-types (>= 1.16, < 3)
|
||||||
mandrill-api (1.0.53)
|
|
||||||
excon (>= 0.16.0, < 1.0)
|
|
||||||
json (>= 1.7.7, < 2.0)
|
|
||||||
memoizable (0.4.2)
|
memoizable (0.4.2)
|
||||||
thread_safe (~> 0.3, >= 0.3.1)
|
thread_safe (~> 0.3, >= 0.3.1)
|
||||||
|
message_format (0.0.3)
|
||||||
|
twitter_cldr (~> 3.1)
|
||||||
method_source (0.8.2)
|
method_source (0.8.2)
|
||||||
mime-types (2.4.3)
|
mime-types (2.99)
|
||||||
mini_magick (4.2.0)
|
mini_magick (4.2.0)
|
||||||
mini_portile (0.6.2)
|
mini_portile (0.6.2)
|
||||||
minitest (5.5.1)
|
minitest (5.8.3)
|
||||||
multi_json (1.11.0)
|
multi_json (1.11.2)
|
||||||
|
multi_xml (0.5.5)
|
||||||
multipart-post (2.0.0)
|
multipart-post (2.0.0)
|
||||||
naught (1.0.0)
|
naught (1.0.0)
|
||||||
nenv (0.2.0)
|
nenv (0.2.0)
|
||||||
@ -216,7 +241,9 @@ GEM
|
|||||||
net-ssh (2.9.2)
|
net-ssh (2.9.2)
|
||||||
net-ssh-gateway (1.2.0)
|
net-ssh-gateway (1.2.0)
|
||||||
net-ssh (>= 2.6.5)
|
net-ssh (>= 2.6.5)
|
||||||
nokogiri (1.6.6.2)
|
netrc (0.10.3)
|
||||||
|
newrelic_rpm (3.11.1.284)
|
||||||
|
nokogiri (1.6.6.4)
|
||||||
mini_portile (~> 0.6.0)
|
mini_portile (~> 0.6.0)
|
||||||
notiffany (0.0.6)
|
notiffany (0.0.6)
|
||||||
nenv (~> 0.1)
|
nenv (~> 0.1)
|
||||||
@ -225,8 +252,28 @@ GEM
|
|||||||
jbuilder (~> 2.0)
|
jbuilder (~> 2.0)
|
||||||
rails (>= 4.2.0)
|
rails (>= 4.2.0)
|
||||||
responders (~> 2.0)
|
responders (~> 2.0)
|
||||||
|
oauth2 (1.0.0)
|
||||||
|
faraday (>= 0.8, < 0.10)
|
||||||
|
jwt (~> 1.0)
|
||||||
|
multi_json (~> 1.3)
|
||||||
|
multi_xml (~> 0.5)
|
||||||
|
rack (~> 1.2)
|
||||||
|
oj (2.12.8)
|
||||||
|
omniauth (1.2.2)
|
||||||
|
hashie (>= 1.2, < 4)
|
||||||
|
rack (~> 1.0)
|
||||||
|
omniauth-oauth2 (1.3.1)
|
||||||
|
oauth2 (~> 1.0)
|
||||||
|
omniauth (~> 1.2)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
|
pdf-core (0.5.1)
|
||||||
pg (0.18.1)
|
pg (0.18.1)
|
||||||
|
prawn (2.0.1)
|
||||||
|
pdf-core (~> 0.5.1)
|
||||||
|
ttfunk (~> 1.4.0)
|
||||||
|
prawn-table (0.2.1)
|
||||||
|
protected_attributes (1.1.3)
|
||||||
|
activemodel (>= 4.0.1, < 5.0)
|
||||||
pry (0.10.1)
|
pry (0.10.1)
|
||||||
coderay (~> 1.1.0)
|
coderay (~> 1.1.0)
|
||||||
method_source (~> 0.8.1)
|
method_source (~> 0.8.1)
|
||||||
@ -235,38 +282,41 @@ GEM
|
|||||||
rack (>= 1.1, < 2.0)
|
rack (>= 1.1, < 2.0)
|
||||||
pundit (1.0.0)
|
pundit (1.0.0)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
rack (1.6.0)
|
rack (1.6.4)
|
||||||
rack-protection (1.5.3)
|
rack-protection (1.5.3)
|
||||||
rack
|
rack
|
||||||
rack-test (0.6.3)
|
rack-test (0.6.3)
|
||||||
rack (>= 1.0)
|
rack (>= 1.0)
|
||||||
rails (4.2.1)
|
railroady (1.4.0)
|
||||||
actionmailer (= 4.2.1)
|
rails (4.2.5)
|
||||||
actionpack (= 4.2.1)
|
actionmailer (= 4.2.5)
|
||||||
actionview (= 4.2.1)
|
actionpack (= 4.2.5)
|
||||||
activejob (= 4.2.1)
|
actionview (= 4.2.5)
|
||||||
activemodel (= 4.2.1)
|
activejob (= 4.2.5)
|
||||||
activerecord (= 4.2.1)
|
activemodel (= 4.2.5)
|
||||||
activesupport (= 4.2.1)
|
activerecord (= 4.2.5)
|
||||||
|
activesupport (= 4.2.5)
|
||||||
bundler (>= 1.3.0, < 2.0)
|
bundler (>= 1.3.0, < 2.0)
|
||||||
railties (= 4.2.1)
|
railties (= 4.2.5)
|
||||||
sprockets-rails
|
sprockets-rails
|
||||||
rails-deprecated_sanitizer (1.0.3)
|
rails-deprecated_sanitizer (1.0.3)
|
||||||
activesupport (>= 4.2.0.alpha)
|
activesupport (>= 4.2.0.alpha)
|
||||||
rails-dom-testing (1.0.6)
|
rails-dom-testing (1.0.7)
|
||||||
activesupport (>= 4.2.0.beta, < 5.0)
|
activesupport (>= 4.2.0.beta, < 5.0)
|
||||||
nokogiri (~> 1.6.0)
|
nokogiri (~> 1.6.0)
|
||||||
rails-deprecated_sanitizer (>= 1.0.1)
|
rails-deprecated_sanitizer (>= 1.0.1)
|
||||||
rails-html-sanitizer (1.0.2)
|
rails-html-sanitizer (1.0.2)
|
||||||
loofah (~> 2.0)
|
loofah (~> 2.0)
|
||||||
|
rails-observers (0.1.2)
|
||||||
|
activemodel (~> 4.0)
|
||||||
rails_12factor (0.0.3)
|
rails_12factor (0.0.3)
|
||||||
rails_serve_static_assets
|
rails_serve_static_assets
|
||||||
rails_stdout_logging
|
rails_stdout_logging
|
||||||
rails_serve_static_assets (0.0.4)
|
rails_serve_static_assets (0.0.4)
|
||||||
rails_stdout_logging (0.0.3)
|
rails_stdout_logging (0.0.3)
|
||||||
railties (4.2.1)
|
railties (4.2.5)
|
||||||
actionpack (= 4.2.1)
|
actionpack (= 4.2.5)
|
||||||
activesupport (= 4.2.1)
|
activesupport (= 4.2.5)
|
||||||
rake (>= 0.8.7)
|
rake (>= 0.8.7)
|
||||||
thor (>= 0.18.1, < 2.0)
|
thor (>= 0.18.1, < 2.0)
|
||||||
raindrops (0.13.0)
|
raindrops (0.13.0)
|
||||||
@ -281,9 +331,13 @@ GEM
|
|||||||
redis (3.2.1)
|
redis (3.2.1)
|
||||||
redis-namespace (1.5.2)
|
redis-namespace (1.5.2)
|
||||||
redis (~> 3.0, >= 3.0.4)
|
redis (~> 3.0, >= 3.0.4)
|
||||||
ref (1.0.5)
|
ref (2.0.0)
|
||||||
responders (2.1.0)
|
responders (2.1.0)
|
||||||
railties (>= 4.2.0, < 5)
|
railties (>= 4.2.0, < 5)
|
||||||
|
rest-client (1.8.0)
|
||||||
|
http-cookie (>= 1.0.2, < 2.0)
|
||||||
|
mime-types (>= 1.16, < 3.0)
|
||||||
|
netrc (~> 0.7)
|
||||||
rolify (4.0.0)
|
rolify (4.0.0)
|
||||||
rspec (3.2.0)
|
rspec (3.2.0)
|
||||||
rspec-core (~> 3.2.0)
|
rspec-core (~> 3.2.0)
|
||||||
@ -324,7 +378,7 @@ GEM
|
|||||||
activerecord (~> 4)
|
activerecord (~> 4)
|
||||||
activesupport (~> 4)
|
activesupport (~> 4)
|
||||||
shellany (0.0.1)
|
shellany (0.0.1)
|
||||||
sidekiq (3.3.3)
|
sidekiq (3.3.4)
|
||||||
celluloid (>= 0.16.0)
|
celluloid (>= 0.16.0)
|
||||||
connection_pool (>= 2.1.1)
|
connection_pool (>= 2.1.1)
|
||||||
json
|
json
|
||||||
@ -340,19 +394,22 @@ GEM
|
|||||||
rack-protection (~> 1.4)
|
rack-protection (~> 1.4)
|
||||||
tilt (>= 1.3, < 3)
|
tilt (>= 1.3, < 3)
|
||||||
slop (3.6.0)
|
slop (3.6.0)
|
||||||
spring (1.3.3)
|
spring (1.3.5)
|
||||||
spring-commands-rspec (1.0.4)
|
spring-commands-rspec (1.0.4)
|
||||||
spring (>= 0.9.1)
|
spring (>= 0.9.1)
|
||||||
sprockets (2.12.3)
|
sprockets (2.12.4)
|
||||||
hike (~> 1.2)
|
hike (~> 1.2)
|
||||||
multi_json (~> 1.0)
|
multi_json (~> 1.0)
|
||||||
rack (~> 1.0)
|
rack (~> 1.0)
|
||||||
tilt (~> 1.1, != 1.3.0)
|
tilt (~> 1.1, != 1.3.0)
|
||||||
sprockets-rails (2.2.4)
|
sprockets-rails (2.3.3)
|
||||||
actionpack (>= 3.0)
|
actionpack (>= 3.0)
|
||||||
activesupport (>= 3.0)
|
activesupport (>= 3.0)
|
||||||
sprockets (>= 2.8, < 4.0)
|
sprockets (>= 2.8, < 4.0)
|
||||||
therubyracer (0.12.1)
|
stripe (1.30.2)
|
||||||
|
json (~> 1.8.1)
|
||||||
|
rest-client (~> 1.4)
|
||||||
|
therubyracer (0.12.0)
|
||||||
libv8 (~> 3.16.14.0)
|
libv8 (~> 3.16.14.0)
|
||||||
ref
|
ref
|
||||||
thor (0.19.1)
|
thor (0.19.1)
|
||||||
@ -360,6 +417,7 @@ GEM
|
|||||||
tilt (1.4.1)
|
tilt (1.4.1)
|
||||||
timers (4.0.1)
|
timers (4.0.1)
|
||||||
hitimes
|
hitimes
|
||||||
|
ttfunk (1.4.0)
|
||||||
twitter (5.14.0)
|
twitter (5.14.0)
|
||||||
addressable (~> 2.3)
|
addressable (~> 2.3)
|
||||||
buftok (~> 0.2.0)
|
buftok (~> 0.2.0)
|
||||||
@ -373,6 +431,11 @@ GEM
|
|||||||
simple_oauth (~> 0.3.0)
|
simple_oauth (~> 0.3.0)
|
||||||
twitter-text (1.11.0)
|
twitter-text (1.11.0)
|
||||||
unf (~> 0.1.0)
|
unf (~> 0.1.0)
|
||||||
|
twitter_cldr (3.2.1)
|
||||||
|
camertron-eprun
|
||||||
|
cldr-plurals-runtime-rb (~> 1.0.0)
|
||||||
|
json
|
||||||
|
tzinfo
|
||||||
tzinfo (1.2.2)
|
tzinfo (1.2.2)
|
||||||
thread_safe (~> 0.1)
|
thread_safe (~> 0.1)
|
||||||
uglifier (2.7.1)
|
uglifier (2.7.1)
|
||||||
@ -385,6 +448,11 @@ GEM
|
|||||||
kgio (~> 2.6)
|
kgio (~> 2.6)
|
||||||
rack
|
rack
|
||||||
raindrops (~> 0.7)
|
raindrops (~> 0.7)
|
||||||
|
virtus (1.0.5)
|
||||||
|
axiom-types (~> 0.1)
|
||||||
|
coercible (~> 1.0)
|
||||||
|
descendants_tracker (~> 0.0, >= 0.0.3)
|
||||||
|
equalizer (~> 0.0, >= 0.0.9)
|
||||||
warden (1.2.3)
|
warden (1.2.3)
|
||||||
rack (>= 1.0)
|
rack (>= 1.0)
|
||||||
web-console (2.1.2)
|
web-console (2.1.2)
|
||||||
@ -398,19 +466,23 @@ PLATFORMS
|
|||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
aasm
|
aasm
|
||||||
angularjs-rails
|
actionpack-page_caching
|
||||||
|
active_record_query_trace
|
||||||
awesome_print
|
awesome_print
|
||||||
bootstrap-sass
|
bootstrap-sass
|
||||||
byebug
|
|
||||||
capistrano
|
capistrano
|
||||||
|
capistrano-maintenance (= 0.0.5)
|
||||||
capistrano-sidekiq
|
capistrano-sidekiq
|
||||||
carrierwave
|
carrierwave
|
||||||
|
chroma
|
||||||
coffee-rails (~> 4.1.0)
|
coffee-rails (~> 4.1.0)
|
||||||
compass-rails (= 2.0.4)
|
compass-rails (= 2.0.4)
|
||||||
database_cleaner
|
database_cleaner
|
||||||
devise
|
devise
|
||||||
devise-async
|
devise-async
|
||||||
disqus_api
|
elasticsearch-model
|
||||||
|
elasticsearch-persistence
|
||||||
|
elasticsearch-rails
|
||||||
factory_girl_rails
|
factory_girl_rails
|
||||||
faker
|
faker
|
||||||
figaro
|
figaro
|
||||||
@ -423,13 +495,22 @@ DEPENDENCIES
|
|||||||
jquery-rails
|
jquery-rails
|
||||||
kaminari
|
kaminari
|
||||||
letter_opener
|
letter_opener
|
||||||
mandrill_dm!
|
message_format
|
||||||
mini_magick
|
mini_magick
|
||||||
|
newrelic_rpm
|
||||||
notify_with
|
notify_with
|
||||||
|
oj
|
||||||
|
omniauth
|
||||||
|
omniauth-oauth2
|
||||||
pg
|
pg
|
||||||
|
prawn
|
||||||
|
prawn-table
|
||||||
|
protected_attributes
|
||||||
puma
|
puma
|
||||||
pundit
|
pundit
|
||||||
rails (= 4.2.1)
|
railroady
|
||||||
|
rails (= 4.2.5)
|
||||||
|
rails-observers
|
||||||
rails_12factor
|
rails_12factor
|
||||||
recurrence
|
recurrence
|
||||||
responders (~> 2.0)
|
responders (~> 2.0)
|
||||||
@ -444,9 +525,13 @@ DEPENDENCIES
|
|||||||
sinatra
|
sinatra
|
||||||
spring
|
spring
|
||||||
spring-commands-rspec
|
spring-commands-rspec
|
||||||
therubyracer
|
stripe (= 1.30.2)
|
||||||
|
therubyracer (= 0.12.0)
|
||||||
twitter
|
twitter
|
||||||
twitter-text
|
twitter-text
|
||||||
uglifier (>= 1.3.0)
|
uglifier (>= 1.3.0)
|
||||||
unicorn
|
unicorn
|
||||||
web-console (~> 2.0)
|
web-console (~> 2.0)
|
||||||
|
|
||||||
|
BUNDLED WITH
|
||||||
|
1.10.6
|
||||||
|
@ -1,3 +1,53 @@
|
|||||||
|
Copyright (C) 2015 La Casemate
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
FabManager uses some external components, which are licenced under the
|
||||||
|
terms of [Apache 2 license](http://www.apache.org/licenses/LICENSE-2.0):
|
||||||
|
|
||||||
|
- [jasny-bootstrap](https://github.com/jasny/bootstrap/)
|
||||||
|
- [elasticsearch](https://github.com/elasticsearch/bower-elasticsearch-js)
|
||||||
|
- [nvd3](https://github.com/novus/nvd3)
|
||||||
|
- [angular-bootstrap-switch](https://github.com/frapontillo/angular-bootstrap-switch)
|
||||||
|
- [elasticsearch-rails](https://github.com/elastic/elasticsearch-rails)
|
||||||
|
- [elasticsearch-model](https://github.com/elastic/elasticsearch-rails/tree/master/elasticsearch-model)
|
||||||
|
- [elasticsearch-persistence](https://github.com/elastic/elasticsearch-rails/tree/master/elasticsearch-persistence)
|
||||||
|
- font [Open Sans](http://www.fontsquirrel.com/fonts/open-sans)
|
||||||
|
|
||||||
|
|
||||||
|
Some other used libraries/components are licenced under the terms of the
|
||||||
|
[General Public License version 2](http://www.gnu.org/licenses/old-licenses/gpl-2.0-faq.en.html):
|
||||||
|
|
||||||
|
- [ruby](https://www.ruby-lang.org)
|
||||||
|
- [railroady](https://github.com/preston/railroady)
|
||||||
|
- [unicorn](https://github.com/defunkt/unicorn)
|
||||||
|
- [prawn](https://github.com/prawnpdf/prawn)
|
||||||
|
- [prawn-table](https://github.com/prawnpdf/prawn-table)
|
||||||
|
|
||||||
|
|
||||||
|
Errors and omissions excepted, the other external libraries used in this
|
||||||
|
project are licenced under the terms of the [MIT Licence](https://opensource.org/licenses/MIT).
|
||||||
|
Please refer to the libraries documentation for more informations about
|
||||||
|
their licences.
|
||||||
|
|
||||||
|
Complete lists of used libraries are available in `bower.json` for the
|
||||||
|
EcmaScript libraries and in `Gemfile` for Ruby libraries.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
Version 3, 19 November 2007
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
@ -616,47 +666,4 @@ an absolute waiver of all civil liability in connection with the
|
|||||||
Program, unless a warranty or assumption of liability accompanies a
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
copy of the Program in return for a fee.
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
How to Apply These Terms to Your New Programs
|
|
||||||
|
|
||||||
If you develop a new program, and you want it to be of the greatest
|
|
||||||
possible use to the public, the best way to achieve this is to make it
|
|
||||||
free software which everyone can redistribute and change under these terms.
|
|
||||||
|
|
||||||
To do so, attach the following notices to the program. It is safest
|
|
||||||
to attach them to the start of each source file to most effectively
|
|
||||||
state the exclusion of warranty; and each file should have at least
|
|
||||||
the "copyright" line and a pointer to where the full notice is found.
|
|
||||||
|
|
||||||
<one line to give the program's name and a brief idea of what it does.>
|
|
||||||
Copyright (C) <year> <name of author>
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as published
|
|
||||||
by the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
|
||||||
|
|
||||||
If your software can interact with users remotely through a computer
|
|
||||||
network, you should also make sure that it provides a way for users to
|
|
||||||
get its source. For example, if your program is a web application, its
|
|
||||||
interface could display a "Source" link that leads users to an archive
|
|
||||||
of the code. There are many ways you could offer source, and different
|
|
||||||
solutions will be better for different programs; see section 13 for the
|
|
||||||
specific requirements.
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
|
||||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
|
||||||
<http://www.gnu.org/licenses/>.
|
|
||||||
|
|
2
Procfile
@ -1,2 +1,2 @@
|
|||||||
web: bundle exec rails server puma -p $PORT
|
web: bundle exec rails server puma -p $PORT -b0.0.0.0
|
||||||
worker: bundle exec sidekiq -C ./config/sidekiq.yml
|
worker: bundle exec sidekiq -C ./config/sidekiq.yml
|
||||||
|
583
README.md
@ -1,148 +1,533 @@
|
|||||||
# README
|
# FabManager
|
||||||
|
|
||||||
This project is the FabLab Manager web application.
|
FabManager is the FabLab management solution. It is web-based, open-source and totally free.
|
||||||
|
|
||||||
The purpose of this web application is to allow users to document their FabLab projects. The FabLab also have the ability
|
|
||||||
to plan some events (workshops or courses) and to expose them to its users.
|
|
||||||
|
|
||||||
This product can be extended to be used as a complete internal management system for a FabLab.
|
##### Table of Contents
|
||||||
|
1. [Software stack](#software-stack)
|
||||||
The underlying technologies are:
|
2. [Contributing](#contributing)
|
||||||
- `Ruby on Rails` for the backend application (server RESTful API)
|
3. [Setup a development environment](#setup-a-development-environment)
|
||||||
- `AngularJS` for the frontend application (web-based graphical user interface)
|
3.1 [General Guidelines](#general-guidelines)
|
||||||
|
3.2 [Environment Configuration](#environment-configuration)
|
||||||
|
4. [PostgreSQL](#postgresql)
|
||||||
|
4.1 [Install PostgreSQL 9.4 on Ubuntu/Debian](#postgresql-on-debian)
|
||||||
|
4.2 [Install and launch PostgreSQL on MacOS X](#postgresql-on-macosx)
|
||||||
|
4.3 [Setup the FabManager database in PostgreSQL](#setup-fabmanager-in-postgresql)
|
||||||
|
5. [ElasticSearch](#elasticsearch)
|
||||||
|
5.1 [Install ElasticSearch on Ubuntu/Debian](#elasticsearch-on-debian)
|
||||||
|
5.2 [Install ElasticSearch on MacOS X](#elasticsearch-on-macosx)
|
||||||
|
5.3 [Setup ElasticSearch for the FabManager](#setup-fabmanager-in-elasticsearch)
|
||||||
|
6. [Internationalization (i18n)](#i18n)
|
||||||
|
6.1 [Translation](#i18n-translation)
|
||||||
|
6.1.1 [Front-end translations](#i18n-translation-front)
|
||||||
|
6.1.2 [Back-end translations](#i18n-translation-back)
|
||||||
|
6.2 [Configuration](#i18n-configuration)
|
||||||
|
6.2.1 [Settings](#i18n-settings)
|
||||||
|
6.2.2 [Applying changes](#i18n-apply)
|
||||||
|
7. [Known issues](#known-issues)
|
||||||
|
8. [Related Documentation](#related-documentation)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 1. Configuration
|
<a name="software-stack"></a>
|
||||||
|
## Software stack
|
||||||
|
|
||||||
The following files must be filled with the correct configuration to allow FabManager to run correctly:
|
FabManager is a Ruby on Rails / AngularJS web application that runs on the following software:
|
||||||
|
|
||||||
- config/environments/production.rb
|
- Ubuntu/Debian
|
||||||
- `mandrill` -> change this if you're using a different mailing system
|
- Ruby 2.2.3
|
||||||
|
- Git 1.9.1+
|
||||||
- config/environments/staging.rb
|
- Redis 2.8.4+
|
||||||
- `config.action_mailer.default_url_options` -> change the URL according to the staging deployment url
|
- Sidekiq 3.3.4+
|
||||||
- `mandrill` -> change this if you're using a different mailing system
|
- Elasticsearch 1.7
|
||||||
|
- PostgreSQL 9.4
|
||||||
|
|
||||||
- config/application.yml
|
<a name="contributing"></a>
|
||||||
- `DEVISE_KEY` -> generate any secret phrase to secure the Devise authentication. You can use the `$ rake secret` command for this purpose.
|
## Contributing
|
||||||
- `SECRET_KEY_BASE` -> generate any secret phrase here to prevent XSS attacks. You can use the `$ rake secret` command for this purpose.
|
|
||||||
- `DEFAULT_MAIL_FROM` -> default e-mail address from which the emails are sent
|
|
||||||
- `MANDRILL_USERNAME` -> if you plan to use mandrill
|
|
||||||
- `MANDRILL_APIKEY` -> if you plan to use mandrill
|
|
||||||
- `TWITTER_NAME` -> twitter api configuration
|
|
||||||
- `TWITTER_CONSUMER_KEY` -> twitter api configuration
|
|
||||||
- `TWITTER_CONSUMER_SECRET` -> twitter api configuration
|
|
||||||
- `TWITTER_ACCESS_TOKEN` -> twitter api configuration
|
|
||||||
- `TWITTER_ACCESS_TOKEN_SECRET` -> twitter api configuration
|
|
||||||
- `GOOGLE_ANALYTICS_ACCOUNT` -> Google analytics account identifier (if you want to use GA)
|
|
||||||
- `APPLICATION_ROOT_URL` -> The public URL where you application is deployed in production (eg. fablab.lacasemate.com)
|
|
||||||
|
|
||||||
- config/mandrill.rb
|
Contributions are welcome. Please read [the contribution guidelines](CONTRIBUTING.md) for more information about the contribution process.
|
||||||
You may change this if you don't want to use mandrill as your production mailing system
|
|
||||||
|
|
||||||
- config/database.yml.default
|
**IMPORTANT**: **do not** update Arshaw/fullCalendar.js as it contains a hack for the remove-event cross.
|
||||||
Copy/Paste this file to `config/database.yml` and modify the configuration according to your postgreSQL configuration
|
|
||||||
|
|
||||||
- config/disqus_api.yml
|
<a name="setup-a-development-environment"></a>
|
||||||
Insert here your identifiers for the Disqus API
|
## Setup a development environment
|
||||||
|
|
||||||
|
<a name="general-guidelines"></a>
|
||||||
|
### General Guidelines
|
||||||
|
|
||||||
|
1. Install RVM with the ruby version specified in the [.ruby-version file](.ruby-version).
|
||||||
## 2. Setup a development environment
|
For more details about the process, Please read the [official RVM documentation](http://rvm.io/rvm/install).
|
||||||
|
|
||||||
1. Install RVM with latest ruby version
|
|
||||||
See http://rvm.io/rvm/install
|
|
||||||
|
|
||||||
2. Retrieve the project from Git
|
2. Retrieve the project from Git
|
||||||
`$ git clone git@github.com:LaCasemate/fab-manager.git`
|
|
||||||
|
|
||||||
3. Install the dependencies
|
```bash
|
||||||
- Ubuntu: `$ sudo apt-get install libpq-dev postgresql redis-server imagemagick`
|
git clone https://github.com/LaCasemate/fab-manager.git
|
||||||
- MacOS: `$ brew install postgresql redis imagemagick`
|
```
|
||||||
|
|
||||||
|
3. Install the software dependencies.
|
||||||
|
- For Ubuntu/Debian:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt-get install libpq-dev postgresql-9.4 redis-server imagemagick
|
||||||
|
```
|
||||||
|
- For MacOS X:
|
||||||
|
|
||||||
4. Init the RVM instance and check it was correctly configured
|
```bash
|
||||||
```
|
brew install postgresql redis imagemagick
|
||||||
$ cd fab-manager
|
```
|
||||||
$ rvm current
|
|
||||||
```
|
4. Init the RVM instance and check it was correctly configured
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd fab-manager
|
||||||
|
rvm current
|
||||||
|
# Must print ruby-X.Y.Z@fablab (where X.Y.Z match the version in .ruby-version)
|
||||||
|
```
|
||||||
|
|
||||||
5. Setup the project requirements
|
5. Install bundler in the current RVM gemset
|
||||||
`$ bundle install`
|
|
||||||
|
```bash
|
||||||
|
gem install bundler
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Install the required ruby gems
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bundle install
|
||||||
|
```
|
||||||
|
|
||||||
6. Build the database. You may have to configure your postgreSQL instance before, as described in chapter `3.2 Setup the FabManager database in PostgreSQL`
|
7. Build the database. You may have to follow the steps described in [the PostgreSQL installation chapter](#postgresql) before, if you don't already have a working installation of PostgreSQL.
|
||||||
`$ rake db:setup`
|
|
||||||
|
```bash
|
||||||
|
rake db:setup
|
||||||
|
```
|
||||||
|
|
||||||
7. Create the pids folder used by sidekiq. If you want to use a different location, you can configure it in `config/sidekiq.yml`
|
8. Create the pids folder used by Sidekiq. If you want to use a different location, you can configure it in `config/sidekiq.yml`
|
||||||
`$ mkdir -p tmp/pids`
|
|
||||||
|
```bash
|
||||||
|
mkdir -p tmp/pids
|
||||||
|
```
|
||||||
|
|
||||||
|
9. Create the default configuration file **and configure it !** (see the [Environment Configuration](#environment-configuration) section)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp config/application.yml.default config/application.yml
|
||||||
|
vi config/application.yml
|
||||||
|
# or use your favorite editor instead of vi (nano, ne...)
|
||||||
|
```
|
||||||
|
|
||||||
8. Configure the application environment variables, as explained in chapter `1. Configuration`
|
10. Start the development web server
|
||||||
|
|
||||||
9. Start the development web server
|
```bash
|
||||||
`$ foreman s -p 3000`
|
foreman s -p 3000
|
||||||
|
```
|
||||||
|
|
||||||
|
11. You should now be able to access your local development FabManager instance by accessing `http://localhost:3000` in your web browser.
|
||||||
|
|
||||||
|
12. You can login as the default administrator using the following credentials:
|
||||||
|
- user: admin@fab-manager.com
|
||||||
|
- password: adminadmin
|
||||||
|
|
||||||
|
<a name="environment-configuration"></a>
|
||||||
|
### Environment Configuration
|
||||||
|
|
||||||
|
The settings in `config/application.yml` configure the environment variables of the application.
|
||||||
|
If you are in a development environment, your can keep the default values, otherwise, in production, values must be configured carefully.
|
||||||
|
|
||||||
|
#### POSTGRES_HOST
|
||||||
|
|
||||||
|
DNS name or IP address of the server hosting the PostgreSQL database of the application (see [PostgreSQL](#postgresql)).
|
||||||
|
|
||||||
|
#### POSTGRES_PASSWORD
|
||||||
|
|
||||||
|
Password for the PostgreSQL user, as specified in `database.yml`.
|
||||||
|
Please see [Setup the FabManager database in PostgreSQL](#setup-fabmanager-in-postgresql) for informations on how to create a user and set his password.
|
||||||
|
|
||||||
|
#### REDIS_HOST
|
||||||
|
|
||||||
|
DNS name or IP address of the server hosting the redis database.
|
||||||
|
|
||||||
|
#### ELASTICSEARCH_HOST
|
||||||
|
|
||||||
|
DNS name or IP address of the server hosting the elasticSearch database.
|
||||||
|
|
||||||
|
#### SECRET_KEY_BASE
|
||||||
|
|
||||||
|
Used by the authentication system to generate random tokens, eg. for resetting passwords.
|
||||||
|
Used by Rails to generate the integrity of signed cookies.
|
||||||
|
You can generate such a random key by running `rake secret`.
|
||||||
|
|
||||||
|
#### STRIPE_API_KEY & STRIPE_PUBLISHABLE_KEY
|
||||||
|
|
||||||
|
Key an secret used to identify you Stripe account through the API.
|
||||||
|
Retrieve them from https://dashboard.stripe.com/account/apikeys.
|
||||||
|
|
||||||
|
#### STRIPE_CURRENCY
|
||||||
|
|
||||||
|
Currency used by stripe to charge the final customer.
|
||||||
|
See https://support.stripe.com/questions/which-currencies-does-stripe-support for a list of available 3-letters ISO code.
|
||||||
|
|
||||||
|
#### INVOICE_PREFIX
|
||||||
|
|
||||||
|
When payments are done on the platform, an invoice will be generate as a PDF file.
|
||||||
|
This value configure the prefix of the PDF file name.
|
||||||
|
|
||||||
|
#### FABLAB_WITHOUT_PLANS
|
||||||
|
|
||||||
|
If set to 'true', the subscription plans will be fully disabled and invisible in the application.
|
||||||
|
|
||||||
|
#### DEFAULT_MAIL_FROM
|
||||||
|
|
||||||
|
When sending notification mails, the platform will use this address to identify the sender.
|
||||||
|
|
||||||
|
#### DELIVERY_METHOD
|
||||||
|
|
||||||
|
Configure the Rails' Action Mailer delivery method.
|
||||||
|
See http://guides.rubyonrails.org/action_mailer_basics.html#action-mailer-configuration for more details.
|
||||||
|
|
||||||
|
#### DEFAULT_HOST, DEFAULT_PROTOCOL, SMTP_ADDRESS, SMTP_PORT, SMTP_USER_NAME & SMTP_PASSWORD
|
||||||
|
|
||||||
|
When DELIVERY_METHOD is set to **smtp**, configure the SMTP server parameters.
|
||||||
|
See http://guides.rubyonrails.org/action_mailer_basics.html#action-mailer-configuration for more details.
|
||||||
|
DEFAULT_HOST is also used to configure Google Analytics.
|
||||||
|
|
||||||
|
#### GA_ID
|
||||||
|
|
||||||
|
Identifier of your Google Analytics account.
|
||||||
|
|
||||||
|
#### DISQUS_SHORTNAME
|
||||||
|
|
||||||
|
Unique identifier of your [Disqus](http://www.disqus.com) forum.
|
||||||
|
Disquq forums are used to allow visitors to comment on projects.
|
||||||
|
See https://help.disqus.com/customer/portal/articles/466208-what-s-a-shortname- for more informations.
|
||||||
|
|
||||||
|
#### TWITTER_NAME
|
||||||
|
|
||||||
|
Identifier of the Twitter account, for witch the last tweet will be displayed on the home page.
|
||||||
|
|
||||||
|
#### TWITTER_CONSUMER_KEY, TWITTER_CONSUMER_SECRET, TWITTER_ACCESS_TOKEN & TWITTER_ACCESS_TOKEN_SECRET
|
||||||
|
|
||||||
|
Keys and secrets to access the twitter API.
|
||||||
|
|
||||||
|
#### Settings related to i18n
|
||||||
|
See the [Settings](#i18n-settings) section of the [Internationalization (i18n)](#i18n) paragraph for a detailed description of these parameters.
|
||||||
|
|
||||||
|
|
||||||
|
<a name="postgresql"></a>
|
||||||
|
## PostgreSQL
|
||||||
|
|
||||||
## 3. PostgreSQL
|
<a name="postgresql-on-debian"></a>
|
||||||
|
### Install PostgreSQL 9.4 on Ubuntu/Debian
|
||||||
|
|
||||||
### 3.1 Launch PostgreSQL on MacOS
|
1. Create the file `/etc/apt/sources.list.d/pgdg.list`, and append it one the following lines:
|
||||||
|
- `deb http://apt.postgresql.org/pub/repos/apt/ trusty-pgdg main` (Ubuntu 14.04 Trusty)
|
||||||
$ ln -sfv /usr/local/opt/postgresql/*.plist ~/Library/LaunchAgents
|
- `deb http://apt.postgresql.org/pub/repos/apt/ jessie-pgdg main` (Debian 8 Jessie)
|
||||||
$ launchctl load ~/Library/LaunchAgents/homebrew.mxcl.postgresql.plist
|
|
||||||
|
|
||||||
The first command will start postgresql at login with launchd. The second will load postgresql now.
|
|
||||||
|
|
||||||
### 3.2 Setup the FabManager database in PostgreSQL
|
2. Import the repository signing key, and update the package lists
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
|
||||||
|
sudo apt-get update
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Install PostgreSQL 9.4
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt-get install postgresql-9.4
|
||||||
|
```
|
||||||
|
|
||||||
|
<a name="postgresql-on-macosx"></a>
|
||||||
|
### Install and launch PostgreSQL on MacOS X
|
||||||
|
|
||||||
|
This assumes you have [Homebrew](http://brew.sh/) installed on your system.
|
||||||
|
Otherwise, please follow the official instructions on the project's website.
|
||||||
|
|
||||||
|
|
||||||
|
1. Update brew and install PostgreSQL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
brew update
|
||||||
|
brew install postgres
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Launch PostgreSQL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start postgresql at login with launchd
|
||||||
|
ln -sfv /usr/local/opt/postgresql/*.plist ~/Library/LaunchAgents
|
||||||
|
# Load PostgreSQL now
|
||||||
|
launchctl load ~/Library/LaunchAgents/homebrew.mxcl.postgresql.plist
|
||||||
|
```
|
||||||
|
|
||||||
|
<a name="setup-fabmanager-in-postgresql"></a>
|
||||||
|
### Setup the FabManager database in PostgreSQL
|
||||||
|
|
||||||
|
Before running `rake db:setup`, you have to make sure that the user configured in [config/database.yml](config/database.yml) for the `development` environment exists.
|
||||||
|
To create it, please follow these instructions:
|
||||||
|
|
||||||
1. Login as the postgres user
|
1. Login as the postgres user
|
||||||
`$ sudo -i -u postgres`
|
|
||||||
|
|
||||||
2. Run the postgreSQL administration command line interface
|
```bash
|
||||||
`$ psql`
|
sudo -i -u postgres
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Run the PostgreSQL administration command line interface
|
||||||
|
|
||||||
|
```bash
|
||||||
|
psql
|
||||||
|
```
|
||||||
|
|
||||||
3. Create a new user in postgres (in this example, the user will be named "sleede")
|
3. Create a new user in PostgreSQL (in this example, the user will be named `sleede`)
|
||||||
`# CREATE USER sleede;`
|
|
||||||
|
```sql
|
||||||
|
CREATE USER sleede;
|
||||||
|
```
|
||||||
|
|
||||||
4. Grant him the right to create databases
|
4. Grant him the right to create databases
|
||||||
`# ALTER ROLE sleede WITH CREATEDB;`
|
|
||||||
|
```sql
|
||||||
|
ALTER ROLE sleede WITH CREATEDB;
|
||||||
|
```
|
||||||
|
|
||||||
5. Then create the fablab database
|
5. Then, create the fablab_development and fablab_test databases
|
||||||
`# CREATE DATABASE fabmanager_development OWNER sleede;`
|
|
||||||
|
```sql
|
||||||
|
CREATE DATABASE fablab_development OWNER sleede;
|
||||||
|
CREATE DATABASE fablab_test OWNER sleede;
|
||||||
|
```
|
||||||
|
|
||||||
6. To finish, attribute a password to this user
|
6. To finish, attribute a password to this user
|
||||||
`# ALTER USER sleede WITH ENCRYPTED PASSWORD 'sleede';`
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 4. Known issue
|
```sql
|
||||||
|
ALTER USER sleede WITH ENCRYPTED PASSWORD 'sleede';
|
||||||
|
```
|
||||||
|
|
||||||
You may encounter the following error message when running the application for the first time:
|
<a name="elasticsearch"></a>
|
||||||
|
## ElasticSearch
|
||||||
|
|
||||||
```bash
|
ElasticSearch is a powerful search engine based on Apache Lucene combined with a NoSQL database used as a cache to index data and quickly process complex requests on it.
|
||||||
Uncaught exception: FATAL: authentification peer échouée pour l'utilisateur « USERNAME »
|
|
||||||
Exiting
|
In FabManager, it is used for the admin's statistics module and to perform searches in projects.
|
||||||
.rvm/gems/ruby-2.2.1@fabmanager/gems/activerecord-4.2.1/lib/active_record/connection_adapters/postgresql_adapter.rb:651:in `initialize'
|
|
||||||
...
|
<a name="elasticsearch-on-debian"></a>
|
||||||
```
|
### Install ElasticSearch on Ubuntu/Debian
|
||||||
|
|
||||||
To solve this issue, edit your `/etc/postgresql/9.4/main/pg_hba.conf` as root and replace the following:
|
For a more detailed guide concerning the ElasticSearch installation, please check the [official documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/setup.html)
|
||||||
|
|
||||||
|
1. Install the OpenJDK's Java Runtime Environment (JRE). ElasticSearch recommends that you install Java 8 update 20 or later.
|
||||||
|
Please check that your distribution's version meet this requirement.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# comment over or replace...
|
sudo apt-get install openjdk-8-jre
|
||||||
local all all peer
|
|
||||||
# ...by the following:
|
|
||||||
local all all trust
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Then, restart postgreSQL to validate the modification (`sudo service postgresql restart`).
|
1. Create the file `/etc/apt/sources.list.d/elasticsearch-1.x.list`, and append it the following line:
|
||||||
|
`deb http://packages.elastic.co/elasticsearch/1.x/debian stable main`
|
||||||
|
|
||||||
|
2. Import the repository signing key, and update the package lists
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wget -qO - https://packages.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add -
|
||||||
|
sudo apt-get update
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Install ElasticSearch 1.7
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt-get install elasticsearch
|
||||||
|
```
|
||||||
|
|
||||||
|
4. To automatically start ElasticSearch during bootup, then, depending if your system is compatible with SysV (eg. Ubuntu 14.04) or uses systemd (eg. Debian 8), you will need to run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# System V
|
||||||
|
sudo update-rc.d elasticsearch defaults 95 10
|
||||||
|
# *** OR *** (systemd)
|
||||||
|
sudo /bin/systemctl daemon-reload
|
||||||
|
sudo /bin/systemctl enable elasticsearch.service
|
||||||
|
```
|
||||||
|
|
||||||
|
<a name="elasticsearch-on-macosx"></a>
|
||||||
|
### Install ElasticSearch on MacOS X
|
||||||
|
|
||||||
|
This assumes you have [Homebrew](http://brew.sh/) installed on your system.
|
||||||
|
Otherwise, please follow the official instructions on the project's website.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
brew update
|
||||||
|
brew install homebrew/versions/elasticsearch17
|
||||||
|
```
|
||||||
|
|
||||||
|
<a name="setup-fabmanager-in-elasticsearch"></a>
|
||||||
|
### Setup ElasticSearch for the FabManager
|
||||||
|
|
||||||
|
1. Launch the associated rake tasks in the project folder.
|
||||||
|
This will create the fields mappings in ElasticSearch DB
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rake fablab:es_build_stats
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Every nights, the statistics for the day that just ended are built automatically at 01:00 (AM).
|
||||||
|
See [schedule.yml](config/schedule.yml) to modify this behavior.
|
||||||
|
If the scheduled task wasn't executed for any reason (eg. you are in a dev environment and your computer was turned off at 1 AM), you can force the statistics data generation in ElasticSearch, running the following commands in a rails console.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rails c
|
||||||
|
```
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# Here for the 200 last days
|
||||||
|
200.times.each do |i|
|
||||||
|
StatisticService.new.generate_statistic({start_date: i.day.ago.beginning_of_day,end_date: i.day.ago.end_of_day})
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
<a name="i18n"></a>
|
||||||
|
## Internationalization (i18n)
|
||||||
|
|
||||||
|
The FabManager application can only run in a single language but this language can easily be changed.
|
||||||
|
|
||||||
|
<a name="i18n-translation"></a>
|
||||||
|
### Translation
|
||||||
|
|
||||||
|
Check the files located in `config/locales`:
|
||||||
|
|
||||||
|
- Front app translations (angular.js) are located in `config/locales/app.scope.XX.yml`.
|
||||||
|
Where scope has one the following meaning :
|
||||||
|
- admin: translations of the administrator views (manage and configure the FabLab).
|
||||||
|
- logged: translations of the end-user's views accessible only to connected users.
|
||||||
|
- public: translation of end-user's views publicly accessible to anyone.
|
||||||
|
- shared: translations shared by many views (like forms or buttons).
|
||||||
|
- Back app translations (Ruby on Rails) are located in `config/locales/XX.yml`.
|
||||||
|
- Emails translations are located in `config/locales/mails.XX.yml`.
|
||||||
|
- Messages related to the authentication system are located in `config/locales/devise.XX.yml`.
|
||||||
|
|
||||||
|
If you plan to translate the application to a new locale, please consider that the reference translation is French.
|
||||||
|
Indeed, in some cases, the English texts/sentences can seems confuse or lack of context as they were originally translated from French.
|
||||||
|
|
||||||
|
To prevent syntax mistakes while translating locale files, we **STRONGLY advise** you to use a text editor witch support syntax coloration for YML and Ruby.
|
||||||
|
|
||||||
|
<a name="i18n-translation-front"></a>
|
||||||
|
#### Front-end translations
|
||||||
|
|
||||||
|
Front-end translations uses [angular-translate](http://angular-translate.github.io) with some interpolations interpreted by angular.js and other interpreted by [MessageFormat](https://github.com/SlexAxton/messageformat.js/).
|
||||||
|
**These two kinds of interpolation use a near but different syntax witch SHOULD NOT be confused.**
|
||||||
|
Please refer to the official [angular-translate documentation](http://angular-translate.github.io/docs/#/guide/14_pluralization) before translating.
|
||||||
|
|
||||||
|
<a name="i18n-translation-back"></a>
|
||||||
|
#### Back-end translations
|
||||||
|
|
||||||
|
Back-end translations uses the [Ruby on Rails syntax](http://guides.rubyonrails.org/i18n.html) but some complex interpolations are interpreted by [MessageFormat](https://github.com/format-message/message-format-rb) and are marked as it in comments.
|
||||||
|
**DO NOT confuse the syntaxes.**
|
||||||
|
|
||||||
|
In each cases, some inline comments are included in the localisation files.
|
||||||
|
They can be recognized as they start with the sharp character (#).
|
||||||
|
These comments are not required to be translated, they are intended to help the translator to have some context informations about the sentence to translate.
|
||||||
|
|
||||||
|
|
||||||
|
<a name="i18n-configuration"></a>
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
Locales configurations are made in `config/application.yml`.
|
||||||
|
If you are in a development environment, your can keep the default values, otherwise, in production, values must be configured carefully.
|
||||||
|
|
||||||
|
<a name="i18n-settings"></a>
|
||||||
|
#### Settings
|
||||||
|
##### RAILS_LOCALE
|
||||||
|
|
||||||
|
Be sure that `config/locales/rails.XX.yml` exists, where `XX` match your configured rails_locale.
|
||||||
|
You can find templates of these files at https://github.com/svenfuchs/rails-i18n/tree/rails-4-x/rails/locale.
|
||||||
|
|
||||||
|
Be aware that **this file MUST contain the CURRENCY symbol used to generate invoices** (among other things).
|
||||||
|
Default is **en**.
|
||||||
|
|
||||||
|
##### MOMENT_LOCALE
|
||||||
|
|
||||||
|
Configure the moment.js library for l10n.
|
||||||
|
|
||||||
|
See `vendor/assets/components/moment/locale/*.js` for a list of available locales.
|
||||||
|
Default is **en** (even if it's not listed).
|
||||||
|
|
||||||
|
##### SUMMERNOTE_LOCALE
|
||||||
|
|
||||||
|
Configure the javascript summernote editor for l10n.
|
||||||
|
|
||||||
|
See `vendor/assets/components/summernote/lang/summernote-*.js` for a list of available locales.
|
||||||
|
Default is **en-US** (even if it's not listed).
|
||||||
|
|
||||||
|
##### ANGULAR_LOCALE
|
||||||
|
|
||||||
|
Configure the locale for angular-i18n.
|
||||||
|
|
||||||
|
Please, be aware that **the configured locale will imply the CURRENCY displayed to front-end users.**
|
||||||
|
|
||||||
|
_Eg.: configuring **fr-fr** will set the currency symbol to **€** but **fr-ca** will set **$** as currency symbol, so setting the `angular_locale` to simple **fr** (without country indication) will probably not do what you expect._
|
||||||
|
|
||||||
|
See `vendor/assets/components/angular-i18n/angular-locale_*.js` for a list of available locales. Default is **en**.
|
||||||
|
|
||||||
|
##### MESSAGEFORMAT_LOCALE
|
||||||
|
|
||||||
|
Configure the messageformat.js library, used by angular-translate.
|
||||||
|
|
||||||
|
See vendor/assets/components/messageformat/locale/*.js for a list of available locales.
|
||||||
|
|
||||||
|
##### FULLCALENDAR_LOCALE
|
||||||
|
|
||||||
|
Configure the fullCalendar JS agenda library.
|
||||||
|
|
||||||
|
See `vendor/assets/components/fullcalendar/dist/lang/*.js` for a list of available locales. Default is **en** (even if it's not listed).
|
||||||
|
|
||||||
|
##### ELASTICSEARCH_LANGUAGE_ANALYZER
|
||||||
|
|
||||||
|
This configure the language analyzer for indexing and searching in projects with ElasticSearch.
|
||||||
|
See https://www.elastic.co/guide/en/elasticsearch/reference/1.7/analysis-lang-analyzer.html for a list of available analyzers (check that the doc version match your installed elasticSearch version).
|
||||||
|
|
||||||
|
##### TIME_ZONE
|
||||||
|
|
||||||
|
In Rails: set Time.zone default to the specified zone and make Active Record auto-convert to this zone. Run `rake time:zones:all` for a list of available time zone names.
|
||||||
|
Default is **UTC**.
|
||||||
|
|
||||||
|
##### WEEK_STARTING_DAY
|
||||||
|
|
||||||
|
Configure the first day of the week in your locale zone (generally monday or sunday).
|
||||||
|
|
||||||
|
##### D3_DATE_FORMAT
|
||||||
|
Date format for dates displayed in statistics charts.
|
||||||
|
See https://github.com/mbostock/d3/wiki/Time-Formatting#format for available formats.
|
||||||
|
|
||||||
|
|
||||||
## 5. Related Documentation
|
<a name="i18n-apply"></a>
|
||||||
- Angular-Bootstrap: http://angular-ui.github.io/bootstrap/
|
#### Applying changes
|
||||||
|
|
||||||
|
After modifying any values concerning the localisation, restart the application (ie. web server) to apply these changes in the i18n configuration.
|
||||||
|
|
||||||
|
|
||||||
## 6. Translations
|
<a name="known-issues"></a>
|
||||||
- French translation is available on the branches [master](../../tree/master) and [dev](../../tree/dev)
|
## Known issues
|
||||||
- English translation is available on the branch [english](../../tree/english)
|
|
||||||
|
- When browsing a machine page, you may encounter an "InterceptError" in the console and the loading bar will stop loading before reaching its ending.
|
||||||
|
This may append if the machine was created through a seed file without any image.
|
||||||
|
To solve this, simply add an image to the machine's profile and refresh the web page.
|
||||||
|
|
||||||
|
- When starting the Ruby on Rails server (eg. `foreman s`) you may receive the following error:
|
||||||
|
|
||||||
|
worker.1 | invalid url: redis::6379
|
||||||
|
web.1 | Exiting
|
||||||
|
worker.1 | ...lib/redis/client.rb...:in `_parse_options'
|
||||||
|
|
||||||
|
This may happens when the `application.yml` file is missing.
|
||||||
|
To solve this issue copy `config/application.yml.default` to `config/application.yml`.
|
||||||
|
This is required before the first start.
|
||||||
|
|
||||||
|
|
||||||
|
<a name="related-documentation"></a>
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Ruby 2.2.3](http://ruby-doc.org/core-2.2.3/)
|
||||||
|
- [Ruby on Rails](http://api.rubyonrails.org)
|
||||||
|
- [AngularJS](https://docs.angularjs.org/api)
|
||||||
|
- [Angular-Bootstrap](http://angular-ui.github.io/bootstrap/)
|
||||||
|
- [ElasticSearch 1.7](https://www.elastic.co/guide/en/elasticsearch/reference/1.7/index.html)
|
||||||
|
|
||||||
|
73
Vagrantfile
vendored
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
# -*- mode: ruby -*-
|
||||||
|
# vi: set ft=ruby :
|
||||||
|
|
||||||
|
# All Vagrant configuration is done below. The "2" in Vagrant.configure
|
||||||
|
# configures the configuration version (we support older styles for
|
||||||
|
# backwards compatibility). Please don't change it unless you know what
|
||||||
|
# you're doing.
|
||||||
|
Vagrant.configure(2) do |config|
|
||||||
|
# The most common configuration options are documented and commented below.
|
||||||
|
# For a complete reference, please see the online documentation at
|
||||||
|
# https://docs.vagrantup.com.
|
||||||
|
|
||||||
|
# Every Vagrant development environment requires a box. You can search for
|
||||||
|
# boxes at https://atlas.hashicorp.com/search.
|
||||||
|
config.vm.box = "ubuntu/trusty64"
|
||||||
|
|
||||||
|
# Disable automatic box update checking. If you disable this, then
|
||||||
|
# boxes will only be checked for updates when the user runs
|
||||||
|
# `vagrant box outdated`. This is not recommended.
|
||||||
|
# config.vm.box_check_update = false
|
||||||
|
|
||||||
|
# Create a forwarded port mapping which allows access to a specific port
|
||||||
|
# within the machine from a port on the host machine. In the example below,
|
||||||
|
# accessing "localhost:8080" will access port 80 on the guest machine.
|
||||||
|
config.vm.network "forwarded_port", guest: 3000, host: 3000 # rails/puma
|
||||||
|
config.vm.network "forwarded_port", guest: 9200, host: 9200 # elasticsearch
|
||||||
|
config.vm.network "forwarded_port", guest: 5432, host: 5432 # postgreSQL
|
||||||
|
|
||||||
|
# Create a private network, which allows host-only access to the machine
|
||||||
|
# using a specific IP.
|
||||||
|
# config.vm.network "private_network", ip: "192.168.33.10"
|
||||||
|
|
||||||
|
# Create a public network, which generally matched to bridged network.
|
||||||
|
# Bridged networks make the machine appear as another physical device on
|
||||||
|
# your network.
|
||||||
|
# config.vm.network "public_network"
|
||||||
|
|
||||||
|
# Share an additional folder to the guest VM. The first argument is
|
||||||
|
# the path on the host to the actual folder. The second argument is
|
||||||
|
# the path on the guest to mount the folder. And the optional third
|
||||||
|
# argument is a set of non-required options.
|
||||||
|
# config.vm.synced_folder "../data", "/vagrant_data"
|
||||||
|
|
||||||
|
# Provider-specific configuration so you can fine-tune various
|
||||||
|
# backing providers for Vagrant. These expose provider-specific options.
|
||||||
|
# Example for VirtualBox:
|
||||||
|
#
|
||||||
|
config.vm.provider "virtualbox" do |vb|
|
||||||
|
# Display the VirtualBox GUI when booting the machine
|
||||||
|
# vb.gui = true
|
||||||
|
|
||||||
|
# Customize the amount of memory on the VM:
|
||||||
|
vb.memory = 512
|
||||||
|
end
|
||||||
|
#
|
||||||
|
# View the documentation for the provider you are using for more
|
||||||
|
# information on available options.
|
||||||
|
|
||||||
|
# Define a Vagrant Push strategy for pushing to Atlas. Other push strategies
|
||||||
|
# such as FTP and Heroku are also available. See the documentation at
|
||||||
|
# https://docs.vagrantup.com/v2/push/atlas.html for more information.
|
||||||
|
# config.push.define "atlas" do |push|
|
||||||
|
# push.app = "YOUR_ATLAS_USERNAME/YOUR_APPLICATION_NAME"
|
||||||
|
# end
|
||||||
|
|
||||||
|
# Enable provisioning with a shell script. Additional provisioners such as
|
||||||
|
# Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the
|
||||||
|
# documentation for more information about their specific syntax and use.
|
||||||
|
# config.vm.provision "shell", inline: <<-SHELL
|
||||||
|
# sudo apt-get update
|
||||||
|
# sudo apt-get install -y apache2
|
||||||
|
# SHELL
|
||||||
|
end
|
Before Width: | Height: | Size: 20 KiB |
BIN
app/assets/images/fablab-logo.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 8.4 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.8 KiB |
BIN
app/assets/images/la_casemate-logo.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
app/assets/images/mastercard.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
BIN
app/assets/images/powered_by_stripe.png
Normal file
After Width: | Height: | Size: 5.0 KiB |
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 613 B |
Before Width: | Height: | Size: 845 B |
BIN
app/assets/images/visa.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
@ -16,48 +16,73 @@ Application.Directives = angular.module('application.directives', []);
|
|||||||
|
|
||||||
angular.module('application', ['ngCookies', 'ngResource', 'ngSanitize', 'ngAnimate', 'ngCookies', 'ui.router', 'ui.bootstrap',
|
angular.module('application', ['ngCookies', 'ngResource', 'ngSanitize', 'ngAnimate', 'ngCookies', 'ui.router', 'ui.bootstrap',
|
||||||
'ngUpload', 'duScroll', 'application.filters','application.services', 'application.directives',
|
'ngUpload', 'duScroll', 'application.filters','application.services', 'application.directives',
|
||||||
'application.constants', 'application.controllers', 'application.router', 'ui.select2', 'angularMoment',
|
'frapontillo.bootstrap-switch', 'application.constants', 'application.controllers', 'application.router',
|
||||||
'Devise', 'DeviseModal', 'angular-growl', 'xeditable', 'checklist-model', 'unsavedChanges', 'angular-loading-bar',
|
'ui.select', 'ui.calendar', 'angularMoment', 'Devise', 'DeviseModal', 'angular-growl', 'xeditable',
|
||||||
'ngTouch', 'angular-google-analytics', 'angularUtils.directives.dirDisqus', 'summernote']).
|
'checklist-model', 'unsavedChanges', 'angular-loading-bar', 'ngTouch', 'angular-google-analytics',
|
||||||
config(['$locationProvider', '$httpProvider', 'AuthProvider', "growlProvider", "unsavedWarningsConfigProvider", "AnalyticsProvider", "datepickerPopupConfig",
|
'angularUtils.directives.dirDisqus', 'summernote', 'elasticsearch', 'angular-medium-editor', 'naif.base64',
|
||||||
function($locationProvider, $httpProvider, AuthProvider, growlProvider, unsavedWarningsConfigProvider, AnalyticsProvider, datepickerPopupConfig) {
|
'minicolors', 'pascalprecht.translate']).
|
||||||
|
config(['$httpProvider', 'AuthProvider', "growlProvider", "unsavedWarningsConfigProvider", "AnalyticsProvider", "uibDatepickerPopupConfig", "$provide", "$translateProvider",
|
||||||
|
function($httpProvider, AuthProvider, growlProvider, unsavedWarningsConfigProvider, AnalyticsProvider, uibDatepickerPopupConfig, $provide, $translateProvider) {
|
||||||
|
|
||||||
|
|
||||||
<% if Rails.env.production? and ENV["GOOGLE_ANALYTICS_ACCOUNT"] != 'UA-YOUR_ID_HERE' and ENV["GOOGLE_ANALYTICS_ACCOUNT"] != nil %>
|
// Google analytics
|
||||||
AnalyticsProvider.setAccount('<%= ENV["GOOGLE_ANALYTICS_ACCOUNT"] %>');
|
<% if Rails.env.production? %>
|
||||||
|
AnalyticsProvider.setAccount(Fablab.gaId);
|
||||||
// track all routes (or not)
|
// track all routes (or not)
|
||||||
AnalyticsProvider.trackPages(true);
|
AnalyticsProvider.trackPages(true);
|
||||||
AnalyticsProvider.setDomainName('<%= ENV["APPLICATION_ROOT_URL"] %>');
|
AnalyticsProvider.setDomainName(Fablab.defaultHost);
|
||||||
AnalyticsProvider.useAnalytics(true);
|
AnalyticsProvider.useAnalytics(true);
|
||||||
AnalyticsProvider.setPageEvent('$stateChangeSuccess');
|
AnalyticsProvider.setPageEvent('$stateChangeSuccess');
|
||||||
<% else %>
|
<% else %>
|
||||||
AnalyticsProvider.setAccount('DISABLED');
|
AnalyticsProvider.setAccount('DISABLED');
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
datepickerPopupConfig.closeText = "Fermer";
|
// Custom messages for the date-picker widget
|
||||||
datepickerPopupConfig.cleartext = "Effacer";
|
uibDatepickerPopupConfig.closeText = Fablab.translations.app.shared.buttons.close;
|
||||||
datepickerPopupConfig.currentText = "Aujourd'hui";
|
uibDatepickerPopupConfig.clearText = Fablab.translations.app.shared.buttons.clear;
|
||||||
|
uibDatepickerPopupConfig.currentText = Fablab.translations.app.shared.buttons.today;
|
||||||
|
|
||||||
// custom message for angular-unsavedChanges
|
// Custom messages for angular-unsavedChanges
|
||||||
unsavedWarningsConfigProvider.navigateMessage = "Vous perdrez les modifications non enregistrées si vous quittez cette page";
|
unsavedWarningsConfigProvider.navigateMessage = Fablab.translations.app.shared.messages.you_will_lose_any_unsaved_modification_if_you_quit_this_page;
|
||||||
unsavedWarningsConfigProvider.reloadMessage = "Vous perdrez les modifications non enregistrées si vous rechargez cette 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);
|
growlProvider.globalTimeToLive(5000);
|
||||||
growlProvider.globalEnableHtml(true);
|
|
||||||
|
|
||||||
$locationProvider.hashPrefix('!');
|
// 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", "$location", "Analytics", function($rootScope, $log, AuthService, Auth, amMoment, $state, editableOptions, $location, Analytics){
|
}]).run(["$rootScope", "$log", "AuthService", "Auth", "amMoment", "$state", "editableOptions",
|
||||||
|
function($rootScope, $log, AuthService, Auth, amMoment, $state, editableOptions) {
|
||||||
|
|
||||||
amMoment.changeLocale('fr');
|
// Angular-moment (date-time manipulations library)
|
||||||
|
amMoment.changeLocale(Fablab.moment_locale);
|
||||||
|
|
||||||
|
// Angular-xeditable (click-to-edit elements, used in admin backoffice)
|
||||||
editableOptions.theme = 'bs3';
|
editableOptions.theme = 'bs3';
|
||||||
|
|
||||||
|
// Alter the UI-Router's $state, registering into some informations 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.prevState = fromState;
|
||||||
$state.prevParams = fromParams;
|
$state.prevParams = fromParams;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Global config: if true, the whole 'Plans & Subscriptions' feature will be disabled in the application
|
||||||
|
$rootScope.fablabWithoutPlans = Fablab.withoutPlans;
|
||||||
|
|
||||||
|
// 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.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@ -67,8 +92,9 @@ config(['$locationProvider', '$httpProvider', 'AuthProvider', "growlProvider", "
|
|||||||
$state.go($state.prevState, $state.prevParams);
|
$state.go($state.prevState, $state.prevParams);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Configuration of the summernote editor (used in project edition)
|
||||||
$rootScope.summernoteOpts = {
|
$rootScope.summernoteOpts = {
|
||||||
lang: 'fr-FR',
|
lang: Fablab.summernote_locale,
|
||||||
height: 200,
|
height: 200,
|
||||||
toolbar: [
|
toolbar: [
|
||||||
['style', ['style']],
|
['style', ['style']],
|
||||||
@ -85,25 +111,16 @@ config(['$locationProvider', '$httpProvider', 'AuthProvider', "growlProvider", "
|
|||||||
maximumImageFileSize: 4096
|
maximumImageFileSize: 4096
|
||||||
};
|
};
|
||||||
|
|
||||||
}]).filter('array', function() {
|
// Prevent the usage of the application for members with incomplete profiles: they will be redirected to
|
||||||
return function(arrayLength) {
|
// the 'profile completion' page. This is especially useful for user's accounts imported through SSO.
|
||||||
if (arrayLength) {
|
$rootScope.$on('$stateChangeStart', function (event, toState) {
|
||||||
arrayLength = Math.ceil(arrayLength);
|
Auth.currentUser().then(function(currentUser) {
|
||||||
var arr = new Array(arrayLength), i = 0;
|
if (currentUser.need_completion && toState.name != 'app.logged.profileCompletion') {
|
||||||
for (; i < arrayLength; i++) {
|
$state.go('app.logged.profileCompletion');
|
||||||
arr[i] = i;
|
}
|
||||||
}
|
});
|
||||||
return arr;
|
});
|
||||||
}
|
|
||||||
};
|
}]).constant('angularMomentConfig', {
|
||||||
}).directive('datepickerPopup', function (){
|
timezone: Fablab.timezone
|
||||||
// fixes https://github.com/angular-ui/bootstrap/issues/2659
|
});
|
||||||
return {
|
|
||||||
restrict: 'EAC',
|
|
||||||
require: 'ngModel',
|
|
||||||
link: function(scope, element, attr, controller) {
|
|
||||||
//remove the default formatter from the input directive to prevent conflict
|
|
||||||
controller.$formatters.shift();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
@ -20,7 +20,6 @@
|
|||||||
//= require jquery-ui/ui/jquery.ui.droppable
|
//= require jquery-ui/ui/jquery.ui.droppable
|
||||||
//= require jquery-ui/ui/jquery.ui.resizable
|
//= require jquery-ui/ui/jquery.ui.resizable
|
||||||
//= require angular
|
//= require angular
|
||||||
//= require angular-i18n/angular-locale_fr-fr.js
|
|
||||||
//= require angular-cookies
|
//= require angular-cookies
|
||||||
//= require angular-resource
|
//= require angular-resource
|
||||||
//= require angular-sanitize
|
//= require angular-sanitize
|
||||||
@ -29,18 +28,18 @@
|
|||||||
//= require angular-touch
|
//= require angular-touch
|
||||||
//= require angular-ui-router/release/angular-ui-router
|
//= require angular-ui-router/release/angular-ui-router
|
||||||
//= require angular-bootstrap/ui-bootstrap-tpls
|
//= require angular-bootstrap/ui-bootstrap-tpls
|
||||||
//= require select2/select2
|
//= require angular-ui-select/dist/select
|
||||||
//= require select2/select2_locale_fr
|
|
||||||
//= require angular-ui-select2/src/select2
|
|
||||||
//= require moment/moment
|
//= require moment/moment
|
||||||
//= require moment/locale/fr
|
//= require moment-timezone/builds/moment-timezone-with-data-2010-2020
|
||||||
|
//= require angular-ui-calendar/src/calendar
|
||||||
|
//= require fullcalendar/dist/fullcalendar
|
||||||
//= require angular-moment/angular-moment
|
//= require angular-moment/angular-moment
|
||||||
//= require ngUpload/ng-upload
|
//= require ngUpload/ng-upload
|
||||||
//= require jasny-bootstrap/js/fileinput
|
//= require jasny-bootstrap/js/fileinput
|
||||||
//= require holderjs/holder
|
//= require holderjs/holder
|
||||||
//= require angular-devise/lib/devise
|
//= require angular-devise/lib/devise
|
||||||
//= require devise-modal
|
//= require devise-modal
|
||||||
//= require angular-growl/build/angular-growl
|
//= require angular-growl-v2/build/angular-growl
|
||||||
//= require angular-xeditable/dist/js/xeditable
|
//= require angular-xeditable/dist/js/xeditable
|
||||||
//= require checklist-model/checklist-model
|
//= require checklist-model/checklist-model
|
||||||
//= require angular-unsavedChanges/src/unsavedChanges
|
//= require angular-unsavedChanges/src/unsavedChanges
|
||||||
@ -50,13 +49,25 @@
|
|||||||
//= require dirDisqus
|
//= require dirDisqus
|
||||||
//= require humanize
|
//= require humanize
|
||||||
//= require underscore/underscore
|
//= require underscore/underscore
|
||||||
|
//= require elasticsearch/elasticsearch.angular
|
||||||
|
//= require d3/d3
|
||||||
|
//= require nvd3/build/nv.d3.js
|
||||||
//= require app
|
//= require app
|
||||||
//= require router
|
//= require router
|
||||||
|
//= require medium-editor/dist/js/medium-editor
|
||||||
|
//= require angular-medium-editor/dist/angular-medium-editor
|
||||||
|
//= require bootstrap-switch/dist/js/bootstrap-switch.min
|
||||||
|
//= require angular-bootstrap-switch/dist/angular-bootstrap-switch.min
|
||||||
|
//= require angular-base64-upload/dist/angular-base64-upload.min
|
||||||
|
//= require summernote/dist/summernote
|
||||||
|
//= require angular-summernote/dist/angular-summernote
|
||||||
|
//= require jquery-minicolors/jquery.minicolors.js
|
||||||
|
//= require angular-minicolors/angular-minicolors.js
|
||||||
|
//= require angular-translate/angular-translate
|
||||||
|
//= require angular-translate-loader-partial/angular-translate-loader-partial
|
||||||
|
//= require messageformat/messageformat
|
||||||
|
//= require angular-translate-interpolation-messageformat/angular-translate-interpolation-messageformat
|
||||||
//= require_tree ./controllers
|
//= require_tree ./controllers
|
||||||
//= require_tree ./services
|
//= require_tree ./services
|
||||||
//= require_tree ./directives
|
//= require_tree ./directives
|
||||||
//= require_tree ./filters
|
//= require_tree ./filters
|
||||||
//= require summernote/dist/summernote
|
|
||||||
//= require summernote/lang/summernote-fr-FR
|
|
||||||
//= require summernote/plugin/summernote-ext-video
|
|
||||||
//= require angular-summernote/dist/angular-summernote
|
|
||||||
|
19
app/assets/javascripts/controllers/about.coffee
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
'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
|
||||||
|
]
|
221
app/assets/javascripts/controllers/admin/authentications.coffee
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
### COMMON CODE ###
|
||||||
|
|
||||||
|
## list of supported authentication methods
|
||||||
|
METHODS = {
|
||||||
|
'DatabaseProvider' : 'Base de données locale',
|
||||||
|
#'OAuthProvider' : 'OAuth 1.0',
|
||||||
|
'OAuth2Provider' : 'OAuth 2.0',
|
||||||
|
#'LdapProvider' : 'LDAP'
|
||||||
|
}
|
||||||
|
|
||||||
|
##
|
||||||
|
# Iterate through the provided array and return the index of the requested element
|
||||||
|
# @param elements {Array} array of objects with property 'id'
|
||||||
|
# @param id {Number} 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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Page listing all authentication providers
|
||||||
|
##
|
||||||
|
Application.Controllers.controller "AuthentificationController", ["$scope", "$state", "$rootScope", "dialogs", "growl", "authProvidersPromise", 'AuthProvider', '_t'
|
||||||
|
, ($scope, $state, $rootScope, dialogs, growl, authProvidersPromise, AuthProvider, _t) ->
|
||||||
|
|
||||||
|
### PUBLIC SCOPE ###
|
||||||
|
|
||||||
|
## full list of authentication providers
|
||||||
|
$scope.providers = authProvidersPromise
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Translate the classname into an explicit textual message
|
||||||
|
# @param type {string} Ruby polymorphic model classname
|
||||||
|
# @returns {string}
|
||||||
|
##
|
||||||
|
$scope.getType = (type) ->
|
||||||
|
text = METHODS[type]
|
||||||
|
if typeof text != 'undefined'
|
||||||
|
return text
|
||||||
|
else
|
||||||
|
return _t('unknown')+type
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Translate the status string into an explicit textual message
|
||||||
|
# @param status {string} active | pending | previous
|
||||||
|
# @returns {string}
|
||||||
|
##
|
||||||
|
$scope.getState = (status) ->
|
||||||
|
switch status
|
||||||
|
when 'active' then _t('active')
|
||||||
|
when 'pending' then _t('pending')
|
||||||
|
when 'previous' then _t('previous_provider')
|
||||||
|
else _t('unknown')+status
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Ask for confirmation then delete the specified provider
|
||||||
|
# @param providers {Array} full list of authentication providers
|
||||||
|
# @param provider {Object} provider to delete
|
||||||
|
##
|
||||||
|
$scope.destroyProvider = (providers, provider) ->
|
||||||
|
dialogs.confirm
|
||||||
|
resolve:
|
||||||
|
object: ->
|
||||||
|
title: _t('confirmation_required')
|
||||||
|
msg: _t('do_you_really_want_to_delete_the_TYPE_authentication_provider_NAME', {TYPE:$scope.getType(provider.providable_type), NAME:provider.name})
|
||||||
|
, ->
|
||||||
|
# the admin has confirmed, delete
|
||||||
|
AuthProvider.delete id: provider.id
|
||||||
|
, ->
|
||||||
|
providers.splice(findIdxById(providers, provider.id), 1)
|
||||||
|
growl.success(_t('authentication_provider_successfully_deleted'))
|
||||||
|
, ->
|
||||||
|
growl.error(_t('an_error_occurred_unable_to_delete_the_specified_provider'))
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Page to add a new authentication provider
|
||||||
|
##
|
||||||
|
Application.Controllers.controller "NewAuthenticationController", ["$scope", "$state", "$rootScope", "dialogs", "growl", "mappingFieldsPromise", "authProvidersPromise", "AuthProvider", '_t'
|
||||||
|
, ($scope, $state, $rootScope, dialogs, growl, mappingFieldsPromise, authProvidersPromise, AuthProvider, _t) ->
|
||||||
|
|
||||||
|
$scope.authMethods = METHODS
|
||||||
|
|
||||||
|
$scope.mappingFields = mappingFieldsPromise
|
||||||
|
|
||||||
|
$scope.mode = 'creation'
|
||||||
|
|
||||||
|
$scope.provider = {
|
||||||
|
name: '',
|
||||||
|
providable_type: '',
|
||||||
|
providable_attributes: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Initialize some provider's specific properties when selecting the provider type
|
||||||
|
##
|
||||||
|
$scope.updateProvidable = ->
|
||||||
|
# === OAuth2Provider ===
|
||||||
|
if $scope.provider.providable_type == 'OAuth2Provider'
|
||||||
|
if typeof $scope.provider.providable_attributes.o_auth2_mappings_attributes == 'undefined'
|
||||||
|
$scope.provider.providable_attributes['o_auth2_mappings_attributes'] = []
|
||||||
|
# Add others providers initializers here if needed ...
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Validate and save the provider parameters in database
|
||||||
|
##
|
||||||
|
$scope.registerProvider = ->
|
||||||
|
# === DatabaseProvider ===
|
||||||
|
if $scope.provider.providable_type == 'DatabaseProvider'
|
||||||
|
# prevent from adding mode than 1
|
||||||
|
for provider in authProvidersPromise
|
||||||
|
if provider.providable_type == 'DatabaseProvider'
|
||||||
|
growl.error _t('a_local_database_provider_already_exists_unable_to_create_another')
|
||||||
|
return false
|
||||||
|
AuthProvider.save auth_provider: $scope.provider, (provider) ->
|
||||||
|
growl.success _t('local_provider_successfully_saved')
|
||||||
|
$state.go('app.admin.members')
|
||||||
|
# === OAuth2Provider ===
|
||||||
|
else if $scope.provider.providable_type == 'OAuth2Provider'
|
||||||
|
# check the ID mapping
|
||||||
|
unless check_oauth2_id_is_mapped($scope.provider.providable_attributes.o_auth2_mappings_attributes)
|
||||||
|
growl.error(_t('it_is_required_to_set_the_matching_between_User.uid_and_the_API_to_add_this_provider'))
|
||||||
|
return false
|
||||||
|
# discourage the use of unsecure SSO
|
||||||
|
unless $scope.provider.providable_attributes.base_url.indexOf('https://') > -1
|
||||||
|
dialogs.confirm
|
||||||
|
size: 'l'
|
||||||
|
resolve:
|
||||||
|
object: ->
|
||||||
|
title: _t('security_issue_detected')
|
||||||
|
msg: _t('beware_the_oauth2_authenticatoin_provider_you_are_about_to_add_isnt_using_HTTPS') +
|
||||||
|
_t('this_is_a_serious_security_issue_on_internet_and_should_never_be_used_except_for_testing_purposes') +
|
||||||
|
_t('do_you_really_want_to_continue')
|
||||||
|
, -> # unsecured http confirmed
|
||||||
|
AuthProvider.save auth_provider: $scope.provider, (provider) ->
|
||||||
|
growl.success _t('unsecured_oauth2_provider_successfully_added')
|
||||||
|
$state.go('app.admin.members')
|
||||||
|
else
|
||||||
|
AuthProvider.save auth_provider: $scope.provider, (provider) ->
|
||||||
|
growl.success _t('oauth2_provider_successfully_added')
|
||||||
|
$state.go('app.admin.members')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Changes the admin's view to the members list page
|
||||||
|
##
|
||||||
|
$scope.cancel = ->
|
||||||
|
$state.go('app.admin.members')
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Page to edit an already added authentication provider
|
||||||
|
##
|
||||||
|
Application.Controllers.controller "EditAuthenticationController", ["$scope", "$state", "$stateParams", "$rootScope", "dialogs", "growl", 'providerPromise', 'mappingFieldsPromise', 'AuthProvider', '_t'
|
||||||
|
, ($scope, $state, $stateParams, $rootScope, dialogs, growl, providerPromise, mappingFieldsPromise, AuthProvider, _t) ->
|
||||||
|
|
||||||
|
$scope.provider = providerPromise
|
||||||
|
|
||||||
|
$scope.authMethods = METHODS
|
||||||
|
|
||||||
|
$scope.mode = 'edition'
|
||||||
|
|
||||||
|
$scope.mappingFields = mappingFieldsPromise
|
||||||
|
|
||||||
|
##
|
||||||
|
# Update the current provider with the new inputs
|
||||||
|
##
|
||||||
|
$scope.updateProvider = ->
|
||||||
|
# check the ID mapping
|
||||||
|
unless check_oauth2_id_is_mapped($scope.provider.providable_attributes.o_auth2_mappings_attributes)
|
||||||
|
growl.error(_t('it_is_required_to_set_the_matching_between_User.uid_and_the_API_to_add_this_provider'))
|
||||||
|
return false
|
||||||
|
AuthProvider.update {id: $scope.provider.id}, {auth_provider: $scope.provider}, (provider) ->
|
||||||
|
growl.success(_t('provider_successfully_updated'))
|
||||||
|
$state.go('app.admin.members')
|
||||||
|
, ->
|
||||||
|
growl.error(_t('an_error_occurred_unable_to_update_the_provider'))
|
||||||
|
|
||||||
|
##
|
||||||
|
# Changes the admin's view to the members list page
|
||||||
|
##
|
||||||
|
$scope.cancel = ->
|
||||||
|
$state.go('app.admin.members')
|
||||||
|
|
||||||
|
]
|
403
app/assets/javascripts/controllers/admin/calendar.coffee.erb
Normal file
@ -0,0 +1,403 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
##
|
||||||
|
# Controller used in the calendar management page
|
||||||
|
##
|
||||||
|
|
||||||
|
Application.Controllers.controller "AdminCalendarController", ["$scope", "$state", "$uibModal", "moment", "Availability", 'Slot', 'Setting', 'growl', 'dialogs', 'availabilitiesPromise', 'bookingWindowStart', 'bookingWindowEnd', 'machinesPromise', '_t'
|
||||||
|
($scope, $state, $uibModal, moment, Availability, Slot, Setting, growl, dialogs, availabilitiesPromise, bookingWindowStart, bookingWindowEnd, machinesPromise, _t) ->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### PRIVATE STATIC CONSTANTS ###
|
||||||
|
|
||||||
|
# The calendar is divided in slots of 30 minutes
|
||||||
|
BASE_SLOT = '00:30:00'
|
||||||
|
|
||||||
|
# The bookings can be positioned every half hours
|
||||||
|
BOOKING_SNAP = '00:30:00'
|
||||||
|
|
||||||
|
# The calendar will be initialized positioned under 9:00 AM
|
||||||
|
DEFAULT_CALENDAR_POSITION = '09:00:00'
|
||||||
|
|
||||||
|
# We do not allow the creation of slots that are not a multiple of 60 minutes
|
||||||
|
SLOT_MULTIPLE = 60
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### PUBLIC SCOPE ###
|
||||||
|
|
||||||
|
## list of the FabLab machines
|
||||||
|
$scope.machines = machinesPromise
|
||||||
|
|
||||||
|
## currently selected availability
|
||||||
|
$scope.availability = null
|
||||||
|
|
||||||
|
## bind the availabilities slots with full-Calendar events
|
||||||
|
$scope.eventSources = []
|
||||||
|
$scope.eventSources.push
|
||||||
|
events: availabilitiesPromise
|
||||||
|
textColor: 'black'
|
||||||
|
|
||||||
|
## after fullCalendar loads, provides access to its methods through $scope.calendar.fullCalendar()
|
||||||
|
$scope.calendar = null
|
||||||
|
|
||||||
|
## fullCalendar (v2) configuration
|
||||||
|
$scope.calendarConfig =
|
||||||
|
timezone: Fablab.timezone
|
||||||
|
lang: Fablab.fullcalendar_locale
|
||||||
|
header:
|
||||||
|
left: 'month agendaWeek'
|
||||||
|
center: 'title'
|
||||||
|
right: 'today prev,next'
|
||||||
|
firstDay: 1 # Week start on monday (France)
|
||||||
|
scrollTime: DEFAULT_CALENDAR_POSITION
|
||||||
|
slotDuration: BASE_SLOT
|
||||||
|
snapDuration: BOOKING_SNAP
|
||||||
|
allDayDefault: false
|
||||||
|
minTime: "00:00:00"
|
||||||
|
maxTime: "24:00:00"
|
||||||
|
height: 'auto'
|
||||||
|
buttonIcons:
|
||||||
|
prev: 'left-single-arrow'
|
||||||
|
next: 'right-single-arrow'
|
||||||
|
timeFormat:
|
||||||
|
agenda:'H:mm'
|
||||||
|
month: 'H(:mm)'
|
||||||
|
axisFormat: 'H:mm'
|
||||||
|
|
||||||
|
allDaySlot: false
|
||||||
|
defaultView: 'agendaWeek'
|
||||||
|
selectable: true
|
||||||
|
selecHelper: true
|
||||||
|
select: (start, end, jsEvent, view) ->
|
||||||
|
calendarSelectCb(start, end, jsEvent, view)
|
||||||
|
eventClick: (event, jsEvent, view)->
|
||||||
|
calendarEventClickCb(event, jsEvent, view)
|
||||||
|
eventRender: (event, element, view) ->
|
||||||
|
eventRenderCb(event, element)
|
||||||
|
|
||||||
|
## fullCalendar time bounds (up & down)
|
||||||
|
$scope.calendarConfig.minTime = moment.duration(moment(bookingWindowStart.setting.value).format('HH:mm:ss'))
|
||||||
|
$scope.calendarConfig.maxTime = moment.duration(moment(bookingWindowEnd.setting.value).format('HH:mm:ss'))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Open a confirmation modal to cancel the booking of a user for the currently selected event.
|
||||||
|
# @param slot {Object} reservation slot of a user, inherited from $resource
|
||||||
|
##
|
||||||
|
$scope.cancelBooking = (slot) ->
|
||||||
|
# open a confirmation dialog
|
||||||
|
dialogs.confirm
|
||||||
|
resolve:
|
||||||
|
object: ->
|
||||||
|
title: _t('confirmation_required')
|
||||||
|
msg: _t("do_you_really_want_to_cancel_the_USER_s_reservation_the_DATE_at_TIME_concerning_RESERVATION"
|
||||||
|
, { GENDER:getGender($scope.currentUser), USER:slot.user.name, DATE:moment(slot.start_at).format('L'), TIME:moment(slot.start_at).format('LT'), RESERVATION:slot.reservable.name }
|
||||||
|
, 'messageformat')
|
||||||
|
, ->
|
||||||
|
# the admin has confirmed, cancel the subscription
|
||||||
|
Slot.cancel {id: slot.slot_id}
|
||||||
|
, (data, status) -> # success
|
||||||
|
# update the canceled_at attribute
|
||||||
|
for resa in $scope.reservations
|
||||||
|
if resa.slot_id == data.id
|
||||||
|
resa.canceled_at = data.canceled_at
|
||||||
|
break
|
||||||
|
# notify the admin
|
||||||
|
growl.success(_t('reservation_was_successfully_cancelled'))
|
||||||
|
, (data, status) -> # failed
|
||||||
|
growl.error(_t('reservation_cancellation_failed'))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Open a confirmation modal to remove a machine for the currently selected availability,
|
||||||
|
# except if it is the last machine of the reservation.
|
||||||
|
# @param machine {Object} must contain the machine ID and name
|
||||||
|
##
|
||||||
|
$scope.removeMachine = (machine) ->
|
||||||
|
if $scope.availability.machine_ids.length == 1
|
||||||
|
growl.error(_t('unable_to_remove_the_last_machine_of_the_slot_delete_the_slot_rather'))
|
||||||
|
else
|
||||||
|
# open a confirmation dialog
|
||||||
|
dialogs.confirm
|
||||||
|
resolve:
|
||||||
|
object: ->
|
||||||
|
title: _t('confirmation_required')
|
||||||
|
msg: _t('do_you_really_want_to_remove_MACHINE_from_this_slot', {GENDER:getGender($scope.currentUser), MACHINE:machine.name}, "messageformat") + ' ' +
|
||||||
|
_t('this_will_prevent_any_new_reservation_on_this_slot_but_wont_cancel_those_existing') + ' ' +
|
||||||
|
_t('beware_this_cannot_be_reverted')
|
||||||
|
, ->
|
||||||
|
# the admin has confirmed, remove the machine
|
||||||
|
machines = $scope.availability.machine_ids
|
||||||
|
for key, m_id in machines
|
||||||
|
if m_id == machine.id
|
||||||
|
machines.splice(key, 1)
|
||||||
|
|
||||||
|
Availability.update {id: $scope.availability.id}, {availability: {machines_attributes: [{id: machine.id, _destroy: true}]}}
|
||||||
|
, (data, status) -> # success
|
||||||
|
# update the machine_ids attribute
|
||||||
|
$scope.availability.machine_ids = data.machine_ids
|
||||||
|
$scope.availability.title = data.title
|
||||||
|
$scope.calendar.fullCalendar 'rerenderEvents'
|
||||||
|
# notify the admin
|
||||||
|
growl.success(_t('the_machine_was_successfully_removed_from_the_slot'))
|
||||||
|
, (data, status) -> # failed
|
||||||
|
growl.error(_t('deletion_failed'))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### PRIVATE SCOPE ###
|
||||||
|
|
||||||
|
##
|
||||||
|
# Return an enumerable meaninful string for the gender of the provider user
|
||||||
|
# @param user {Object} Database user record
|
||||||
|
# @return {string} 'male' or 'female'
|
||||||
|
##
|
||||||
|
getGender = (user) ->
|
||||||
|
if user.profile
|
||||||
|
if user.profile.gender == "true" then 'male' else 'female'
|
||||||
|
else 'other'
|
||||||
|
|
||||||
|
# Triggered when the admin drag on the agenda to create a new reservable slot.
|
||||||
|
# @see http://fullcalendar.io/docs/selection/select_callback/
|
||||||
|
##
|
||||||
|
calendarSelectCb = (start, end, jsEvent, view) ->
|
||||||
|
start = moment.tz(start.toISOString(), Fablab.timezone)
|
||||||
|
end = moment.tz(end.toISOString(), Fablab.timezone)
|
||||||
|
# first we check that the selected slot is an N-hours multiple (ie. not decimal)
|
||||||
|
if Number.isInteger(parseInt((end.valueOf() - start.valueOf()) / (SLOT_MULTIPLE * 1000), 10)/SLOT_MULTIPLE)
|
||||||
|
today = new Date()
|
||||||
|
if (parseInt((start.valueOf() - today) / (60 * 1000), 10) >= 0)
|
||||||
|
# then we open a modal window to let the admin specify the slot type
|
||||||
|
modalInstance = $uibModal.open
|
||||||
|
templateUrl: '<%= asset_path "admin/calendar/eventModal.html" %>'
|
||||||
|
controller: 'CreateEventModalController'
|
||||||
|
resolve:
|
||||||
|
start: -> start
|
||||||
|
end: -> end
|
||||||
|
# when the modal is closed, we send the slot to the server for saving
|
||||||
|
modalInstance.result.then (availability) ->
|
||||||
|
$scope.calendar.fullCalendar 'renderEvent',
|
||||||
|
id: availability.id
|
||||||
|
title: availability.title,
|
||||||
|
start: availability.start_at
|
||||||
|
end: availability.end_at
|
||||||
|
textColor: 'black'
|
||||||
|
backgroundColor: availability.backgroundColor
|
||||||
|
borderColor: availability.borderColor
|
||||||
|
tag_ids: availability.tag_ids
|
||||||
|
machine_ids: availability.machine_ids
|
||||||
|
, true
|
||||||
|
, ->
|
||||||
|
$scope.calendar.fullCalendar('unselect')
|
||||||
|
|
||||||
|
$scope.calendar.fullCalendar('unselect')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Triggered when the admin clicks on a availability slot in the agenda.
|
||||||
|
# @see http://fullcalendar.io/docs/mouse/eventClick/
|
||||||
|
##
|
||||||
|
calendarEventClickCb = (event, jsEvent, view) ->
|
||||||
|
|
||||||
|
$scope.availability = event
|
||||||
|
|
||||||
|
# if the user has clicked on the delete event button, delete the event
|
||||||
|
if ($(jsEvent.target).hasClass('remove-event'))
|
||||||
|
Availability.delete id: event.id, ->
|
||||||
|
$scope.calendar.fullCalendar 'removeEvents', event.id
|
||||||
|
for _event, i in $scope.eventSources[0].events
|
||||||
|
if _event.id == event.id
|
||||||
|
$scope.eventSources[0].events.splice(i,1)
|
||||||
|
|
||||||
|
growl.success(_t('the_slot_START-END_has_been_successfully_deleted', {START:+moment(event.start).format('LL LT'), END:moment(event.end).format('LT')}))
|
||||||
|
,->
|
||||||
|
growl.error(_t('unable_to_delete_the_slot_START-END_because_it_s_already_reserved_by_a_member', {START:+moment(event.start).format('LL LT'), END:moment(event.end).format('LT')}))
|
||||||
|
# if the user has only clicked on the event, display its reservations
|
||||||
|
else
|
||||||
|
Availability.reservations {id: event.id}, (reservations) ->
|
||||||
|
$scope.reservations = reservations
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Triggered when fullCalendar tries to graphicaly render an event block.
|
||||||
|
# Append the event tag into the block, just after the event title.
|
||||||
|
# @see http://fullcalendar.io/docs/event_rendering/eventRender/
|
||||||
|
##
|
||||||
|
eventRenderCb = (event, element) ->
|
||||||
|
if event.tag_ids.length > 0
|
||||||
|
Availability.get {id: event.id}, (avail) ->
|
||||||
|
html = ''
|
||||||
|
for tag in avail.tags
|
||||||
|
html += "<span class='label label-success text-white'>#{tag.name}</span> "
|
||||||
|
element.find('.fc-title').append("<br/>"+html)
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Controller used in the slot creation modal window
|
||||||
|
##
|
||||||
|
Application.Controllers.controller 'CreateEventModalController', ["$scope", "$uibModalInstance", "moment", "start", "end", "Machine", "Availability", "Training", 'Tag', 'growl', '_t', ($scope, $uibModalInstance, moment, start, end, Machine, Availability, Training, Tag, growl, _t) ->
|
||||||
|
|
||||||
|
## $uibModal parameter
|
||||||
|
$scope.start = start
|
||||||
|
|
||||||
|
## $uibModal parameter
|
||||||
|
$scope.end = end
|
||||||
|
|
||||||
|
## machines list
|
||||||
|
$scope.machines = []
|
||||||
|
|
||||||
|
## trainings list
|
||||||
|
$scope.trainings = []
|
||||||
|
|
||||||
|
## machines associated with the created slot
|
||||||
|
$scope.selectedMachines = []
|
||||||
|
|
||||||
|
## the user is not able to edit the ending time of the availability, unless he set the type to 'training'
|
||||||
|
$scope.endDateReadOnly = true
|
||||||
|
|
||||||
|
## timepickers configuration
|
||||||
|
$scope.timepickers =
|
||||||
|
start:
|
||||||
|
hstep: 1
|
||||||
|
mstep: 5
|
||||||
|
end:
|
||||||
|
hstep: 1
|
||||||
|
mstep: 5
|
||||||
|
|
||||||
|
## slot details
|
||||||
|
$scope.availability =
|
||||||
|
start_at: start
|
||||||
|
end_at: end
|
||||||
|
available_type: 'machines' # default
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Adds or removes the provided machine from the current slot
|
||||||
|
# @param machine {Object}
|
||||||
|
##
|
||||||
|
$scope.toggleSelection = (machine)->
|
||||||
|
index = $scope.selectedMachines.indexOf(machine)
|
||||||
|
if index > -1
|
||||||
|
$scope.selectedMachines.splice(index, 1)
|
||||||
|
else
|
||||||
|
$scope.selectedMachines.push(machine)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Callback for the modal window validation: save the slot and closes the modal
|
||||||
|
##
|
||||||
|
$scope.ok = ->
|
||||||
|
if $scope.availability.available_type == "machines"
|
||||||
|
if $scope.selectedMachines.length > 0
|
||||||
|
$scope.availability.machine_ids = $scope.selectedMachines.map (m) -> m.id
|
||||||
|
else
|
||||||
|
growl.error(_t('you_should_link_a_training_or_a_machine_to_this_slot'))
|
||||||
|
return
|
||||||
|
else
|
||||||
|
$scope.availability.training_ids = [$scope.selectedTraining.id]
|
||||||
|
Availability.save
|
||||||
|
availability: $scope.availability
|
||||||
|
, (availability) ->
|
||||||
|
$uibModalInstance.close(availability)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Callback to cancel the slot creation
|
||||||
|
##
|
||||||
|
$scope.cancel = ->
|
||||||
|
$uibModalInstance.dismiss('cancel')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Switches the slot type : machine availability or training availability
|
||||||
|
##
|
||||||
|
$scope.changeAvailableType = ->
|
||||||
|
if $scope.availability.available_type == "machines"
|
||||||
|
$scope.availability.available_type = "training"
|
||||||
|
else
|
||||||
|
$scope.availability.available_type = "machines"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# For training avaiabilities, set the maximum number of people allowed to register on this slot
|
||||||
|
##
|
||||||
|
$scope.setNbTotalPlaces = ->
|
||||||
|
$scope.availability.nb_total_places = $scope.selectedTraining.nb_total_places
|
||||||
|
|
||||||
|
|
||||||
|
### PRIVATE SCOPE ###
|
||||||
|
|
||||||
|
##
|
||||||
|
# Kind of constructor: these actions will be realized first when the controller is loaded
|
||||||
|
##
|
||||||
|
initialize = ->
|
||||||
|
Machine.query().$promise.then (data)->
|
||||||
|
$scope.machines = data.map (d) ->
|
||||||
|
id: d.id
|
||||||
|
name: d.name
|
||||||
|
Training.query().$promise.then (data)->
|
||||||
|
$scope.trainings = data.map (d) ->
|
||||||
|
id: d.id
|
||||||
|
name: d.name
|
||||||
|
nb_total_places: d.nb_total_places
|
||||||
|
if $scope.trainings.length > 0
|
||||||
|
$scope.selectedTraining = $scope.trainings[0]
|
||||||
|
$scope.setNbTotalPlaces()
|
||||||
|
Tag.query().$promise.then (data) ->
|
||||||
|
$scope.tags = data
|
||||||
|
|
||||||
|
## When we configure a machine availability, do not let the user change the end time, as the total
|
||||||
|
## time must be dividable by 60 minutes (base slot duration). For training availabilities, the user
|
||||||
|
## can configure any duration as it does not matters.
|
||||||
|
$scope.$watch 'availability.available_type', (newValue, oldValue, scope) ->
|
||||||
|
if newValue == 'machines'
|
||||||
|
$scope.endDateReadOnly = true
|
||||||
|
diff = moment($scope.end).diff($scope.start, 'hours') # the result is rounded down by moment.js
|
||||||
|
$scope.end = moment($scope.start).add(diff, 'hours').toDate()
|
||||||
|
$scope.availability.end_at = $scope.end
|
||||||
|
else
|
||||||
|
$scope.endDateReadOnly = false
|
||||||
|
|
||||||
|
## When the start date is changed, if we are configuring a machine availability,
|
||||||
|
## maintain the relative length of the slot (ie. change the end time accordingly)
|
||||||
|
$scope.$watch 'start', (newValue, oldValue, scope) ->
|
||||||
|
# for machine availabilities, adjust the end time
|
||||||
|
if $scope.availability.available_type == 'machines'
|
||||||
|
end = moment($scope.end)
|
||||||
|
end.add(moment(newValue).diff(oldValue), 'milliseconds')
|
||||||
|
$scope.end = end.toDate()
|
||||||
|
else # for training availabilities
|
||||||
|
# prevent the admin from setting the begining after the and
|
||||||
|
if moment(newValue).add(1, 'hour').isAfter($scope.end)
|
||||||
|
$scope.start = oldValue
|
||||||
|
# update availability object
|
||||||
|
$scope.availability.start_at = $scope.start
|
||||||
|
|
||||||
|
## Maintain consistency between the end time and the date object in the availability object
|
||||||
|
$scope.$watch 'end', (newValue, oldValue, scope) ->
|
||||||
|
## we prevent the admin from setting the end of the availability before its begining
|
||||||
|
if moment($scope.start).add(1, 'hour').isAfter(newValue)
|
||||||
|
$scope.end = oldValue
|
||||||
|
# update availability object
|
||||||
|
$scope.availability.end_at = $scope.end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## !!! MUST BE CALLED AT THE END of the controller
|
||||||
|
initialize()
|
||||||
|
]
|
@ -14,8 +14,8 @@
|
|||||||
# - $scope.addFile()
|
# - $scope.addFile()
|
||||||
# - $scope.deleteFile(file)
|
# - $scope.deleteFile(file)
|
||||||
# - $scope.fileinputClass(v)
|
# - $scope.fileinputClass(v)
|
||||||
# - $scope.openStartDatePicker($event)
|
# - $scope.toggleStartDatePicker($event)
|
||||||
# - $scope.openEndDatePicker($event)
|
# - $scope.toggleEndDatePicker($event)
|
||||||
# - $scope.toggleRecurrenceEnd(e)
|
# - $scope.toggleRecurrenceEnd(e)
|
||||||
#
|
#
|
||||||
# Requires :
|
# Requires :
|
||||||
@ -23,7 +23,7 @@
|
|||||||
# - $state (Ui-Router) [ 'app.public.events_list' ]
|
# - $state (Ui-Router) [ 'app.public.events_list' ]
|
||||||
##
|
##
|
||||||
class EventsController
|
class EventsController
|
||||||
constructor: ($scope, $state, Event, Category) ->
|
constructor: ($scope, $state, $locale, Event, Category) ->
|
||||||
|
|
||||||
## Retrieve the list of categories from the server (stage, atelier, ...)
|
## Retrieve the list of categories from the server (stage, atelier, ...)
|
||||||
Category.query().$promise.then (data)->
|
Category.query().$promise.then (data)->
|
||||||
@ -33,12 +33,12 @@ class EventsController
|
|||||||
|
|
||||||
## default parameters for AngularUI-Bootstrap datepicker
|
## default parameters for AngularUI-Bootstrap datepicker
|
||||||
$scope.datePicker =
|
$scope.datePicker =
|
||||||
format: 'dd/MM/yyyy'
|
format: $locale.DATETIME_FORMATS.shortDate
|
||||||
startOpened: false # default: datePicker is not shown
|
startOpened: false # default: datePicker is not shown
|
||||||
endOpened: false
|
endOpened: false
|
||||||
recurrenceEndOpened: false
|
recurrenceEndOpened: false
|
||||||
options:
|
options:
|
||||||
startingDay: 1 # France: the week starts on monday
|
startingDay: Fablab.weekStartingDay
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -136,21 +136,20 @@ class EventsController
|
|||||||
##
|
##
|
||||||
# Controller used in the events listing page (admin view)
|
# Controller used in the events listing page (admin view)
|
||||||
##
|
##
|
||||||
Application.Controllers.controller "adminEventsController", ["$scope", "$state", 'Event', ($scope, $state, Event) ->
|
Application.Controllers.controller "AdminEventsController", ["$scope", "$state", 'Event', 'eventsPromise', ($scope, $state, Event, eventsPromise) ->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### PUBLIC SCOPE ###
|
### PUBLIC SCOPE ###
|
||||||
|
|
||||||
## The events displayed on the page
|
|
||||||
$scope.events = []
|
|
||||||
|
|
||||||
## By default, the pagination mode is activated to limit the page size
|
## By default, the pagination mode is activated to limit the page size
|
||||||
$scope.paginateActive = true
|
$scope.paginateActive = true
|
||||||
|
|
||||||
## The currently displayed page number
|
## The events displayed on the page
|
||||||
$scope.page = 1
|
$scope.events = eventsPromise
|
||||||
|
|
||||||
|
## Current virtual page
|
||||||
|
$scope.page = 2
|
||||||
|
|
||||||
##
|
##
|
||||||
# Adds a bucket of events to the bottom of the page, grouped by month
|
# Adds a bucket of events to the bottom of the page, grouped by month
|
||||||
@ -158,10 +157,7 @@ Application.Controllers.controller "adminEventsController", ["$scope", "$state",
|
|||||||
$scope.loadMoreEvents = ->
|
$scope.loadMoreEvents = ->
|
||||||
Event.query {page: $scope.page}, (data)->
|
Event.query {page: $scope.page}, (data)->
|
||||||
$scope.events = $scope.events.concat data
|
$scope.events = $scope.events.concat data
|
||||||
if data.length
|
paginationCheck(data, $scope.events)
|
||||||
$scope.paginateActive = false if $scope.events.length >= data[0].nb_total_events
|
|
||||||
else
|
|
||||||
$scope.paginateActive = false
|
|
||||||
$scope.page += 1
|
$scope.page += 1
|
||||||
|
|
||||||
|
|
||||||
@ -172,10 +168,40 @@ Application.Controllers.controller "adminEventsController", ["$scope", "$state",
|
|||||||
# Kind of constructor: these actions will be realized first when the controller is loaded
|
# Kind of constructor: these actions will be realized first when the controller is loaded
|
||||||
##
|
##
|
||||||
initialize = ->
|
initialize = ->
|
||||||
$scope.loadMoreEvents()
|
paginationCheck(eventsPromise, $scope.events)
|
||||||
|
|
||||||
## !!! MUST BE CALLED AT THE END of the controller
|
|
||||||
|
##
|
||||||
|
# Check if all events are already displayed OR if the button 'load more events'
|
||||||
|
# is required
|
||||||
|
# @param lastEvents {Array} last events loaded onto the diplay (ie. last "page")
|
||||||
|
# @param events {Array} full list of events displayed on the page (not only the last retrieved)
|
||||||
|
##
|
||||||
|
paginationCheck = (lastEvents, events)->
|
||||||
|
if lastEvents.length > 0
|
||||||
|
$scope.paginateActive = false if events.length >= lastEvents[0].nb_total_events
|
||||||
|
else
|
||||||
|
$scope.paginateActive = false
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# init the controller (call at the end !)
|
||||||
initialize()
|
initialize()
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Controller used in the reservations listing page for a specific event
|
||||||
|
##
|
||||||
|
Application.Controllers.controller "ShowEventReservationsController", ["$scope", 'eventPromise', 'reservationsPromise', ($scope, eventPromise, reservationsPromise) ->
|
||||||
|
|
||||||
|
## retrieve the event from the ID provided in the current URL
|
||||||
|
$scope.event = eventPromise
|
||||||
|
|
||||||
|
## list of reservations for the current event
|
||||||
|
$scope.reservations = reservationsPromise
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -183,7 +209,8 @@ Application.Controllers.controller "adminEventsController", ["$scope", "$state",
|
|||||||
##
|
##
|
||||||
# Controller used in the event creation page
|
# Controller used in the event creation page
|
||||||
##
|
##
|
||||||
Application.Controllers.controller "newEventController", ["$scope", "$state", 'Event', 'Category', 'CSRF', ($scope, $state, Event, Category, CSRF) ->
|
Application.Controllers.controller "NewEventController", ["$scope", "$state", "$locale", 'Event', 'Category', 'CSRF', '_t'
|
||||||
|
, ($scope, $state, $locale, Event, Category, CSRF, _t) ->
|
||||||
CSRF.setMetaTags()
|
CSRF.setMetaTags()
|
||||||
|
|
||||||
## API URL where the form will be posted
|
## API URL where the form will be posted
|
||||||
@ -204,15 +231,18 @@ Application.Controllers.controller "newEventController", ["$scope", "$state", 'E
|
|||||||
|
|
||||||
## Possible types of recurrences for an event
|
## Possible types of recurrences for an event
|
||||||
$scope.recurrenceTypes = [
|
$scope.recurrenceTypes = [
|
||||||
{label: 'Aucune', value: 'none'},
|
{label: _t('none'), value: 'none'},
|
||||||
{label: 'Tous les jours', value: 'day'},
|
{label: _t('every_days'), value: 'day'},
|
||||||
{label: 'Chaque semaine', value: 'week'},
|
{label: _t('every_week'), value: 'week'},
|
||||||
{label: 'Chaque mois', value: 'month'},
|
{label: _t('every_month'), value: 'month'},
|
||||||
{label: 'Chaque année', value: 'year'}
|
{label: _t('every_year'), value: 'year'}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
## currency symbol for the current locale (cf. angular-i18n)
|
||||||
|
$scope.currencySymbol = $locale.NUMBER_FORMATS.CURRENCY_SYM;
|
||||||
|
|
||||||
## Using the EventsController
|
## Using the EventsController
|
||||||
new EventsController($scope, $state, Event, Category)
|
new EventsController($scope, $state, $locale, Event, Category)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -220,8 +250,12 @@ Application.Controllers.controller "newEventController", ["$scope", "$state", 'E
|
|||||||
##
|
##
|
||||||
# Controller used in the events edition page
|
# Controller used in the events edition page
|
||||||
##
|
##
|
||||||
Application.Controllers.controller "editEventController", ["$scope", "$state", "$stateParams", 'Event', 'Category', 'CSRF', ($scope, $state, $stateParams, Event, Category, CSRF) ->
|
Application.Controllers.controller "EditEventController", ["$scope", "$state", "$stateParams", "$locale", 'Event', 'Category', 'CSRF', 'eventPromise'
|
||||||
CSRF.setMetaTags()
|
, ($scope, $state, $stateParams, $locale, Event, Category, CSRF, eventPromise) ->
|
||||||
|
|
||||||
|
### PUBLIC SCOPE ###
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## API URL where the form will be posted
|
## API URL where the form will be posted
|
||||||
$scope.actionUrl = "/api/events/" + $stateParams.id
|
$scope.actionUrl = "/api/events/" + $stateParams.id
|
||||||
@ -230,13 +264,32 @@ Application.Controllers.controller "editEventController", ["$scope", "$state", "
|
|||||||
$scope.method = 'put'
|
$scope.method = 'put'
|
||||||
|
|
||||||
## Retrieve the event details, in case of error the user is redirected to the events listing
|
## Retrieve the event details, in case of error the user is redirected to the events listing
|
||||||
Event.get {id: $stateParams.id}
|
$scope.event = eventPromise
|
||||||
, (event)->
|
|
||||||
$scope.event = event
|
|
||||||
return
|
|
||||||
, ->
|
|
||||||
$state.go('app.public.events_list')
|
|
||||||
|
|
||||||
## Using the EventsController
|
## currency symbol for the current locale (cf. angular-i18n)
|
||||||
new EventsController($scope, $state, Event, Category)
|
$scope.currencySymbol = $locale.NUMBER_FORMATS.CURRENCY_SYM;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### PRIVATE SCOPE ###
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Kind of constructor: these actions will be realized first when the controller is loaded
|
||||||
|
##
|
||||||
|
initialize = ->
|
||||||
|
CSRF.setMetaTags()
|
||||||
|
|
||||||
|
# init the dates to JS objects
|
||||||
|
$scope.event.start_date = moment($scope.event.start_date).toDate()
|
||||||
|
$scope.event.end_date = moment($scope.event.end_date).toDate()
|
||||||
|
|
||||||
|
## Using the EventsController
|
||||||
|
new EventsController($scope, $state, $locale, Event, Category)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## !!! MUST BE CALLED AT THE END of the controller
|
||||||
|
initialize()
|
||||||
]
|
]
|
||||||
|
648
app/assets/javascripts/controllers/admin/graphs.coffee
Normal file
@ -0,0 +1,648 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
Application.Controllers.controller "GraphsController", ["$scope", "$state", '$locale', "$rootScope", 'es', 'Statistics', '_t'
|
||||||
|
, ($scope, $state, $locale, $rootScope, es, Statistics, _t) ->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### PRIVATE STATIC CONSTANTS ###
|
||||||
|
|
||||||
|
## height of the HTML/SVG charts elements in pixels
|
||||||
|
CHART_HEIGHT = 500
|
||||||
|
|
||||||
|
## Label of the charts' horizontal axes
|
||||||
|
X_AXIS_LABEL = _t('date')
|
||||||
|
|
||||||
|
## Label of the charts' vertical axes
|
||||||
|
Y_AXIS_LABEL = _t('number')
|
||||||
|
|
||||||
|
## Colors for the line charts. Each new line uses the next color in this array
|
||||||
|
CHART_COLORS = ['#b35a94', '#1c5794', '#00b49e', '#6fac48', '#ebcf4a', '#fd7e33', '#ca3436', '#a26e3a']
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### PUBLIC SCOPE ###
|
||||||
|
|
||||||
|
## ui-view transitions optimization: if true, the charts will never be refreshed
|
||||||
|
$scope.preventRefresh = false
|
||||||
|
|
||||||
|
## statistics structure in elasticSearch
|
||||||
|
$scope.statistics = []
|
||||||
|
|
||||||
|
## statistics data recovered from elasticSearch
|
||||||
|
$scope.data = null
|
||||||
|
|
||||||
|
## default interval: one day
|
||||||
|
$scope.display =
|
||||||
|
interval: 'week'
|
||||||
|
|
||||||
|
## active tab will be set here
|
||||||
|
$scope.selectedIndex = null
|
||||||
|
|
||||||
|
## for palmares graphs, filters values are stored here
|
||||||
|
$scope.ranking =
|
||||||
|
sortCriterion: 'ca'
|
||||||
|
groupCriterion: 'subType'
|
||||||
|
|
||||||
|
## default: we do not open the datepicker menu
|
||||||
|
$scope.datePicker =
|
||||||
|
show: false
|
||||||
|
|
||||||
|
## datePicker parameters for interval beginning
|
||||||
|
$scope.datePickerStart =
|
||||||
|
format: $locale.DATETIME_FORMATS.shortDate
|
||||||
|
opened: false # default: datePicker is not shown
|
||||||
|
minDate: null
|
||||||
|
maxDate: moment().subtract(1, 'day').toDate()
|
||||||
|
selected: moment().utc().subtract(1, 'months').subtract(1, 'day').startOf('day').toDate()
|
||||||
|
options:
|
||||||
|
startingDay: Fablab.weekStartingDay
|
||||||
|
|
||||||
|
## datePicker parameters for interval ending
|
||||||
|
$scope.datePickerEnd =
|
||||||
|
format: $locale.DATETIME_FORMATS.shortDate
|
||||||
|
opened: false # default: datePicker is not shown
|
||||||
|
minDate: null
|
||||||
|
maxDate: moment().subtract(1, 'day').toDate()
|
||||||
|
selected: moment().subtract(1, 'day').endOf('day').toDate()
|
||||||
|
options:
|
||||||
|
startingDay: Fablab.weekStartingDay
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Callback to open the datepicker (interval start)
|
||||||
|
# @param {Object} jQuery event object
|
||||||
|
##
|
||||||
|
$scope.toggleStartDatePicker = ($event) ->
|
||||||
|
toggleDatePicker($event, $scope.datePickerStart)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Callback to open the datepicker (interval end)
|
||||||
|
# @param {Object} jQuery event object
|
||||||
|
##
|
||||||
|
$scope.toggleEndDatePicker = ($event) ->
|
||||||
|
toggleDatePicker($event, $scope.datePickerEnd)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Callback called when the active tab is changed.
|
||||||
|
# Recover the current tab and store its value in $scope.selectedIndex
|
||||||
|
# @param tab {Object} elasticsearch statistic structure
|
||||||
|
##
|
||||||
|
$scope.setActiveTab = (tab) ->
|
||||||
|
$scope.selectedIndex = tab
|
||||||
|
$scope.ranking.groupCriterion = 'subType'
|
||||||
|
if tab.ca
|
||||||
|
$scope.ranking.sortCriterion = 'ca'
|
||||||
|
else
|
||||||
|
$scope.ranking.sortCriterion = tab.types[0].key
|
||||||
|
refreshChart()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Callback to close the date-picking popup and refresh the results
|
||||||
|
##
|
||||||
|
$scope.validateDateChange = ->
|
||||||
|
$scope.datePicker.show = false
|
||||||
|
refreshChart()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### PRIVATE SCOPE ###
|
||||||
|
|
||||||
|
##
|
||||||
|
# Kind of constructor: these actions will be realized first when the controller is loaded
|
||||||
|
##
|
||||||
|
initialize = ->
|
||||||
|
Statistics.query (stats) ->
|
||||||
|
$scope.statistics = stats
|
||||||
|
# watch the interval changes to refresh the graph
|
||||||
|
$scope.$watch (scope) ->
|
||||||
|
return scope.display.interval
|
||||||
|
, (newValue, oldValue) ->
|
||||||
|
refreshChart()
|
||||||
|
$scope.$watch (scope) ->
|
||||||
|
return scope.ranking.sortCriterion
|
||||||
|
, (newValue, oldValue) ->
|
||||||
|
refreshChart()
|
||||||
|
$scope.$watch (scope) ->
|
||||||
|
return scope.ranking.groupCriterion
|
||||||
|
, (newValue, oldValue) ->
|
||||||
|
refreshChart()
|
||||||
|
refreshChart()
|
||||||
|
|
||||||
|
# workaround for angular-bootstrap::tabs behavior: on tab deletion, another tab will be selected
|
||||||
|
# which will cause every tabs to reload, one by one, when the view is closed
|
||||||
|
$rootScope.$on '$stateChangeStart', (event, toState, toParams, fromState, fromParams) ->
|
||||||
|
if fromState.name == 'app.admin.stats_graphs' and Object.keys(fromParams).length == 0
|
||||||
|
$scope.preventRefresh = true
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Generic function to toggle a bootstrap datePicker
|
||||||
|
# @param $event {Object} jQuery event object
|
||||||
|
# @param datePicker {Object} settings object of the concerned datepicker. Must have an 'opened' property
|
||||||
|
##
|
||||||
|
toggleDatePicker = ($event, datePicker) ->
|
||||||
|
$event.preventDefault()
|
||||||
|
$event.stopPropagation()
|
||||||
|
datePicker.opened = !datePicker.opened
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Query elasticSearch according to the current parameters and update the chart
|
||||||
|
##
|
||||||
|
refreshChart = ->
|
||||||
|
if $scope.selectedIndex and !$scope.preventRefresh
|
||||||
|
query $scope.selectedIndex, (aggregations, error)->
|
||||||
|
if error
|
||||||
|
console.error(error)
|
||||||
|
else
|
||||||
|
if $scope.selectedIndex.graph.chart_type != 'discreteBarChart'
|
||||||
|
$scope.data = formatAggregations(aggregations)
|
||||||
|
angular.forEach $scope.data, (datum, key) ->
|
||||||
|
updateChart($scope.selectedIndex.graph.chart_type, datum, key)
|
||||||
|
else
|
||||||
|
$scope.data = formatRankingAggregations(aggregations, $scope.selectedIndex.graph.limit, $scope.ranking.groupCriterion)
|
||||||
|
updateChart($scope.selectedIndex.graph.chart_type, $scope.data.ranking, $scope.selectedIndex.es_type_key)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Callback used in NVD3 to print timestamps as literal dates on the X axis
|
||||||
|
##
|
||||||
|
xAxisTickFormatFunction = (d, x, y) ->
|
||||||
|
### WARNING !! These tests (typeof/instanceof) may become broken on nvd3 update ###
|
||||||
|
if $scope.display.interval == 'day'
|
||||||
|
if typeof d == 'number' or d instanceof Date
|
||||||
|
d3.time.format(Fablab.d3DateFormat) moment(d).toDate()
|
||||||
|
else # typeof d == 'string'
|
||||||
|
d
|
||||||
|
else if $scope.display.interval == 'week'
|
||||||
|
if typeof x == 'number' or d instanceof Date
|
||||||
|
d3.time.format(_t('week_short')+' %U') moment(d).toDate()
|
||||||
|
else if typeof d == 'number'
|
||||||
|
_t('week_of_START_to_END', {START:moment(d).format('L'), END:moment(d).add(6, 'days').format('L')})
|
||||||
|
else # typeof d == 'string'
|
||||||
|
d
|
||||||
|
else if $scope.display.interval == 'month'
|
||||||
|
if typeof d == 'number'
|
||||||
|
label = moment(d).format('MMMM YYYY')
|
||||||
|
label.substr(0,1).toUpperCase()+label.substr(1).toLowerCase()
|
||||||
|
else # typeof d == 'string'
|
||||||
|
d
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Format aggregations as retuned by elasticSearch to an understandable format for NVD3
|
||||||
|
# @param aggs {Object} as returned by elasticsearch
|
||||||
|
##
|
||||||
|
formatAggregations = (aggs) ->
|
||||||
|
format = {}
|
||||||
|
|
||||||
|
angular.forEach aggs, (type, type_key) -> # go through aggs[$TYPE] where $TYPE = month|year|hour|booking|...
|
||||||
|
format[type_key] = []
|
||||||
|
if type.subgroups
|
||||||
|
angular.forEach type.subgroups.buckets, (subgroup) -> # go through aggs.$TYPE.subgroups.buckets where each bucket represent a $SUBTYPE
|
||||||
|
angular.forEach $scope.selectedIndex.types, (cur_type) -> # in the mean time, go through the types of the current index (active tab) ...
|
||||||
|
if cur_type.key == type_key # ... looking for the type matching $TYPE
|
||||||
|
for it_st in [0.. cur_type.subtypes.length-1] by 1 # when we've found it, iterate over its subtypes ...
|
||||||
|
cur_subtype = cur_type.subtypes[it_st]
|
||||||
|
if subgroup.key == cur_subtype.key # ... which match $SUBTYPE
|
||||||
|
# then we construct NVD3 dataSource according to these informations
|
||||||
|
dataSource =
|
||||||
|
values: []
|
||||||
|
key: cur_subtype.label
|
||||||
|
total : 0
|
||||||
|
color: CHART_COLORS[it_st]
|
||||||
|
area: true
|
||||||
|
# finally, we iterate over 'intervals' buckets witch contains
|
||||||
|
# per date aggregations for our current dataSource
|
||||||
|
angular.forEach subgroup.intervals.buckets, (interval) ->
|
||||||
|
dataSource.values.push
|
||||||
|
x: interval.key
|
||||||
|
y: interval.total.value
|
||||||
|
dataSource.total += parseInt(interval.total.value)
|
||||||
|
dataSource.key += ' (' + dataSource.total + ')'
|
||||||
|
format[type_key].push dataSource
|
||||||
|
format
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Format aggregations for ranking charts to an understandable format for NVD3
|
||||||
|
# @param aggs {Object} as returned by elasticsearch
|
||||||
|
# @param limit {number} limit the number of stats in the bar chart
|
||||||
|
# @param typeKey {String} field name witch results are grouped by
|
||||||
|
##
|
||||||
|
formatRankingAggregations = (aggs, limit, typeKey) ->
|
||||||
|
format =
|
||||||
|
ranking: []
|
||||||
|
|
||||||
|
it = 0
|
||||||
|
while (it < aggs.subgroups.buckets.length)
|
||||||
|
bucket = aggs.subgroups.buckets[it]
|
||||||
|
dataSource =
|
||||||
|
values: []
|
||||||
|
key: getRankingLabel(bucket.key, typeKey)
|
||||||
|
color: CHART_COLORS[it]
|
||||||
|
area: true
|
||||||
|
dataSource.values.push
|
||||||
|
x: getRankingLabel(bucket.key, typeKey)
|
||||||
|
y: bucket.total.value
|
||||||
|
format.ranking.push(dataSource)
|
||||||
|
it++
|
||||||
|
getY = (object)->
|
||||||
|
object.values[0].y
|
||||||
|
format.ranking = stableSort(format.ranking, 'DESC', getY).slice(0, limit)
|
||||||
|
for i in [0..format.ranking.length] by 1
|
||||||
|
if typeof format.ranking[i] == 'undefined' then format.ranking.splice(i,1)
|
||||||
|
format
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# For BarCharts, return the label for a given bar
|
||||||
|
# @param key {string} raw value of the label
|
||||||
|
# @param typeKey {string} name of the field the results are grouped by
|
||||||
|
##
|
||||||
|
getRankingLabel = (key, typeKey) ->
|
||||||
|
if $scope.selectedIndex
|
||||||
|
if (typeKey == 'subType')
|
||||||
|
for type in $scope.selectedIndex.types
|
||||||
|
for subtype in type.subtypes
|
||||||
|
if (subtype.key == key)
|
||||||
|
return subtype.label
|
||||||
|
else
|
||||||
|
for field in $scope.selectedIndex.additional_fields
|
||||||
|
if (field.key == typeKey)
|
||||||
|
switch field.data_type
|
||||||
|
when 'date' then return moment(key).format('LL')
|
||||||
|
when 'list' then return key.name
|
||||||
|
else return key
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Prepare the elasticSearch query for the stats matching the current controller's parameters
|
||||||
|
# @param index {{id:{number}, es_type_key:{string}, label:{string}, table:{boolean}, additional_fields:{Array},
|
||||||
|
# types:{Array}, graph:{Object}}} elasticSearch type in stats index to query
|
||||||
|
# @param callback {function} function be to run after results were retrieved,
|
||||||
|
# it will receive two parameters : results {Array}, error {String} (if any)
|
||||||
|
##
|
||||||
|
query = (index, callback) ->
|
||||||
|
# invalid callback handeling
|
||||||
|
if typeof(callback) != "function"
|
||||||
|
console.error('[graphsController::query] Error: invalid callback provided')
|
||||||
|
return
|
||||||
|
if !index
|
||||||
|
callback([], '[graphsController::query] Error: invalid index provided')
|
||||||
|
return
|
||||||
|
|
||||||
|
if index.graph.chart_type != 'discreteBarChart'
|
||||||
|
# list statistics types
|
||||||
|
stat_types = []
|
||||||
|
for t in index.types
|
||||||
|
if t.graph
|
||||||
|
stat_types.push(t.key)
|
||||||
|
|
||||||
|
# exception handeling
|
||||||
|
if stat_types.length == 0
|
||||||
|
callback([], "Error: Unable to retrieve any graphical statistic types in the provided index")
|
||||||
|
|
||||||
|
type_it = 0
|
||||||
|
results = {}
|
||||||
|
error = ''
|
||||||
|
recursiveCb = ->
|
||||||
|
if type_it < stat_types.length
|
||||||
|
queryElasticStats index.es_type_key, stat_types[type_it], (prevResults, prevError)->
|
||||||
|
if (prevError)
|
||||||
|
console.error('[graphsController::query] '+prevError)
|
||||||
|
error += '\n'+prevError
|
||||||
|
results[stat_types[type_it]] = prevResults
|
||||||
|
type_it++
|
||||||
|
recursiveCb()
|
||||||
|
else
|
||||||
|
callback(results)
|
||||||
|
recursiveCb()
|
||||||
|
else # palmares (ranking)
|
||||||
|
queryElasticRanking index.es_type_key, $scope.ranking.groupCriterion, $scope.ranking.sortCriterion, index.graph.limit, (results, error) ->
|
||||||
|
if (error)
|
||||||
|
callback([], error)
|
||||||
|
else
|
||||||
|
callback(results)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Run the elasticSearch query to retreive the /stats/type aggregations
|
||||||
|
# @param esType {String} elasticSearch document type (subscription|machine|training|...)
|
||||||
|
# @param statType {String} statistics type (year|month|hour|booking|...)
|
||||||
|
# @param callback {function} function be to run after results were retrieved,
|
||||||
|
# it will receive two parameters : results {Array}, error {String} (if any)
|
||||||
|
##
|
||||||
|
queryElasticStats = (esType, statType, callback) ->
|
||||||
|
# handle invalid callback
|
||||||
|
if typeof(callback) != "function"
|
||||||
|
console.error('[graphsController::queryElasticStats] Error: invalid callback provided')
|
||||||
|
return
|
||||||
|
if !esType or !statType
|
||||||
|
callback([], '[graphsController::queryElasticStats] Error: invalid parameters provided')
|
||||||
|
|
||||||
|
# run query
|
||||||
|
es.search
|
||||||
|
"index": "stats"
|
||||||
|
"type": esType
|
||||||
|
"searchType": "count"
|
||||||
|
"body": buildElasticAggregationsQuery(statType, $scope.display.interval, moment($scope.datePickerStart.selected), moment($scope.datePickerEnd.selected))
|
||||||
|
, (error, response) ->
|
||||||
|
if (error)
|
||||||
|
callback([], "Error: something unexpected occurred during elasticSearch query: "+error)
|
||||||
|
else
|
||||||
|
callback(response.aggregations)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# For ranking displays, run the elasticSearch query to retreive the /stats/type aggregations
|
||||||
|
# @param esType {String} elasticSearch document type (subscription|machine|training|...)
|
||||||
|
# @param statType {String} statistics type (year|month|hour|booking|...)
|
||||||
|
# @param callback {function} function be to run after results were retrieved,
|
||||||
|
# it will receive two parameters : results {Array}, error {String} (if any)
|
||||||
|
##
|
||||||
|
queryElasticRanking = (esType, groupKey, sortKey, limit, callback) ->
|
||||||
|
# handle invalid callback
|
||||||
|
if typeof(callback) != "function"
|
||||||
|
console.error('[graphsController::queryElasticRanking] Error: invalid callback provided')
|
||||||
|
return
|
||||||
|
if !esType or !groupKey or !sortKey or typeof limit != 'number'
|
||||||
|
callback([], '[graphsController::queryElasticRanking] Error: invalid parameters provided')
|
||||||
|
|
||||||
|
# run query
|
||||||
|
es.search
|
||||||
|
"index": "stats"
|
||||||
|
"type": esType
|
||||||
|
"searchType": "count"
|
||||||
|
"body": buildElasticAggregationsRankingQuery(groupKey, sortKey, moment($scope.datePickerStart.selected), moment($scope.datePickerEnd.selected))
|
||||||
|
, (error, response) ->
|
||||||
|
if (error)
|
||||||
|
callback([], "Error: something unexpected occurred during elasticSearch query: "+error)
|
||||||
|
else
|
||||||
|
callback(response.aggregations)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Parse a final elastic results bucket and return a D3 compatible object
|
||||||
|
# @param bucket {{key_as_string:{String}, key:{Number}, doc_count:{Number}, total:{{value:{Number}}}}} interval bucket
|
||||||
|
##
|
||||||
|
parseElasticBucket = (bucket) ->
|
||||||
|
[ bucket.key, bucket.total.value ]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Build an object representing the content of the REST-JSON query to elasticSearch, based on the parameters
|
||||||
|
# currently defined for data aggegations.
|
||||||
|
# @param type {String} statistics type (visit|rdv|rating|ca|plan|account|search|...)
|
||||||
|
# @param interval {String} statistics interval (year|quarter|month|week|day|hour|minute|second)
|
||||||
|
# @param intervalBegin {moment} statitics interval beginning (moment.js type)
|
||||||
|
# @param intervalEnd {moment} statitics interval ending (moment.js type)
|
||||||
|
##
|
||||||
|
buildElasticAggregationsQuery = (type, interval, intervalBegin, intervalEnd) ->
|
||||||
|
q =
|
||||||
|
"query":
|
||||||
|
"bool":
|
||||||
|
"must": [
|
||||||
|
{
|
||||||
|
"match":
|
||||||
|
"type": type
|
||||||
|
}
|
||||||
|
{
|
||||||
|
"range":
|
||||||
|
"date":
|
||||||
|
"gte": intervalBegin.format()
|
||||||
|
"lte": intervalEnd.format()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
"aggregations":
|
||||||
|
"subgroups":
|
||||||
|
"terms":
|
||||||
|
"field": "subType" #TODO allow aggregate by custom field
|
||||||
|
"aggregations":
|
||||||
|
"intervals":
|
||||||
|
"date_histogram":
|
||||||
|
"field": "date"
|
||||||
|
"interval": interval
|
||||||
|
"min_doc_count": 0
|
||||||
|
"extended_bounds":
|
||||||
|
"min": intervalBegin.valueOf()
|
||||||
|
"max": intervalEnd.valueOf()
|
||||||
|
"aggregations":
|
||||||
|
"total":
|
||||||
|
"sum":
|
||||||
|
"field": "stat"
|
||||||
|
|
||||||
|
# scale weeks on sunday as nvd3 supports only these weeks
|
||||||
|
if interval == 'week'
|
||||||
|
q.aggregations.subgroups.aggregations.intervals.date_histogram['post_offset'] = '-1d'
|
||||||
|
q.aggregations.subgroups.aggregations.intervals.date_histogram['pre_offset'] = '-1d'
|
||||||
|
# scale days to UTC time
|
||||||
|
else if interval == 'day'
|
||||||
|
offset = moment().utcOffset()
|
||||||
|
q.aggregations.subgroups.aggregations.intervals.date_histogram['post_offset'] = (-offset)+'m'
|
||||||
|
q
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Build an object representing the content of the REST-JSON query to elasticSearch, based on the parameters
|
||||||
|
# currently defined for data aggegations.
|
||||||
|
# @param groupKey {String} statistics subtype or custom field
|
||||||
|
# @param sortKey {String} statistics type or 'ca'
|
||||||
|
# @param intervalBegin {moment} statitics interval beginning (moment.js type)
|
||||||
|
# @param intervalEnd {moment} statitics interval ending (moment.js type)
|
||||||
|
##
|
||||||
|
buildElasticAggregationsRankingQuery = (groupKey, sortKey, intervalBegin, intervalEnd) ->
|
||||||
|
q =
|
||||||
|
"query":
|
||||||
|
"bool":
|
||||||
|
"must": [
|
||||||
|
{
|
||||||
|
"range":
|
||||||
|
"date":
|
||||||
|
"gte": intervalBegin.format()
|
||||||
|
"lte": intervalEnd.format()
|
||||||
|
}
|
||||||
|
{
|
||||||
|
"term":
|
||||||
|
"type": "booking"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
"aggregations":
|
||||||
|
"subgroups":
|
||||||
|
"terms":
|
||||||
|
"field": "subType"
|
||||||
|
"aggregations":
|
||||||
|
"total":
|
||||||
|
"sum":
|
||||||
|
"field": "stat"
|
||||||
|
|
||||||
|
# we group the results by the custom given key (eg. by event date)
|
||||||
|
q.aggregations.subgroups.terms =
|
||||||
|
field: groupKey
|
||||||
|
size: 0
|
||||||
|
|
||||||
|
# results must be sorted and limited later by angular
|
||||||
|
if sortKey != 'ca'
|
||||||
|
angular.forEach q.query.bool.must, (must) ->
|
||||||
|
if must.term
|
||||||
|
must.term.type = sortKey
|
||||||
|
else
|
||||||
|
q.aggregations.subgroups.aggregations.total.sum.field = sortKey
|
||||||
|
|
||||||
|
q
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Redraw the NDV3 chart using the provided data
|
||||||
|
# @param chart_type {String} stackedAreaChart|discreteBarChart|lineChart
|
||||||
|
# @param data {Array} array of NVD3 dataSources
|
||||||
|
# @param type {String} which chart to update (statistic type key)
|
||||||
|
##
|
||||||
|
updateChart = (chart_type, data, type) ->
|
||||||
|
|
||||||
|
id = "#chart-"+type+" svg"
|
||||||
|
|
||||||
|
# clean old charts
|
||||||
|
d3.selectAll(id+" > *").remove()
|
||||||
|
|
||||||
|
nv.addGraph ->
|
||||||
|
# no data or many dates, display line charts
|
||||||
|
if data.length == 0 or (data[0].values.length > 1 and (chart_type != 'discreteBarChart'))
|
||||||
|
if chart_type == 'stackedAreaChart'
|
||||||
|
chart = nv.models.stackedAreaChart().useInteractiveGuideline(true)
|
||||||
|
else
|
||||||
|
chart = nv.models.lineChart().useInteractiveGuideline(true)
|
||||||
|
|
||||||
|
if data.length > 0
|
||||||
|
if $scope.display.interval == 'day'
|
||||||
|
setTimeScale(chart.xAxis, chart.xScale, [d3.time.day, data[0].values.length])
|
||||||
|
else if $scope.display.interval == 'week'
|
||||||
|
setTimeScale(chart.xAxis, chart.xScale, [d3.time.week, data[0].values.length])
|
||||||
|
else if $scope.display.interval == 'month'
|
||||||
|
setTimeScale(chart.xAxis, chart.xScale, [d3.time.month, data[0].values.length])
|
||||||
|
|
||||||
|
chart.xAxis.tickFormat(xAxisTickFormatFunction)
|
||||||
|
chart.yAxis.tickFormat(d3.format('d'))
|
||||||
|
|
||||||
|
chart.xAxis.axisLabel(X_AXIS_LABEL)
|
||||||
|
chart.yAxis.axisLabel(Y_AXIS_LABEL)
|
||||||
|
|
||||||
|
# only one date, display histograms
|
||||||
|
else
|
||||||
|
chart = nv.models.discreteBarChart()
|
||||||
|
chart.tooltip.enabled(false)
|
||||||
|
chart.showValues(true)
|
||||||
|
chart.x (d) -> d.label
|
||||||
|
chart.y (d) -> d.value
|
||||||
|
data = prepareDataForBarChart(data, type)
|
||||||
|
|
||||||
|
# common for each charts
|
||||||
|
chart.margin({left: 100, right: 100})
|
||||||
|
chart.noData(_t('no_data_for_this_period'))
|
||||||
|
chart.height( CHART_HEIGHT )
|
||||||
|
|
||||||
|
# add new chart to the page
|
||||||
|
d3.select(id).datum(data).transition().duration(350).call(chart)
|
||||||
|
|
||||||
|
# resize the graph when the page is resized
|
||||||
|
nv.utils.windowResize(chart.update)
|
||||||
|
# return the chart
|
||||||
|
chart
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Given an NVD3 line chart axis, scale it to display ordinated dates, according to the given arguments
|
||||||
|
##
|
||||||
|
setTimeScale = (nvd3Axis, nvd3Scale, argsArray) ->
|
||||||
|
scale = d3.time.scale()
|
||||||
|
|
||||||
|
nvd3Axis.scale(scale)
|
||||||
|
nvd3Scale(scale)
|
||||||
|
|
||||||
|
if (not argsArray and not argsArray.length)
|
||||||
|
oldTicks = nvd3Axis.axis.ticks
|
||||||
|
nvd3Axis.axis.ticks = ->
|
||||||
|
oldTicks.apply(nvd3Axis.axis, argsArray)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Translate line chart data in dates row to bar chart data, one bar per type.
|
||||||
|
##
|
||||||
|
prepareDataForBarChart = (data, type) ->
|
||||||
|
newData = [
|
||||||
|
key: type
|
||||||
|
values: []
|
||||||
|
]
|
||||||
|
for info in data
|
||||||
|
if info
|
||||||
|
newData[0].values.push
|
||||||
|
"label": info.key
|
||||||
|
"value": info.values[0].y
|
||||||
|
"color": info.color
|
||||||
|
|
||||||
|
newData
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Sort the provided array, in the specified order, on the value returned by the callback.
|
||||||
|
# This is a stable-sorting algorithm implementation, ie. two call with the same array will return the same results
|
||||||
|
# orders, especially with equal values.
|
||||||
|
# @param array {Array} the array to sort
|
||||||
|
# @param order {string} 'ASC' or 'DESC'
|
||||||
|
# @param getValue {function} the callback which will return the value on which the sort will occurs
|
||||||
|
# @returns {Array}
|
||||||
|
##
|
||||||
|
stableSort = (array, order, getValue) ->
|
||||||
|
# prepare sorting
|
||||||
|
keys_order = []
|
||||||
|
result = []
|
||||||
|
for i in [0..array.length] by 1
|
||||||
|
keys_order[array[i]] = i;
|
||||||
|
result.push(array[i]);
|
||||||
|
|
||||||
|
# callback for javascript native Array.sort()
|
||||||
|
sort_fc = (a, b) ->
|
||||||
|
val_a = getValue(a)
|
||||||
|
val_b = getValue(b)
|
||||||
|
if val_a == val_b
|
||||||
|
return keys_order[a] - keys_order[b]
|
||||||
|
if val_a < val_b
|
||||||
|
if order == 'ASC' then return -1
|
||||||
|
else return 1
|
||||||
|
else
|
||||||
|
if order == 'ASC' then return 1
|
||||||
|
else return -1
|
||||||
|
|
||||||
|
# finish the sort
|
||||||
|
result.sort(sort_fc)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## !!! MUST BE CALLED AT THE END of the controller
|
||||||
|
initialize()
|
||||||
|
]
|
64
app/assets/javascripts/controllers/admin/groups.coffee.erb
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
Application.Controllers.controller "GroupsController", ["$scope", 'groupsPromise', 'Group', 'growl', '_t', ($scope, groupsPromise, Group, growl, _t) ->
|
||||||
|
|
||||||
|
## List of users groups
|
||||||
|
$scope.groups = groupsPromise
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Removes the newly inserted but not saved group / Cancel the current group modification
|
||||||
|
# @param rowform {Object} see http://vitalets.github.io/angular-xeditable/
|
||||||
|
# @param index {number} group index in the $scope.groups array
|
||||||
|
##
|
||||||
|
$scope.cancelGroup = (rowform, index) ->
|
||||||
|
if $scope.groups[index].id?
|
||||||
|
rowform.$cancel()
|
||||||
|
else
|
||||||
|
$scope.groups.splice(index, 1)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Creates a new empty entry in the $scope.groups array
|
||||||
|
##
|
||||||
|
$scope.addGroup = ->
|
||||||
|
$scope.inserted =
|
||||||
|
name: ''
|
||||||
|
$scope.groups.push($scope.inserted)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Saves a new group / Update an existing group to the server (form validation callback)
|
||||||
|
# @param data {Object} group name
|
||||||
|
# @param [data] {number} group id, in case of update
|
||||||
|
##
|
||||||
|
$scope.saveGroup = (data, id) ->
|
||||||
|
if id?
|
||||||
|
Group.update {id: id}, { group: data }, (response) ->
|
||||||
|
growl.success(_t('changes_successfully_saved'))
|
||||||
|
, (error) ->
|
||||||
|
growl.error(_t('an_error_occurred_while_saving_changes'))
|
||||||
|
else
|
||||||
|
Group.save { group: data }, (resp)->
|
||||||
|
growl.success(_t('new_group_successfully_saved'))
|
||||||
|
$scope.groups[$scope.groups.length-1].id = resp.id
|
||||||
|
, (error) ->
|
||||||
|
growl.error(_t('an_error_occurred_when_saving_the_new_group'))
|
||||||
|
$scope.groups.splice($scope.groups.length-1, 1)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Deletes the group at the specified index
|
||||||
|
# @param index {number} group index in the $scope.groups array
|
||||||
|
##
|
||||||
|
$scope.removeGroup = (index) ->
|
||||||
|
Group.delete { id: $scope.groups[index].id }, (resp) ->
|
||||||
|
growl.success(_t('group_successfully_deleted'))
|
||||||
|
$scope.groups.splice(index, 1)
|
||||||
|
, (error) ->
|
||||||
|
growl.error(_t('unable_to_delete_group_because_some_users_and_or_groups_are_still_linked_to_it'))
|
||||||
|
|
||||||
|
|
||||||
|
]
|
492
app/assets/javascripts/controllers/admin/invoices.coffee.erb
Normal file
@ -0,0 +1,492 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
##
|
||||||
|
# Controller used in the admin invoices listing page
|
||||||
|
##
|
||||||
|
Application.Controllers.controller "InvoicesController", ["$scope", "$state", 'Invoice', '$uibModal', "growl", "$filter", 'Setting', 'settings', '_t'
|
||||||
|
, ($scope, $state, Invoice, $uibModal, growl, $filter, Setting, settings, _t) ->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### PUBLIC SCOPE ###
|
||||||
|
|
||||||
|
## List of all users invoices
|
||||||
|
$scope.invoices = Invoice.query()
|
||||||
|
|
||||||
|
## Default invoices ordering/sorting
|
||||||
|
$scope.orderInvoice = '-reference'
|
||||||
|
|
||||||
|
## Invoices parameters
|
||||||
|
$scope.invoice =
|
||||||
|
logo: null
|
||||||
|
reference:
|
||||||
|
model: ''
|
||||||
|
help: null
|
||||||
|
templateUrl: 'editReference.html'
|
||||||
|
code:
|
||||||
|
model: ''
|
||||||
|
active: true
|
||||||
|
templateUrl: 'editCode.html'
|
||||||
|
number:
|
||||||
|
model: ''
|
||||||
|
help: null
|
||||||
|
templateUrl: 'editNumber.html'
|
||||||
|
VAT:
|
||||||
|
rate: 19.6
|
||||||
|
active: false
|
||||||
|
templateUrl: 'editVAT.html'
|
||||||
|
text:
|
||||||
|
content: ''
|
||||||
|
legals:
|
||||||
|
content: ''
|
||||||
|
|
||||||
|
## Placeholding date for the invoice creation
|
||||||
|
$scope.today = moment()
|
||||||
|
|
||||||
|
## Placeholding date for the reservation begin
|
||||||
|
$scope.inOneWeek = moment().add(1, 'week').startOf('hour')
|
||||||
|
|
||||||
|
## Placeholding date for the reservation end
|
||||||
|
$scope.inOneWeekAndOneHour = moment().add(1, 'week').add(1, 'hour').startOf('hour')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Change the invoices ordering criterion to the one provided
|
||||||
|
# @param orderBy {string} ordering criterion
|
||||||
|
##
|
||||||
|
$scope.setOrderInvoice = (orderBy)->
|
||||||
|
if $scope.orderInvoice == orderBy
|
||||||
|
$scope.orderInvoice = '-'+orderBy
|
||||||
|
else
|
||||||
|
$scope.orderInvoice = orderBy
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Open a modal window asking the admin the details to refund the user about the provided invoice
|
||||||
|
# @param invoice {Object} invoice inherited from angular's $resource
|
||||||
|
##
|
||||||
|
$scope.generateAvoirForInvoice = (invoice)->
|
||||||
|
# open modal
|
||||||
|
modalInstance = $uibModal.open
|
||||||
|
templateUrl: '<%= asset_path "admin/invoices/avoirModal.html" %>'
|
||||||
|
controller: 'AvoirModalController'
|
||||||
|
resolve:
|
||||||
|
invoice: -> invoice
|
||||||
|
|
||||||
|
# once done, update the invoice model and inform the admin
|
||||||
|
modalInstance.result.then (res) ->
|
||||||
|
$scope.invoices.unshift res.avoir
|
||||||
|
Invoice.get {id: invoice.id}, (data) ->
|
||||||
|
invoice.has_avoir = data.has_avoir
|
||||||
|
growl.success(_t('refund_invoice_successfully_created'))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Generate an invoice reference sample from the parametrized model
|
||||||
|
# @returns {string} invoice reference sample
|
||||||
|
##
|
||||||
|
$scope.mkReference = ->
|
||||||
|
sample = $scope.invoice.reference.model
|
||||||
|
if sample
|
||||||
|
# invoice number per day (dd..dd)
|
||||||
|
sample = sample.replace(/d+(?![^\[]*])/g, (match, offset, string) ->
|
||||||
|
padWithZeros(2, match.length)
|
||||||
|
)
|
||||||
|
# invoice number per month (mm..mm)
|
||||||
|
sample = sample.replace(/m+(?![^\[]*])/g, (match, offset, string) ->
|
||||||
|
padWithZeros(12, match.length)
|
||||||
|
)
|
||||||
|
# invoice number per year (yy..yy)
|
||||||
|
sample = sample.replace(/y+(?![^\[]*])/g, (match, offset, string) ->
|
||||||
|
padWithZeros(8, match.length)
|
||||||
|
)
|
||||||
|
# date informations
|
||||||
|
sample = sample.replace(/[YMD]+(?![^\[]*])/g, (match, offset, string) ->
|
||||||
|
$scope.today.format(match)
|
||||||
|
)
|
||||||
|
# information about online selling (X[text])
|
||||||
|
sample = sample.replace(/X\[([^\]]+)\]/g, (match, p1, offset, string) ->
|
||||||
|
p1
|
||||||
|
)
|
||||||
|
# information about refunds (R[text]) - does not apply here
|
||||||
|
sample = sample.replace(/R\[([^\]]+)\]/g, "")
|
||||||
|
sample
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Generate an order nmuber sample from the parametrized model
|
||||||
|
# @returns {string} invoice reference sample
|
||||||
|
##
|
||||||
|
$scope.mkNumber = ->
|
||||||
|
sample = $scope.invoice.number.model
|
||||||
|
if sample
|
||||||
|
# global order number (nn..nn)
|
||||||
|
sample = sample.replace(/n+(?![^\[]*])/g, (match, offset, string) ->
|
||||||
|
padWithZeros(327, match.length)
|
||||||
|
)
|
||||||
|
# order number per year (yy..yy)
|
||||||
|
sample = sample.replace(/y+(?![^\[]*])/g, (match, offset, string) ->
|
||||||
|
padWithZeros(8, match.length)
|
||||||
|
)
|
||||||
|
# order number per month (mm..mm)
|
||||||
|
sample = sample.replace(/m+(?![^\[]*])/g, (match, offset, string) ->
|
||||||
|
padWithZeros(12, match.length)
|
||||||
|
)
|
||||||
|
# order number per day (dd..dd)
|
||||||
|
sample = sample.replace(/d+(?![^\[]*])/g, (match, offset, string) ->
|
||||||
|
padWithZeros(2, match.length)
|
||||||
|
)
|
||||||
|
# date informations
|
||||||
|
sample = sample.replace(/[YMD]+(?![^\[]*])/g, (match, offset, string) ->
|
||||||
|
$scope.today.format(match)
|
||||||
|
)
|
||||||
|
sample
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Open a modal dialog allowing the user to edit the invoice reference generation template
|
||||||
|
##
|
||||||
|
$scope.openEditReference = ->
|
||||||
|
modalInstance = $uibModal.open
|
||||||
|
animation: true,
|
||||||
|
templateUrl: $scope.invoice.reference.templateUrl,
|
||||||
|
size: 'lg',
|
||||||
|
resolve:
|
||||||
|
model: ->
|
||||||
|
$scope.invoice.reference.model
|
||||||
|
controller: ($scope, $uibModalInstance, model) ->
|
||||||
|
$scope.model = model
|
||||||
|
$scope.ok = ->
|
||||||
|
$uibModalInstance.close($scope.model)
|
||||||
|
$scope.cancel = ->
|
||||||
|
$uibModalInstance.dismiss('cancel')
|
||||||
|
|
||||||
|
modalInstance.result.then (model) ->
|
||||||
|
Setting.update { name: 'invoice_reference' }, { value: model }, (data)->
|
||||||
|
$scope.invoice.reference.model = model
|
||||||
|
growl.success(_t('invoice_reference_successfully_saved'))
|
||||||
|
, (error)->
|
||||||
|
growl.error(_t('an_error_occurred_while_saving_invoice_reference'))
|
||||||
|
console.error(error)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Open a modal dialog allowing the user to edit the invoice code
|
||||||
|
##
|
||||||
|
$scope.openEditCode = ->
|
||||||
|
modalInstance = $uibModal.open
|
||||||
|
animation: true,
|
||||||
|
templateUrl: $scope.invoice.code.templateUrl,
|
||||||
|
size: 'lg',
|
||||||
|
resolve:
|
||||||
|
model: ->
|
||||||
|
$scope.invoice.code.model
|
||||||
|
active: ->
|
||||||
|
$scope.invoice.code.active
|
||||||
|
controller: ($scope, $uibModalInstance, model, active) ->
|
||||||
|
$scope.codeModel = model
|
||||||
|
$scope.isSelected = active
|
||||||
|
|
||||||
|
|
||||||
|
$scope.ok = ->
|
||||||
|
$uibModalInstance.close({model: $scope.codeModel, active: $scope.isSelected})
|
||||||
|
$scope.cancel = ->
|
||||||
|
$uibModalInstance.dismiss('cancel')
|
||||||
|
|
||||||
|
modalInstance.result.then (result) ->
|
||||||
|
Setting.update { name: 'invoice_code-value' }, { value: result.model }, (data)->
|
||||||
|
$scope.invoice.code.model = result.model
|
||||||
|
if result.active
|
||||||
|
growl.success(_t('invoicing_code_succesfully_saved'))
|
||||||
|
, (error)->
|
||||||
|
growl.error(_t('an_error_occurred_while_saving_the_invoicing_code'))
|
||||||
|
console.error(error)
|
||||||
|
|
||||||
|
Setting.update { name: 'invoice_code-active' }, { value: if result.active then "true" else "false" }, (data)->
|
||||||
|
$scope.invoice.code.active = result.active
|
||||||
|
if result.active
|
||||||
|
growl.success(_t('code_successfully_activated'))
|
||||||
|
else
|
||||||
|
growl.success(_t('code_successfully_disabled'))
|
||||||
|
, (error)->
|
||||||
|
growl.error(_t('an_error_occurred_while_activating_the_invoicing_code'))
|
||||||
|
console.error(error)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Open a modal dialog allowing the user to edit the invoice number
|
||||||
|
##
|
||||||
|
$scope.openEditInvoiceNb = ->
|
||||||
|
modalInstance = $uibModal.open
|
||||||
|
animation: true,
|
||||||
|
templateUrl: $scope.invoice.number.templateUrl,
|
||||||
|
size: 'lg',
|
||||||
|
resolve:
|
||||||
|
model: ->
|
||||||
|
$scope.invoice.number.model
|
||||||
|
controller: ($scope, $uibModalInstance, model) ->
|
||||||
|
$scope.model = model
|
||||||
|
$scope.ok = ->
|
||||||
|
$uibModalInstance.close($scope.model)
|
||||||
|
$scope.cancel = ->
|
||||||
|
$uibModalInstance.dismiss('cancel')
|
||||||
|
|
||||||
|
modalInstance.result.then (model) ->
|
||||||
|
Setting.update { name: 'invoice_order-nb' }, { value: model }, (data)->
|
||||||
|
$scope.invoice.number.model = model
|
||||||
|
growl.success(_t('order_number_successfully_saved'))
|
||||||
|
, (error)->
|
||||||
|
growl.error(_t('an_error_occurred_while_saving_the_order_number'))
|
||||||
|
console.error(error)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Open a modal dialog allowing the user to edit the VAT parameters for the invoices
|
||||||
|
# The VAT can be disabled and its rate can be configured
|
||||||
|
##
|
||||||
|
$scope.openEditVAT = ->
|
||||||
|
modalInstance = $uibModal.open
|
||||||
|
animation: true,
|
||||||
|
templateUrl: $scope.invoice.VAT.templateUrl,
|
||||||
|
size: 'lg',
|
||||||
|
resolve:
|
||||||
|
rate: ->
|
||||||
|
$scope.invoice.VAT.rate
|
||||||
|
active: ->
|
||||||
|
$scope.invoice.VAT.active
|
||||||
|
controller: ($scope, $uibModalInstance, rate, active) ->
|
||||||
|
$scope.rate = rate
|
||||||
|
$scope.isSelected = active
|
||||||
|
|
||||||
|
|
||||||
|
$scope.ok = ->
|
||||||
|
$uibModalInstance.close({rate: $scope.rate, active: $scope.isSelected})
|
||||||
|
$scope.cancel = ->
|
||||||
|
$uibModalInstance.dismiss('cancel')
|
||||||
|
|
||||||
|
modalInstance.result.then (result) ->
|
||||||
|
Setting.update { name: 'invoice_VAT-rate' }, { value: result.rate+"" }, (data)->
|
||||||
|
$scope.invoice.VAT.rate = result.rate
|
||||||
|
if result.active
|
||||||
|
growl.success(_t('VAT_rate_successfully_saved'))
|
||||||
|
, (error)->
|
||||||
|
growl.error(_t('an_error_occurred_while_saving_the_VAT_rate'))
|
||||||
|
console.error(error)
|
||||||
|
|
||||||
|
Setting.update { name: 'invoice_VAT-active' }, { value: if result.active then "true" else "false" }, (data)->
|
||||||
|
$scope.invoice.VAT.active = result.active
|
||||||
|
if result.active
|
||||||
|
growl.success(_t('VAT_successfully_activated'))
|
||||||
|
else
|
||||||
|
growl.success(_t('VAT_successfully_disabled'))
|
||||||
|
, (error)->
|
||||||
|
growl.error(_t('an_error_occurred_while_activating_the_VAT'))
|
||||||
|
console.error(error)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Callback to save the value of the text zone when editing is done
|
||||||
|
##
|
||||||
|
$scope.textEditEnd = (event) ->
|
||||||
|
parsed = parseHtml($scope.invoice.text.content)
|
||||||
|
Setting.update { name: 'invoice_text' }, { value: parsed }, (data)->
|
||||||
|
$scope.invoice.text.content = parsed
|
||||||
|
growl.success(_t('text_successfully_saved'))
|
||||||
|
, (error)->
|
||||||
|
growl.error(_t('an_error_occurred_while_saving_the_text'))
|
||||||
|
console.error(error)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Callback to save the value of the legal informations zone when editing is done
|
||||||
|
##
|
||||||
|
$scope.legalsEditEnd = (event) ->
|
||||||
|
parsed = parseHtml($scope.invoice.legals.content)
|
||||||
|
Setting.update { name: 'invoice_legals' }, { value: parsed }, (data)->
|
||||||
|
$scope.invoice.legals.content = parsed
|
||||||
|
growl.success(_t('address_and_legal_information_successfully_saved'))
|
||||||
|
, (error)->
|
||||||
|
growl.error(_t('an_error_occurred_while_saving_the_address_and_the_legal_information'))
|
||||||
|
console.error(error)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### PRIVATE SCOPE ###
|
||||||
|
|
||||||
|
##
|
||||||
|
# Kind of constructor: these actions will be realized first when the controller is loaded
|
||||||
|
##
|
||||||
|
initialize = ->
|
||||||
|
# retrieve settings from the DB through the API
|
||||||
|
$scope.invoice.legals.content = settings['invoice_legals']
|
||||||
|
$scope.invoice.text.content = settings['invoice_text']
|
||||||
|
$scope.invoice.VAT.rate = parseFloat(settings['invoice_VAT-rate'])
|
||||||
|
$scope.invoice.VAT.active = (settings['invoice_VAT-active'] == "true")
|
||||||
|
$scope.invoice.number.model = settings['invoice_order-nb']
|
||||||
|
$scope.invoice.code.model = settings['invoice_code-value']
|
||||||
|
$scope.invoice.code.active = (settings['invoice_code-active'] == "true")
|
||||||
|
$scope.invoice.reference.model = settings['invoice_reference']
|
||||||
|
$scope.invoice.logo =
|
||||||
|
filetype: 'image/png'
|
||||||
|
filename: 'logo.png'
|
||||||
|
base64: settings['invoice_logo']
|
||||||
|
|
||||||
|
# Watch the logo, when a change occurs, save it
|
||||||
|
$scope.$watch 'invoice.logo', ->
|
||||||
|
if $scope.invoice.logo and $scope.invoice.logo.filesize
|
||||||
|
Setting.update { name: 'invoice_logo' }, { value: $scope.invoice.logo.base64 }, (data)->
|
||||||
|
growl.success(_t('logo_successfully_saved'))
|
||||||
|
, (error)->
|
||||||
|
growl.error(_t('an_error_occurred_while_saving_the_logo'))
|
||||||
|
console.error(error)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Output the given integer with leading zeros. If the given value is longer than the given
|
||||||
|
# length, it will be truncated.
|
||||||
|
# @param value {number} the integer to pad
|
||||||
|
# @param length {number} the length of the resulting string.
|
||||||
|
##
|
||||||
|
padWithZeros = (value, length) ->
|
||||||
|
(1e15+value+"").slice(-length)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Remove every unsupported html tag from the given html text (like <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
|
||||||
|
''
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## !!! MUST BE CALLED AT THE END of the controller
|
||||||
|
initialize()
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Controller used in the invoice refunding modal window
|
||||||
|
##
|
||||||
|
Application.Controllers.controller 'AvoirModalController', ["$scope", "$uibModalInstance", '$locale', "invoice", "Invoice", "growl", '_t'
|
||||||
|
, ($scope, $uibModalInstance, $locale, invoice, Invoice, growl, _t) ->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### PUBLIC SCOPE ###
|
||||||
|
|
||||||
|
## invoice linked to the current refund
|
||||||
|
$scope.invoice = invoice
|
||||||
|
|
||||||
|
## Associative array containing invoice_item ids associated with boolean values
|
||||||
|
$scope.partial = {}
|
||||||
|
|
||||||
|
## Default refund parameters
|
||||||
|
$scope.avoir =
|
||||||
|
invoice_id: invoice.id
|
||||||
|
subscription_to_expire: false
|
||||||
|
invoice_items_ids: []
|
||||||
|
|
||||||
|
## Possible refunding methods
|
||||||
|
$scope.avoirModes = [
|
||||||
|
{name: _t('none'), value: 'none'}
|
||||||
|
{name: _t('by_cash'), value: 'cash'}
|
||||||
|
{name: _t('by_cheque'), value: 'cheque'}
|
||||||
|
{name: _t('by_transfer'), value: 'transfer'}
|
||||||
|
]
|
||||||
|
|
||||||
|
## If a subscription was took with the current invoice, should it be canceled or not
|
||||||
|
$scope.subscriptionExpireOptions = {}
|
||||||
|
$scope.subscriptionExpireOptions[_t('yes')] = true
|
||||||
|
$scope.subscriptionExpireOptions[_t('no')] = false
|
||||||
|
|
||||||
|
## AngularUI-Bootstrap datepicker parameters to define when to refund
|
||||||
|
$scope.datePicker =
|
||||||
|
format: $locale.DATETIME_FORMATS.shortDate
|
||||||
|
opened: false # default: datePicker is not shown
|
||||||
|
options:
|
||||||
|
startingDay: Fablab.weekStartingDay
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Callback to open the datepicker
|
||||||
|
##
|
||||||
|
$scope.openDatePicker = ($event) ->
|
||||||
|
$event.preventDefault()
|
||||||
|
$event.stopPropagation()
|
||||||
|
$scope.datePicker.opened = true
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Validate the refunding and generate a refund invoice
|
||||||
|
##
|
||||||
|
$scope.ok = ->
|
||||||
|
# check that at least 1 element of the invoice is refunded
|
||||||
|
$scope.avoir.invoice_items_ids = []
|
||||||
|
for itemId, refundItem of $scope.partial
|
||||||
|
$scope.avoir.invoice_items_ids.push(parseInt(itemId)) if refundItem
|
||||||
|
|
||||||
|
if $scope.avoir.invoice_items_ids.length is 0
|
||||||
|
growl.error(_t('you_must_select_at_least_one_element_to_create_a_refund'))
|
||||||
|
else
|
||||||
|
Invoice.save {avoir: $scope.avoir}, (avoir) ->
|
||||||
|
# success
|
||||||
|
$uibModalInstance.close({avoir:avoir, invoice:$scope.invoice})
|
||||||
|
, (err) ->
|
||||||
|
# failed
|
||||||
|
growl.error(_t('unable_to_create_the_refund'))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Cancel the refund, dismiss the modal window
|
||||||
|
##
|
||||||
|
$scope.cancel = ->
|
||||||
|
$uibModalInstance.dismiss('cancel')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### PRIVATE SCOPE ###
|
||||||
|
|
||||||
|
##
|
||||||
|
# Kind of constructor: these actions will be realized first when the controller is loaded
|
||||||
|
##
|
||||||
|
initialize = ->
|
||||||
|
## if the invoice was payed with stripe, allow to refund through stripe
|
||||||
|
Invoice.get {id: invoice.id}, (data) ->
|
||||||
|
$scope.invoice = data
|
||||||
|
# default : all elements of the invoice are refund
|
||||||
|
for item in data.items
|
||||||
|
$scope.partial[item.id] = (typeof item.avoir_item_id isnt 'number')
|
||||||
|
|
||||||
|
if invoice.stripe
|
||||||
|
$scope.avoirModes.push {name: _t('online_payment'), value: 'stripe'}
|
||||||
|
|
||||||
|
|
||||||
|
## !!! MUST BE CALLED AT THE END of the controller
|
||||||
|
initialize()
|
||||||
|
]
|
@ -1,153 +0,0 @@
|
|||||||
'use strict'
|
|
||||||
|
|
||||||
### COMMON CODE ###
|
|
||||||
|
|
||||||
##
|
|
||||||
# Provides a set of common properties and methods to the $scope parameter. They are used
|
|
||||||
# in the various members' admin controllers.
|
|
||||||
#
|
|
||||||
# Provides :
|
|
||||||
# - $scope.groups = [{Group}]
|
|
||||||
# - $scope.datePicker = {}
|
|
||||||
# - $scope.submited(content)
|
|
||||||
# - $scope.cancel()
|
|
||||||
# - $scope.fileinputClass(v)
|
|
||||||
# - $scope.openDatePicker($event)
|
|
||||||
#
|
|
||||||
# Requires :
|
|
||||||
# - $state (Ui-Router) [ 'app.admin.members' ]
|
|
||||||
##
|
|
||||||
class MembersController
|
|
||||||
constructor: ($scope, $state, Group) ->
|
|
||||||
|
|
||||||
## Retrieve the profiles groups (eg. students ...)
|
|
||||||
Group.query (groups) ->
|
|
||||||
$scope.groups = groups
|
|
||||||
$scope.user.group_id = $scope.groups[0].id
|
|
||||||
|
|
||||||
## Default parameters for AngularUI-Bootstrap datepicker
|
|
||||||
$scope.datePicker =
|
|
||||||
format: 'dd/MM/yyyy'
|
|
||||||
opened: false # default: datePicker is not shown
|
|
||||||
options:
|
|
||||||
startingDay: 1 # France: the week starts on monday
|
|
||||||
|
|
||||||
|
|
||||||
##
|
|
||||||
# Shows the birth day datepicker
|
|
||||||
# @param $event {Object} jQuery event object
|
|
||||||
##
|
|
||||||
$scope.openDatePicker = ($event) ->
|
|
||||||
$event.preventDefault()
|
|
||||||
$event.stopPropagation()
|
|
||||||
$scope.datePicker.opened = true
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
##
|
|
||||||
# For use with ngUpload (https://github.com/twilson63/ngUpload).
|
|
||||||
# Intended to be the callback when an upload is done: any raised error will be stacked in the
|
|
||||||
# $scope.alerts array. If everything goes fine, the user is redirected to the members listing page.
|
|
||||||
# @param content {Object} JSON - The upload's result
|
|
||||||
##
|
|
||||||
$scope.submited = (content) ->
|
|
||||||
if !content.id?
|
|
||||||
$scope.alerts = []
|
|
||||||
angular.forEach content, (v, k)->
|
|
||||||
angular.forEach v, (err)->
|
|
||||||
$scope.alerts.push
|
|
||||||
msg: k+': '+err,
|
|
||||||
type: 'danger'
|
|
||||||
else
|
|
||||||
$state.go('app.admin.members')
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
##
|
|
||||||
# Changes the admin's view to the members list page
|
|
||||||
##
|
|
||||||
$scope.cancel = ->
|
|
||||||
$state.go('app.admin.members')
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
##
|
|
||||||
# For use with 'ng-class', returns the CSS class name for the uploads previews.
|
|
||||||
# The preview may show a placeholder or the content of the file depending on the upload state.
|
|
||||||
# @param v {*} any attribute, will be tested for truthiness (see JS evaluation rules)
|
|
||||||
##
|
|
||||||
$scope.fileinputClass = (v)->
|
|
||||||
if v
|
|
||||||
'fileinput-exists'
|
|
||||||
else
|
|
||||||
'fileinput-new'
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
##
|
|
||||||
# Controller used in the member edition page
|
|
||||||
##
|
|
||||||
Application.Controllers.controller "editMemberController", ["$scope", "$state", "$stateParams", "Member", 'dialogs', 'growl', 'Group', 'CSRF', ($scope, $state, $stateParams, Member, dialogs, growl, Group, CSRF) ->
|
|
||||||
CSRF.setMetaTags()
|
|
||||||
|
|
||||||
|
|
||||||
### PUBLIC SCOPE ###
|
|
||||||
|
|
||||||
## API URL where the form will be posted
|
|
||||||
$scope.actionUrl = "/api/members/" + $stateParams.id
|
|
||||||
|
|
||||||
## Form action on the above URL
|
|
||||||
$scope.method = 'patch'
|
|
||||||
|
|
||||||
## The user to edit
|
|
||||||
$scope.user = {}
|
|
||||||
|
|
||||||
## Profiles types (student/standard/...)
|
|
||||||
$scope.groups = []
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### PRIVATE SCOPE ###
|
|
||||||
|
|
||||||
##
|
|
||||||
# Kind of constructor: these actions will be realized first when the controller is loaded
|
|
||||||
##
|
|
||||||
initialize = ->
|
|
||||||
## Retrieve the member's profile details
|
|
||||||
Member.get {id: $stateParams.id}, (resp)->
|
|
||||||
$scope.user = resp
|
|
||||||
|
|
||||||
## Using the MembersController
|
|
||||||
new MembersController($scope, $state, Group)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## !!! MUST BE CALLED AT THE END of the controller
|
|
||||||
initialize()
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
##
|
|
||||||
# Controller used in the member's creation page (admin view)
|
|
||||||
##
|
|
||||||
Application.Controllers.controller "newMemberController", ["$scope", "$state", "$stateParams", "Member", 'Group', 'CSRF', ($scope, $state, $stateParams, Member, Group, CSRF) ->
|
|
||||||
CSRF.setMetaTags()
|
|
||||||
|
|
||||||
### PUBLIC SCOPE ###
|
|
||||||
|
|
||||||
## API URL where the form will be posted
|
|
||||||
$scope.actionUrl = "/api/members"
|
|
||||||
|
|
||||||
## Form action on the above URL
|
|
||||||
$scope.method = 'post'
|
|
||||||
|
|
||||||
## Default member's profile parameters
|
|
||||||
$scope.user =
|
|
||||||
plan_interval: ''
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Using the MembersController
|
|
||||||
new MembersController($scope, $state, Group)
|
|
||||||
]
|
|
439
app/assets/javascripts/controllers/admin/members.coffee.erb
Normal file
@ -0,0 +1,439 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
### COMMON CODE ###
|
||||||
|
|
||||||
|
##
|
||||||
|
# Provides a set of common properties and methods to the $scope parameter. They are used
|
||||||
|
# in the various members' admin controllers.
|
||||||
|
#
|
||||||
|
# Provides :
|
||||||
|
# - $scope.groups = [{Group}]
|
||||||
|
# - $scope.trainings = [{Training}]
|
||||||
|
# - $scope.plans = []
|
||||||
|
# - $scope.datePicker = {}
|
||||||
|
# - $scope.submited(content)
|
||||||
|
# - $scope.cancel()
|
||||||
|
# - $scope.fileinputClass(v)
|
||||||
|
# - $scope.openDatePicker($event)
|
||||||
|
# - $scope.openSubscriptionDatePicker($event)
|
||||||
|
#
|
||||||
|
# Requires :
|
||||||
|
# - $state (Ui-Router) [ 'app.admin.members' ]
|
||||||
|
##
|
||||||
|
class MembersController
|
||||||
|
constructor: ($scope, $state, $locale, Group, Training) ->
|
||||||
|
|
||||||
|
## Retrieve the profiles groups (eg. students ...)
|
||||||
|
Group.query (groups) ->
|
||||||
|
$scope.groups = groups
|
||||||
|
|
||||||
|
## Retrieve the list the available trainings
|
||||||
|
Training.query().$promise.then (data)->
|
||||||
|
$scope.trainings = data.map (d) ->
|
||||||
|
id: d.id
|
||||||
|
name: d.name
|
||||||
|
|
||||||
|
## Default parameters for AngularUI-Bootstrap datepicker
|
||||||
|
$scope.datePicker =
|
||||||
|
format: $locale.DATETIME_FORMATS.shortDate
|
||||||
|
opened: false # default: datePicker is not shown
|
||||||
|
subscription_date_opened: false
|
||||||
|
options:
|
||||||
|
startingDay: Fablab.weekStartingDay
|
||||||
|
|
||||||
|
##
|
||||||
|
# Shows the birth day datepicker
|
||||||
|
# @param $event {Object} jQuery event object
|
||||||
|
##
|
||||||
|
$scope.openDatePicker = ($event) ->
|
||||||
|
$event.preventDefault()
|
||||||
|
$event.stopPropagation()
|
||||||
|
$scope.datePicker.opened = true
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Shows the end of subscription datepicker
|
||||||
|
# @param $event {Object} jQuery event object
|
||||||
|
##
|
||||||
|
$scope.openSubscriptionDatePicker = ($event) ->
|
||||||
|
$event.preventDefault()
|
||||||
|
$event.stopPropagation()
|
||||||
|
$scope.datePicker.subscription_date_opened = true
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# For use with ngUpload (https://github.com/twilson63/ngUpload).
|
||||||
|
# Intended to be the callback when an upload is done: any raised error will be stacked in the
|
||||||
|
# $scope.alerts array. If everything goes fine, the user is redirected to the members listing page.
|
||||||
|
# @param content {Object} JSON - The upload's result
|
||||||
|
##
|
||||||
|
$scope.submited = (content) ->
|
||||||
|
if !content.id?
|
||||||
|
$scope.alerts = []
|
||||||
|
angular.forEach content, (v, k)->
|
||||||
|
angular.forEach v, (err)->
|
||||||
|
$scope.alerts.push
|
||||||
|
msg: k+': '+err,
|
||||||
|
type: 'danger'
|
||||||
|
else
|
||||||
|
$state.go('app.admin.members')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Changes the admin's view to the members list page
|
||||||
|
##
|
||||||
|
$scope.cancel = ->
|
||||||
|
$state.go('app.admin.members')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# For use with 'ng-class', returns the CSS class name for the uploads previews.
|
||||||
|
# The preview may show a placeholder or the content of the file depending on the upload state.
|
||||||
|
# @param v {*} any attribute, will be tested for truthiness (see JS evaluation rules)
|
||||||
|
##
|
||||||
|
$scope.fileinputClass = (v)->
|
||||||
|
if v
|
||||||
|
'fileinput-exists'
|
||||||
|
else
|
||||||
|
'fileinput-new'
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Controller used in the members/groups management page
|
||||||
|
##
|
||||||
|
Application.Controllers.controller "AdminMembersController", ["$scope", 'membersPromise', 'adminsPromise', 'growl', 'Admin', 'dialogs', '_t'
|
||||||
|
, ($scope, membersPromise, adminsPromise, growl, Admin, dialogs, _t) ->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### PUBLIC SCOPE ###
|
||||||
|
|
||||||
|
## members list
|
||||||
|
$scope.members = membersPromise
|
||||||
|
|
||||||
|
## admins list
|
||||||
|
$scope.admins = adminsPromise.admins
|
||||||
|
|
||||||
|
## Members ordering/sorting. Default: not sorted
|
||||||
|
$scope.orderMember = null
|
||||||
|
|
||||||
|
## Admins ordering/sorting. Default: not sorted
|
||||||
|
$scope.orderAdmin = null
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Change the members ordering criterion to the one provided
|
||||||
|
# @param orderBy {string} ordering criterion
|
||||||
|
##
|
||||||
|
$scope.setOrderMember = (orderBy)->
|
||||||
|
if $scope.orderMember == orderBy
|
||||||
|
$scope.orderMember = '-'+orderBy
|
||||||
|
else
|
||||||
|
$scope.orderMember = orderBy
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Change the admins ordering criterion to the one provided
|
||||||
|
# @param orderBy {string} ordering criterion
|
||||||
|
##
|
||||||
|
$scope.setOrderAdmin = (orderAdmin)->
|
||||||
|
if $scope.orderAdmin == orderAdmin
|
||||||
|
$scope.orderAdmin = '-'+orderAdmin
|
||||||
|
else
|
||||||
|
$scope.orderAdmin = orderAdmin
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Ask for confirmation then delete the specified administrator
|
||||||
|
# @param admins {Array} full list of administrators
|
||||||
|
# @param admin {Object} administrator to delete
|
||||||
|
##
|
||||||
|
$scope.destroyAdmin = (admins, admin)->
|
||||||
|
dialogs.confirm
|
||||||
|
resolve:
|
||||||
|
object: ->
|
||||||
|
title: _t('confirmation_required')
|
||||||
|
msg: _t('do_you_really_want_to_delete_this_administrator_this_cannot_be_undone')
|
||||||
|
, -> # cancel confirmed
|
||||||
|
Admin.delete id: admin.id, ->
|
||||||
|
admins.splice(findAdminIdxById(admins, admin.id), 1)
|
||||||
|
growl.success(_t('administrator_successfully_deleted'))
|
||||||
|
, (error)->
|
||||||
|
growl.error(_t('unable_to_delete_the_administrator'))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### PRIVATE SCOPE ###
|
||||||
|
|
||||||
|
##
|
||||||
|
# Iterate through the provided array and return the index of the requested admin
|
||||||
|
# @param admins {Array} full list of users with role 'admin'
|
||||||
|
# @param id {Number} user id of the admin to retrieve in the list
|
||||||
|
# @returns {Number} index of the requested admin, in the provided array
|
||||||
|
##
|
||||||
|
findAdminIdxById = (admins, id)->
|
||||||
|
(admins.map (admin)->
|
||||||
|
admin.id
|
||||||
|
).indexOf(id)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Controller used in the member edition page
|
||||||
|
##
|
||||||
|
Application.Controllers.controller "EditMemberController", ["$scope", "$state", "$stateParams", '$locale', "Member", 'Training', 'dialogs', 'growl', 'Group', 'Subscription', 'CSRF', 'memberPromise', 'tagsPromise', '$uibModal', 'Plan', '$filter', '_t'
|
||||||
|
, ($scope, $state, $stateParams, $locale, Member, Training, dialogs, growl, Group, Subscription, CSRF, memberPromise, tagsPromise, $uibModal, Plan, $filter, _t) ->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### PUBLIC SCOPE ###
|
||||||
|
|
||||||
|
## API URL where the form will be posted
|
||||||
|
$scope.actionUrl = "/api/members/" + $stateParams.id
|
||||||
|
|
||||||
|
## Form action on the above URL
|
||||||
|
$scope.method = 'patch'
|
||||||
|
|
||||||
|
## List of tags associables with user
|
||||||
|
$scope.tags = tagsPromise
|
||||||
|
|
||||||
|
## The user to edit
|
||||||
|
$scope.user = memberPromise
|
||||||
|
|
||||||
|
## the user subscription
|
||||||
|
if $scope.user.subscribed_plan? and $scope.user.subscription?
|
||||||
|
$scope.subscription = $scope.user.subscription
|
||||||
|
$scope.subscription.expired_at = $scope.subscription.expired_at
|
||||||
|
else
|
||||||
|
Plan.query group_id: $scope.user.group_id, (plans)->
|
||||||
|
$scope.plans = plans
|
||||||
|
for plan in $scope.plans
|
||||||
|
plan.nameToDisplay = $filter('humanReadablePlanName')(plan)
|
||||||
|
|
||||||
|
|
||||||
|
## Available trainings list
|
||||||
|
$scope.trainings = []
|
||||||
|
|
||||||
|
## Profiles types (student/standard/...)
|
||||||
|
$scope.groups = []
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Open a modal dialog, allowing the admin to extend the current user's subscription (freely or not)
|
||||||
|
# @param subscription {Object} User's subscription object
|
||||||
|
# @param free {boolean} True if the extent is offered, false otherwise
|
||||||
|
##
|
||||||
|
$scope.updateSubscriptionModal = (subscription, free)->
|
||||||
|
modalInstance = $uibModal.open
|
||||||
|
animation: true,
|
||||||
|
templateUrl: '<%= asset_path "admin/subscriptions/expired_at_modal.html" %>'
|
||||||
|
size: 'lg',
|
||||||
|
controller: ['$scope', '$uibModalInstance', 'Subscription', ($scope, $uibModalInstance, Subscription) ->
|
||||||
|
$scope.new_expired_at = angular.copy(subscription.expired_at)
|
||||||
|
$scope.free = free
|
||||||
|
$scope.datePicker =
|
||||||
|
opened: false
|
||||||
|
format: $locale.DATETIME_FORMATS.shortDate
|
||||||
|
options:
|
||||||
|
startingDay: Fablab.weekStartingDay
|
||||||
|
minDate: new Date
|
||||||
|
|
||||||
|
$scope.openDatePicker = (ev)->
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
$scope.datePicker.opened = true
|
||||||
|
|
||||||
|
|
||||||
|
$scope.ok = ->
|
||||||
|
Subscription.update { id: subscription.id }, { subscription: { expired_at: $scope.new_expired_at, free: free } }, (_subscription)->
|
||||||
|
growl.success(_t('you_successfully_changed_the_expiration_date_of_the_user_s_subscription'))
|
||||||
|
$uibModalInstance.close(_subscription)
|
||||||
|
, (error)->
|
||||||
|
growl.error(_t('a_problem_occurred_while_saving_the_date'))
|
||||||
|
$scope.cancel = ->
|
||||||
|
$uibModalInstance.dismiss('cancel')
|
||||||
|
]
|
||||||
|
# once the form was validated succesfully ...
|
||||||
|
modalInstance.result.then (subscription) ->
|
||||||
|
$scope.subscription.expired_at = subscription.expired_at
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Open a modal dialog allowing the admin to set a subscription for the given user.
|
||||||
|
# @param user {Object} User object, user currently reviewed, as recovered from GET /api/members/:id
|
||||||
|
# @param plans {Array} List of plans, availables for the currently reviewed user, as recovered from GET /api/plans
|
||||||
|
##
|
||||||
|
$scope.createSubscriptionModal = (user, plans)->
|
||||||
|
modalInstance = $uibModal.open
|
||||||
|
animation: true,
|
||||||
|
templateUrl: '<%= asset_path "admin/subscriptions/create_modal.html" %>'
|
||||||
|
size: 'lg',
|
||||||
|
controller: ['$scope', '$uibModalInstance', 'Subscription', 'Group', ($scope, $uibModalInstance, Subscription, Group) ->
|
||||||
|
|
||||||
|
## selected user
|
||||||
|
$scope.user = user
|
||||||
|
|
||||||
|
## available plans for the selected user
|
||||||
|
$scope.plans = plans
|
||||||
|
|
||||||
|
##
|
||||||
|
# Generate a string identifying the given plan by literal humain-readable name
|
||||||
|
# @param plan {Object} Plan object, as recovered from GET /api/plan/:id
|
||||||
|
# @param groups {Array} List of Groups objects, as recovered from GET /api/groups
|
||||||
|
# @param short {boolean} If true, the generated name will contains the group slug, otherwise the group full name
|
||||||
|
# will be included.
|
||||||
|
# @returns {String}
|
||||||
|
##
|
||||||
|
$scope.humanReadablePlanName = (plan, groups, short)->
|
||||||
|
"#{$filter('humanReadablePlanName')(plan, groups, short)}"
|
||||||
|
|
||||||
|
##
|
||||||
|
# Modal dialog validation callback
|
||||||
|
##
|
||||||
|
$scope.ok = ->
|
||||||
|
$scope.subscription.user_id = user.id
|
||||||
|
Subscription.save { }, { subscription: $scope.subscription }, (_subscription)->
|
||||||
|
|
||||||
|
growl.success(_t('subscription_successfully_purchased'))
|
||||||
|
$uibModalInstance.close(_subscription)
|
||||||
|
$state.reload()
|
||||||
|
, (error)->
|
||||||
|
growl.error(_t('a_problem_occurred_while_taking_the_subscription'))
|
||||||
|
|
||||||
|
##
|
||||||
|
# Modal dialog cancellation callback
|
||||||
|
##
|
||||||
|
$scope.cancel = ->
|
||||||
|
$uibModalInstance.dismiss('cancel')
|
||||||
|
]
|
||||||
|
# once the form was validated succesfully ...
|
||||||
|
modalInstance.result.then (subscription) ->
|
||||||
|
$scope.subscription = subscription
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### PRIVATE SCOPE ###
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Kind of constructor: these actions will be realized first when the controller is loaded
|
||||||
|
##
|
||||||
|
initialize = ->
|
||||||
|
CSRF.setMetaTags()
|
||||||
|
|
||||||
|
# init the birth date to JS object
|
||||||
|
$scope.user.profile.birthday = moment($scope.user.profile.birthday).toDate()
|
||||||
|
|
||||||
|
## the user subscription
|
||||||
|
if $scope.user.subscribed_plan? and $scope.user.subscription?
|
||||||
|
$scope.subscription = $scope.user.subscription
|
||||||
|
$scope.subscription.expired_at = $scope.subscription.expired_at
|
||||||
|
else
|
||||||
|
Plan.query group_id: $scope.user.group_id, (plans)->
|
||||||
|
$scope.plans = plans
|
||||||
|
for plan in $scope.plans
|
||||||
|
plan.nameToDisplay = "#{plan.base_name} - #{plan.interval}"
|
||||||
|
|
||||||
|
# Using the MembersController
|
||||||
|
new MembersController($scope, $state, $locale, Group, Training)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## !!! MUST BE CALLED AT THE END of the controller
|
||||||
|
initialize()
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Controller used in the member's creation page (admin view)
|
||||||
|
##
|
||||||
|
Application.Controllers.controller "NewMemberController", ["$scope", "$state", "$locale", "$stateParams", "Member", 'Training', 'Group', 'CSRF'
|
||||||
|
, ($scope, $state, $locale, $stateParams, Member, Training, Group, CSRF) ->
|
||||||
|
|
||||||
|
CSRF.setMetaTags()
|
||||||
|
|
||||||
|
### PUBLIC SCOPE ###
|
||||||
|
|
||||||
|
## API URL where the form will be posted
|
||||||
|
$scope.actionUrl = "/api/members"
|
||||||
|
|
||||||
|
## Form action on the above URL
|
||||||
|
$scope.method = 'post'
|
||||||
|
|
||||||
|
## Default member's profile parameters
|
||||||
|
$scope.user =
|
||||||
|
plan_interval: ''
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Using the MembersController
|
||||||
|
new MembersController($scope, $state, $locale, Group, Training)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Controller used in the admin's creation page (admin view)
|
||||||
|
##
|
||||||
|
Application.Controllers.controller 'NewAdminController', ['$state', '$scope', '$locale', 'Admin', 'growl', ($state, $scope, $locale, Admin, growl)->
|
||||||
|
|
||||||
|
## default admin profile
|
||||||
|
$scope.admin =
|
||||||
|
profile_attributes:
|
||||||
|
gender: true
|
||||||
|
|
||||||
|
## Default parameters for AngularUI-Bootstrap datepicker
|
||||||
|
$scope.datePicker =
|
||||||
|
format: $locale.DATETIME_FORMATS.shortDate
|
||||||
|
opened: false
|
||||||
|
options:
|
||||||
|
startingDay: Fablab.weekStartingDay
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Shows the birth day datepicker
|
||||||
|
# @param $event {Object} jQuery event object
|
||||||
|
##
|
||||||
|
$scope.openDatePicker = ($event)->
|
||||||
|
$scope.datePicker.opened = true
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Send the new admin, currently stored in $scope.admin, to the server for database saving
|
||||||
|
##
|
||||||
|
$scope.saveAdmin = ->
|
||||||
|
Admin.save {}, { admin: $scope.admin }, ->
|
||||||
|
growl.success(_t('administrator_successfully_created_he_will_receive_his_connection_directives_by_email', {GENDER:getGender($scope.admin)}, "messageformat"))
|
||||||
|
$state.go('app.admin.members')
|
||||||
|
, (error)->
|
||||||
|
console.log(error)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### PRIVATE SCOPE ###
|
||||||
|
|
||||||
|
##
|
||||||
|
# Return an enumerable meaninful string for the gender of the provider user
|
||||||
|
# @param user {Object} Database user record
|
||||||
|
# @return {string} 'male' or 'female'
|
||||||
|
##
|
||||||
|
getGender = (user) ->
|
||||||
|
if user.profile_attributes
|
||||||
|
if user.profile_attributes.gender then 'male' else 'female'
|
||||||
|
else 'other'
|
||||||
|
|
||||||
|
|
||||||
|
]
|
260
app/assets/javascripts/controllers/admin/plans.coffee.erb
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
### COMMON CODE ###
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class PlanController
|
||||||
|
|
||||||
|
constructor: ($scope, groups, plans, machines, prices, partners, CSRF) ->
|
||||||
|
# protection against request forgery
|
||||||
|
CSRF.setMetaTags()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## groups list
|
||||||
|
$scope.groups = groups
|
||||||
|
|
||||||
|
## plans list
|
||||||
|
$scope.plans = plans
|
||||||
|
|
||||||
|
## machines list
|
||||||
|
$scope.machines = machines
|
||||||
|
|
||||||
|
## users with role 'partner', notifiables for a partner plan
|
||||||
|
$scope.partners = partners.users
|
||||||
|
|
||||||
|
## Subscriptions prices, machines prices and training prices, per groups
|
||||||
|
$scope.group_pricing = prices
|
||||||
|
|
||||||
|
##
|
||||||
|
# For use with 'ng-class', returns the CSS class name for the uploads previews.
|
||||||
|
# The preview may show a placeholder or the content of the file depending on the upload state.
|
||||||
|
# @param v {*} any attribute, will be tested for truthiness (see JS evaluation rules)
|
||||||
|
##
|
||||||
|
$scope.fileinputClass = (v)->
|
||||||
|
if v
|
||||||
|
'fileinput-exists'
|
||||||
|
else
|
||||||
|
'fileinput-new'
|
||||||
|
|
||||||
|
##
|
||||||
|
# Mark the provided file for deletion
|
||||||
|
# @param file {Object}
|
||||||
|
##
|
||||||
|
$scope.deleteFile = (file) ->
|
||||||
|
if file? and file.id?
|
||||||
|
file._destroy = true
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Retrieve a plan from its numeric identifier
|
||||||
|
# @param id {number} plan ID
|
||||||
|
# @returns {Object} Plan, inherits from $resource
|
||||||
|
##
|
||||||
|
$scope.getPlanFromId = (id) ->
|
||||||
|
for plan in $scope.plans
|
||||||
|
if plan.id == id
|
||||||
|
return plan
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Controller used in the plan creation form
|
||||||
|
##
|
||||||
|
Application.Controllers.controller 'NewPlanController', ['$scope', '$uibModal', 'groups', 'plans', 'machines', 'prices', 'partners', 'CSRF', '$state', 'growl', '_t', '$locale'
|
||||||
|
, ($scope, $uibModal, groups, plans, machines, prices, partners, CSRF, $state, growl, _t, $locale) ->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### PRIVATE STATIC CONSTANTS ###
|
||||||
|
|
||||||
|
## when creating a new contact for a partner plan, this ID will be sent to the server
|
||||||
|
NEW_PARTNER_ID: null
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### PUBLIC SCOPE ###
|
||||||
|
|
||||||
|
## current form is used to create a new plan
|
||||||
|
$scope.mode = 'creation'
|
||||||
|
|
||||||
|
## prices bindings
|
||||||
|
$scope.prices =
|
||||||
|
training: {}
|
||||||
|
machine: {}
|
||||||
|
|
||||||
|
## form inputs bindings
|
||||||
|
$scope.plan =
|
||||||
|
type: null
|
||||||
|
group_id: null
|
||||||
|
interval: null
|
||||||
|
intervalCount: 0
|
||||||
|
amount: null
|
||||||
|
isRolling: false
|
||||||
|
partnerId: null
|
||||||
|
partnerContact: null
|
||||||
|
ui_weight: 0
|
||||||
|
|
||||||
|
## API URL where the form will be posted
|
||||||
|
$scope.actionUrl = "/api/plans/"
|
||||||
|
|
||||||
|
## HTTP method for the rest API
|
||||||
|
$scope.method = 'POST'
|
||||||
|
|
||||||
|
|
||||||
|
## currency symbol for the current locale (cf. angular-i18n)
|
||||||
|
$scope.currencySymbol = $locale.NUMBER_FORMATS.CURRENCY_SYM;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Checks if the partner contact is a valid data. Used in the form validation process
|
||||||
|
# @returns {boolean}
|
||||||
|
##
|
||||||
|
$scope.partnerIsValid = ->
|
||||||
|
($scope.plan.type == "Plan") or ($scope.plan.partnerId or ($scope.plan.partnerContact and $scope.plan.partnerContact.email))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Open a modal dialog allowing the admin to create a new partner user
|
||||||
|
##
|
||||||
|
$scope.openPartnerNewModal = (subscription)->
|
||||||
|
modalInstance = $uibModal.open
|
||||||
|
animation: true,
|
||||||
|
templateUrl: '<%= asset_path "shared/_partner_new_modal.html" %>'
|
||||||
|
size: 'lg',
|
||||||
|
controller: ['$scope', '$uibModalInstance', 'User', ($scope, $uibModalInstance, User) ->
|
||||||
|
$scope.partner = {}
|
||||||
|
|
||||||
|
$scope.ok = ->
|
||||||
|
User.save {}, { user: $scope.partner }, (user)->
|
||||||
|
$scope.partner.id = user.id
|
||||||
|
$scope.partner.name = "#{user.first_name} #{user.last_name}"
|
||||||
|
$uibModalInstance.close($scope.partner)
|
||||||
|
, (error)->
|
||||||
|
growl.error(_t('unable_to_save_this_user_check_that_there_isnt_an_already_a_user_with_the_same_name'))
|
||||||
|
$scope.cancel = ->
|
||||||
|
$uibModalInstance.dismiss('cancel')
|
||||||
|
]
|
||||||
|
# once the form was validated succesfully ...
|
||||||
|
modalInstance.result.then (partner) ->
|
||||||
|
$scope.partners.push(partner)
|
||||||
|
$scope.plan.partnerId = partner.id
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Display some messages and redirect the user, once the form was submitted, depending on the result status
|
||||||
|
# (failed/succeeded).
|
||||||
|
# @param content {Object}
|
||||||
|
##
|
||||||
|
$scope.afterSubmit = (content) ->
|
||||||
|
if !content.id? and !content.plan_ids?
|
||||||
|
growl.error(_t('unable_to_create_the_subscription_please_try_again'))
|
||||||
|
else
|
||||||
|
growl.success(_t('successfully_created_subscription(s)_dont_forget_to_redefine_prices'))
|
||||||
|
if content.plan_ids?
|
||||||
|
$state.go('app.admin.pricing')
|
||||||
|
else
|
||||||
|
if content.id?
|
||||||
|
$state.go('app.admin.plans.edit', {id: content.id})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
new PlanController($scope, groups, plans, machines, prices, partners, CSRF)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Controller used in the plan edition form
|
||||||
|
##
|
||||||
|
Application.Controllers.controller 'EditPlanController', ['$scope', 'groups', 'plans', 'planPromise', 'machines', 'prices', 'partners', 'CSRF', '$state', '$stateParams', 'growl', '$filter', '_t', '$locale'
|
||||||
|
, ($scope, groups, plans, planPromise, machines, prices, partners, CSRF, $state, $stateParams, growl, $filter, _t, $locale) ->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### PUBLIC SCOPE ###
|
||||||
|
$scope.groups = groups
|
||||||
|
## current form is used for edition mode
|
||||||
|
$scope.mode = 'edition'
|
||||||
|
|
||||||
|
## edited plan data
|
||||||
|
$scope.plan = planPromise
|
||||||
|
$scope.plan.type = "Plan" if $scope.plan.type == null
|
||||||
|
|
||||||
|
## API URL where the form will be posted
|
||||||
|
$scope.actionUrl = "/api/plans/" + $stateParams.id
|
||||||
|
|
||||||
|
## HTTP method for the rest API
|
||||||
|
$scope.method = 'PATCH'
|
||||||
|
|
||||||
|
|
||||||
|
## currency symbol for the current locale (cf. angular-i18n)
|
||||||
|
$scope.currencySymbol = $locale.NUMBER_FORMATS.CURRENCY_SYM;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# If a parent plan was set ($scope.plan.parent), the prices will be copied from this parent plan into
|
||||||
|
# the current plan prices list. Otherwise, the current plan prices will be erased.
|
||||||
|
##
|
||||||
|
$scope.copyPricesFromPlan = ->
|
||||||
|
if $scope.plan.parent
|
||||||
|
parentPlan = $scope.getPlanFromId($scope.plan.parent)
|
||||||
|
for parentPrice in parentPlan.prices
|
||||||
|
for childKey, childPrice of $scope.plan.prices
|
||||||
|
if childPrice.priceable_type == parentPrice.priceable_type and childPrice.priceable_id == parentPrice.priceable_id
|
||||||
|
$scope.plan.prices[childKey].amount = parentPrice.amount
|
||||||
|
break
|
||||||
|
# if no plan were selected, unset every prices
|
||||||
|
else
|
||||||
|
for key, price of $scope.plan.prices
|
||||||
|
$scope.plan.prices[key].amount = 0
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Display some messages once the form was submitted, depending on the result status (failed/succeeded)
|
||||||
|
# @param content {Object}
|
||||||
|
##
|
||||||
|
$scope.afterSubmit = (content) ->
|
||||||
|
if !content.id? and !content.plan_ids?
|
||||||
|
growl.error(_t('unable_to_save_subscription_changes_please_try_again'))
|
||||||
|
else
|
||||||
|
growl.success(_t('subscription_successfully_changed'))
|
||||||
|
$state.go('app.admin.pricing')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Generate a string identifying the given plan by literal humain-readable name
|
||||||
|
# @param plan {Object} Plan object, as recovered from GET /api/plan/:id
|
||||||
|
# @param groups {Array} List of Groups objects, as recovered from GET /api/groups
|
||||||
|
# @param short {boolean} If true, the generated name will contains the group slug, otherwise the group full name
|
||||||
|
# will be included.
|
||||||
|
# @returns {String}
|
||||||
|
##
|
||||||
|
$scope.humanReadablePlanName = (plan, groups, short)->
|
||||||
|
"#{$filter('humanReadablePlanName')(plan, groups, short)}"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### PRIVATE SCOPE ###
|
||||||
|
|
||||||
|
##
|
||||||
|
# Kind of constructor: these actions will be realized first when the controller is loaded
|
||||||
|
##
|
||||||
|
initialize = ->
|
||||||
|
# Using the PlansController
|
||||||
|
new PlanController($scope, groups, plans, machines, prices, partners, CSRF)
|
||||||
|
|
||||||
|
## !!! MUST BE CALLED AT THE END of the controller
|
||||||
|
initialize()
|
||||||
|
]
|
383
app/assets/javascripts/controllers/admin/pricing.coffee.erb
Normal file
@ -0,0 +1,383 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
##
|
||||||
|
# Controller used in the prices edition page
|
||||||
|
##
|
||||||
|
Application.Controllers.controller "EditPricingController", ["$scope", "$state", '$uibModal', 'Training', 'TrainingsPricing', 'Machine', '$filter', 'Credit', 'Pricing', 'Plan', 'plans', 'groups', 'growl', 'machinesPricesPromise', 'Price', 'dialogs', 'trainingsPricingsPromise', '_t'
|
||||||
|
, ($scope, $state, $uibModal, Training, TrainingsPricing, Machine, $filter, Credit, Pricing, Plan, plans, groups, growl, machinesPricesPromise, Price, dialogs, trainingsPricingsPromise, _t) ->
|
||||||
|
|
||||||
|
### PUBLIC SCOPE ###
|
||||||
|
## List of machines prices (not considering any plan)
|
||||||
|
$scope.machinesPrices = machinesPricesPromise.prices
|
||||||
|
|
||||||
|
## List of trainings pricing
|
||||||
|
$scope.trainingsPricings = trainingsPricingsPromise
|
||||||
|
|
||||||
|
## List of available subscriptions plans (eg. student/month, PME/year ...)
|
||||||
|
$scope.plans = plans
|
||||||
|
|
||||||
|
## List of groups (eg. normal, student ...)
|
||||||
|
$scope.groups = groups
|
||||||
|
|
||||||
|
## Associate free machine hours with subscriptions
|
||||||
|
$scope.machineCredits = []
|
||||||
|
|
||||||
|
## Array of associations (plan <-> training)
|
||||||
|
$scope.trainingCredits = []
|
||||||
|
|
||||||
|
## Associate a plan with all its trainings ids
|
||||||
|
$scope.trainingCreditsGroups = {}
|
||||||
|
|
||||||
|
## List of trainings
|
||||||
|
$scope.trainings = []
|
||||||
|
|
||||||
|
## List of machines
|
||||||
|
$scope.machines = []
|
||||||
|
|
||||||
|
## The plans list ordering. Default: by group
|
||||||
|
$scope.orderPlans = 'group_id'
|
||||||
|
|
||||||
|
## Status of the drop-down menu in Credits tab
|
||||||
|
$scope.status =
|
||||||
|
isopen: false
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
$scope.findTrainingsPricing = (trainingsPricings, trainingId, groupId)->
|
||||||
|
for trainingsPricing in trainingsPricings
|
||||||
|
if trainingsPricing.training_id == trainingId and trainingsPricing.group_id == groupId
|
||||||
|
return trainingsPricing
|
||||||
|
|
||||||
|
|
||||||
|
$scope.updateTrainingsPricing = (data, trainingsPricing)->
|
||||||
|
if data?
|
||||||
|
TrainingsPricing.update({ id: trainingsPricing.id }, { trainings_pricing: { amount: data } }).$promise
|
||||||
|
else
|
||||||
|
_t('please_specify_a_number')
|
||||||
|
|
||||||
|
##
|
||||||
|
# Retrieve a plan from its given identifier and returns it
|
||||||
|
# @param id {number} plan ID
|
||||||
|
# @returns {Object} Plan, inherits from $resource
|
||||||
|
##
|
||||||
|
$scope.getPlanFromId = (id) ->
|
||||||
|
for plan in $scope.plans
|
||||||
|
if plan.id == parseInt(id)
|
||||||
|
return plan
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Retrieve a group from its given identifier and returns it
|
||||||
|
# @param id {number} group ID
|
||||||
|
# @returns {Object} Group, inherits from $resource
|
||||||
|
##
|
||||||
|
$scope.getGroupFromId = (groups, id) ->
|
||||||
|
for group in groups
|
||||||
|
if group.id == parseInt(id)
|
||||||
|
return group
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Returns a human readable string of named trainings, according to the provided array.
|
||||||
|
# $scope.trainings may contains the full list of training. The returned string will only contains the trainings
|
||||||
|
# whom ID are given in the provided parameter
|
||||||
|
# @param trainings {Array<number>} trainings IDs array
|
||||||
|
##
|
||||||
|
$scope.showTrainings = (trainings) ->
|
||||||
|
unless angular.isArray(trainings) and trainings.length > 0
|
||||||
|
return _t('none')
|
||||||
|
|
||||||
|
selected = []
|
||||||
|
angular.forEach $scope.trainings, (t) ->
|
||||||
|
if trainings.indexOf(t.id) >= 0
|
||||||
|
selected.push t.name
|
||||||
|
return if selected.length then selected.join(' | ') else _t('none')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Validation callback when editing training's credits. Save the changes.
|
||||||
|
# @param newdata {Object} training and associated plans
|
||||||
|
# @param planId {number|string} plan id
|
||||||
|
##
|
||||||
|
$scope.saveTrainingCredits = (newdata, planId) ->
|
||||||
|
# save the number of credits
|
||||||
|
Plan.update {id: planId},
|
||||||
|
training_credit_nb: newdata.training_credits
|
||||||
|
, angular.noop() # do nothing in case of success
|
||||||
|
, (error) ->
|
||||||
|
growl.error(_t('an_error_occurred_while_saving_the_number_of_credits'))
|
||||||
|
|
||||||
|
# save the associated trainings
|
||||||
|
angular.forEach $scope.trainingCreditsGroups, (original, key) ->
|
||||||
|
if parseInt(key) == parseInt(planId) # we've got the original data
|
||||||
|
if original.join('_') != newdata.training_ids.join('_') # if any changes
|
||||||
|
# iterate through the previous credits to remove
|
||||||
|
angular.forEach original, (oldTrainingId) ->
|
||||||
|
if newdata.training_ids.indexOf(oldTrainingId) == -1
|
||||||
|
tc = findTrainingCredit(oldTrainingId, planId)
|
||||||
|
if tc
|
||||||
|
tc.$delete {}
|
||||||
|
, ->
|
||||||
|
$scope.trainingCredits.splice($scope.trainingCredits.indexOf(tc), 1)
|
||||||
|
$scope.trainingCreditsGroups[planId].splice($scope.trainingCreditsGroups[planId].indexOf(tc.id), 1)
|
||||||
|
, (error) ->
|
||||||
|
growl.error(_t('an_error_occurred_while_deleting_credit_with_the_TRAINING', {TRAINING:tc.creditable.name}))
|
||||||
|
else
|
||||||
|
growl.error(_t('an_error_occurred_unable_to_find_the_credit_to_revoke'))
|
||||||
|
|
||||||
|
# iterate through the new credits to add
|
||||||
|
angular.forEach newdata.training_ids, (newTrainingId) ->
|
||||||
|
if original.indexOf(newTrainingId) == -1
|
||||||
|
Credit.save
|
||||||
|
credit:
|
||||||
|
creditable_id: newTrainingId
|
||||||
|
creditable_type: 'Training'
|
||||||
|
plan_id: planId
|
||||||
|
, (newTc) -> # success
|
||||||
|
$scope.trainingCredits.push(newTc)
|
||||||
|
$scope.trainingCreditsGroups[newTc.plan_id].push(newTc.creditable_id)
|
||||||
|
, (error) -> # failed
|
||||||
|
training = getTrainingFromId(newTrainingId)
|
||||||
|
growl.error(_t('an_error_occurred_while_creating_credit_with_the_TRAINING', {TRAINING: training.name}))
|
||||||
|
console.error(error)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Cancel the current training credit modification
|
||||||
|
# @param rowform {Object} see http://vitalets.github.io/angular-xeditable/
|
||||||
|
##
|
||||||
|
$scope.cancelTrainingCredit = (rowform) ->
|
||||||
|
rowform.$cancel()
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Create a new empty entry in the $scope.machineCredits array
|
||||||
|
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
|
||||||
|
##
|
||||||
|
$scope.addMachineCredit = (e)->
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
$scope.inserted =
|
||||||
|
creditable_type: 'Machine'
|
||||||
|
$scope.machineCredits.push($scope.inserted)
|
||||||
|
$scope.status.isopen = !$scope.status.isopen
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# In the Credits tab, while editing a machine credit row, select the current machine from the
|
||||||
|
# drop-down list of machines as the current item.
|
||||||
|
# @param credit {Object} credit object, inherited from $resource
|
||||||
|
##
|
||||||
|
$scope.showCreditableName = (credit) ->
|
||||||
|
selected = _t('not_set')
|
||||||
|
if credit and credit.creditable_id
|
||||||
|
angular.forEach $scope.machines, (m)->
|
||||||
|
if m.id == credit.creditable_id
|
||||||
|
selected = m.name+' ( id. '+m.id+' )'
|
||||||
|
return selected
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Validation callback when editing machine's credits. Save the changes.
|
||||||
|
# This will prevent the creation of two credits associating the same machine and plan.
|
||||||
|
# @param data {Object} machine, associated plan and number of credit hours.
|
||||||
|
# @param [id] {number} credit id for edition, create a new credit object if not provided
|
||||||
|
##
|
||||||
|
$scope.saveMachineCredit = (data, id) ->
|
||||||
|
for mc in $scope.machineCredits
|
||||||
|
if mc.plan_id == data.plan_id and mc.creditable_id == data.creditable_id and (id == null or mc.id != id)
|
||||||
|
growl.error(_t('error_a_credit_linking_this_machine_with_that_subscription_already_exists'))
|
||||||
|
unless id
|
||||||
|
$scope.machineCredits.pop()
|
||||||
|
return false
|
||||||
|
|
||||||
|
if id?
|
||||||
|
Credit.update {id: id}, credit: data, ->
|
||||||
|
growl.success(_t('changes_have_been_successfully_saved'))
|
||||||
|
else
|
||||||
|
data.creditable_type = 'Machine'
|
||||||
|
Credit.save
|
||||||
|
credit: data
|
||||||
|
, (resp) ->
|
||||||
|
$scope.machineCredits[$scope.machineCredits.length-1].id = resp.id
|
||||||
|
growl.success(_t('credit_was_successfully_saved'))
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Removes the newly inserted but not saved machine credit / Cancel the current machine credit modification
|
||||||
|
# @param rowform {Object} see http://vitalets.github.io/angular-xeditable/
|
||||||
|
# @param index {number} theme index in the $scope.machineCredits array
|
||||||
|
##
|
||||||
|
$scope.cancelMachineCredit = (rowform, index) ->
|
||||||
|
if $scope.machineCredits[index].id?
|
||||||
|
rowform.$cancel()
|
||||||
|
else
|
||||||
|
$scope.machineCredits.splice(index, 1)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Deletes the machine credit at the specified index
|
||||||
|
# @param index {number} machine credit index in the $scope.machineCredits array
|
||||||
|
##
|
||||||
|
$scope.removeMachineCredit = (index) ->
|
||||||
|
Credit.delete $scope.machineCredits[index]
|
||||||
|
$scope.machineCredits.splice(index, 1)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# If the plan does not have a type, return a default value for display purposes
|
||||||
|
# @param type {string|undefined|null} plan's type (eg. 'partner')
|
||||||
|
# @returns {string}
|
||||||
|
##
|
||||||
|
$scope.getPlanType = (type) ->
|
||||||
|
if type == 'PartnerPlan'
|
||||||
|
return _t('partner')
|
||||||
|
else return _t('standard')
|
||||||
|
|
||||||
|
##
|
||||||
|
# Change the plans ordering criterion to the one provided
|
||||||
|
# @param orderBy {string} ordering criterion
|
||||||
|
##
|
||||||
|
$scope.setOrderPlans = (orderBy) ->
|
||||||
|
if $scope.orderPlans == orderBy
|
||||||
|
$scope.orderPlans = '-'+orderBy
|
||||||
|
else
|
||||||
|
$scope.orderPlans = orderBy
|
||||||
|
|
||||||
|
##
|
||||||
|
# Retrieve a price from prices array by a machineId and a groupId
|
||||||
|
##
|
||||||
|
$scope.findPriceBy = (prices, machineId, groupId)->
|
||||||
|
for price in prices
|
||||||
|
if price.priceable_id == machineId and price.group_id == groupId
|
||||||
|
return price
|
||||||
|
|
||||||
|
##
|
||||||
|
# update a price for a machine and a group, not considering any plan
|
||||||
|
##
|
||||||
|
$scope.updatePrice = (data, price)->
|
||||||
|
if data?
|
||||||
|
Price.update({ id: price.id }, { price: { amount: data } }).$promise
|
||||||
|
else
|
||||||
|
_t('please_specify_a_number')
|
||||||
|
|
||||||
|
##
|
||||||
|
# Delete the specified subcription plan
|
||||||
|
# @param id {number} plan id
|
||||||
|
##
|
||||||
|
$scope.deletePlan = (plans, id) ->
|
||||||
|
if typeof id != 'number'
|
||||||
|
console.error('[editPricingController::deletePlan] Error: invalid id parameter')
|
||||||
|
else
|
||||||
|
# open a confirmation dialog
|
||||||
|
dialogs.confirm
|
||||||
|
resolve:
|
||||||
|
object: ->
|
||||||
|
title: _t('confirmation_required')
|
||||||
|
msg: _t('do_you_really_want_to_delete_this_subscription_plan')
|
||||||
|
, ->
|
||||||
|
# the admin has confirmed, delete the plan
|
||||||
|
Plan.delete {id: id}, (res) ->
|
||||||
|
growl.success(_t('subscription_plan_was_successfully_deleted'))
|
||||||
|
$scope.plans.splice(findPlanIdxById(plans, id), 1)
|
||||||
|
|
||||||
|
, (error) ->
|
||||||
|
console.error('[editPricingController::deletePlan] Error: '+error.statusText) if error.statusText
|
||||||
|
growl.error(_t('unable_to_delete_the_specified_subscription_an_error_occurred'))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Generate a string identifying the given plan by literal humain-readable name
|
||||||
|
# @param plan {Object} Plan object, as recovered from GET /api/plan/:id
|
||||||
|
# @param groups {Array} List of Groups objects, as recovered from GET /api/groups
|
||||||
|
# @param short {boolean} If true, the generated name will contains the group slug, otherwise the group full name
|
||||||
|
# will be included.
|
||||||
|
# @returns {String}
|
||||||
|
##
|
||||||
|
$scope.humanReadablePlanName = (plan, groups, short)->
|
||||||
|
"#{$filter('humanReadablePlanName')(plan, groups, short)}"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### PRIVATE SCOPE ###
|
||||||
|
|
||||||
|
findPlanIdxById = (plans, id)->
|
||||||
|
(plans.map (plan)->
|
||||||
|
plan.id
|
||||||
|
).indexOf(id)
|
||||||
|
|
||||||
|
##
|
||||||
|
# Kind of constructor: these actions will be realized first when the controller is loaded
|
||||||
|
##
|
||||||
|
initialize = ->
|
||||||
|
|
||||||
|
Credit.query({creditable_type: 'Training'}).$promise.then (data)->
|
||||||
|
$scope.trainingCredits = data
|
||||||
|
$scope.trainingCreditsGroups = groupCreditsByPlan(data)
|
||||||
|
|
||||||
|
## adds empty array for plan which hasn't any credits yet
|
||||||
|
for plan in $scope.plans
|
||||||
|
unless $scope.trainingCreditsGroups[plan.id]?
|
||||||
|
$scope.trainingCreditsGroups[plan.id] = []
|
||||||
|
|
||||||
|
Credit.query({creditable_type: 'Machine'}).$promise.then (data)->
|
||||||
|
$scope.machineCredits = data
|
||||||
|
|
||||||
|
Training.query().$promise.then (data)->
|
||||||
|
$scope.trainings = data
|
||||||
|
|
||||||
|
Machine.query().$promise.then (data)->
|
||||||
|
$scope.machines = data
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Group the given credits array into a map associating the plan ID with its associated trainings/machines
|
||||||
|
# @return {Object} the association map
|
||||||
|
##
|
||||||
|
groupCreditsByPlan = (credits) ->
|
||||||
|
creditsMap = {}
|
||||||
|
angular.forEach credits, (c) ->
|
||||||
|
unless creditsMap[c.plan_id]
|
||||||
|
creditsMap[c.plan_id] = []
|
||||||
|
creditsMap[c.plan_id].push(c.creditable_id)
|
||||||
|
creditsMap
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Iterate through $scope.traininfCredits to find the credit matching the given criterion
|
||||||
|
# @param trainingId {number|string} training ID
|
||||||
|
# @param planId {number|string} plan ID
|
||||||
|
##
|
||||||
|
findTrainingCredit = (trainingId, planId) ->
|
||||||
|
trainingId = parseInt(trainingId)
|
||||||
|
planId = parseInt(planId)
|
||||||
|
|
||||||
|
for credit in $scope.trainingCredits
|
||||||
|
if credit.plan_id == planId and credit.creditable_id == trainingId
|
||||||
|
return credit
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Retrieve a training from its given identifier and returns it
|
||||||
|
# @param id {number} training ID
|
||||||
|
# @returns {Object} Training inherited from $resource
|
||||||
|
##
|
||||||
|
getTrainingFromId = (id) ->
|
||||||
|
for training in $scope.trainings
|
||||||
|
if training.id == parseInt(id)
|
||||||
|
return training
|
||||||
|
|
||||||
|
|
||||||
|
## !!! MUST BE CALLED AT THE END of the controller
|
||||||
|
initialize()
|
||||||
|
]
|
@ -1,17 +1,16 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
Application.Controllers.controller "projectElementsController", ["$scope", "$state", 'Component', 'Licence', 'Theme', ($scope, $state, Component, Licence, Theme) ->
|
Application.Controllers.controller "ProjectElementsController", ["$scope", "$state", 'Component', 'Licence', 'Theme', 'componentsPromise', 'licencesPromise', 'themesPromise'
|
||||||
|
, ($scope, $state, Component, Licence, Theme, componentsPromise, licencesPromise, themesPromise) ->
|
||||||
|
|
||||||
## Materials list (plastic, wood ...)
|
## Materials list (plastic, wood ...)
|
||||||
$scope.components = Component.query()
|
$scope.components = componentsPromise
|
||||||
|
|
||||||
## Licences list (Creative Common ...)
|
## Licences list (Creative Common ...)
|
||||||
$scope.licences = Licence.query()
|
$scope.licences = licencesPromise
|
||||||
|
|
||||||
## Themes list (cooking, sport ...)
|
## Themes list (cooking, sport ...)
|
||||||
$scope.themes = Theme.query()
|
$scope.themes = themesPromise
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
# Saves a new component / Update an existing material to the server (form validation callback)
|
# Saves a new component / Update an existing material to the server (form validation callback)
|
||||||
@ -153,5 +152,3 @@ Application.Controllers.controller "projectElementsController", ["$scope", "$sta
|
|||||||
else
|
else
|
||||||
$scope.licences.splice(index, 1)
|
$scope.licences.splice(index, 1)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
196
app/assets/javascripts/controllers/admin/settings.coffee
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
Application.Controllers.controller "SettingsController", ["$scope", 'Setting', 'growl', 'settingsPromise', 'cgvFile', 'cguFile', 'logoFile', 'logoBlackFile', 'faviconFile', 'CSRF', '_t'
|
||||||
|
($scope, Setting, growl, settingsPromise, cgvFile, cguFile, logoFile, logoBlackFile, faviconFile, CSRF, _t) ->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### PUBLIC SCOPE ###
|
||||||
|
|
||||||
|
## timepickers steps configuration
|
||||||
|
$scope.timepicker =
|
||||||
|
hstep: 1
|
||||||
|
mstep: 15
|
||||||
|
|
||||||
|
## API URL where the upload forms will be posted
|
||||||
|
$scope.actionUrl =
|
||||||
|
cgu: "/api/custom_assets"
|
||||||
|
cgv: "/api/custom_assets"
|
||||||
|
logo: "/api/custom_assets"
|
||||||
|
logoBlack: "/api/custom_assets"
|
||||||
|
favicon: "/api/custom_assets"
|
||||||
|
|
||||||
|
## Form actions on the above URL
|
||||||
|
$scope.methods =
|
||||||
|
cgu: "post"
|
||||||
|
cgv: "post"
|
||||||
|
logo: "post"
|
||||||
|
logoBlack: "post"
|
||||||
|
favicon: "post"
|
||||||
|
|
||||||
|
## Are we uploading the files currently (if so, display the loader)
|
||||||
|
$scope.loader =
|
||||||
|
cgu: false
|
||||||
|
cgv: false
|
||||||
|
|
||||||
|
## various parametrable settings
|
||||||
|
$scope.twitterSetting = { name: 'twitter_name', value: settingsPromise.twitter_name }
|
||||||
|
$scope.aboutTitleSetting = { name: 'about_title', value: settingsPromise.about_title }
|
||||||
|
$scope.aboutBodySetting = { name: 'about_body', value: settingsPromise.about_body }
|
||||||
|
$scope.aboutContactsSetting = { name: 'about_contacts', value: settingsPromise.about_contacts }
|
||||||
|
$scope.homeBlogpostSetting = { name: 'home_blogpost', value: settingsPromise.home_blogpost }
|
||||||
|
$scope.machineExplicationsAlert = { name: 'machine_explications_alert', value: settingsPromise.machine_explications_alert }
|
||||||
|
$scope.trainingExplicationsAlert = { name: 'training_explications_alert', value: settingsPromise.training_explications_alert }
|
||||||
|
$scope.trainingInformationMessage = { name: 'training_information_message', value: settingsPromise.training_information_message}
|
||||||
|
$scope.subscriptionExplicationsAlert = { name: 'subscription_explications_alert', value: settingsPromise.subscription_explications_alert }
|
||||||
|
$scope.eventReducedAmountAlert = { name: 'event_reduced_amount_alert', value: settingsPromise.event_reduced_amount_alert }
|
||||||
|
$scope.windowStart = { name: 'booking_window_start', value: settingsPromise.booking_window_start }
|
||||||
|
$scope.windowEnd = { name: 'booking_window_end', value: settingsPromise.booking_window_end }
|
||||||
|
$scope.mainColorSetting = { name: 'main_color', value: settingsPromise.main_color }
|
||||||
|
$scope.secondColorSetting = { name: 'secondary_color', value: settingsPromise.secondary_color }
|
||||||
|
$scope.fablabName = { name: 'fablab_name', value: settingsPromise.fablab_name }
|
||||||
|
$scope.nameGenre = { name: 'name_genre', value: settingsPromise.name_genre }
|
||||||
|
$scope.cguFile = cguFile.custom_asset
|
||||||
|
$scope.cgvFile = cgvFile.custom_asset
|
||||||
|
$scope.customLogo = logoFile.custom_asset
|
||||||
|
$scope.customLogoBlack = logoBlackFile.custom_asset
|
||||||
|
$scope.customFavicon = faviconFile.custom_asset
|
||||||
|
|
||||||
|
$scope.enableMove =
|
||||||
|
name: 'booking_move_enable'
|
||||||
|
value: (settingsPromise.booking_move_enable == "true")
|
||||||
|
|
||||||
|
$scope.moveDelay =
|
||||||
|
name: 'booking_move_delay'
|
||||||
|
value: parseInt(settingsPromise.booking_move_delay)
|
||||||
|
|
||||||
|
$scope.enableCancel =
|
||||||
|
name: 'booking_cancel_enable'
|
||||||
|
value: (settingsPromise.booking_cancel_enable == "true")
|
||||||
|
|
||||||
|
$scope.cancelDelay =
|
||||||
|
name: 'booking_cancel_delay'
|
||||||
|
value: parseInt(settingsPromise.booking_cancel_delay)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# For use with 'ng-class', returns the CSS class name for the uploads previews.
|
||||||
|
# The preview may show a placeholder or the content of the file depending on the upload state.
|
||||||
|
# @param v {*} any attribute, will be tested for truthiness (see JS evaluation rules)
|
||||||
|
##
|
||||||
|
$scope.fileinputClass = (v)->
|
||||||
|
if v
|
||||||
|
'fileinput-exists'
|
||||||
|
else
|
||||||
|
'fileinput-new'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Callback to save the setting value to the database
|
||||||
|
# @param setting {{value:*, name:string}} note that the value will be stringified
|
||||||
|
##
|
||||||
|
$scope.save = (setting)->
|
||||||
|
# trim empty html
|
||||||
|
if setting.value == "<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('customization_of_SETTING_successfully_saved', {SETTING:setting.name}))
|
||||||
|
, (error)->
|
||||||
|
console.log(error)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# For use with ngUpload (https://github.com/twilson63/ngUpload).
|
||||||
|
# Intended to be the callback when the upload is done: Any raised error will be displayed in a growl
|
||||||
|
# message. If everything goes fine, a growl success message is shown.
|
||||||
|
# @param content {Object} JSON - The upload's result
|
||||||
|
##
|
||||||
|
$scope.submited = (content) ->
|
||||||
|
if !content.custom_asset?
|
||||||
|
$scope.alerts = []
|
||||||
|
angular.forEach content, (v, k)->
|
||||||
|
angular.forEach v, (err)->
|
||||||
|
growl.error(err)
|
||||||
|
else
|
||||||
|
growl.success(_t('file_successfully_updated'))
|
||||||
|
if content.custom_asset.name is 'cgu-file'
|
||||||
|
$scope.cguFile = content.custom_asset
|
||||||
|
$scope.methods.cgu = 'put'
|
||||||
|
$scope.actionUrl.cgu += '/cgu-file' unless $scope.actionUrl.cgu.indexOf('/cgu-file') > 0
|
||||||
|
$scope.loader.cgu = false
|
||||||
|
else if content.custom_asset.name is 'cgv-file'
|
||||||
|
$scope.cgvFile = content.custom_asset
|
||||||
|
$scope.methods.cgv = 'put'
|
||||||
|
$scope.actionUrl.cgv += '/cgv-file' unless $scope.actionUrl.cgv.indexOf('/cgv-file') > 0
|
||||||
|
$scope.loader.cgv = false
|
||||||
|
else if content.custom_asset.name is 'logo-file'
|
||||||
|
$scope.logoFile = content.custom_asset
|
||||||
|
$scope.methods.logo = 'put'
|
||||||
|
$scope.actionUrl.logo += '/logo-file' unless $scope.actionUrl.logo.indexOf('/logo-file') > 0
|
||||||
|
else if content.custom_asset.name is 'logo-black-file'
|
||||||
|
$scope.logoBlackFile = content.custom_asset
|
||||||
|
$scope.methods.logoBlack = 'put'
|
||||||
|
$scope.actionUrl.logoBlack += '/logo-black-file' unless $scope.actionUrl.logoBlack.indexOf('/logo-black-file') > 0
|
||||||
|
else if content.custom_asset.name is 'favicon-file'
|
||||||
|
$scope.faviconFile = content.custom_asset
|
||||||
|
$scope.methods.favicon = 'put'
|
||||||
|
$scope.actionUrl.favicon += '/favicon-file' unless $scope.actionUrl.favicon.indexOf('/favicon-file') > 0
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# @param target {String} 'cgu' | 'cgv'
|
||||||
|
##
|
||||||
|
$scope.addLoader = (target) ->
|
||||||
|
$scope.loader[target] = true
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### PRIVATE SCOPE ###
|
||||||
|
|
||||||
|
##
|
||||||
|
# Kind of constructor: these actions will be realized first when the controller is loaded
|
||||||
|
##
|
||||||
|
initialize = ->
|
||||||
|
# set the authenticity tokens in the forms
|
||||||
|
CSRF.setMetaTags()
|
||||||
|
|
||||||
|
# we prevent the admin from setting the closing time before the opening time
|
||||||
|
$scope.$watch 'windowEnd.value', (newValue, oldValue, scope) ->
|
||||||
|
if $scope.windowStart and moment($scope.windowStart.value).isAfter(newValue)
|
||||||
|
$scope.windowEnd.value = oldValue
|
||||||
|
|
||||||
|
# change form methods to PUT if items already exists
|
||||||
|
if cguFile.custom_asset
|
||||||
|
$scope.methods.cgu = 'put'
|
||||||
|
$scope.actionUrl.cgu += '/cgu-file'
|
||||||
|
if cgvFile.custom_asset
|
||||||
|
$scope.methods.cgv = 'put'
|
||||||
|
$scope.actionUrl.cgv += '/cgv-file'
|
||||||
|
if logoFile.custom_asset
|
||||||
|
$scope.methods.logo = 'put'
|
||||||
|
$scope.actionUrl.logo += '/logo-file'
|
||||||
|
if logoBlackFile.custom_asset
|
||||||
|
$scope.methods.logoBlack = 'put'
|
||||||
|
$scope.actionUrl.logoBlack += '/logo-black-file'
|
||||||
|
if faviconFile.custom_asset
|
||||||
|
$scope.methods.favicon = 'put'
|
||||||
|
$scope.actionUrl.favicon += '/favicon-file'
|
||||||
|
|
||||||
|
|
||||||
|
# init the controller (call at the end !)
|
||||||
|
initialize()
|
||||||
|
|
||||||
|
]
|
453
app/assets/javascripts/controllers/admin/statistics.coffee
Normal file
@ -0,0 +1,453 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
Application.Controllers.controller "StatisticsController", ["$scope", "$state", "$rootScope", "$locale", "Statistics", "es", "Member", '_t'
|
||||||
|
, ($scope, $state, $rootScope, $locale, Statistics, es, Member, _t) ->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### PUBLIC SCOPE ###
|
||||||
|
|
||||||
|
## ui-view transitions optimization: if true, the stats will never be refreshed
|
||||||
|
$scope.preventRefresh = false
|
||||||
|
|
||||||
|
## statistics structure in elasticSearch
|
||||||
|
$scope.statistics = []
|
||||||
|
|
||||||
|
## fablab users list
|
||||||
|
$scope.members = []
|
||||||
|
|
||||||
|
## statistics data recovered from elasticSearch
|
||||||
|
$scope.data = null
|
||||||
|
|
||||||
|
## configuration of the widget allowing to pick the ages range
|
||||||
|
$scope.agePicker =
|
||||||
|
show: false
|
||||||
|
start: null
|
||||||
|
end: null
|
||||||
|
|
||||||
|
## total CA for the current view
|
||||||
|
$scope.sumCA = 0
|
||||||
|
|
||||||
|
## average users' age for the current view
|
||||||
|
$scope.averageAge = 0
|
||||||
|
|
||||||
|
## total of the stat field for non simple types
|
||||||
|
$scope.sumStat = 0
|
||||||
|
|
||||||
|
## default: results are not sorted
|
||||||
|
$scope.sorting =
|
||||||
|
ca: 'none'
|
||||||
|
|
||||||
|
## active tab will be set here
|
||||||
|
$scope.selectedIndex = null
|
||||||
|
|
||||||
|
## type filter binding
|
||||||
|
$scope.type =
|
||||||
|
selected: null
|
||||||
|
active: null
|
||||||
|
|
||||||
|
## selected custom filter
|
||||||
|
$scope.customFilter =
|
||||||
|
show: false
|
||||||
|
criterion: {}
|
||||||
|
value : null
|
||||||
|
exclude: false
|
||||||
|
datePicker:
|
||||||
|
format: $locale.DATETIME_FORMATS.shortDate
|
||||||
|
opened: false # default: datePicker is not shown
|
||||||
|
minDate: null
|
||||||
|
maxDate: moment().toDate()
|
||||||
|
options:
|
||||||
|
startingDay: 1 # France: the week starts on monday
|
||||||
|
|
||||||
|
## available custom filters
|
||||||
|
$scope.filters = []
|
||||||
|
|
||||||
|
## default: we do not open the datepicker menu
|
||||||
|
$scope.datePicker =
|
||||||
|
show: false
|
||||||
|
|
||||||
|
## datePicker parameters for interval beginning
|
||||||
|
$scope.datePickerStart =
|
||||||
|
format: $locale.DATETIME_FORMATS.shortDate
|
||||||
|
opened: false # default: datePicker is not shown
|
||||||
|
minDate: null
|
||||||
|
maxDate: moment().subtract(1, 'day').toDate()
|
||||||
|
selected: moment().utc().subtract(1, 'months').subtract(1, 'day').startOf('day').toDate()
|
||||||
|
options:
|
||||||
|
startingDay: Fablab.weekStartingDay
|
||||||
|
|
||||||
|
## datePicker parameters for interval ending
|
||||||
|
$scope.datePickerEnd =
|
||||||
|
format: $locale.DATETIME_FORMATS.shortDate
|
||||||
|
opened: false # default: datePicker is not shown
|
||||||
|
minDate: null
|
||||||
|
maxDate: moment().subtract(1, 'day').toDate()
|
||||||
|
selected: moment().subtract(1, 'day').endOf('day').toDate()
|
||||||
|
options:
|
||||||
|
startingDay: Fablab.weekStartingDay
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Callback to open the datepicker (interval start)
|
||||||
|
# @param $event {Object} jQuery event object
|
||||||
|
##
|
||||||
|
$scope.toggleStartDatePicker = ($event) ->
|
||||||
|
toggleDatePicker($event, $scope.datePickerStart)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Callback to open the datepicker (interval end)
|
||||||
|
# @param $event {Object} jQuery event object
|
||||||
|
##
|
||||||
|
$scope.toggleEndDatePicker = ($event) ->
|
||||||
|
toggleDatePicker($event, $scope.datePickerEnd)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Callback to open the datepicker (custom filter)
|
||||||
|
# @param $event {Object} jQuery event object
|
||||||
|
##
|
||||||
|
$scope.toggleCustomDatePicker = ($event) ->
|
||||||
|
toggleDatePicker($event, $scope.customFilter.datePicker)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Callback called when the active tab is changed.
|
||||||
|
# recover the current tab and store its value in $scope.selectedIndex
|
||||||
|
# @param tab {Object} elasticsearch statistic structure
|
||||||
|
##
|
||||||
|
$scope.setActiveTab = (tab) ->
|
||||||
|
$scope.selectedIndex = tab
|
||||||
|
$scope.type.selected = tab.types[0]
|
||||||
|
$scope.type.active = $scope.type.selected
|
||||||
|
$scope.customFilter.criterion = {}
|
||||||
|
$scope.customFilter.value = null
|
||||||
|
$scope.customFilter.exclude = false
|
||||||
|
$scope.sorting.ca = 'none'
|
||||||
|
buildCustomFiltersList()
|
||||||
|
refreshStats()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Callback to validate the filters and send a new request to elastic
|
||||||
|
##
|
||||||
|
$scope.validateFilterChange = ->
|
||||||
|
$scope.agePicker.show = false
|
||||||
|
$scope.customFilter.show = false
|
||||||
|
$scope.type.active = $scope.type.selected
|
||||||
|
buildCustomFiltersList()
|
||||||
|
refreshStats()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Callback to validate the dates range and refresh the data from elastic
|
||||||
|
##
|
||||||
|
$scope.validateDateChange = ->
|
||||||
|
$scope.datePicker.show = false
|
||||||
|
refreshStats()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Parse the given date and return a user-friendly string
|
||||||
|
# @param date {Date} JS date or ant moment.js compatible date string
|
||||||
|
##
|
||||||
|
$scope.formatDate = (date) ->
|
||||||
|
moment(date).format("LL")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Parse the sex and return a user-friendly string
|
||||||
|
# @param sex {string} 'male' | 'female'
|
||||||
|
##
|
||||||
|
$scope.formatSex = (sex) ->
|
||||||
|
if sex == 'male'
|
||||||
|
return _t('man')
|
||||||
|
if sex == 'female'
|
||||||
|
return t('woman')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Retrieve the label for the given subtype in the current type
|
||||||
|
# @param key {string} statistic subtype key
|
||||||
|
##
|
||||||
|
$scope.formatSubtype = (key) ->
|
||||||
|
label = ""
|
||||||
|
angular.forEach $scope.type.active.subtypes, (subtype) ->
|
||||||
|
if subtype.key == key
|
||||||
|
label = subtype.label
|
||||||
|
label
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Helper usable in ng-switch to determine the input type to display for custom filter value
|
||||||
|
# @param filter {Object} custom filter criterion
|
||||||
|
##
|
||||||
|
$scope.getCustomValueInputType = (filter) ->
|
||||||
|
if filter and filter.values
|
||||||
|
if typeof(filter.values[0]) == 'string'
|
||||||
|
return filter.values[0]
|
||||||
|
else if typeof(filter.values[0] == 'object')
|
||||||
|
return 'input_select'
|
||||||
|
else
|
||||||
|
'input_text'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Change the sorting order and refresh the results to match the new order
|
||||||
|
# @param filter {Object} any filter
|
||||||
|
##
|
||||||
|
$scope.toggleSorting = (filter) ->
|
||||||
|
switch $scope.sorting[filter]
|
||||||
|
when 'none' then $scope.sorting[filter] = 'asc'
|
||||||
|
when 'asc' then $scope.sorting[filter] = 'desc'
|
||||||
|
when 'desc' then $scope.sorting[filter] = 'none'
|
||||||
|
refreshStats()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Return the user's name from his given ID
|
||||||
|
# @param id {number} user ID
|
||||||
|
##
|
||||||
|
$scope.getUserNameFromId = (id) ->
|
||||||
|
if $scope.members.length == 0
|
||||||
|
return "ID "+id
|
||||||
|
else
|
||||||
|
for member in $scope.members
|
||||||
|
if member.id == id
|
||||||
|
return member.name
|
||||||
|
return "ID "+id
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### PRIVATE SCOPE ###
|
||||||
|
|
||||||
|
##
|
||||||
|
# Kind of constructor: these actions will be realized first when the controller is loaded
|
||||||
|
##
|
||||||
|
initialize = ->
|
||||||
|
Statistics.query (stats) ->
|
||||||
|
$scope.statistics = stats
|
||||||
|
|
||||||
|
Member.query (members) ->
|
||||||
|
$scope.members = members
|
||||||
|
|
||||||
|
# workaround for angular-bootstrap::tabs behavior: on tab deletion, another tab will be selected
|
||||||
|
# which will cause every tabs to reload, one by one, when the view is closed
|
||||||
|
$rootScope.$on '$stateChangeStart', (event, toState, toParams, fromState, fromParams) ->
|
||||||
|
if fromState.name == 'app.admin.statistics' and Object.keys(fromParams).length == 0
|
||||||
|
$scope.preventRefresh = true
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Generic function to toggle a bootstrap datePicker
|
||||||
|
# @param $event {Object} jQuery event object
|
||||||
|
# @param datePicker {Object} settings object of the concerned datepicker. Must have an 'opened' property
|
||||||
|
##
|
||||||
|
toggleDatePicker = ($event, datePicker) ->
|
||||||
|
$event.preventDefault()
|
||||||
|
$event.stopPropagation()
|
||||||
|
datePicker.opened = !datePicker.opened
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Force update the statistics table, querying elasticSearch according to the current config values
|
||||||
|
##
|
||||||
|
refreshStats = ->
|
||||||
|
if $scope.selectedIndex and !$scope.preventRefresh
|
||||||
|
$scope.data = []
|
||||||
|
$scope.sumCA = 0
|
||||||
|
$scope.averageAge = 0
|
||||||
|
$scope.sumStat = 0
|
||||||
|
custom = null
|
||||||
|
if $scope.customFilter.criterion and $scope.customFilter.criterion.key and $scope.customFilter.value
|
||||||
|
custom = {}
|
||||||
|
custom.key = $scope.customFilter.criterion.key
|
||||||
|
custom.value = $scope.customFilter.value
|
||||||
|
custom.exclude = $scope.customFilter.exclude
|
||||||
|
queryElasticStats $scope.selectedIndex.es_type_key, $scope.type.active.key, custom, (res, err)->
|
||||||
|
if (err)
|
||||||
|
console.error("[statisticsController::refreshStats] Unable to refresh due to "+err)
|
||||||
|
else
|
||||||
|
$scope.data = res.hits
|
||||||
|
sumCA = 0
|
||||||
|
sumAge = 0
|
||||||
|
sumStat = 0
|
||||||
|
if $scope.data.length > 0
|
||||||
|
angular.forEach $scope.data, (datum) ->
|
||||||
|
if datum._source.ca
|
||||||
|
sumCA += parseInt(datum._source.ca)
|
||||||
|
if datum._source.age
|
||||||
|
sumAge += parseInt(datum._source.age)
|
||||||
|
if datum._source.stat
|
||||||
|
sumStat += parseInt(datum._source.stat)
|
||||||
|
sumAge /= $scope.data.length
|
||||||
|
$scope.sumCA = sumCA
|
||||||
|
$scope.averageAge = Math.round(sumAge*100)/100
|
||||||
|
$scope.sumStat = sumStat
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Run the elasticSearch query to retreive the /stats/type aggregations
|
||||||
|
# @param index {String} elasticSearch document type (account|event|machine|project|subscription|training)
|
||||||
|
# @param type {String} statistics type (month|year|booking|hour|user|project)
|
||||||
|
# @param custom {{key:{string}, value:{string}}|null} custom filter property or null to disable this filter
|
||||||
|
# @param callback {function} function be to run after results were retrieved, it will receive
|
||||||
|
# two parameters : results {Array}, error {String} (if any)
|
||||||
|
##
|
||||||
|
queryElasticStats = (index, type, custom, callback) ->
|
||||||
|
# handle invalid callback
|
||||||
|
if typeof(callback) != "function"
|
||||||
|
console.error('[statisticsController::queryElasticStats] Error: invalid callback provided')
|
||||||
|
return
|
||||||
|
|
||||||
|
# run query
|
||||||
|
es.search
|
||||||
|
"index": "stats"
|
||||||
|
"type": index
|
||||||
|
"size": 1000000000
|
||||||
|
"body": buildElasticDataQuery(type, custom, $scope.agePicker.start, $scope.agePicker.end, moment($scope.datePickerStart.selected), moment($scope.datePickerEnd.selected), $scope.sorting)
|
||||||
|
, (error, response) ->
|
||||||
|
if (error)
|
||||||
|
callback([], "Error: something unexpected occurred during elasticSearch query: "+error)
|
||||||
|
else
|
||||||
|
callback(response.hits)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Build an object representing the content of the REST-JSON query to elasticSearch,
|
||||||
|
# based on the provided parameters for row data recovering.
|
||||||
|
# @param type {String} statistics type (month|year|booking|hour|user|project)
|
||||||
|
# @param custom {{key:{string}, value:{string}}|null} custom filter property or null to disable this filter
|
||||||
|
# @param ageMin {Number|null} filter by age: range lower value OR null to do not filter
|
||||||
|
# @param ageMax {Number|null} filter by age: range higher value OR null to do not filter
|
||||||
|
# @param intervalBegin {moment} statitics interval beginning (moment.js type)
|
||||||
|
# @param intervalEnd {moment} statitics interval ending (moment.js type)
|
||||||
|
# @param sortings {Array|null} elasticSearch criteria for sorting the results
|
||||||
|
##
|
||||||
|
buildElasticDataQuery = (type, custom, ageMin, ageMax, intervalBegin, intervalEnd, sortings) ->
|
||||||
|
q =
|
||||||
|
"query":
|
||||||
|
"bool":
|
||||||
|
"must": [
|
||||||
|
{
|
||||||
|
"term":
|
||||||
|
"type": type
|
||||||
|
}
|
||||||
|
{
|
||||||
|
"range":
|
||||||
|
"date":
|
||||||
|
"gte": intervalBegin.format()
|
||||||
|
"lte": intervalEnd.format()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
# optional date range
|
||||||
|
if ageMin && ageMax
|
||||||
|
q.query.bool.must.push
|
||||||
|
"range":
|
||||||
|
"age":
|
||||||
|
"gte": ageMin
|
||||||
|
"lte": ageMax
|
||||||
|
# optional criterion
|
||||||
|
if custom
|
||||||
|
criterion = {
|
||||||
|
"match" : {}
|
||||||
|
}
|
||||||
|
switch $scope.getCustomValueInputType($scope.customFilter.criterion)
|
||||||
|
when 'input_date' then criterion.match[custom.key] = moment(custom.value).format('YYYY-MM-DD')
|
||||||
|
when 'input_select' then criterion.match[custom.key] = custom.value.key
|
||||||
|
when 'input_list' then criterion.match[custom.key+".name"] = custom.value
|
||||||
|
else criterion.match[custom.key] = custom.value
|
||||||
|
|
||||||
|
if (custom.exclude)
|
||||||
|
q = "query": {
|
||||||
|
"filtered": {
|
||||||
|
"query": q.query,
|
||||||
|
"filter": {
|
||||||
|
"not": {
|
||||||
|
"term": criterion.match
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
q.query.bool.must.push(criterion)
|
||||||
|
|
||||||
|
|
||||||
|
if sortings
|
||||||
|
q["sort"] = buildElasticSortCriteria(sortings)
|
||||||
|
q
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Parse the provided criteria array and return the corresponding elasticSearch syntax
|
||||||
|
# @param criteria {Array} array of {key_to_sort:order}
|
||||||
|
##
|
||||||
|
buildElasticSortCriteria = (criteria) ->
|
||||||
|
crits = []
|
||||||
|
angular.forEach criteria, (value, key) ->
|
||||||
|
if typeof value != 'undefined' and value != null and value != 'none'
|
||||||
|
c = {}
|
||||||
|
c[key] = {'order': value}
|
||||||
|
crits.push(c)
|
||||||
|
crits
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Fullfil the list of available options in the custom filter panel. The list will be based on common
|
||||||
|
# properties and on index-specific properties (additional_fields)
|
||||||
|
##
|
||||||
|
buildCustomFiltersList = ->
|
||||||
|
$scope.filters = []
|
||||||
|
|
||||||
|
filters = [
|
||||||
|
{key: 'date', label: _t('date'), values: ['input_date']},
|
||||||
|
{key: 'userId', label: _t('user_id'), values: ['input_number']},
|
||||||
|
{key: 'gender', label: _t('gender'), values: [{key:'male', label:_t('man')}, {key:'female', label:_t('woman')}]},
|
||||||
|
{key: 'age', label: _t('age'), values: ['input_number']},
|
||||||
|
{key: 'subType', label: _t('type'), values: $scope.type.active.subtypes},
|
||||||
|
{key: 'ca', label: _t('revenue'), values: ['input_number']}
|
||||||
|
]
|
||||||
|
|
||||||
|
$scope.filters = filters
|
||||||
|
|
||||||
|
if !$scope.type.active.simple
|
||||||
|
f = {key: 'stat', label: $scope.type.active.label, values: ['input_number']}
|
||||||
|
$scope.filters.push(f)
|
||||||
|
|
||||||
|
angular.forEach $scope.selectedIndex.additional_fields, (field) ->
|
||||||
|
filter = {key: field.key, label: field.label, values:[]}
|
||||||
|
switch field.data_type
|
||||||
|
when 'index' then filter.values.push('input_number')
|
||||||
|
when 'number' then filter.values.push('input_number')
|
||||||
|
when 'date' then filter.values.push('input_date')
|
||||||
|
when 'list' then filter.values.push('input_list')
|
||||||
|
else filter.values.push('input_text')
|
||||||
|
|
||||||
|
$scope.filters.push(filter)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# init the controller (call at the end !)
|
||||||
|
initialize()
|
||||||
|
|
||||||
|
]
|
65
app/assets/javascripts/controllers/admin/tags.coffee.erb
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
Application.Controllers.controller "TagsController", ["$scope", 'tagsPromise', 'Tag', 'growl', '_t', ($scope, tagsPromise, Tag, growl, _t) ->
|
||||||
|
|
||||||
|
## List of users's tags
|
||||||
|
$scope.tags = tagsPromise
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Removes the newly inserted but not saved tag / Cancel the current tag modification
|
||||||
|
# @param rowform {Object} see http://vitalets.github.io/angular-xeditable/
|
||||||
|
# @param index {number} tag index in the $scope.tags array
|
||||||
|
##
|
||||||
|
$scope.cancelTag = (rowform, index) ->
|
||||||
|
if $scope.tags[index].id?
|
||||||
|
rowform.$cancel()
|
||||||
|
else
|
||||||
|
$scope.tags.splice(index, 1)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Creates a new empty entry in the $scope.tags array
|
||||||
|
##
|
||||||
|
$scope.addTag = ->
|
||||||
|
$scope.inserted =
|
||||||
|
name: ''
|
||||||
|
$scope.tags.push($scope.inserted)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Saves a new tag / Update an existing tag to the server (form validation callback)
|
||||||
|
# @param data {Object} tag name
|
||||||
|
# @param [data] {number} tag id, in case of update
|
||||||
|
##
|
||||||
|
$scope.saveTag = (data, id) ->
|
||||||
|
if id?
|
||||||
|
Tag.update {id: id}, { tag: data }, (response) ->
|
||||||
|
growl.success(_t('changes_successfully_saved'))
|
||||||
|
, (error) ->
|
||||||
|
growl.error(_t('an_error_occurred_while_saving_changes'))
|
||||||
|
else
|
||||||
|
Tag.save { tag: data }, (resp)->
|
||||||
|
growl.success(_t('new_tag_successfully_saved'))
|
||||||
|
$scope.tags[$scope.tags.length-1].id = resp.id
|
||||||
|
, (error) ->
|
||||||
|
growl.error(_t('an_error_occurred_while_saving_the_new_tag'))
|
||||||
|
$scope.tags.splice($scope.tags.length-1, 1)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Deletes the tag at the specified index
|
||||||
|
# @param index {number} tag index in the $scope.tags array
|
||||||
|
##
|
||||||
|
$scope.removeTag = (index) ->
|
||||||
|
# TODO add confirmation : les utilisateurs seront déasociés
|
||||||
|
Tag.delete { id: $scope.tags[index].id }, (resp) ->
|
||||||
|
growl.success(_t('tag_successfully_deleted'))
|
||||||
|
$scope.tags.splice(index, 1)
|
||||||
|
, (error) ->
|
||||||
|
growl.error(_t('an_error_occurred_and_the_tag_deletion_failed'))
|
||||||
|
|
||||||
|
|
||||||
|
]
|
230
app/assets/javascripts/controllers/admin/trainings.coffee.erb
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
Application.Controllers.controller "TrainingsController", ["$scope", "$state", "$uibModal", 'Training', 'trainingsPromise', 'machinesPromise', '_t', 'growl'
|
||||||
|
, ($scope, $state, $uibModal, Training, trainingsPromise, machinesPromise, _t, growl) ->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### PUBLIC SCOPE ###
|
||||||
|
|
||||||
|
## list of trainings
|
||||||
|
$scope.trainings = trainingsPromise
|
||||||
|
|
||||||
|
## simplified list of machines
|
||||||
|
$scope.machines = machinesPromise
|
||||||
|
|
||||||
|
## list of training availabilies, grouped by date
|
||||||
|
$scope.groupedAvailabilities = {}
|
||||||
|
|
||||||
|
## default: accordions are not open
|
||||||
|
$scope.accordions = {}
|
||||||
|
|
||||||
|
## Binding for the parseInt function
|
||||||
|
$scope.parseInt = parseInt
|
||||||
|
|
||||||
|
##
|
||||||
|
# In the trainings listing tab, return the stringified list of machines associated with the provided training
|
||||||
|
# @param training {Object} Training object, inherited from $resource
|
||||||
|
# @returns {string}
|
||||||
|
##
|
||||||
|
$scope.showMachines = (training) ->
|
||||||
|
selected = []
|
||||||
|
angular.forEach $scope.machines, (m) ->
|
||||||
|
if (training.machine_ids.indexOf(m.id) >= 0)
|
||||||
|
selected.push(m.name)
|
||||||
|
return if selected.length then selected.join(', ') else _t('none')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Create a new empty training object and append it to the $scope.trainings list
|
||||||
|
##
|
||||||
|
$scope.addTraining = ->
|
||||||
|
$scope.inserted =
|
||||||
|
name: ''
|
||||||
|
machine_ids: []
|
||||||
|
$scope.trainings.push($scope.inserted)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Saves a new training / Update an existing training to the server (form validation callback)
|
||||||
|
# @param data {Object} training name, associated machine(s) and default places number
|
||||||
|
# @param id {number} training id, in case of update
|
||||||
|
##
|
||||||
|
$scope.saveTraining = (data, id) ->
|
||||||
|
if id?
|
||||||
|
Training.update {id: id},
|
||||||
|
training: data
|
||||||
|
else
|
||||||
|
Training.save
|
||||||
|
training: data
|
||||||
|
, (resp) ->
|
||||||
|
$scope.trainings[$scope.trainings.length-1] = resp
|
||||||
|
console.log(resp)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Removes the newly inserted but not saved training / Cancel the current training modification
|
||||||
|
# @param rowform {Object} see http://vitalets.github.io/angular-xeditable/
|
||||||
|
# @param index {number} training index in the $scope.trainings array
|
||||||
|
##
|
||||||
|
$scope.cancelTraining = (rowform, index) ->
|
||||||
|
if $scope.trainings[index].id?
|
||||||
|
rowform.$cancel()
|
||||||
|
else
|
||||||
|
$scope.trainings.splice(index, 1)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# In the trainings monitoring tab, callback to open a modal window displaying the current bookings for the
|
||||||
|
# provided training slot. The admin will be then able to validate the training for the users that followed
|
||||||
|
# the training.
|
||||||
|
# @param training {Object} Training object, inherited from $resource
|
||||||
|
# @param availability {Object} time slot when the training occurs
|
||||||
|
##
|
||||||
|
$scope.showReservations = (training, availability) ->
|
||||||
|
$uibModal.open
|
||||||
|
templateUrl: '<%= asset_path "admin/trainings/validTrainingModal.html" %>'
|
||||||
|
controller: ['$scope', '$uibModalInstance', ($scope, $uibModalInstance) ->
|
||||||
|
$scope.availability = availability
|
||||||
|
|
||||||
|
$scope.usersToValid = []
|
||||||
|
|
||||||
|
##
|
||||||
|
# Mark/unmark the provided user for training validation
|
||||||
|
# @param user {Object} from the availability.reservation_users list
|
||||||
|
##
|
||||||
|
$scope.toggleSelection = (user) ->
|
||||||
|
index = $scope.usersToValid.indexOf(user)
|
||||||
|
if index > -1
|
||||||
|
$scope.usersToValid.splice(index, 1)
|
||||||
|
else
|
||||||
|
$scope.usersToValid.push user
|
||||||
|
|
||||||
|
##
|
||||||
|
# Validates the modifications (training validations) and save them to the server
|
||||||
|
##
|
||||||
|
$scope.ok = ->
|
||||||
|
users = $scope.usersToValid.map (u) ->
|
||||||
|
u.id
|
||||||
|
Training.update {id: training.id},
|
||||||
|
training:
|
||||||
|
users: users
|
||||||
|
, -> # success
|
||||||
|
angular.forEach $scope.usersToValid, (u) ->
|
||||||
|
u.is_valid = true
|
||||||
|
$scope.usersToValid = []
|
||||||
|
$uibModalInstance.close(training)
|
||||||
|
|
||||||
|
##
|
||||||
|
# Cancel the modifications and close the modal window
|
||||||
|
##
|
||||||
|
$scope.cancel = ->
|
||||||
|
$uibModalInstance.dismiss('cancel')
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Delete the provided training and, in case of sucess, remove it from the trainings list afterwards
|
||||||
|
# @param index {number} index of the provided training in $scope.trainings
|
||||||
|
# @param training {Object} training to delete
|
||||||
|
##
|
||||||
|
$scope.removeTraining = (index, training)->
|
||||||
|
training.$delete ->
|
||||||
|
$scope.trainings.splice(index, 1)
|
||||||
|
growl.info(_t('training_successfully_deleted'))
|
||||||
|
, (error)->
|
||||||
|
growl.warning(_t('unable_to_delete_the_training_because_some_users_alredy_booked_it'))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Open the modal to edit description of the training
|
||||||
|
# @param training {Object} Training to edit description
|
||||||
|
##
|
||||||
|
$scope.openModalToSetDescription = (training)->
|
||||||
|
$uibModal.open(
|
||||||
|
templateUrl: "<%= asset_path 'admin/trainings/modal_edit.html' %>"
|
||||||
|
controller: ['$scope', '$uibModalInstance', 'Training', 'growl', ($scope, $uibModalInstance, Training, growl)->
|
||||||
|
$scope.training = training
|
||||||
|
$scope.save = ->
|
||||||
|
Training.update id: training.id, { training: { description: $scope.training.description } }, (training)->
|
||||||
|
$uibModalInstance.close()
|
||||||
|
growl.success(_t('description_was_successfully_saved'))
|
||||||
|
return
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Takes a month number and return its localized literal name
|
||||||
|
# @param {Number} from 0 to 11
|
||||||
|
# @returns {String} eg. 'janvier'
|
||||||
|
##
|
||||||
|
$scope.formatMonth = (number) ->
|
||||||
|
number = parseInt(number)
|
||||||
|
moment().month(number).format('MMMM')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Given a day, month and year, return a localized literal name for the day
|
||||||
|
# @param day {Number} from 1 to 31
|
||||||
|
# @param month {Number} from 0 to 11
|
||||||
|
# @param year {Number} Gregorian's year number
|
||||||
|
# @returns {String} eg. 'mercredi 12'
|
||||||
|
##
|
||||||
|
$scope.formatDay = (day, month, year) ->
|
||||||
|
day = parseInt(day)
|
||||||
|
month = parseInt(month)
|
||||||
|
year = parseInt(year)
|
||||||
|
|
||||||
|
moment({year: year, month:month, day:day}).format('dddd D')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### PRIVATE SCOPE ###
|
||||||
|
|
||||||
|
##
|
||||||
|
# Kind of constructor: these actions will be realized first when the controller is loaded
|
||||||
|
##
|
||||||
|
initialize = ->
|
||||||
|
$scope.groupedAvailabilities = groupAvailabilities($scope.trainings)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Group the trainings availabilites by trainings and by dates and return the resulting tree
|
||||||
|
# @param trainings {Array} $scope.trainings is expected here
|
||||||
|
# @returns {Object} Tree constructed as /training_name/year/month/day/[availabilities]
|
||||||
|
##
|
||||||
|
groupAvailabilities = (trainings) ->
|
||||||
|
tree = {}
|
||||||
|
for training in trainings
|
||||||
|
tree[training.name] = {}
|
||||||
|
tree[training.name].training = training
|
||||||
|
for availability in training.availabilities
|
||||||
|
start = moment(availability.start_at)
|
||||||
|
|
||||||
|
# init the tree structure
|
||||||
|
if typeof tree[training.name][start.year()] == 'undefined'
|
||||||
|
tree[training.name][start.year()] = {}
|
||||||
|
if typeof tree[training.name][start.year()][start.month()] == 'undefined'
|
||||||
|
tree[training.name][start.year()][start.month()] = {}
|
||||||
|
if typeof tree[training.name][start.year()][start.month()][start.date()] == 'undefined'
|
||||||
|
tree[training.name][start.year()][start.month()][start.date()] = []
|
||||||
|
|
||||||
|
# add the availability at its right place
|
||||||
|
tree[training.name][start.year()][start.month()][start.date()].push( availability )
|
||||||
|
tree
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# init the controller (call at the end !)
|
||||||
|
initialize()
|
||||||
|
]
|
@ -1,6 +1,7 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
Application.Controllers.controller 'ApplicationController', ["$rootScope", "$scope", "Session", "AuthService", "Auth", "$modal", "$state", 'growl', 'Notification', '$interval', ($rootScope, $scope, Session, AuthService, Auth, $modal, $state, growl, Notification, $interval) ->
|
Application.Controllers.controller 'ApplicationController', ["$rootScope", "$scope", "$window", "Session", "AuthService", "Auth", "$uibModal", "$state", 'growl', 'Notification', '$interval', "Setting", '$locale', '_t'
|
||||||
|
, ($rootScope, $scope, $window, Session, AuthService, Auth, $uibModal, $state, growl, Notification, $interval, Setting, $locale, _t) ->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -18,14 +19,14 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
|
|||||||
# @param user {Object} Rails/Devise user
|
# @param user {Object} Rails/Devise user
|
||||||
##
|
##
|
||||||
$scope.setCurrentUser = (user) ->
|
$scope.setCurrentUser = (user) ->
|
||||||
$scope.currentUser = user
|
$rootScope.currentUser = user
|
||||||
Session.create(user);
|
Session.create(user);
|
||||||
getNotifications()
|
getNotifications()
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
# Login callback
|
# Login callback
|
||||||
# @param e {Object} jQuery event
|
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
|
||||||
# @param callback {function}
|
# @param callback {function}
|
||||||
##
|
##
|
||||||
$scope.login = (e, callback) ->
|
$scope.login = (e, callback) ->
|
||||||
@ -36,14 +37,14 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
|
|||||||
|
|
||||||
##
|
##
|
||||||
# Logout callback
|
# Logout callback
|
||||||
# @param e {Object} jQuery event
|
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
|
||||||
##
|
##
|
||||||
$scope.logout = (e) ->
|
$scope.logout = (e) ->
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
Auth.logout().then (oldUser) ->
|
Auth.logout().then (oldUser) ->
|
||||||
# console.log(oldUser.name + " you're signed out now.");
|
# console.log(oldUser.name + " you're signed out now.");
|
||||||
Session.destroy()
|
Session.destroy()
|
||||||
$scope.currentUser = null
|
$rootScope.currentUser = null
|
||||||
$rootScope.toCheckNotifications = false
|
$rootScope.toCheckNotifications = false
|
||||||
$scope.notifications = []
|
$scope.notifications = []
|
||||||
$state.go('app.public.home')
|
$state.go('app.public.home')
|
||||||
@ -54,21 +55,21 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
|
|||||||
|
|
||||||
##
|
##
|
||||||
# Open the modal window allowing the user to create an account.
|
# Open the modal window allowing the user to create an account.
|
||||||
# @param e {Object} jQuery event
|
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
|
||||||
##
|
##
|
||||||
$scope.signup = (e) ->
|
$scope.signup = (e) ->
|
||||||
e.preventDefault() if e
|
e.preventDefault() if e
|
||||||
|
|
||||||
$modal.open
|
$uibModal.open
|
||||||
templateUrl: '<%= asset_path "shared/signupModal.html" %>'
|
templateUrl: '<%= asset_path "shared/signupModal.html" %>'
|
||||||
size: 'md'
|
size: 'md'
|
||||||
controller: ['$scope', '$modalInstance', 'Group', ($scope, $modalInstance, Group) ->
|
controller: ['$scope', '$uibModalInstance', 'Group', 'CustomAsset', ($scope, $uibModalInstance, Group, CustomAsset) ->
|
||||||
# default parameters for the date picker in the account creation modal
|
# default parameters for the date picker in the account creation modal
|
||||||
$scope.datePicker =
|
$scope.datePicker =
|
||||||
format: 'dd/MM/yyyy'
|
format: $locale.DATETIME_FORMATS.shortDate
|
||||||
opened: false
|
opened: false
|
||||||
options:
|
options:
|
||||||
startingDay: 1
|
startingDay: Fablab.weekStartingDay
|
||||||
|
|
||||||
# callback to open the date picker (account creation modal)
|
# callback to open the date picker (account creation modal)
|
||||||
$scope.openDatePicker = ($event) ->
|
$scope.openDatePicker = ($event) ->
|
||||||
@ -80,6 +81,10 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
|
|||||||
Group.query (groups) ->
|
Group.query (groups) ->
|
||||||
$scope.groups = groups
|
$scope.groups = groups
|
||||||
|
|
||||||
|
# retrieve the CGU
|
||||||
|
CustomAsset.get {name: 'cgu-file'}, (cgu) ->
|
||||||
|
$scope.cgu = cgu.custom_asset
|
||||||
|
|
||||||
# default user's parameters
|
# default user's parameters
|
||||||
$scope.user =
|
$scope.user =
|
||||||
is_allow_contact: true
|
is_allow_contact: true
|
||||||
@ -95,7 +100,7 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
|
|||||||
$scope.alerts = []
|
$scope.alerts = []
|
||||||
Auth.register($scope.user).then (user) ->
|
Auth.register($scope.user).then (user) ->
|
||||||
# creation successful
|
# creation successful
|
||||||
$modalInstance.close(user)
|
$uibModalInstance.close(user)
|
||||||
, (error) ->
|
, (error) ->
|
||||||
# creation failed...
|
# creation failed...
|
||||||
angular.forEach error.data.errors, (v, k) ->
|
angular.forEach error.data.errors, (v, k) ->
|
||||||
@ -114,10 +119,10 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
|
|||||||
# @param token {string} security token for password changing. The user should have recieved it by mail
|
# @param token {string} security token for password changing. The user should have recieved it by mail
|
||||||
##
|
##
|
||||||
$scope.editPassword = (token) ->
|
$scope.editPassword = (token) ->
|
||||||
$modal.open
|
$uibModal.open
|
||||||
templateUrl: '<%= asset_path "shared/passwordEditModal.html" %>'
|
templateUrl: '<%= asset_path "shared/passwordEditModal.html" %>'
|
||||||
size: 'md'
|
size: 'md'
|
||||||
controller: ['$scope', '$modalInstance', '$http', ($scope, $modalInstance, $http) ->
|
controller: ['$scope', '$uibModalInstance', '$http', '_t', ($scope, $uibModalInstance, $http, _t) ->
|
||||||
$scope.user =
|
$scope.user =
|
||||||
reset_password_token: token
|
reset_password_token: token
|
||||||
$scope.alerts = []
|
$scope.alerts = []
|
||||||
@ -127,7 +132,7 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
|
|||||||
$scope.changePassword = ->
|
$scope.changePassword = ->
|
||||||
$scope.alerts = []
|
$scope.alerts = []
|
||||||
$http.put('/users/password.json', {user: $scope.user}).success (data) ->
|
$http.put('/users/password.json', {user: $scope.user}).success (data) ->
|
||||||
$modalInstance.close()
|
$uibModalInstance.close()
|
||||||
.error (data) ->
|
.error (data) ->
|
||||||
angular.forEach data.errors, (v, k) ->
|
angular.forEach data.errors, (v, k) ->
|
||||||
angular.forEach v, (err) ->
|
angular.forEach v, (err) ->
|
||||||
@ -136,20 +141,20 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
|
|||||||
type: 'danger'
|
type: 'danger'
|
||||||
]
|
]
|
||||||
.result['finally'](null).then (user) ->
|
.result['finally'](null).then (user) ->
|
||||||
growl.addInfoMessage('Votre mot de passe a bien été modifié.')
|
growl.success(_t('your_password_was_successfully_changed'))
|
||||||
Auth.login().then (user) ->
|
Auth.login().then (user) ->
|
||||||
$scope.setCurrentUser(user)
|
$scope.setCurrentUser(user)
|
||||||
, (error) ->
|
, (error) ->
|
||||||
# Authentication failed...
|
# Authentication failed...
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
# Compact/Expend the width of the left navigation bar
|
# Compact/Expend the width of the left navigation bar
|
||||||
# @param e {Object} jQuery event object
|
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
|
||||||
##
|
##
|
||||||
$scope.toggleNavSize = (event) ->
|
$scope.toggleNavSize = (event) ->
|
||||||
if typeof event == 'undefined'
|
if typeof event == 'undefined'
|
||||||
console.error '[applicationController::toggleNavSize] Missing event parameter'
|
console.error '[ApplicationController::toggleNavSize] Missing event parameter'
|
||||||
return
|
return
|
||||||
|
|
||||||
toggler = $(event.target)
|
toggler = $(event.target)
|
||||||
@ -184,14 +189,16 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
|
|||||||
|
|
||||||
|
|
||||||
### PRIVATE SCOPE ###
|
### PRIVATE SCOPE ###
|
||||||
|
|
||||||
##
|
##
|
||||||
# Kind of constructor: these actions will be realized first when the controller is loaded
|
# Kind of constructor: these actions will be realized first when the controller is loaded
|
||||||
##
|
##
|
||||||
initialize = ->
|
initialize = ->
|
||||||
|
|
||||||
# try to retrieve any currently logged user
|
# try to retrieve any currently logged user
|
||||||
Auth.login().then (user) ->
|
Auth.login().then (user) ->
|
||||||
$scope.setCurrentUser(user)
|
$scope.setCurrentUser(user)
|
||||||
|
if user.need_completion
|
||||||
|
$state.transitionTo('app.logged.profileCompletion')
|
||||||
, (error) ->
|
, (error) ->
|
||||||
# Authentication failed...
|
# Authentication failed...
|
||||||
$rootScope.toCheckNotifications = false
|
$rootScope.toCheckNotifications = false
|
||||||
@ -205,11 +212,17 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
|
|||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
if AuthService.isAuthenticated()
|
if AuthService.isAuthenticated()
|
||||||
# user is not allowed
|
# user is not allowed
|
||||||
console.log('user is not allowed')
|
console.error('[ApplicationController::initialize] user is not allowed')
|
||||||
else
|
else
|
||||||
# user is not logged in
|
# user is not logged in
|
||||||
openLoginModal(toState, toParams)
|
openLoginModal(toState, toParams)
|
||||||
|
|
||||||
|
Setting.get { name: 'fablab_name' }, (data)->
|
||||||
|
$scope.fablabName = data.setting.value
|
||||||
|
Setting.get { name: 'name_genre' }, (data)->
|
||||||
|
$scope.nameGenre = data.setting.value
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# shorthands
|
# shorthands
|
||||||
$scope.isAuthenticated = Auth.isAuthenticated;
|
$scope.isAuthenticated = Auth.isAuthenticated;
|
||||||
@ -223,7 +236,7 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
|
|||||||
##
|
##
|
||||||
getNotifications = ->
|
getNotifications = ->
|
||||||
$rootScope.toCheckNotifications = true
|
$rootScope.toCheckNotifications = true
|
||||||
unless $rootScope.checkNotificationsIsInit or !$scope.currentUser
|
unless $rootScope.checkNotificationsIsInit or !$rootScope.currentUser
|
||||||
$scope.notifications = Notification.query {is_read: false}
|
$scope.notifications = Notification.query {is_read: false}
|
||||||
$scope.$watch 'notifications', (newValue, oldValue) ->
|
$scope.$watch 'notifications', (newValue, oldValue) ->
|
||||||
diff = []
|
diff = []
|
||||||
@ -239,7 +252,7 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
|
|||||||
|
|
||||||
|
|
||||||
angular.forEach diff, (notification, key) ->
|
angular.forEach diff, (notification, key) ->
|
||||||
growl.addInfoMessage(notification.message.description)
|
growl.info(notification.message.description)
|
||||||
|
|
||||||
, true
|
, true
|
||||||
|
|
||||||
@ -257,35 +270,39 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
|
|||||||
# Open the modal window allowing the user to log in.
|
# Open the modal window allowing the user to log in.
|
||||||
##
|
##
|
||||||
openLoginModal = (toState, toParams, callback) ->
|
openLoginModal = (toState, toParams, callback) ->
|
||||||
$modal.open
|
<% active_provider = AuthProvider.active %>
|
||||||
|
<% if active_provider.providable_type != DatabaseProvider.name %>
|
||||||
|
$window.location.href = '<%=user_omniauth_authorize_path(AuthProvider.active.strategy_name.to_sym)%>'
|
||||||
|
<% else %>
|
||||||
|
$uibModal.open
|
||||||
templateUrl: '<%= asset_path "shared/deviseModal.html" %>'
|
templateUrl: '<%= asset_path "shared/deviseModal.html" %>'
|
||||||
size: 'sm'
|
size: 'sm'
|
||||||
controller: ['$scope', '$modalInstance', ($scope, $modalInstance) ->
|
controller: ['$scope', '$uibModalInstance', '_t', ($scope, $uibModalInstance, _t) ->
|
||||||
user = $scope.user = {}
|
user = $scope.user = {}
|
||||||
$scope.login = () ->
|
$scope.login = () ->
|
||||||
Auth.login(user).then (user) ->
|
Auth.login(user).then (user) ->
|
||||||
# Authentification succeeded ...
|
# Authentification succeeded ...
|
||||||
$modalInstance.close(user)
|
$uibModalInstance.close(user)
|
||||||
if callback and typeof callback is "function"
|
if callback and typeof callback is "function"
|
||||||
callback(user)
|
callback(user)
|
||||||
, (error) ->
|
, (error) ->
|
||||||
# Authentication failed...
|
# Authentication failed...
|
||||||
$scope.alerts = []
|
$scope.alerts = []
|
||||||
$scope.alerts.push
|
$scope.alerts.push
|
||||||
msg: 'E-mail ou mot de passe incorrect.'
|
msg: _t('wrong_email_or_password')
|
||||||
type: 'danger'
|
type: 'danger'
|
||||||
|
|
||||||
# handle modal behaviors. The provided reason will be used to define the following actions
|
# handle modal behaviors. The provided reason will be used to define the following actions
|
||||||
$scope.dismiss = ->
|
$scope.dismiss = ->
|
||||||
$modalInstance.dismiss('cancel')
|
$uibModalInstance.dismiss('cancel')
|
||||||
|
|
||||||
$scope.openSignup = (e) ->
|
$scope.openSignup = (e) ->
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
$modalInstance.dismiss('signup')
|
$uibModalInstance.dismiss('signup')
|
||||||
|
|
||||||
$scope.openResetPassword = (e) ->
|
$scope.openResetPassword = (e) ->
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
$modalInstance.dismiss('resetPassword')
|
$uibModalInstance.dismiss('resetPassword')
|
||||||
]
|
]
|
||||||
|
|
||||||
# what to do when the modal is closed
|
# what to do when the modal is closed
|
||||||
@ -303,25 +320,26 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
|
|||||||
$scope.signup()
|
$scope.signup()
|
||||||
else if reason is 'resetPassword'
|
else if reason is 'resetPassword'
|
||||||
# open the 'reset password' modal
|
# open the 'reset password' modal
|
||||||
$modal.open
|
$uibModal.open
|
||||||
templateUrl: '<%= asset_path "shared/passwordNewModal.html" %>'
|
templateUrl: '<%= asset_path "shared/passwordNewModal.html" %>'
|
||||||
size: 'sm'
|
size: 'sm'
|
||||||
controller: ['$scope', '$modalInstance', '$http', ($scope, $modalInstance, $http) ->
|
controller: ['$scope', '$uibModalInstance', '$http', ($scope, $uibModalInstance, $http) ->
|
||||||
$scope.user = {email: ''}
|
$scope.user = {email: ''}
|
||||||
$scope.sendReset = () ->
|
$scope.sendReset = () ->
|
||||||
$scope.alerts = []
|
$scope.alerts = []
|
||||||
$http.post('/users/password.json', {user: $scope.user}).success ->
|
$http.post('/users/password.json', {user: $scope.user}).success ->
|
||||||
$modalInstance.close()
|
$uibModalInstance.close()
|
||||||
.error ->
|
.error ->
|
||||||
$scope.alerts.push
|
$scope.alerts.push
|
||||||
msg: "Votre adresse email n'existe pas."
|
msg: _t('your_email_address_is_unknown')
|
||||||
type: 'danger'
|
type: 'danger'
|
||||||
|
|
||||||
]
|
]
|
||||||
.result['finally'](null).then ->
|
.result['finally'](null).then ->
|
||||||
growl.addInfoMessage('Vous allez recevoir sous quelques minutes un e-mail vous indiquant comment réinitialiser votre mot de passe.')
|
growl.info(_t('you_will_receive_in_a_moment_an_email_with_instructions_to_reset_your_password'))
|
||||||
|
|
||||||
# otherwise the user just closed the modal
|
# otherwise the user just closed the modal
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,43 +1,7 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
##
|
Application.Controllers.controller "DashboardController", ["$scope", 'memberPromise', ($scope, memberPromise) ->
|
||||||
# Controller used on the private projects listing page (my dashboard/projects)
|
|
||||||
##
|
|
||||||
Application.Controllers.controller "dashboardProjectsController", ["$scope", 'Member', ($scope, Member) ->
|
|
||||||
|
|
||||||
## Current user's profile
|
## Current user's profile
|
||||||
$scope.user = Member.get {id: $scope.currentUser.id}
|
$scope.user = memberPromise
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
##
|
|
||||||
# Controller used on the personal trainings page (my dashboard/trainings)
|
|
||||||
##
|
|
||||||
Application.Controllers.controller "dashboardTrainingsController", ["$scope", 'Member', ($scope, Member) ->
|
|
||||||
|
|
||||||
## Current user's profile
|
|
||||||
$scope.user = Member.get {id: $scope.currentUser.id}
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
##
|
|
||||||
# Controller used on the private events page (my dashboard/events)
|
|
||||||
##
|
|
||||||
Application.Controllers.controller "dashboardEventsController", ["$scope", 'Member', ($scope, Member) ->
|
|
||||||
|
|
||||||
## Current user's profile
|
|
||||||
$scope.user = Member.get {id: $scope.currentUser.id}
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
##
|
|
||||||
# Controller used on the personal invoices listing page (my dashboard/invoices)
|
|
||||||
##
|
|
||||||
Application.Controllers.controller "dashboardInvoicesController", ["$scope", 'Member', ($scope, Member) ->
|
|
||||||
|
|
||||||
## Current user's profile
|
|
||||||
$scope.user = Member.get {id: $scope.currentUser.id}
|
|
||||||
]
|
]
|
||||||
|
@ -1,71 +1,72 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
Application.Controllers.controller "eventsController", ["$scope", "$state", 'Event', ($scope, $state, Event) ->
|
Application.Controllers.controller "EventsController", ["$scope", "$state", 'Event', ($scope, $state, Event) ->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### PRIVATE STATIC CONSTANTS ###
|
### PRIVATE STATIC CONSTANTS ###
|
||||||
|
|
||||||
# Number of events added to the page when the user clicks on 'load next events'
|
# Number of events added to the page when the user clicks on 'load next events'
|
||||||
EVENTS_PER_PAGE = 12
|
EVENTS_PER_PAGE = 12
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### PUBLIC SCOPE ###
|
### PUBLIC SCOPE ###
|
||||||
|
|
||||||
## The events displayed on the page
|
## The events displayed on the page
|
||||||
$scope.events = []
|
$scope.events = []
|
||||||
|
|
||||||
## By default, the pagination mode is activated to limit the page size
|
## By default, the pagination mode is activated to limit the page size
|
||||||
$scope.paginateActive = true
|
$scope.paginateActive = true
|
||||||
|
|
||||||
## The currently displayed page number
|
## The currently displayed page number
|
||||||
$scope.page = 1
|
$scope.page = 1
|
||||||
|
|
||||||
##
|
##
|
||||||
# Adds EVENTS_PER_PAGE events to the bottom of the page, grouped by month
|
# Adds EVENTS_PER_PAGE events to the bottom of the page, grouped by month
|
||||||
##
|
##
|
||||||
$scope.loadMoreEvents = ->
|
$scope.loadMoreEvents = ->
|
||||||
Event.query {page: $scope.page}, (data) ->
|
Event.query {page: $scope.page}, (data) ->
|
||||||
$scope.events = $scope.events.concat data
|
$scope.events = $scope.events.concat data
|
||||||
if data.length > 0
|
if data.length > 0
|
||||||
$scope.paginateActive = false if ($scope.page-2)*EVENTS_PER_PAGE+data.length >= data[0].nb_total_events
|
$scope.paginateActive = false if ($scope.page-2)*EVENTS_PER_PAGE+data.length >= data[0].nb_total_events
|
||||||
|
|
||||||
$scope.eventsGroupByMonth = _.groupBy($scope.events, (obj) ->
|
$scope.eventsGroupByMonth = _.groupBy($scope.events, (obj) ->
|
||||||
_.map ['month', 'year'], (key, value) -> obj[key]
|
_.map ['month', 'year'], (key, value) -> obj[key]
|
||||||
)
|
)
|
||||||
$scope.monthOrder = _.sortBy _.keys($scope.eventsGroupByMonth), (k)->
|
$scope.monthOrder = _.sortBy _.keys($scope.eventsGroupByMonth), (k)->
|
||||||
monthYearArray = k.split(',')
|
monthYearArray = k.split(',')
|
||||||
date = new Date()
|
date = new Date()
|
||||||
date.setMonth(monthYearArray[0])
|
date.setMonth(monthYearArray[0])
|
||||||
date.setYear(monthYearArray[1])
|
date.setYear(monthYearArray[1])
|
||||||
return -date.getTime()
|
return -date.getTime()
|
||||||
else
|
else
|
||||||
$scope.paginateActive = false
|
$scope.paginateActive = false
|
||||||
$scope.page += 1
|
$scope.page += 1
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
# Callback to redirect the user to the specified event page
|
# Callback to redirect the user to the specified event page
|
||||||
# @param event {{id:number}}
|
# @param event {{id:number}}
|
||||||
##
|
##
|
||||||
$scope.showEvent = (event) ->
|
$scope.showEvent = (event) ->
|
||||||
$state.go('app.public.events_show', {id: event.id})
|
$state.go('app.public.events_show', {id: event.id})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### PRIVATE SCOPE ###
|
### PRIVATE SCOPE ###
|
||||||
|
|
||||||
##
|
##
|
||||||
# Kind of constructor: these actions will be realized first when the controller is loaded
|
# Kind of constructor: these actions will be realized first when the controller is loaded
|
||||||
##
|
##
|
||||||
initialize = ->
|
initialize = ->
|
||||||
$scope.loadMoreEvents()
|
$scope.loadMoreEvents()
|
||||||
|
|
||||||
|
|
||||||
## !!! MUST BE CALLED AT THE END of the controller
|
|
||||||
initialize()
|
## !!! MUST BE CALLED AT THE END of the controller
|
||||||
|
initialize()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -74,47 +75,440 @@ Application.Controllers.controller "eventsController", ["$scope", "$state", 'Eve
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
Application.Controllers.controller "showEventController", ["$scope", "$state", "$stateParams", "Event", '$modal', 'Member', ($scope, $state, $stateParams, Event, $modal, Member) ->
|
Application.Controllers.controller "ShowEventController", ["$scope", "$state", "$stateParams", "Event", '$uibModal', 'Member', 'Reservation', 'Price', 'CustomAsset', 'eventPromise', 'reducedAmountAlert', 'growl', '_t'
|
||||||
|
($scope, $state, $stateParams, Event, $uibModal, Member, Reservation, Price, CustomAsset, eventPromise, reducedAmountAlert, growl, _t) ->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### PUBLIC SCOPE ###
|
### PUBLIC SCOPE ###
|
||||||
|
$scope.reducedAmountAlert = reducedAmountAlert.setting.value
|
||||||
|
|
||||||
## current event details
|
## reservations for the currently shown event
|
||||||
$scope.event = {}
|
$scope.reservations = []
|
||||||
|
|
||||||
|
## user to deal with
|
||||||
|
$scope.ctrl =
|
||||||
|
member: {}
|
||||||
|
|
||||||
|
## parameters for a new reservation
|
||||||
|
$scope.reserve =
|
||||||
|
nbPlaces: []
|
||||||
|
nbReducedPlaces: []
|
||||||
|
nbReservePlaces: 0
|
||||||
|
nbReserveReducedPlaces: 0
|
||||||
|
toReserve: false
|
||||||
|
amountTotal : 0
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
##
|
# get the details for the current event (event's id is recovered from the current URL)
|
||||||
# Callback to delete the provided event (admins only)
|
$scope.event = eventPromise
|
||||||
# @param event {$resource} angular's Event $resource
|
|
||||||
##
|
|
||||||
$scope.deleteEvent = (event) ->
|
|
||||||
event.$delete ->
|
|
||||||
$state.go('app.public.events_list')
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### PRIVATE SCOPE ###
|
##
|
||||||
|
# Callback to delete the provided event (admins only)
|
||||||
##
|
# @param event {$resource} angular's Event $resource
|
||||||
# Kind of constructor: these actions will be realized first when the controller is loaded
|
##
|
||||||
##
|
$scope.deleteEvent = (event) ->
|
||||||
initialize = ->
|
event.$delete ->
|
||||||
|
$state.go('app.public.events_list')
|
||||||
# get the details for the current event (event's id is recovered from the current URL)
|
|
||||||
Event.get {id: $stateParams.id}
|
|
||||||
, (data) ->
|
|
||||||
$scope.event = data
|
|
||||||
if !$scope.event.reduced_amount
|
|
||||||
$scope.event.reduced_amount = 0
|
|
||||||
return
|
|
||||||
, ->
|
|
||||||
$state.go('app.public.events_list')
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## !!! MUST BE CALLED AT THE END of the controller
|
##
|
||||||
initialize()
|
# Callback to call when the number of places change in the current booking
|
||||||
|
##
|
||||||
|
$scope.changeNbPlaces = ->
|
||||||
|
reste = $scope.event.nb_free_places - $scope.reserve.nbReservePlaces
|
||||||
|
$scope.reserve.nbReducedPlaces = [0..reste]
|
||||||
|
$scope.computeEventAmount()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Callback to call when the number of discounted places change in the current booking
|
||||||
|
##
|
||||||
|
$scope.changeNbReducedPlaces = ->
|
||||||
|
reste = $scope.event.nb_free_places - $scope.reserve.nbReserveReducedPlaces
|
||||||
|
$scope.reserve.nbPlaces = [0..reste]
|
||||||
|
$scope.computeEventAmount()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Callback to reset the current reservation parameters
|
||||||
|
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
|
||||||
|
##
|
||||||
|
$scope.cancelReserve = (e)->
|
||||||
|
e.preventDefault()
|
||||||
|
resetEventReserve()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Callback to allow the user to set the details for his reservation
|
||||||
|
##
|
||||||
|
$scope.reserveEvent = ->
|
||||||
|
if $scope.event.nb_total_places > 0
|
||||||
|
$scope.reserveSuccess = false
|
||||||
|
if !$scope.isAuthenticated()
|
||||||
|
$scope.login null, (user)->
|
||||||
|
$scope.reserve.toReserve = !$scope.reserve.toReserve
|
||||||
|
if user.role isnt 'admin'
|
||||||
|
$scope.ctrl.member = user
|
||||||
|
else
|
||||||
|
Member.query (members) ->
|
||||||
|
$scope.members = members
|
||||||
|
else
|
||||||
|
$scope.reserve.toReserve = !$scope.reserve.toReserve
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Callback to deal with the reservations of the user selected in the dropdown list instead of the current user's
|
||||||
|
# reservations. (admins only)
|
||||||
|
##
|
||||||
|
$scope.updateMember = ->
|
||||||
|
resetEventReserve()
|
||||||
|
$scope.reserveSuccess = false
|
||||||
|
if $scope.ctrl.member
|
||||||
|
getReservations($scope.event.id, 'Event', $scope.ctrl.member.id)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Callback to trigger the payment process of the current reservation
|
||||||
|
##
|
||||||
|
$scope.payEvent = ->
|
||||||
|
|
||||||
|
# first, we check that a user was selected
|
||||||
|
if Object.keys($scope.ctrl.member).length > 0
|
||||||
|
reservation = mkReservation($scope.ctrl.member, $scope.reserve, $scope.event)
|
||||||
|
|
||||||
|
if $scope.currentUser.role isnt 'admin' and $scope.reserve.amountTotal > 0
|
||||||
|
payByStripe(reservation)
|
||||||
|
else
|
||||||
|
if $scope.currentUser.role is 'admin' or $scope.reserve.amountTotal is 0
|
||||||
|
payOnSite(reservation)
|
||||||
|
else
|
||||||
|
# otherwise we alert, this error musn't occur when the current user is not admin
|
||||||
|
growl.error(_t('please_select_a_member_first'))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Callback to validate the booking of a free event
|
||||||
|
##
|
||||||
|
$scope.validReserveEvent = ->
|
||||||
|
reservation =
|
||||||
|
user_id: $scope.ctrl.member.id
|
||||||
|
reservable_id: $scope.event.id
|
||||||
|
reservable_type: 'Event'
|
||||||
|
slots_attributes: []
|
||||||
|
nb_reserve_places: $scope.reserve.nbReservePlaces
|
||||||
|
nb_reserve_reduced_places: $scope.reserve.nbReserveReducedPlaces
|
||||||
|
reservation.slots_attributes.push
|
||||||
|
start_at: $scope.event.start_date
|
||||||
|
end_at: $scope.event.end_date
|
||||||
|
availability_id: $scope.event.availability.id
|
||||||
|
$scope.attempting = true
|
||||||
|
Reservation.save reservation: reservation, (reservation) ->
|
||||||
|
afterPayment(reservation)
|
||||||
|
$scope.attempting = false
|
||||||
|
, (response)->
|
||||||
|
$scope.alerts = []
|
||||||
|
$scope.alerts.push
|
||||||
|
msg: response.data.card[0]
|
||||||
|
type: 'danger'
|
||||||
|
$scope.attempting = false
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Callback to alter an already booked reservation date. A modal window will be opened to allow the user to choose
|
||||||
|
# a new date for his reservation (if any available)
|
||||||
|
# @param reservation {{id:number, reservable_id:number, nb_reserve_places:number, nb_reserve_reduced_places:number}}
|
||||||
|
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
|
||||||
|
##
|
||||||
|
$scope.modifyReservation = (reservation, e)->
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
|
index = $scope.reservations.indexOf(reservation)
|
||||||
|
$uibModal.open
|
||||||
|
templateUrl: '<%= asset_path "events/modify_event_reservation_modal.html" %>'
|
||||||
|
resolve:
|
||||||
|
event: -> $scope.event
|
||||||
|
reservation: -> reservation
|
||||||
|
controller: ['$scope', '$uibModalInstance', 'event', 'reservation', 'Reservation', ($scope, $uibModalInstance, event, reservation, Reservation) ->
|
||||||
|
# we copy the controller's resolved parameters into the scope
|
||||||
|
$scope.event = event
|
||||||
|
$scope.reservation = angular.copy reservation
|
||||||
|
|
||||||
|
# set the reservable_id to the first available event
|
||||||
|
for e in event.recurrence_events
|
||||||
|
if e.nb_free_places > (reservation.nb_reserve_places + reservation.nb_reserve_reduced_places)
|
||||||
|
$scope.reservation.reservable_id = e.id
|
||||||
|
break
|
||||||
|
|
||||||
|
# Callback to validate the new reservation's date
|
||||||
|
$scope.ok = ->
|
||||||
|
eventToPlace = null
|
||||||
|
angular.forEach event.recurrence_events, (e)->
|
||||||
|
if e.id is parseInt($scope.reservation.reservable_id, 10)
|
||||||
|
eventToPlace = e
|
||||||
|
$scope.reservation.slots[0].start_at = eventToPlace.start_date
|
||||||
|
$scope.reservation.slots[0].end_at = eventToPlace.end_date
|
||||||
|
$scope.reservation.slots[0].availability_id = eventToPlace.availability_id
|
||||||
|
$scope.reservation.slots_attributes = $scope.reservation.slots
|
||||||
|
$scope.attempting = true
|
||||||
|
Reservation.update {id: reservation.id}, {reservation: $scope.reservation}, (reservation) ->
|
||||||
|
$uibModalInstance.close(reservation)
|
||||||
|
$scope.attempting = true
|
||||||
|
, (response)->
|
||||||
|
$scope.alerts = []
|
||||||
|
angular.forEach response, (v, k)->
|
||||||
|
angular.forEach v, (err)->
|
||||||
|
$scope.alerts.push({msg: k+': '+err, type: 'danger'})
|
||||||
|
$scope.attempting = false
|
||||||
|
|
||||||
|
# Callback to cancel the modification
|
||||||
|
$scope.cancel = ->
|
||||||
|
$uibModalInstance.dismiss('cancel')
|
||||||
|
]
|
||||||
|
.result['finally'](null).then (reservation)->
|
||||||
|
$scope.reservations.splice(index, 1)
|
||||||
|
$scope.event.nb_free_places = $scope.event.nb_free_places + reservation.nb_reserve_places + reservation.nb_reserve_reduced_places
|
||||||
|
angular.forEach $scope.event.recurrence_events, (e)->
|
||||||
|
if e.id is parseInt(reservation.reservable_id, 10)
|
||||||
|
e.nb_free_places = e.nb_free_places - reservation.nb_reserve_places - reservation.nb_reserve_reduced_places
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Checks if the provided reservation is able to be modified
|
||||||
|
# @param reservation {{nb_reserve_places:number, nb_reserve_reduced_places:number}}
|
||||||
|
##
|
||||||
|
$scope.reservationCanModify = (reservation)->
|
||||||
|
isAble = false
|
||||||
|
angular.forEach $scope.event.recurrence_events, (e)->
|
||||||
|
isAble = true if e.nb_free_places > (reservation.nb_reserve_places + reservation.nb_reserve_reduced_places)
|
||||||
|
isAble
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Compute the total amount for the current reservation according to the previously set parameters
|
||||||
|
# and assign the result in $scope.reserve.amountTotal
|
||||||
|
##
|
||||||
|
$scope.computeEventAmount = ->
|
||||||
|
# first we check that a user was selected
|
||||||
|
if Object.keys($scope.ctrl.member).length > 0
|
||||||
|
r = mkReservation($scope.ctrl.member, $scope.reserve, $scope.event)
|
||||||
|
Price.compute {reservation: r}, (res) ->
|
||||||
|
$scope.reserve.amountTotal = res.price
|
||||||
|
else
|
||||||
|
$scope.reserve.amountTotal = null
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### PRIVATE SCOPE ###
|
||||||
|
|
||||||
|
##
|
||||||
|
# Kind of constructor: these actions will be realized first when the controller is loaded
|
||||||
|
##
|
||||||
|
initialize = ->
|
||||||
|
# gather the current user or the list of users if the current user is an admin
|
||||||
|
if $scope.currentUser
|
||||||
|
if $scope.currentUser.role isnt 'admin'
|
||||||
|
$scope.ctrl.member = $scope.currentUser
|
||||||
|
else
|
||||||
|
Member.query (members) ->
|
||||||
|
$scope.members = members
|
||||||
|
|
||||||
|
# check that the event's reduced rate is initialized
|
||||||
|
if !$scope.event.reduced_amount
|
||||||
|
$scope.event.reduced_amount = 0
|
||||||
|
|
||||||
|
# initialize the "reserve" object with the event's data
|
||||||
|
$scope.reserve.nbPlaces = [0..$scope.event.nb_free_places]
|
||||||
|
$scope.reserve.nbReducedPlaces = [0..$scope.event.nb_free_places]
|
||||||
|
|
||||||
|
# if non-admin, get the current user's reservations into $scope.reservations
|
||||||
|
if $scope.currentUser
|
||||||
|
getReservations($scope.event.id, 'Event', $scope.currentUser.id)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Retrieve the reservations for the couple event / user
|
||||||
|
# @param reservable_id {number} the current event id
|
||||||
|
# @param reservable_type {string} 'Event'
|
||||||
|
# @param user_id {number} the user's id (current or managed)
|
||||||
|
##
|
||||||
|
getReservations = (reservable_id, reservable_type, user_id)->
|
||||||
|
Reservation.query(reservable_id: reservable_id, reservable_type: reservable_type, user_id: user_id).$promise.then (reservations)->
|
||||||
|
$scope.reservations = reservations
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Create an hash map implementing the Reservation specs
|
||||||
|
# @param member {Object} User as retreived from the API: current user / selected user if current is admin
|
||||||
|
# @param reserve {Object} Reservation parameters (places...)
|
||||||
|
# @param event {Object} Current event (Atelier/Stage)
|
||||||
|
# @return {{user_id:Number, reservable_id:Number, reservable_type:String, slots_attributes:Array<Object>, nb_reserve_places:Number, nb_reserve_reduced_places:Number}}
|
||||||
|
##
|
||||||
|
mkReservation = (member, reserve, event) ->
|
||||||
|
reservation =
|
||||||
|
user_id: member.id
|
||||||
|
reservable_id: event.id
|
||||||
|
reservable_type: 'Event'
|
||||||
|
slots_attributes: []
|
||||||
|
nb_reserve_places: reserve.nbReservePlaces
|
||||||
|
nb_reserve_reduced_places: reserve.nbReserveReducedPlaces
|
||||||
|
|
||||||
|
reservation.slots_attributes.push
|
||||||
|
start_at: event.start_date
|
||||||
|
end_at: event.end_date
|
||||||
|
availability_id: event.availability.id
|
||||||
|
offered: event.offered || false
|
||||||
|
|
||||||
|
reservation
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Set the current reservation to the default values. This implies to reservation form to be hidden.
|
||||||
|
##
|
||||||
|
resetEventReserve = ->
|
||||||
|
if $scope.event
|
||||||
|
$scope.reserve =
|
||||||
|
nbPlaces: [0..$scope.event.nb_free_places]
|
||||||
|
nbReducedPlaces: [0..$scope.event.nb_free_places]
|
||||||
|
nbReservePlaces: 0
|
||||||
|
nbReserveReducedPlaces: 0
|
||||||
|
toReserve: false
|
||||||
|
amountTotal : 0
|
||||||
|
$scope.event.offered = false
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Open a modal window which trigger the stripe payment process
|
||||||
|
# @param reservation {Object} to book
|
||||||
|
##
|
||||||
|
payByStripe = (reservation) ->
|
||||||
|
$uibModal.open
|
||||||
|
templateUrl: '<%= asset_path "stripe/payment_modal.html" %>'
|
||||||
|
size: 'md'
|
||||||
|
resolve:
|
||||||
|
reservation: ->
|
||||||
|
reservation
|
||||||
|
price: ->
|
||||||
|
Price.compute({reservation: reservation}).$promise
|
||||||
|
cgv: ->
|
||||||
|
CustomAsset.get({name: 'cgv-file'}).$promise
|
||||||
|
objectToPay: ->
|
||||||
|
eventToReserve: $scope.event
|
||||||
|
reserve: $scope.reserve
|
||||||
|
member: $scope.ctrl.member
|
||||||
|
controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'cgv', 'Auth', 'Reservation', 'growl', ($scope, $uibModalInstance, $state, reservation, price, cgv, Auth, Reservation, growl) ->
|
||||||
|
# Price
|
||||||
|
$scope.amount = price.price
|
||||||
|
|
||||||
|
# CGV
|
||||||
|
$scope.cgv = cgv.custom_asset
|
||||||
|
|
||||||
|
# Reservation
|
||||||
|
$scope.reservation = reservation
|
||||||
|
|
||||||
|
# Callback for the stripe payment authorization
|
||||||
|
$scope.payment = (status, response) ->
|
||||||
|
if response.error
|
||||||
|
growl.error(response.error.message)
|
||||||
|
else
|
||||||
|
$scope.attempting = true
|
||||||
|
$scope.reservation.card_token = response.id
|
||||||
|
Reservation.save reservation: $scope.reservation, (reservation) ->
|
||||||
|
$uibModalInstance.close(reservation)
|
||||||
|
, (response)->
|
||||||
|
$scope.alerts = []
|
||||||
|
$scope.alerts.push
|
||||||
|
msg: response.data.card[0]
|
||||||
|
type: 'danger'
|
||||||
|
$scope.attempting = false
|
||||||
|
]
|
||||||
|
.result['finally'](null).then (reservation)->
|
||||||
|
afterPayment(reservation)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Open a modal window which trigger the local payment process
|
||||||
|
# @param reservation {Object} to book
|
||||||
|
##
|
||||||
|
payOnSite = (reservation) ->
|
||||||
|
$uibModal.open
|
||||||
|
templateUrl: '<%= asset_path "shared/valid_reservation_modal.html" %>'
|
||||||
|
size: 'sm'
|
||||||
|
resolve:
|
||||||
|
reservation: ->
|
||||||
|
reservation
|
||||||
|
price: ->
|
||||||
|
Price.compute({reservation: reservation}).$promise
|
||||||
|
controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'Auth', 'Reservation', ($scope, $uibModalInstance, $state, reservation, price, Auth, Reservation) ->
|
||||||
|
# Price
|
||||||
|
$scope.amount = price.price
|
||||||
|
|
||||||
|
# Reservation
|
||||||
|
$scope.reservation = reservation
|
||||||
|
|
||||||
|
# Button label
|
||||||
|
if $scope.amount > 0
|
||||||
|
$scope.validButtonName = _t('confirm_(payment_on_site)')
|
||||||
|
else
|
||||||
|
$scope.validButtonName = _t('confirm')
|
||||||
|
|
||||||
|
# Callback to validate the payment
|
||||||
|
$scope.ok = ->
|
||||||
|
$scope.attempting = true
|
||||||
|
Reservation.save reservation: $scope.reservation, (reservation) ->
|
||||||
|
$uibModalInstance.close(reservation)
|
||||||
|
$scope.attempting = true
|
||||||
|
, (response)->
|
||||||
|
$scope.alerts = []
|
||||||
|
angular.forEach response, (v, k)->
|
||||||
|
angular.forEach v, (err)->
|
||||||
|
$scope.alerts.push
|
||||||
|
msg: k+': '+err
|
||||||
|
type: 'danger'
|
||||||
|
$scope.attempting = false
|
||||||
|
|
||||||
|
# Callback to cancel the payment
|
||||||
|
$scope.cancel = ->
|
||||||
|
$uibModalInstance.dismiss('cancel')
|
||||||
|
]
|
||||||
|
.result['finally'](null).then (reservation)->
|
||||||
|
afterPayment(reservation)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# What to do after the payment was successful
|
||||||
|
# @param resveration {Object} booked reservation
|
||||||
|
##
|
||||||
|
afterPayment = (reservation)->
|
||||||
|
$scope.event.nb_free_places = $scope.event.nb_free_places - reservation.nb_reserve_places - reservation.nb_reserve_reduced_places
|
||||||
|
resetEventReserve()
|
||||||
|
$scope.reserveSuccess = true
|
||||||
|
$scope.reservations.push reservation
|
||||||
|
if $scope.currentUser.role == 'admin'
|
||||||
|
$scope.ctrl.member = null
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## !!! MUST BE CALLED AT THE END of the controller
|
||||||
|
initialize()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1,35 +1,34 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
Application.Controllers.controller "homeController", ['$scope', '$stateParams', 'Member', 'Twitter', 'Project', 'Event', ($scope, $stateParams, Member, Twitter, Project, Event) ->
|
Application.Controllers.controller "HomeController", ['$scope', '$stateParams', 'Twitter', 'lastMembersPromise', 'lastProjectsPromise', 'upcomingEventsPromise', 'homeBlogpostPromise', 'twitterNamePromise', ($scope, $stateParams, Twitter, lastMembersPromise, lastProjectsPromise, upcomingEventsPromise, homeBlogpostPromise, twitterNamePromise)->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### PRIVATE STATIC CONSTANTS ###
|
|
||||||
|
|
||||||
# The 4 last users will be displayed on the home page
|
|
||||||
LAST_MEMBERS_LIMIT = 4
|
|
||||||
|
|
||||||
# Only the last tweet is shown
|
|
||||||
LAST_TWEETS_LIMIT = 1
|
|
||||||
|
|
||||||
# The 3 closest events are shown
|
|
||||||
LAST_EVENTS_LIMIT = 3
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### PUBLIC SCOPE ###
|
### PUBLIC SCOPE ###
|
||||||
|
|
||||||
## The last registered members who confirmed their addresses
|
## The last registered members who confirmed their addresses
|
||||||
$scope.last_members = []
|
$scope.lastMembers = lastMembersPromise
|
||||||
|
|
||||||
## The last tweets from the Fablab official twitter account
|
## The last tweets from the Fablab official twitter account
|
||||||
$scope.last_tweets = []
|
$scope.lastTweets = []
|
||||||
|
|
||||||
## The last projects published/documented on the plateform
|
## The last projects published/documented on the plateform
|
||||||
$scope.last_projects = []
|
$scope.lastProjects = lastProjectsPromise
|
||||||
|
|
||||||
## The closest upcoming events
|
## The closest upcoming events
|
||||||
$scope.upcoming_events = []
|
$scope.upcomingEvents = upcomingEventsPromise
|
||||||
|
|
||||||
|
## The admin blogpost
|
||||||
|
$scope.homeBlogpost = homeBlogpostPromise.setting.value
|
||||||
|
|
||||||
|
## Twitter username
|
||||||
|
$scope.twitterName = twitterNamePromise.setting.value
|
||||||
|
|
||||||
|
##
|
||||||
|
# Test if the provided event run on a single day or not
|
||||||
|
# @param event {Object} single event from the $scope.upcomingEvents array
|
||||||
|
# @returns {boolean} false if the event runs on more that 1 day
|
||||||
|
##
|
||||||
|
$scope.isOneDayEvent = (event) ->
|
||||||
|
moment(event.start_date).isSame(event.end_date, 'day')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -39,20 +38,15 @@ Application.Controllers.controller "homeController", ['$scope', '$stateParams',
|
|||||||
# Kind of constructor: these actions will be realized first when the controller is loaded
|
# Kind of constructor: these actions will be realized first when the controller is loaded
|
||||||
##
|
##
|
||||||
initialize = ->
|
initialize = ->
|
||||||
# display the reset password dialog if the parameter was provided
|
# we retrieve tweets from here instead of ui-router's promise because, if adblock stop the API request,
|
||||||
|
# this prevent the whole home page to be blocked
|
||||||
|
$scope.lastTweets = Twitter.query(limit: 1)
|
||||||
|
|
||||||
|
# if we recieve a token to reset the password as GET parameter, trigger the
|
||||||
|
# changePassword modal from the parent controller
|
||||||
if $stateParams.reset_password_token
|
if $stateParams.reset_password_token
|
||||||
$scope.$parent.editPassword($stateParams.reset_password_token)
|
$scope.$parent.editPassword($stateParams.reset_password_token)
|
||||||
|
|
||||||
# initialize the homepage data
|
|
||||||
Member.lastSubscribed {limit: LAST_MEMBERS_LIMIT}, (members) ->
|
|
||||||
$scope.last_members = members
|
|
||||||
Twitter.query {limit: LAST_TWEETS_LIMIT}, (tweets) ->
|
|
||||||
$scope.last_tweets = tweets
|
|
||||||
Project.lastPublished (projects) ->
|
|
||||||
$scope.last_projects = projects
|
|
||||||
Event.upcoming {limit: LAST_EVENTS_LIMIT}, (events) ->
|
|
||||||
$scope.upcoming_events = events
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## !!! MUST BE CALLED AT THE END of the controller
|
## !!! MUST BE CALLED AT THE END of the controller
|
||||||
|
@ -74,19 +74,96 @@ class MachinesController
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Manages the transition when a user clicks on the reservation button.
|
||||||
|
# According to the status of user currently logged into the system, redirect him to the reservation page,
|
||||||
|
# or display a modal window asking him to complete a training before he can book a machine reservation.
|
||||||
|
# @param machine {{id:number}} An object containg the id of the machine to book,
|
||||||
|
# the object will be completed before the fonction returns.
|
||||||
|
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
|
||||||
|
##
|
||||||
|
_reserveMachine = (machine, e) ->
|
||||||
|
_this = this
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
|
# retrieve the full machine object
|
||||||
|
machine = _this.Machine.get {id: machine.id}, ->
|
||||||
|
|
||||||
|
# if the currently logged'in user has completed the training for this machine, or this machine does not require
|
||||||
|
# a prior training, just redirect him to the machine's booking page
|
||||||
|
if machine.current_user_is_training or machine.trainings.length == 0
|
||||||
|
_this.$state.go('app.logged.machines_reserve', {id: machine.id})
|
||||||
|
else
|
||||||
|
# otherwise, if a user is authenticated ...
|
||||||
|
if _this.$scope.isAuthenticated()
|
||||||
|
# ... and have booked a training for this machine, tell him that he must wait for an admin to validate
|
||||||
|
# the training before he can book the reservation
|
||||||
|
if machine.current_user_training_reservation
|
||||||
|
_this.$uibModal.open
|
||||||
|
templateUrl: '<%= asset_path "machines/training_reservation_modal.html" %>'
|
||||||
|
controller: ['$scope', '$uibModalInstance', '$state', ($scope, $uibModalInstance, $state) ->
|
||||||
|
$scope.machine = machine
|
||||||
|
$scope.cancel = ->
|
||||||
|
$uibModalInstance.dismiss('cancel')
|
||||||
|
]
|
||||||
|
# ... but does not have booked the training, tell him to register for a training session first
|
||||||
|
else
|
||||||
|
_this.$uibModal.open
|
||||||
|
templateUrl: '<%= asset_path "machines/request_training_modal.html" %>'
|
||||||
|
controller: ['$scope', '$uibModalInstance', '$state', ($scope, $uibModalInstance, $state) ->
|
||||||
|
$scope.machine = machine
|
||||||
|
$scope.member = _this.$scope.currentUser
|
||||||
|
|
||||||
|
# transform the name of the trainings associated with the machine to integrate them in a sentence
|
||||||
|
$scope.humanizeTrainings = ->
|
||||||
|
text = ''
|
||||||
|
angular.forEach $scope.machine.trainings, (training) ->
|
||||||
|
if text.length > 0
|
||||||
|
text += _this._t('_or_the_')
|
||||||
|
text += training.name.substr(0,1).toLowerCase() + training.name.substr(1)
|
||||||
|
text
|
||||||
|
|
||||||
|
# modal is close with validation
|
||||||
|
$scope.ok = ->
|
||||||
|
$state.go('app.logged.trainings_reserve')
|
||||||
|
$uibModalInstance.close(machine)
|
||||||
|
|
||||||
|
# modal is closed with escaping
|
||||||
|
$scope.cancel = (e)->
|
||||||
|
e.preventDefault()
|
||||||
|
$uibModalInstance.dismiss('cancel')
|
||||||
|
]
|
||||||
|
# if the user is not logged, open the login modal window
|
||||||
|
else
|
||||||
|
_this.$scope.login()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
# Controller used in the public listing page, allowing everyone to see the list of machines
|
# Controller used in the public listing page, allowing everyone to see the list of machines
|
||||||
##
|
##
|
||||||
Application.Controllers.controller "machinesController", ["$scope", "$state", 'Machine', '$modal', ($scope, $state, Machine, $modal) ->
|
Application.Controllers.controller "MachinesController", ["$scope", "$state", '_t', 'Machine', '$uibModal', 'machinesPromise', ($scope, $state, _t, Machine, $uibModal, machinesPromise) ->
|
||||||
|
|
||||||
## Retrieve the list of machines
|
## Retrieve the list of machines
|
||||||
$scope.machines = Machine.query()
|
$scope.machines = machinesPromise
|
||||||
|
|
||||||
##
|
##
|
||||||
# Redirect the user to the machine details page
|
# Redirect the user to the machine details page
|
||||||
##
|
##
|
||||||
$scope.showMachine = (machine) ->
|
$scope.showMachine = (machine) ->
|
||||||
$state.go('app.public.machines_show', {id: machine.slug})
|
$state.go('app.public.machines_show', {id: machine.slug})
|
||||||
|
|
||||||
|
##
|
||||||
|
# Callback to book a reservation for the current machine
|
||||||
|
##
|
||||||
|
$scope.reserveMachine = _reserveMachine.bind
|
||||||
|
$scope: $scope
|
||||||
|
$state: $state
|
||||||
|
_t: _t
|
||||||
|
$uibModal: $uibModal
|
||||||
|
Machine: Machine
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -94,7 +171,7 @@ Application.Controllers.controller "machinesController", ["$scope", "$state", 'M
|
|||||||
##
|
##
|
||||||
# Controller used in the machine creation page (admin)
|
# Controller used in the machine creation page (admin)
|
||||||
##
|
##
|
||||||
Application.Controllers.controller "newMachineController", ["$scope", "$state", 'CSRF', ($scope, $state, CSRF) ->
|
Application.Controllers.controller "NewMachineController", ["$scope", "$state", 'CSRF',($scope, $state, CSRF) ->
|
||||||
CSRF.setMetaTags()
|
CSRF.setMetaTags()
|
||||||
|
|
||||||
## API URL where the form will be posted
|
## API URL where the form will be posted
|
||||||
@ -116,8 +193,11 @@ Application.Controllers.controller "newMachineController", ["$scope", "$state",
|
|||||||
##
|
##
|
||||||
# Controller used in the machine edition page (admin)
|
# Controller used in the machine edition page (admin)
|
||||||
##
|
##
|
||||||
Application.Controllers.controller "editMachineController", ["$scope", "$state", '$stateParams', 'Machine', 'CSRF', ($scope, $state, $stateParams, Machine, CSRF) ->
|
Application.Controllers.controller "EditMachineController", ["$scope", '$state', '$stateParams', 'machinePromise', 'CSRF', ($scope, $state, $stateParams, machinePromise, CSRF) ->
|
||||||
CSRF.setMetaTags()
|
|
||||||
|
|
||||||
|
|
||||||
|
### PUBLIC SCOPE ###
|
||||||
|
|
||||||
## API URL where the form will be posted
|
## API URL where the form will be posted
|
||||||
$scope.actionUrl = "/api/machines/" + $stateParams.id
|
$scope.actionUrl = "/api/machines/" + $stateParams.id
|
||||||
@ -126,14 +206,24 @@ Application.Controllers.controller "editMachineController", ["$scope", "$state",
|
|||||||
$scope.method = "put"
|
$scope.method = "put"
|
||||||
|
|
||||||
## Retrieve the details for the machine id in the URL, if an error occurs redirect the user to the machines list
|
## Retrieve the details for the machine id in the URL, if an error occurs redirect the user to the machines list
|
||||||
$scope.machine = Machine.get {id: $stateParams.id}
|
$scope.machine = machinePromise
|
||||||
, ->
|
|
||||||
return
|
|
||||||
, ->
|
|
||||||
$state.go('app.public.machines_list')
|
|
||||||
|
|
||||||
## Using the MachinesController
|
|
||||||
new MachinesController($scope, $state)
|
|
||||||
|
### PRIVATE SCOPE ###
|
||||||
|
|
||||||
|
##
|
||||||
|
# Kind of constructor: these actions will be realized first when the controller is loaded
|
||||||
|
##
|
||||||
|
initialize = ->
|
||||||
|
CSRF.setMetaTags()
|
||||||
|
|
||||||
|
## Using the MachinesController
|
||||||
|
new MachinesController($scope, $state)
|
||||||
|
|
||||||
|
|
||||||
|
## !!! MUST BE CALLED AT THE END of the controller
|
||||||
|
initialize()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -141,14 +231,11 @@ Application.Controllers.controller "editMachineController", ["$scope", "$state",
|
|||||||
##
|
##
|
||||||
# Controller used in the machine details page (public)
|
# Controller used in the machine details page (public)
|
||||||
##
|
##
|
||||||
Application.Controllers.controller "showMachineController", ['$scope', '$state', '$modal', '$stateParams', 'Machine', ($scope, $state, $modal, $stateParams, Machine) ->
|
Application.Controllers.controller "ShowMachineController", ['$scope', '$state', '$uibModal', '$stateParams', '_t', 'Machine', 'growl', 'machinePromise'
|
||||||
|
, ($scope, $state, $uibModal, $stateParams, _t, Machine, growl, machinePromise) ->
|
||||||
|
|
||||||
## Retrieve the details for the machine id in the URL, if an error occurs redirect the user to the machines list
|
## Retrieve the details for the machine id in the URL, if an error occurs redirect the user to the machines list
|
||||||
$scope.machine = Machine.get {id: $stateParams.id}
|
$scope.machine = machinePromise
|
||||||
, ->
|
|
||||||
return
|
|
||||||
, ->
|
|
||||||
$state.go('app.public.machines_list')
|
|
||||||
|
|
||||||
##
|
##
|
||||||
# Callback to delete the current machine (admins only)
|
# Callback to delete the current machine (admins only)
|
||||||
@ -156,9 +243,730 @@ Application.Controllers.controller "showMachineController", ['$scope', '$state',
|
|||||||
$scope.delete = (machine) ->
|
$scope.delete = (machine) ->
|
||||||
# check the permissions
|
# check the permissions
|
||||||
if $scope.currentUser.role isnt 'admin'
|
if $scope.currentUser.role isnt 'admin'
|
||||||
console.error 'Unauthorized operation'
|
console.error _t('unauthorized_operation')
|
||||||
else
|
else
|
||||||
# delete the machine then redirect to the machines listing
|
# delete the machine then redirect to the machines listing
|
||||||
machine.$delete ->
|
machine.$delete ->
|
||||||
$state.go('app.public.machines_list')
|
$state.go('app.public.machines_list')
|
||||||
|
, (error)->
|
||||||
|
growl.warning(_t('the_machine_cant_be_deleted_because_it_is_already_reserved_by_some_users'))
|
||||||
|
##
|
||||||
|
# Callback to book a reservation for the current machine
|
||||||
|
##
|
||||||
|
$scope.reserveMachine = _reserveMachine.bind
|
||||||
|
$scope: $scope
|
||||||
|
$state: $state
|
||||||
|
_t: _t
|
||||||
|
$uibModal: $uibModal
|
||||||
|
Machine: Machine
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Controller used in the machine reservation page (for logged users who have completed the training and admins).
|
||||||
|
# This controller workflow is pretty similar to the trainings reservation controller.
|
||||||
|
##
|
||||||
|
|
||||||
|
Application.Controllers.controller "ReserveMachineController", ["$scope", "$state", '$stateParams', "$uibModal", '_t', "moment", 'Machine', 'Auth', 'dialogs', '$timeout', 'Price', 'Member', 'Availability', 'Slot', 'Setting', 'CustomAsset', 'plansPromise', 'groupsPromise', 'growl', 'settingsPromise',
|
||||||
|
($scope, $state, $stateParams, $uibModal, _t, moment, Machine, Auth, dialogs, $timeout, Price, Member, Availability, Slot, Setting, CustomAsset, plansPromise, groupsPromise, growl, settingsPromise) ->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### PRIVATE STATIC CONSTANTS ###
|
||||||
|
|
||||||
|
# The calendar is divided in slots of 60 minutes
|
||||||
|
BASE_SLOT = '01:00:00'
|
||||||
|
|
||||||
|
# The calendar will be initialized positioned under 9:00 AM
|
||||||
|
DEFAULT_CALENDAR_POSITION = '09:00:00'
|
||||||
|
|
||||||
|
# The user is unable to modify his already booked reservation 1 day before it occurs
|
||||||
|
PREVENT_BOOKING_MODIFICATION_DELAY = 1
|
||||||
|
|
||||||
|
# Slot already booked by the current user
|
||||||
|
FREE_SLOT_BORDER_COLOR = '#e4cd78'
|
||||||
|
|
||||||
|
# Slot already booked by another user
|
||||||
|
UNAVAILABLE_SLOT_BORDER_COLOR = '#1d98ec'
|
||||||
|
|
||||||
|
# Slot free to be booked
|
||||||
|
BOOKED_SLOT_BORDER_COLOR = '#b2e774'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### PUBLIC SCOPE ###
|
||||||
|
|
||||||
|
## after fullCalendar loads, provides access to its methods through $scope.calendar.fullCalendar()
|
||||||
|
$scope.calendar = null
|
||||||
|
|
||||||
|
## bind the machine availabilities with full-Calendar events
|
||||||
|
$scope.eventSources = []
|
||||||
|
|
||||||
|
## fullCalendar event. The last selected slot that the user want to book
|
||||||
|
$scope.slotToPlace = null
|
||||||
|
|
||||||
|
## fullCalendar event. An already booked slot that the user want to modify
|
||||||
|
$scope.slotToModify = null
|
||||||
|
|
||||||
|
## indicates the state of the current view : calendar or plans informations
|
||||||
|
$scope.plansAreShown = false
|
||||||
|
|
||||||
|
## will store the user's plan if he choosed to buy one
|
||||||
|
$scope.selectedPlan = null
|
||||||
|
|
||||||
|
## array of fullCalendar events. Slots where the user want to book
|
||||||
|
$scope.eventsReserved = []
|
||||||
|
|
||||||
|
## total amount of the bill to pay
|
||||||
|
$scope.amountTotal = 0
|
||||||
|
|
||||||
|
## is the user allowed to change the date of his booking
|
||||||
|
$scope.enableBookingMove = true
|
||||||
|
|
||||||
|
## how many hours before the reservation, the user is still allowed to change his booking
|
||||||
|
$scope.moveBookingDelay = 24
|
||||||
|
|
||||||
|
## list of plans, classified by group
|
||||||
|
$scope.plansClassifiedByGroup = []
|
||||||
|
for group in groupsPromise
|
||||||
|
groupObj = { id: group.id, name: group.name, plans: [] }
|
||||||
|
for plan in plansPromise
|
||||||
|
groupObj.plans.push(plan) if plan.group_id == group.id
|
||||||
|
$scope.plansClassifiedByGroup.push(groupObj)
|
||||||
|
|
||||||
|
## the user to deal with, ie. the current user for non-admins
|
||||||
|
$scope.ctrl =
|
||||||
|
member: {}
|
||||||
|
|
||||||
|
## fablab users list
|
||||||
|
$scope.members = []
|
||||||
|
|
||||||
|
## current machine to reserve
|
||||||
|
$scope.machine = {}
|
||||||
|
|
||||||
|
## fullCalendar (v2) configuration
|
||||||
|
$scope.calendarConfig =
|
||||||
|
timezone: Fablab.timezone
|
||||||
|
lang: Fablab.fullcalendar_locale
|
||||||
|
header:
|
||||||
|
left: 'month agendaWeek'
|
||||||
|
center: 'title'
|
||||||
|
right: 'today prev,next'
|
||||||
|
firstDay: 1 # Week start on monday (France)
|
||||||
|
scrollTime: DEFAULT_CALENDAR_POSITION
|
||||||
|
slotDuration: BASE_SLOT
|
||||||
|
allDayDefault: false
|
||||||
|
minTime: '00:00:00'
|
||||||
|
maxTime: '24:00:00'
|
||||||
|
height: 'auto'
|
||||||
|
buttonIcons:
|
||||||
|
prev: 'left-single-arrow'
|
||||||
|
next: 'right-single-arrow'
|
||||||
|
timeFormat:
|
||||||
|
agenda:'H:mm'
|
||||||
|
month: 'H(:mm)'
|
||||||
|
axisFormat: 'H:mm'
|
||||||
|
|
||||||
|
allDaySlot: false
|
||||||
|
defaultView: 'agendaWeek'
|
||||||
|
editable: false
|
||||||
|
eventClick: (event, jsEvent, view) ->
|
||||||
|
calendarEventClickCb(event, jsEvent, view)
|
||||||
|
eventRender: (event, element, view) ->
|
||||||
|
eventRenderCb(event, element)
|
||||||
|
|
||||||
|
## Global config: message to the end user concerning the subscriptions rules
|
||||||
|
$scope.subscriptionExplicationsAlert = settingsPromise.subscription_explications_alert
|
||||||
|
|
||||||
|
## Gloabl config: message to the end user concerning the machine bookings
|
||||||
|
$scope.machineExplicationsAlert = settingsPromise.machine_explications_alert
|
||||||
|
|
||||||
|
## Global config: is the user authorized to change his bookings slots?
|
||||||
|
$scope.enableBookingMove = (settingsPromise.booking_move_enable == "true")
|
||||||
|
|
||||||
|
## Global config: delay in hours before a booking while changing the booking slot is forbidden
|
||||||
|
$scope.moveBookingDelay = parseInt(settingsPromise.booking_move_delay)
|
||||||
|
|
||||||
|
## Global config: is the user authorized to cancel his bookings?
|
||||||
|
$scope.enableBookingCancel = (settingsPromise.booking_cancel_enable == "true")
|
||||||
|
|
||||||
|
## Global config: delay in hours before a booking while the cancellation is forbidden
|
||||||
|
$scope.cancelBookingDelay = parseInt(settingsPromise.booking_cancel_delay)
|
||||||
|
|
||||||
|
## Global config: calendar window in the morning
|
||||||
|
$scope.calendarConfig.minTime = moment.duration(moment(settingsPromise.booking_window_start).format('HH:mm:ss'))
|
||||||
|
|
||||||
|
## Global config: calendar window in the evening
|
||||||
|
$scope.calendarConfig.maxTime = moment.duration(moment(settingsPromise.booking_window_end).format('HH:mm:ss'))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Cancel the current booking modification, removing the previously booked slot from the selection
|
||||||
|
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
|
||||||
|
##
|
||||||
|
$scope.removeSlotToModify = (e) ->
|
||||||
|
e.preventDefault()
|
||||||
|
if $scope.slotToPlace
|
||||||
|
$scope.slotToPlace.backgroundColor = 'white'
|
||||||
|
$scope.slotToPlace.title = ''
|
||||||
|
$scope.slotToPlace = null
|
||||||
|
$scope.slotToModify.title = if $scope.currentUser.role isnt 'admin' then _t('i_ve_reserved') else _t('not_available')
|
||||||
|
$scope.slotToModify.backgroundColor = 'white'
|
||||||
|
$scope.slotToModify = null
|
||||||
|
$scope.calendar.fullCalendar 'rerenderEvents'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# When modifying an already booked reservation, cancel the choice of the new slot
|
||||||
|
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
|
||||||
|
##
|
||||||
|
$scope.removeSlotToPlace = (e)->
|
||||||
|
e.preventDefault()
|
||||||
|
$scope.slotToPlace.backgroundColor = 'white'
|
||||||
|
$scope.slotToPlace.title = ''
|
||||||
|
$scope.slotToPlace = null
|
||||||
|
$scope.calendar.fullCalendar 'rerenderEvents'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# When modifying an already booked reservation, confirm the modification.
|
||||||
|
##
|
||||||
|
$scope.modifyMachineSlot = ->
|
||||||
|
Slot.update {id: $scope.slotToModify.id},
|
||||||
|
slot:
|
||||||
|
start_at: $scope.slotToPlace.start
|
||||||
|
end_at: $scope.slotToPlace.end
|
||||||
|
availability_id: $scope.slotToPlace.availability_id
|
||||||
|
, -> # success
|
||||||
|
$scope.modifiedSlots =
|
||||||
|
newReservedSlot: $scope.slotToPlace
|
||||||
|
oldReservedSlot: $scope.slotToModify
|
||||||
|
$scope.slotToPlace.title = if $scope.currentUser.role isnt 'admin' then _t('i_ve_reserved') else _t('not_available')
|
||||||
|
$scope.slotToPlace.backgroundColor = 'white'
|
||||||
|
$scope.slotToPlace.borderColor = $scope.slotToModify.borderColor
|
||||||
|
$scope.slotToPlace.id = $scope.slotToModify.id
|
||||||
|
$scope.slotToPlace.is_reserved = true
|
||||||
|
$scope.slotToPlace.can_modify = true
|
||||||
|
$scope.slotToPlace = null
|
||||||
|
|
||||||
|
$scope.slotToModify.backgroundColor = 'white'
|
||||||
|
$scope.slotToModify.title = ''
|
||||||
|
$scope.slotToModify.borderColor = FREE_SLOT_BORDER_COLOR
|
||||||
|
$scope.slotToModify.id = null
|
||||||
|
$scope.slotToModify.is_reserved = false
|
||||||
|
$scope.slotToModify.can_modify = false
|
||||||
|
$scope.slotToModify = null
|
||||||
|
|
||||||
|
$scope.calendar.fullCalendar 'rerenderEvents'
|
||||||
|
, (err) -> # failure
|
||||||
|
growl.error(_t('unable_to_change_the_reservation'))
|
||||||
|
console.error(err)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Cancel the current booking modification, reseting the whole process
|
||||||
|
##
|
||||||
|
$scope.cancelModifyMachineSlot = ->
|
||||||
|
$scope.slotToPlace.backgroundColor = 'white'
|
||||||
|
$scope.slotToPlace.title = ''
|
||||||
|
$scope.slotToPlace = null
|
||||||
|
$scope.slotToModify.title = if $scope.currentUser.role isnt 'admin' then _t('i_ve_reserved') else _t('not_available')
|
||||||
|
$scope.slotToModify.backgroundColor = 'white'
|
||||||
|
$scope.slotToModify = null
|
||||||
|
|
||||||
|
$scope.calendar.fullCalendar 'rerenderEvents'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Callback to deal with the reservations of the user selected in the dropdown list instead of the current user's
|
||||||
|
# reservations. (admins only)
|
||||||
|
##
|
||||||
|
$scope.updateMember = ->
|
||||||
|
$scope.paidMachineSlots = null
|
||||||
|
$scope.plansAreShown = false
|
||||||
|
$scope.selectedPlan = null
|
||||||
|
updateCartPrice()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Add the provided slot to the shopping cart (state transition from free to 'about to be reserved')
|
||||||
|
# and increment the total amount of the cart if needed.
|
||||||
|
# @param machineSlot {Object} fullCalendar event object
|
||||||
|
##
|
||||||
|
$scope.validMachineSlot = (machineSlot)->
|
||||||
|
machineSlot.isValid = true
|
||||||
|
updateCartPrice()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Remove the provided slot from the shopping cart (state transition from 'about to be reserved' to free)
|
||||||
|
# and decrement the total amount of the cart if needed.
|
||||||
|
# @param machineSlot {Object} fullCalendar event object
|
||||||
|
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
|
||||||
|
##
|
||||||
|
$scope.removeMachineSlot = (machineSlot, e)->
|
||||||
|
e.preventDefault() if e
|
||||||
|
machineSlot.backgroundColor = 'white'
|
||||||
|
machineSlot.borderColor = FREE_SLOT_BORDER_COLOR
|
||||||
|
machineSlot.title = ''
|
||||||
|
machineSlot.isValid = false
|
||||||
|
|
||||||
|
if machineSlot.machine.is_reduced_amount
|
||||||
|
angular.forEach $scope.ctrl.member.machine_credits, (credit)->
|
||||||
|
if credit.machine_id = machineSlot.machine.id
|
||||||
|
credit.hours_used--
|
||||||
|
machineSlot.machine.is_reduced_amount = false
|
||||||
|
|
||||||
|
index = $scope.eventsReserved.indexOf(machineSlot)
|
||||||
|
$scope.eventsReserved.splice(index, 1)
|
||||||
|
if $scope.eventsReserved.length == 0
|
||||||
|
if $scope.plansAreShown
|
||||||
|
$scope.selectedPlan = null
|
||||||
|
$scope.plansAreShown = false
|
||||||
|
updateCartPrice()
|
||||||
|
$timeout ->
|
||||||
|
$scope.calendar.fullCalendar 'refetchEvents'
|
||||||
|
$scope.calendar.fullCalendar 'rerenderEvents'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Checks that every selected slots were added to the shopping cart. Ie. will return false if
|
||||||
|
# any checked slot was not validated by the user.
|
||||||
|
##
|
||||||
|
$scope.machineSlotsValid = ->
|
||||||
|
isValid = true
|
||||||
|
angular.forEach $scope.eventsReserved, (m)->
|
||||||
|
isValid = false if !m.isValid
|
||||||
|
isValid
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Changes the user current view from the plan subsription screen to the machine reservation agenda
|
||||||
|
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
|
||||||
|
##
|
||||||
|
$scope.doNotSubscribePlan = (e)->
|
||||||
|
e.preventDefault()
|
||||||
|
$scope.plansAreShown = false
|
||||||
|
$scope.selectPlan($scope.selectedPlan)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Validates the shopping chart and redirect the user to the payment step
|
||||||
|
##
|
||||||
|
$scope.payMachine = ->
|
||||||
|
|
||||||
|
# first, we check that a user was selected
|
||||||
|
if Object.keys($scope.ctrl.member).length > 0
|
||||||
|
reservation = mkReservation($scope.ctrl.member, $scope.eventsReserved, $scope.selectedPlan)
|
||||||
|
|
||||||
|
if $scope.currentUser.role isnt 'admin' and $scope.amountTotal > 0
|
||||||
|
payByStripe(reservation)
|
||||||
|
else
|
||||||
|
if $scope.currentUser.role is 'admin' or $scope.amountTotal is 0
|
||||||
|
payOnSite(reservation)
|
||||||
|
else
|
||||||
|
# otherwise we alert, this error musn't occur when the current user is not admin
|
||||||
|
growl.error(_t('please_select_a_member_first'))
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Switch the user's view from the reservation agenda to the plan subscription
|
||||||
|
##
|
||||||
|
$scope.showPlans = ->
|
||||||
|
$scope.plansAreShown = true
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Add the provided plan to the current shopping cart
|
||||||
|
# @param plan {Object} the plan to subscribe
|
||||||
|
##
|
||||||
|
$scope.selectPlan = (plan) ->
|
||||||
|
if $scope.isAuthenticated()
|
||||||
|
angular.forEach $scope.eventsReserved, (machineSlot)->
|
||||||
|
angular.forEach $scope.ctrl.member.machine_credits, (credit)->
|
||||||
|
if credit.machine_id = machineSlot.machine.id
|
||||||
|
credit.hours_used = 0
|
||||||
|
machineSlot.machine.is_reduced_amount = false
|
||||||
|
|
||||||
|
if $scope.selectedPlan != plan
|
||||||
|
$scope.selectedPlan = plan
|
||||||
|
else
|
||||||
|
$scope.selectedPlan = null
|
||||||
|
updateCartPrice()
|
||||||
|
else
|
||||||
|
$scope.login null, ->
|
||||||
|
$scope.selectedPlan = plan
|
||||||
|
updateCartPrice()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Checks if $scope.slotToModify and $scope.slotToPlace have tag incompatibilities
|
||||||
|
# @returns {boolean} true in case of incompatibility
|
||||||
|
##
|
||||||
|
$scope.tagMissmatch = ->
|
||||||
|
for tag in $scope.slotToModify.tags
|
||||||
|
if tag.id not in $scope.slotToPlace.tag_ids
|
||||||
|
return true
|
||||||
|
false
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### PRIVATE SCOPE ###
|
||||||
|
|
||||||
|
##
|
||||||
|
# Kind of constructor: these actions will be realized first when the controller is loaded
|
||||||
|
##
|
||||||
|
initialize = ->
|
||||||
|
Availability.machine {machineId: $stateParams.id}, (availabilities) ->
|
||||||
|
$scope.eventSources.push
|
||||||
|
events: availabilities
|
||||||
|
textColor: 'black'
|
||||||
|
|
||||||
|
if $scope.currentUser.role isnt 'admin'
|
||||||
|
$scope.ctrl.member = $scope.currentUser
|
||||||
|
else
|
||||||
|
Member.query {requested_attributes:'[subscription,credits]'}, (members) ->
|
||||||
|
$scope.members = members
|
||||||
|
|
||||||
|
$scope.machine = Machine.get {id: $stateParams.id}
|
||||||
|
, ->
|
||||||
|
return
|
||||||
|
, ->
|
||||||
|
$state.go('app.public.machines_list')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Create an hash map implementing the Reservation specs
|
||||||
|
# @param member {Object} User as retreived from the API: current user / selected user if current is admin
|
||||||
|
# @param slots {Array<Object>} Array of fullCalendar events: slots selected on the calendar
|
||||||
|
# @param [plan] {Object} Plan as retrived from the API: plan to buy with the current reservation
|
||||||
|
# @return {{user_id:Number, reservable_id:Number, reservable_type:String, slots_attributes:Array<Object>, plan_id:Number|null}}
|
||||||
|
##
|
||||||
|
mkReservation = (member, slots, plan = null) ->
|
||||||
|
reservation =
|
||||||
|
user_id: member.id
|
||||||
|
reservable_id: (slots[0].machine.id if slots.length > 0)
|
||||||
|
reservable_type: 'Machine'
|
||||||
|
slots_attributes: []
|
||||||
|
plan_id: (plan.id if plan)
|
||||||
|
angular.forEach slots, (slot, key) ->
|
||||||
|
reservation.slots_attributes.push
|
||||||
|
start_at: slot.start
|
||||||
|
end_at: slot.end
|
||||||
|
availability_id: slot.availability_id
|
||||||
|
offered: slot.offered || false
|
||||||
|
|
||||||
|
reservation
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Update the total price of the current selection/reservation
|
||||||
|
##
|
||||||
|
updateCartPrice = ->
|
||||||
|
if Object.keys($scope.ctrl.member).length > 0
|
||||||
|
r = mkReservation($scope.ctrl.member, $scope.eventsReserved, $scope.selectedPlan)
|
||||||
|
Price.compute {reservation: r}, (res) ->
|
||||||
|
$scope.amountTotal = res.price
|
||||||
|
setSlotsDetails(res.details)
|
||||||
|
else
|
||||||
|
# otherwise we alert, this error musn't occur when the current user is not admin
|
||||||
|
growl.warning(_t('please_select_a_member_first'))
|
||||||
|
$scope.amountTotal = null
|
||||||
|
|
||||||
|
|
||||||
|
setSlotsDetails = (details) ->
|
||||||
|
angular.forEach $scope.eventsReserved, (slot) ->
|
||||||
|
angular.forEach details.slots, (s) ->
|
||||||
|
if moment(s.start_at).isSame(slot.start)
|
||||||
|
slot.promo = s.promo
|
||||||
|
slot.price = s.price
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Triggered when the user click on a reservation slot in the agenda.
|
||||||
|
# Defines the behavior to adopt depending on the slot status (already booked, free, ready to be reserved ...),
|
||||||
|
# the user's subscription (current or about to be took) and the time (the user cannot modify a booked reservation
|
||||||
|
# if it's too late).
|
||||||
|
##
|
||||||
|
calendarEventClickCb = (event, jsEvent, view) ->
|
||||||
|
|
||||||
|
if !event.is_reserved && !$scope.slotToModify
|
||||||
|
index = $scope.eventsReserved.indexOf(event)
|
||||||
|
if index == -1
|
||||||
|
event.backgroundColor = FREE_SLOT_BORDER_COLOR
|
||||||
|
event.title = _t('i_reserve')
|
||||||
|
$scope.eventsReserved.push event
|
||||||
|
else
|
||||||
|
$scope.removeMachineSlot(event)
|
||||||
|
$scope.paidMachineSlots = null
|
||||||
|
$scope.selectedPlan = null
|
||||||
|
$scope.modifiedSlots = null
|
||||||
|
else if !event.is_reserved && $scope.slotToModify
|
||||||
|
if $scope.slotToPlace
|
||||||
|
$scope.slotToPlace.backgroundColor = 'white'
|
||||||
|
$scope.slotToPlace.title = ''
|
||||||
|
$scope.slotToPlace = event
|
||||||
|
event.backgroundColor = '#bbb'
|
||||||
|
event.title = _t('i_shift')
|
||||||
|
else if event.is_reserved and (slotCanBeModified(event) or slotCanBeCanceled(event)) and !$scope.slotToModify and $scope.eventsReserved.length == 0
|
||||||
|
event.movable = slotCanBeModified(event)
|
||||||
|
event.cancelable = slotCanBeCanceled(event)
|
||||||
|
dialogs.confirm
|
||||||
|
templateUrl: '<%= asset_path "shared/confirm_modify_slot_modal.html" %>'
|
||||||
|
resolve:
|
||||||
|
object: -> event
|
||||||
|
, (type) ->
|
||||||
|
if type == 'move'
|
||||||
|
$scope.modifiedSlots = null
|
||||||
|
$scope.slotToModify = event
|
||||||
|
event.backgroundColor = '#eee'
|
||||||
|
event.title = _t('i_change')
|
||||||
|
$scope.calendar.fullCalendar 'rerenderEvents'
|
||||||
|
else if type == 'cancel'
|
||||||
|
dialogs.confirm
|
||||||
|
resolve:
|
||||||
|
object: ->
|
||||||
|
title: _t('confirmation_required')
|
||||||
|
msg: _t('do_you_really_want_to_cancel_this_reservation')
|
||||||
|
, -> # cancel confirmed
|
||||||
|
Slot.cancel {id: event.id}, -> # successfully canceled
|
||||||
|
growl.success _t('reservation_was_cancelled_successfully')
|
||||||
|
$scope.canceledSlot = event
|
||||||
|
$scope.canceledSlot.backgroundColor = 'white'
|
||||||
|
$scope.canceledSlot.title = ''
|
||||||
|
$scope.canceledSlot.borderColor = FREE_SLOT_BORDER_COLOR
|
||||||
|
$scope.canceledSlot.id = null
|
||||||
|
$scope.canceledSlot.is_reserved = false
|
||||||
|
$scope.canceledSlot.can_modify = false
|
||||||
|
$scope.canceledSlot = null
|
||||||
|
$scope.calendar.fullCalendar 'rerenderEvents'
|
||||||
|
, -> # error while canceling
|
||||||
|
growl.error _t('cancellation_failed')
|
||||||
|
, ->
|
||||||
|
$scope.paidMachineSlots = null
|
||||||
|
$scope.selectedPlan = null
|
||||||
|
$scope.modifiedSlots = null
|
||||||
|
|
||||||
|
$scope.calendar.fullCalendar 'rerenderEvents'
|
||||||
|
|
||||||
|
updateCartPrice()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Triggered when fullCalendar tries to graphicaly render an event block.
|
||||||
|
# Append the event tag into the block, just after the event title.
|
||||||
|
# @see http://fullcalendar.io/docs/event_rendering/eventRender/
|
||||||
|
##
|
||||||
|
eventRenderCb = (event, element) ->
|
||||||
|
if $scope.currentUser.role is 'admin' and event.tags.length > 0
|
||||||
|
html = ''
|
||||||
|
for tag in event.tags
|
||||||
|
html += "<span class='label label-success text-white' title='#{tag.name}'>#{tag.name}</span>"
|
||||||
|
element.find('.fc-time').append(html)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Open a modal window that allows the user to process a credit card payment for his current shopping cart.
|
||||||
|
##
|
||||||
|
payByStripe = (reservation) ->
|
||||||
|
|
||||||
|
$uibModal.open
|
||||||
|
templateUrl: '<%= asset_path "stripe/payment_modal.html" %>'
|
||||||
|
size: 'md'
|
||||||
|
resolve:
|
||||||
|
reservation: ->
|
||||||
|
reservation
|
||||||
|
price: ->
|
||||||
|
Price.compute({reservation: reservation}).$promise
|
||||||
|
cgv: ->
|
||||||
|
CustomAsset.get({name: 'cgv-file'}).$promise
|
||||||
|
controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'cgv', 'Auth', 'Reservation', ($scope, $uibModalInstance, $state, reservation, price, cgv, Auth, Reservation) ->
|
||||||
|
# Price
|
||||||
|
$scope.amount = price.price
|
||||||
|
|
||||||
|
# CGV
|
||||||
|
$scope.cgv = cgv.custom_asset
|
||||||
|
|
||||||
|
# Reservation
|
||||||
|
$scope.reservation = reservation
|
||||||
|
|
||||||
|
##
|
||||||
|
# Callback to process the payment with Stripe, triggered on button click
|
||||||
|
##
|
||||||
|
$scope.payment = (status, response) ->
|
||||||
|
if response.error
|
||||||
|
growl.error(response.error.message)
|
||||||
|
else
|
||||||
|
$scope.attempting = true
|
||||||
|
$scope.reservation.card_token = response.id
|
||||||
|
Reservation.save reservation: $scope.reservation, (reservation) ->
|
||||||
|
$uibModalInstance.close(reservation)
|
||||||
|
, (response)->
|
||||||
|
$scope.alerts = []
|
||||||
|
$scope.alerts.push
|
||||||
|
msg: response.data.card[0]
|
||||||
|
type: 'danger'
|
||||||
|
$scope.attempting = false
|
||||||
|
]
|
||||||
|
.result['finally'](null).then (reservation)->
|
||||||
|
afterPayment(reservation)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Open a modal window that allows the user to process a local payment for his current shopping cart (admin only).
|
||||||
|
##
|
||||||
|
payOnSite = (reservation) ->
|
||||||
|
|
||||||
|
$uibModal.open
|
||||||
|
templateUrl: '<%= asset_path "shared/valid_reservation_modal.html" %>'
|
||||||
|
size: 'sm'
|
||||||
|
resolve:
|
||||||
|
reservation: ->
|
||||||
|
reservation
|
||||||
|
price: ->
|
||||||
|
Price.compute({reservation: reservation}).$promise
|
||||||
|
controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'Auth', 'Reservation', ($scope, $uibModalInstance, $state, reservation, price, Auth, Reservation) ->
|
||||||
|
|
||||||
|
# Price
|
||||||
|
$scope.amount = price.price
|
||||||
|
|
||||||
|
# Reservation
|
||||||
|
$scope.reservation = reservation
|
||||||
|
|
||||||
|
# Button label
|
||||||
|
if $scope.amount > 0
|
||||||
|
$scope.validButtonName = _t('confirm_(payment_on_site)')
|
||||||
|
else
|
||||||
|
$scope.validButtonName = _t('confirm')
|
||||||
|
|
||||||
|
##
|
||||||
|
# Callback to process the local payment, triggered on button click
|
||||||
|
##
|
||||||
|
$scope.ok = ->
|
||||||
|
$scope.attempting = true
|
||||||
|
Reservation.save reservation: $scope.reservation, (reservation) ->
|
||||||
|
$uibModalInstance.close(reservation)
|
||||||
|
$scope.attempting = true
|
||||||
|
, (response)->
|
||||||
|
$scope.alerts = []
|
||||||
|
$scope.alerts.push({msg: _t('a_problem_occured_during_the_payment_process_please_try_again_later'), type: 'danger' })
|
||||||
|
$scope.attempting = false
|
||||||
|
$scope.cancel = ->
|
||||||
|
$uibModalInstance.dismiss('cancel')
|
||||||
|
]
|
||||||
|
.result['finally'](null).then (reservation)->
|
||||||
|
afterPayment(reservation)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Determines if the provided booked slot is able to be modified by the user.
|
||||||
|
# @param slot {Object} fullCalendar event object
|
||||||
|
##
|
||||||
|
slotCanBeModified = (slot)->
|
||||||
|
return true if $scope.currentUser.role is 'admin'
|
||||||
|
slotStart = moment(slot.start)
|
||||||
|
now = moment()
|
||||||
|
if slot.can_modify and $scope.enableBookingMove and slotStart.diff(now, "hours") >= $scope.moveBookingDelay
|
||||||
|
return true
|
||||||
|
else
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Determines if the provided booked slot is able to be canceled by the user.
|
||||||
|
# @param slot {Object} fullCalendar event object
|
||||||
|
##
|
||||||
|
slotCanBeCanceled = (slot) ->
|
||||||
|
return true if $scope.currentUser.role is 'admin'
|
||||||
|
slotStart = moment(slot.start)
|
||||||
|
now = moment()
|
||||||
|
if slot.can_modify and $scope.enableBookingCancel and slotStart.diff(now, "hours") >= $scope.cancelBookingDelay
|
||||||
|
return true
|
||||||
|
else
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Once the reservation is booked (payment process successfully completed), change the event style
|
||||||
|
# in fullCalendar, update the user's subscription and free-credits if needed
|
||||||
|
# @param reservation {Object}
|
||||||
|
##
|
||||||
|
afterPayment = (reservation)->
|
||||||
|
angular.forEach $scope.eventsReserved, (machineSlot, key) ->
|
||||||
|
machineSlot.is_reserved = true
|
||||||
|
machineSlot.can_modify = true
|
||||||
|
if $scope.currentUser.role isnt 'admin'
|
||||||
|
machineSlot.title = _t('i_ve_reserved')
|
||||||
|
machineSlot.borderColor = BOOKED_SLOT_BORDER_COLOR
|
||||||
|
updateMachineSlot(machineSlot, reservation, $scope.currentUser)
|
||||||
|
else
|
||||||
|
machineSlot.title = _t('not_available')
|
||||||
|
machineSlot.borderColor = UNAVAILABLE_SLOT_BORDER_COLOR
|
||||||
|
updateMachineSlot(machineSlot, reservation, $scope.ctrl.member)
|
||||||
|
machineSlot.backgroundColor = 'white'
|
||||||
|
$scope.paidMachineSlots = $scope.eventsReserved
|
||||||
|
|
||||||
|
$scope.eventsReserved = []
|
||||||
|
|
||||||
|
if $scope.selectedPlan
|
||||||
|
$scope.ctrl.member.subscribed_plan = angular.copy($scope.selectedPlan)
|
||||||
|
Auth._currentUser.subscribed_plan = angular.copy($scope.selectedPlan)
|
||||||
|
$scope.plansAreShown = false
|
||||||
|
|
||||||
|
$scope.calendar.fullCalendar 'refetchEvents'
|
||||||
|
$scope.calendar.fullCalendar 'rerenderEvents'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# After payment, update the id of the newly reserved slot with the id returned by the server.
|
||||||
|
# This will allow the user to modify the reservation he just booked. The associated user will also be registered
|
||||||
|
# with the slot.
|
||||||
|
# @param slot {Object}
|
||||||
|
# @param reservation {Object}
|
||||||
|
# @param user {Object} user associated with the slot
|
||||||
|
##
|
||||||
|
updateMachineSlot = (slot, reservation, user)->
|
||||||
|
angular.forEach reservation.slots, (s)->
|
||||||
|
if slot.start.isSame(s.start_at)
|
||||||
|
slot.id = s.id
|
||||||
|
slot.user = user
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Search for the requested plan in the provided array and return its price.
|
||||||
|
# @param plansArray {Array} full list of plans
|
||||||
|
# @param planId {Number} plan identifier
|
||||||
|
# @returns {Number|null} price of the given plan or null if not found
|
||||||
|
##
|
||||||
|
findAmountByPlanId = (plansArray, planId)->
|
||||||
|
for plan in plansArray
|
||||||
|
return plan.amount if plan.plan_id == planId
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
## !!! MUST BE CALLED AT THE END of the controller
|
||||||
|
initialize()
|
||||||
]
|
]
|
||||||
|
@ -3,55 +3,97 @@
|
|||||||
##
|
##
|
||||||
# Navigation controller. List the links availables in the left navigation pane and their icon.
|
# Navigation controller. List the links availables in the left navigation pane and their icon.
|
||||||
##
|
##
|
||||||
Application.Controllers.controller "mainNavController", ["$scope", "$location", "$cookies", ($scope, $location, $cookies) ->
|
Application.Controllers.controller "MainNavController", ["$scope", "$location", "$cookies", ($scope, $location, $cookies) ->
|
||||||
|
|
||||||
## Common links (public application)
|
## Common links (public application)
|
||||||
$scope.navLinks = [
|
$scope.navLinks = [
|
||||||
{
|
{
|
||||||
state: 'app.public.home'
|
state: 'app.public.home'
|
||||||
linkText: 'Accueil'
|
linkText: 'home'
|
||||||
linkIcon: 'home'
|
linkIcon: 'home'
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
state: 'app.public.machines_list'
|
state: 'app.public.machines_list'
|
||||||
linkText: 'Liste des machines'
|
linkText: 'reserve_a_machine'
|
||||||
linkIcon: 'gears'
|
linkIcon: 'calendar'
|
||||||
|
}
|
||||||
|
{
|
||||||
|
state: 'app.logged.trainings_reserve'
|
||||||
|
linkText: 'trainings_registrations'
|
||||||
|
linkIcon: 'graduation-cap'
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
state: 'app.public.events_list'
|
state: 'app.public.events_list'
|
||||||
linkText: 'Liste des stages et ateliers'
|
linkText: 'courses_and_workshops_registrations'
|
||||||
linkIcon: 'tags'
|
linkIcon: 'tags'
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
state: 'app.public.projects_list'
|
state: 'app.public.projects_list'
|
||||||
linkText: 'Galerie de projets'
|
linkText: 'projects_gallery'
|
||||||
linkIcon: 'th'
|
linkIcon: 'th'
|
||||||
}
|
}
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
## Admin links (backoffice application)
|
unless Fablab.withoutPlans
|
||||||
|
$scope.navLinks.push({
|
||||||
|
state: 'app.public.plans'
|
||||||
|
linkText: 'subscriptions'
|
||||||
|
linkIcon: 'credit-card'
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
$scope.adminNavLinks = [
|
$scope.adminNavLinks = [
|
||||||
|
{
|
||||||
|
state: 'app.admin.trainings'
|
||||||
|
linkText: 'trainings_monitoring'
|
||||||
|
linkIcon: 'graduation-cap'
|
||||||
|
}
|
||||||
|
{
|
||||||
|
state: 'app.admin.calendar'
|
||||||
|
linkText: 'manage_the_calendar'
|
||||||
|
linkIcon: 'calendar'
|
||||||
|
}
|
||||||
{
|
{
|
||||||
state: 'app.admin.members'
|
state: 'app.admin.members'
|
||||||
linkText: 'Suivi utilisateurs'
|
linkText: 'manage_the_users'
|
||||||
linkIcon: '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'
|
state: 'app.admin.events'
|
||||||
linkText: 'Suivi stages et ateliers'
|
linkText: 'courses_and_workshops_monitoring'
|
||||||
linkIcon: 'tags'
|
linkIcon: 'tags'
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
state: 'app.public.machines_list'
|
state: 'app.public.machines_list'
|
||||||
linkText: 'Gérer les machines'
|
linkText: 'manage_the_machines'
|
||||||
linkIcon: 'cogs'
|
linkIcon: 'cogs'
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
state: 'app.admin.project_elements'
|
state: 'app.admin.project_elements'
|
||||||
linkText: 'Gérer les éléments Projets'
|
linkText: 'manage_the_projects_elements'
|
||||||
linkIcon: 'tasks'
|
linkIcon: 'tasks'
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
state: 'app.admin.statistics'
|
||||||
|
linkText: 'statistics'
|
||||||
|
linkIcon: 'bar-chart-o'
|
||||||
|
}
|
||||||
|
{
|
||||||
|
state: 'app.admin.settings'
|
||||||
|
linkText: 'customization'
|
||||||
|
linkIcon: 'gear'
|
||||||
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
]
|
]
|
||||||
|
@ -3,23 +3,11 @@
|
|||||||
##
|
##
|
||||||
# Controller used in the members listing page
|
# Controller used in the members listing page
|
||||||
##
|
##
|
||||||
Application.Controllers.controller "membersController", ["$scope", "$state", 'Member', ($scope, $state, Member) ->
|
Application.Controllers.controller "MembersController", ["$scope", 'membersPromise', ($scope, membersPromise) ->
|
||||||
|
|
||||||
## members list
|
## members list
|
||||||
$scope.members = Member.query()
|
$scope.members = membersPromise
|
||||||
|
|
||||||
## Merbers ordering/sorting. Default: not sorted
|
|
||||||
$scope.orderMember = null
|
|
||||||
|
|
||||||
##
|
|
||||||
# Change the members ordering criterion to the one provided
|
|
||||||
# @param orderBy {string} ordering criterion
|
|
||||||
##
|
|
||||||
$scope.setOrderMember = (orderBy)->
|
|
||||||
if $scope.orderMember == orderBy
|
|
||||||
$scope.orderMember = '-'+orderBy
|
|
||||||
else
|
|
||||||
$scope.orderMember = orderBy
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -27,24 +15,71 @@ Application.Controllers.controller "membersController", ["$scope", "$state", 'Me
|
|||||||
##
|
##
|
||||||
# Controller used when editing the current user's profile
|
# Controller used when editing the current user's profile
|
||||||
##
|
##
|
||||||
Application.Controllers.controller "editProfileController", ["$scope", "$state", "Member", "Auth", 'growl', 'dialogs', 'CSRF', ($scope, $state, Member, Auth, growl, dialogs, CSRF) ->
|
Application.Controllers.controller "EditProfileController", ["$scope", "$rootScope", "$state", "$window", '$locale', "Member", "Auth", "Session", "activeProviderPromise", 'growl', 'dialogs', 'CSRF', 'memberPromise', 'groups', '_t'
|
||||||
CSRF.setMetaTags()
|
, ($scope, $rootScope, $state, $window, $locale, Member, Auth, Session, activeProviderPromise, growl, dialogs, CSRF, memberPromise, groups, _t) ->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### PUBLIC SCOPE ###
|
||||||
|
|
||||||
## API URL where the form will be posted
|
## API URL where the form will be posted
|
||||||
$scope.actionUrl = "/api/members/" + $scope.currentUser.id
|
$scope.actionUrl = "/api/members/" + $scope.currentUser.id
|
||||||
|
|
||||||
|
## list of groups
|
||||||
|
$scope.groups = groups
|
||||||
|
|
||||||
## Form action on the above URL
|
## Form action on the above URL
|
||||||
$scope.method = 'patch'
|
$scope.method = 'patch'
|
||||||
|
|
||||||
## Current user's profile
|
## Current user's profile
|
||||||
$scope.user = Member.get {id: $scope.currentUser.id}
|
$scope.user = memberPromise
|
||||||
|
|
||||||
|
## default : do not show the group changing form
|
||||||
|
$scope.group =
|
||||||
|
change: false
|
||||||
|
|
||||||
|
## group ID of the current/selected user
|
||||||
|
$scope.userGroup = memberPromise.group_id
|
||||||
|
|
||||||
|
## active authentication provider parameters
|
||||||
|
$scope.activeProvider = activeProviderPromise
|
||||||
|
|
||||||
|
## allow the user to change his password except if he connect from an SSO
|
||||||
|
$scope.preventPassword = false
|
||||||
|
|
||||||
|
## mapping of fields to disable
|
||||||
|
$scope.preventField = {}
|
||||||
|
|
||||||
## Angular-Bootstrap datepicker configuration for birthday
|
## Angular-Bootstrap datepicker configuration for birthday
|
||||||
$scope.datePicker =
|
$scope.datePicker =
|
||||||
format: 'dd/MM/yyyy'
|
format: $locale.DATETIME_FORMATS.shortDate
|
||||||
opened: false # default: datePicker is not shown
|
opened: false # default: datePicker is not shown
|
||||||
options:
|
options:
|
||||||
startingDay: 1 # France: the week starts on monday
|
startingDay: Fablab.weekStartingDay
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Return the group object, identified by the ID set in $scope.userGroup
|
||||||
|
##
|
||||||
|
$scope.getUserGroup = ->
|
||||||
|
for group in $scope.groups
|
||||||
|
if group.id == $scope.userGroup
|
||||||
|
return group
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Change the group of the current user to the one set in $scope.userGroup
|
||||||
|
##
|
||||||
|
$scope.selectGroup = ->
|
||||||
|
Member.update {id: $scope.user.id}, {user: {group_id: $scope.userGroup}}, (user) ->
|
||||||
|
$scope.user = user
|
||||||
|
$scope.group.change = false
|
||||||
|
growl.success(_t('your_group_has_been_successfully_changed'))
|
||||||
|
, (err) ->
|
||||||
|
growl.error(_t('an_unexpected_error_prevented_your_group_from_being_changed'))
|
||||||
|
console.error(err)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -81,10 +116,32 @@ Application.Controllers.controller "editProfileController", ["$scope", "$state",
|
|||||||
Auth._currentUser.name = content.name
|
Auth._currentUser.name = content.name
|
||||||
$scope.currentUser = content
|
$scope.currentUser = content
|
||||||
Auth._currentUser = content
|
Auth._currentUser = content
|
||||||
|
$rootScope.currentUser = content
|
||||||
$state.go('app.public.home')
|
$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.
|
# 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.
|
# The preview may show a placeholder or the content of the file depending on the upload state.
|
||||||
@ -95,6 +152,52 @@ Application.Controllers.controller "editProfileController", ["$scope", "$state",
|
|||||||
'fileinput-exists'
|
'fileinput-exists'
|
||||||
else
|
else
|
||||||
'fileinput-new'
|
'fileinput-new'
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Check if the of the properties editable by the user are linked to the SSO
|
||||||
|
# @return {boolean} true if some editable fields are mapped with the SSO, false otherwise
|
||||||
|
##
|
||||||
|
$scope.hasSsoFields = ->
|
||||||
|
# if check if keys > 1 because there's a minimum of 1 mapping (id <-> provider-uid)
|
||||||
|
# so the user may want to edit his profile on the SSO if at least 2 mappings exists
|
||||||
|
Object.keys($scope.preventField).length > 1
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Disconnect and re-connect the user to the SSO to force the synchronisation of the profile's data
|
||||||
|
##
|
||||||
|
$scope.syncProfile = ->
|
||||||
|
Auth.logout().then (oldUser) ->
|
||||||
|
Session.destroy()
|
||||||
|
$rootScope.currentUser = null
|
||||||
|
$rootScope.toCheckNotifications = false
|
||||||
|
$scope.notifications = []
|
||||||
|
$window.location.href = $scope.activeProvider.link_to_sso_connect
|
||||||
|
|
||||||
|
|
||||||
|
### PRIVATE SCOPE ###
|
||||||
|
|
||||||
|
##
|
||||||
|
# Kind of constructor: these actions will be realized first when the controller is loaded
|
||||||
|
##
|
||||||
|
initialize = ->
|
||||||
|
CSRF.setMetaTags()
|
||||||
|
|
||||||
|
# init the birth date to JS object
|
||||||
|
$scope.user.profile.birthday = moment($scope.user.profile.birthday).toDate()
|
||||||
|
|
||||||
|
if $scope.activeProvider.providable_type != 'DatabaseProvider'
|
||||||
|
$scope.preventPassword = true
|
||||||
|
# bind fields protection with sso fields
|
||||||
|
angular.forEach activeProviderPromise.mapping, (map) ->
|
||||||
|
$scope.preventField[map] = true
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## !!! MUST BE CALLED AT THE END of the controller
|
||||||
|
initialize()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -102,8 +205,8 @@ Application.Controllers.controller "editProfileController", ["$scope", "$state",
|
|||||||
##
|
##
|
||||||
# Controller used on the public user's profile page (seeing another user's profile)
|
# Controller used on the public user's profile page (seeing another user's profile)
|
||||||
##
|
##
|
||||||
Application.Controllers.controller "showProfileController", ["$scope", "$stateParams", 'Member', ($scope, $stateParams, Member) ->
|
Application.Controllers.controller "ShowProfileController", ["$scope", "$stateParams", 'Member', 'memberPromise', ($scope, $stateParams, Member, memberPromise) ->
|
||||||
|
|
||||||
## Selected user's profile (id from the current URL)
|
## Selected user's profile (id from the current URL)
|
||||||
$scope.user = Member.get {id: $stateParams.id}
|
$scope.user = memberPromise
|
||||||
]
|
]
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
# Controller used in notifications page
|
# Controller used in notifications page
|
||||||
# inherits $scope.$parent.notifications (unread notifications) from ApplicationController
|
# inherits $scope.$parent.notifications (unread notifications) from ApplicationController
|
||||||
##
|
##
|
||||||
Application.Controllers.controller "notificationsController", ["$scope", 'Notification', ($scope, Notification) ->
|
Application.Controllers.controller "NotificationsController", ["$scope", 'Notification', ($scope, Notification) ->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -32,7 +32,7 @@ Application.Controllers.controller "notificationsController", ["$scope", 'Notifi
|
|||||||
# Mark the provided notification as read, updating its status on the server and moving it
|
# Mark the provided notification as read, updating its status on the server and moving it
|
||||||
# to the already read notifications list.
|
# to the already read notifications list.
|
||||||
# @param notification {{id:number}} the notification to mark as read
|
# @param notification {{id:number}} the notification to mark as read
|
||||||
# @param e {Object} jQuery event object
|
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
|
||||||
##
|
##
|
||||||
$scope.markAsRead = (notification, e) ->
|
$scope.markAsRead = (notification, e) ->
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
232
app/assets/javascripts/controllers/plans.coffee.erb
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
Application.Controllers.controller "PlansIndexController", ["$scope", "$state", '$uibModal', 'Auth', 'dialogs', 'growl', 'plansPromise', 'groupsPromise', 'Subscription', 'Member', 'subscriptionExplicationsPromise', '_t'
|
||||||
|
, ($scope, $state, $uibModal, Auth, dialogs, growl, plansPromise, groupsPromise, Subscription, Member, subscriptionExplicationsPromise, _t) ->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### PUBLIC SCOPE ###
|
||||||
|
|
||||||
|
## list of groups
|
||||||
|
$scope.groups = groupsPromise
|
||||||
|
|
||||||
|
## default : do not show the group changing form
|
||||||
|
$scope.changeGroup = false
|
||||||
|
|
||||||
|
## group ID of the current/selected user
|
||||||
|
$scope.userGroup = null
|
||||||
|
|
||||||
|
## list of plans, classified by group
|
||||||
|
$scope.plansClassifiedByGroup = []
|
||||||
|
for group in groupsPromise
|
||||||
|
groupObj = { id: group.id, name: group.name, plans: [] }
|
||||||
|
for plan in plansPromise
|
||||||
|
groupObj.plans.push(plan) if plan.group_id == group.id
|
||||||
|
$scope.plansClassifiedByGroup.push(groupObj)
|
||||||
|
|
||||||
|
## user to deal with
|
||||||
|
$scope.ctrl =
|
||||||
|
member: null
|
||||||
|
member_id: null
|
||||||
|
|
||||||
|
## already subscribed plan of the current user
|
||||||
|
$scope.paidPlan = null
|
||||||
|
|
||||||
|
## plan to subscribe (shopping cart)
|
||||||
|
$scope.selectedPlan = null
|
||||||
|
|
||||||
|
##
|
||||||
|
$scope.subscriptionExplicationsAlert = subscriptionExplicationsPromise.setting.value
|
||||||
|
|
||||||
|
##
|
||||||
|
# Callback to deal with the subscription of the user selected in the dropdown list instead of the current user's
|
||||||
|
# subscription. (admins only)
|
||||||
|
##
|
||||||
|
$scope.updateMember = ->
|
||||||
|
$scope.selectedPlan = null
|
||||||
|
$scope.paidPlan = null
|
||||||
|
$scope.userGroup = $scope.ctrl.member.group_id
|
||||||
|
$scope.changeGroup = false
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Add the provided plan to the shopping basket
|
||||||
|
# @param plan {Object} The plan to subscribe to
|
||||||
|
##
|
||||||
|
$scope.selectPlan = (plan) ->
|
||||||
|
if $scope.isAuthenticated()
|
||||||
|
if $scope.selectedPlan != plan
|
||||||
|
$scope.selectedPlan = plan
|
||||||
|
else
|
||||||
|
$scope.selectedPlan = null
|
||||||
|
else
|
||||||
|
$scope.login()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Callback to trigger the payment process of the subscription
|
||||||
|
##
|
||||||
|
$scope.openSubscribePlanModal = ->
|
||||||
|
if $scope.currentUser.role isnt 'admin'
|
||||||
|
payByStripe()
|
||||||
|
else
|
||||||
|
payOnSite()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Return the group object, identified by the ID set in $scope.userGroup
|
||||||
|
##
|
||||||
|
$scope.getUserGroup = ->
|
||||||
|
for group in $scope.groups
|
||||||
|
if group.id == $scope.userGroup
|
||||||
|
return group
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Change the group of the current/selected user to the one set in $scope.userGroup
|
||||||
|
##
|
||||||
|
$scope.selectGroup = ->
|
||||||
|
Member.update {id: $scope.ctrl.member.id}, {user: {group_id: $scope.userGroup}}, (user) ->
|
||||||
|
$scope.ctrl.member = user
|
||||||
|
$scope.changeGroup = false
|
||||||
|
if $scope.currentUser.role isnt 'admin'
|
||||||
|
$scope.currentUser = user
|
||||||
|
growl.success(_t('your_group_was_successfully_changed'))
|
||||||
|
else
|
||||||
|
growl.success(_t('the_user_s_group_was_successfully_changed'))
|
||||||
|
, (err) ->
|
||||||
|
if $scope.currentUser.role isnt 'admin'
|
||||||
|
growl.error(_t('an_error_prevented_your_group_from_being_changed'))
|
||||||
|
else
|
||||||
|
growl.error(_t('an_error_prevented_to_change_the_user_s_group'))
|
||||||
|
console.error(err)
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Return an enumerable meaninful string for the gender of the provider user
|
||||||
|
# @param user {Object} Database user record
|
||||||
|
# @return {string} 'male' or 'female'
|
||||||
|
##
|
||||||
|
$scope.getGender = (user) ->
|
||||||
|
if user.profile
|
||||||
|
if user.profile.gender == "true" then 'male' else 'female'
|
||||||
|
else 'other'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### PRIVATE SCOPE ###
|
||||||
|
|
||||||
|
##
|
||||||
|
# Kind of constructor: these actions will be realized first when the controller is loaded
|
||||||
|
##
|
||||||
|
initialize = ->
|
||||||
|
if $scope.currentUser
|
||||||
|
if $scope.currentUser.role isnt 'admin'
|
||||||
|
$scope.ctrl.member = $scope.currentUser
|
||||||
|
$scope.paidPlan = $scope.currentUser.subscribed_plan
|
||||||
|
$scope.userGroup = $scope.currentUser.group_id
|
||||||
|
else
|
||||||
|
Member.query {requested_attributes:'[subscription]'}, (members) ->
|
||||||
|
membersNoPlan = []
|
||||||
|
angular.forEach members, (v)->
|
||||||
|
membersNoPlan.push v unless v.subscribed_plan
|
||||||
|
$scope.members = membersNoPlan
|
||||||
|
|
||||||
|
$scope.$on 'devise:new-session', (event, user)->
|
||||||
|
$scope.ctrl.member = user
|
||||||
|
|
||||||
|
|
||||||
|
$scope.isInFuture = (dateTime)->
|
||||||
|
if moment().diff(moment(dateTime)) < 0
|
||||||
|
true
|
||||||
|
else
|
||||||
|
false
|
||||||
|
|
||||||
|
##
|
||||||
|
# Open a modal window which trigger the stripe payment process
|
||||||
|
##
|
||||||
|
payByStripe = ->
|
||||||
|
$uibModal.open
|
||||||
|
templateUrl: '<%= asset_path "stripe/payment_modal.html" %>'
|
||||||
|
size: 'md'
|
||||||
|
resolve:
|
||||||
|
selectedPlan: -> $scope.selectedPlan
|
||||||
|
member: -> $scope.ctrl.member
|
||||||
|
controller: ['$scope', '$uibModalInstance', '$state', 'selectedPlan', 'member', 'Subscription', 'CustomAsset', ($scope, $uibModalInstance, $state, selectedPlan, member, Subscription, CustomAsset) ->
|
||||||
|
$scope.amount = selectedPlan.amount
|
||||||
|
$scope.selectedPlan = selectedPlan
|
||||||
|
# retrieve the CGV
|
||||||
|
CustomAsset.get {name: 'cgv-file'}, (cgv) ->
|
||||||
|
$scope.cgv = cgv.custom_asset
|
||||||
|
$scope.payment = (status, response) ->
|
||||||
|
if response.error
|
||||||
|
growl.error(response.error.message)
|
||||||
|
else
|
||||||
|
$scope.attempting = true
|
||||||
|
Subscription.save
|
||||||
|
subscription:
|
||||||
|
plan_id: selectedPlan.id
|
||||||
|
user_id: member.id
|
||||||
|
card_token: response.id
|
||||||
|
, (data, status) -> # success
|
||||||
|
$uibModalInstance.close(data)
|
||||||
|
, (data, status) -> # failed
|
||||||
|
$scope.alerts = []
|
||||||
|
$scope.alerts.push({msg: _t('an_error_occured_during_the_payment_process_please_try_again_later'), type: 'danger' })
|
||||||
|
$scope.attempting = false
|
||||||
|
]
|
||||||
|
.result['finally'](null).then (subscription)->
|
||||||
|
$scope.ctrl.member.subscribed_plan = angular.copy($scope.selectedPlan)
|
||||||
|
Auth._currentUser.subscribed_plan = angular.copy($scope.selectedPlan)
|
||||||
|
$scope.paidPlan = angular.copy($scope.selectedPlan)
|
||||||
|
$scope.selectedPlan = null
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Open a modal window which trigger the local payment process
|
||||||
|
##
|
||||||
|
payOnSite = ->
|
||||||
|
$uibModal.open
|
||||||
|
templateUrl: '<%= asset_path "plans/payment_modal.html" %>'
|
||||||
|
size: 'sm'
|
||||||
|
resolve:
|
||||||
|
selectedPlan: -> $scope.selectedPlan
|
||||||
|
member: -> $scope.ctrl.member
|
||||||
|
controller: ['$scope', '$uibModalInstance', '$state', 'selectedPlan', 'member', 'Subscription', ($scope, $uibModalInstance, $state, selectedPlan, member, Subscription) ->
|
||||||
|
$scope.plan = selectedPlan
|
||||||
|
$scope.member = member
|
||||||
|
$scope.ok = ->
|
||||||
|
$scope.attempting = true
|
||||||
|
Subscription.save
|
||||||
|
subscription:
|
||||||
|
plan_id: selectedPlan.id
|
||||||
|
user_id: member.id
|
||||||
|
, (data, status) -> # success
|
||||||
|
$uibModalInstance.close(data)
|
||||||
|
, (data, status) -> # failed
|
||||||
|
$scope.alerts = []
|
||||||
|
$scope.alerts.push({msg: _t('an_error_occured_during_the_payment_process_please_try_again_later'), type: 'danger' })
|
||||||
|
$scope.attempting = false
|
||||||
|
|
||||||
|
$scope.cancel = ->
|
||||||
|
$uibModalInstance.dismiss('cancel')
|
||||||
|
]
|
||||||
|
.result['finally'](null).then (reservation)->
|
||||||
|
$scope.ctrl.member.subscribed_plan = angular.copy($scope.selectedPlan)
|
||||||
|
Auth._currentUser.subscribed_plan = angular.copy($scope.selectedPlan)
|
||||||
|
index = $scope.members.indexOf($scope.ctrl.member)
|
||||||
|
$scope.members.splice(index, 1)
|
||||||
|
$scope.ctrl.member = null
|
||||||
|
$scope.paidPlan = angular.copy($scope.selectedPlan)
|
||||||
|
$scope.selectedPlan = null
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## !!! MUST BE CALLED AT THE END of the controller
|
||||||
|
initialize()
|
||||||
|
]
|
152
app/assets/javascripts/controllers/profile.coffee
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
Application.Controllers.controller "CompleteProfileController", ["$scope", "$rootScope", "$state", "_t", "$locale", "growl", "CSRF", "Auth", "Member", "settingsPromise", "activeProviderPromise", "groupsPromise", "cguFile", "memberPromise"
|
||||||
|
, ($scope, $rootScope, $state, _t, $locale, growl, CSRF, Auth, Member, settingsPromise, activeProviderPromise, groupsPromise, cguFile, memberPromise) ->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### PUBLIC SCOPE ###
|
||||||
|
|
||||||
|
## API URL where the form will be posted
|
||||||
|
$scope.actionUrl = "/api/members/" + memberPromise.id
|
||||||
|
|
||||||
|
## Form action on the above URL
|
||||||
|
$scope.method = 'patch'
|
||||||
|
|
||||||
|
## genre of the application name (eg. "_le_ Fablab" or "_la_ Fabrique")
|
||||||
|
$scope.nameGenre = settingsPromise.name_genre
|
||||||
|
|
||||||
|
## name of the current fablab application (eg. "Fablab de la Casemate")
|
||||||
|
$scope.fablabName = settingsPromise.fablab_name
|
||||||
|
|
||||||
|
## informations from the current SSO provider
|
||||||
|
$scope.activeProvider = activeProviderPromise
|
||||||
|
|
||||||
|
## list of user's groups (student/standard/...)
|
||||||
|
$scope.groups = groupsPromise
|
||||||
|
|
||||||
|
## current user, contains informations retrieved from the SSO
|
||||||
|
$scope.user = memberPromise
|
||||||
|
|
||||||
|
## disallow the user to change his password as he connect from SSO
|
||||||
|
$scope.preventPassword = true
|
||||||
|
|
||||||
|
## mapping of fields to disable
|
||||||
|
$scope.preventField = {}
|
||||||
|
|
||||||
|
## CGU
|
||||||
|
$scope.cgu = cguFile.custom_asset
|
||||||
|
|
||||||
|
## Angular-Bootstrap datepicker configuration for birthday
|
||||||
|
$scope.datePicker =
|
||||||
|
format: $locale.DATETIME_FORMATS.shortDate
|
||||||
|
opened: false # default: datePicker is not shown
|
||||||
|
options:
|
||||||
|
startingDay: Fablab.weekStartingDay
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Callback to diplay the datepicker as a dropdown when clicking on the input field
|
||||||
|
# @param $event {Object} jQuery event object
|
||||||
|
##
|
||||||
|
$scope.openDatePicker = ($event) ->
|
||||||
|
$event.preventDefault()
|
||||||
|
$event.stopPropagation()
|
||||||
|
$scope.datePicker.opened = true
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# For use with ngUpload (https://github.com/twilson63/ngUpload).
|
||||||
|
# Intended to be the callback when the upload is done: any raised error will be stacked in the
|
||||||
|
# $scope.alerts array. If everything goes fine, the user's profile is updated and the user is
|
||||||
|
# redirected to the home page
|
||||||
|
# @param content {Object} JSON - The upload's result
|
||||||
|
##
|
||||||
|
$scope.submited = (content) ->
|
||||||
|
if !content.id?
|
||||||
|
$scope.alerts = []
|
||||||
|
angular.forEach content, (v, k)->
|
||||||
|
angular.forEach v, (err)->
|
||||||
|
$scope.alerts.push
|
||||||
|
msg: k+': '+err,
|
||||||
|
type: 'danger'
|
||||||
|
else
|
||||||
|
$scope.user.profile.user_avatar = content.profile.user_avatar
|
||||||
|
Auth._currentUser.profile.user_avatar = content.profile.user_avatar
|
||||||
|
$scope.user.name = content.name
|
||||||
|
Auth._currentUser.name = content.name
|
||||||
|
$scope.user = content
|
||||||
|
Auth._currentUser = content
|
||||||
|
$rootScope.currentUser = content
|
||||||
|
$state.go('app.public.home')
|
||||||
|
|
||||||
|
##
|
||||||
|
# For use with 'ng-class', returns the CSS class name for the uploads previews.
|
||||||
|
# The preview may show a placeholder or the content of the file depending on the upload state.
|
||||||
|
# @param v {*} any attribute, will be tested for truthiness (see JS evaluation rules)
|
||||||
|
##
|
||||||
|
$scope.fileinputClass = (v)->
|
||||||
|
if v
|
||||||
|
'fileinput-exists'
|
||||||
|
else
|
||||||
|
'fileinput-new'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Merge the current user into the account with the given auth_token
|
||||||
|
##
|
||||||
|
$scope.registerAuthToken = ->
|
||||||
|
Member.merge {id: $rootScope.currentUser.id}, {user: {auth_token: $scope.user.auth_token}}, (user) ->
|
||||||
|
$scope.user = user
|
||||||
|
Auth._currentUser = user
|
||||||
|
$rootScope.currentUser = user
|
||||||
|
$state.go('app.public.home')
|
||||||
|
, (err) ->
|
||||||
|
if err.data.error
|
||||||
|
growl.error(err.data.error)
|
||||||
|
else
|
||||||
|
growl.error(_t('an_unexpected_error_occurred_check_your_authentication_code'))
|
||||||
|
console.error(err)
|
||||||
|
|
||||||
|
##
|
||||||
|
# Return the email given by the SSO provider, parsed if needed
|
||||||
|
# @return {String} E-mail of the current user
|
||||||
|
##
|
||||||
|
$scope.ssoEmail = ->
|
||||||
|
email = memberPromise.email
|
||||||
|
if email
|
||||||
|
duplicate = email.match(/^<([^>]+)>.{20}-duplicate$/)
|
||||||
|
if duplicate
|
||||||
|
return duplicate[1]
|
||||||
|
email
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### PRIVATE SCOPE ###
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Kind of constructor: these actions will be realized first when the controller is loaded
|
||||||
|
##
|
||||||
|
initialize = ->
|
||||||
|
CSRF.setMetaTags()
|
||||||
|
|
||||||
|
# init the birth date to JS object
|
||||||
|
$scope.user.profile.birthday = moment($scope.user.profile.birthday).toDate()
|
||||||
|
|
||||||
|
# bind fields protection with sso fields
|
||||||
|
angular.forEach activeProviderPromise.mapping, (map) ->
|
||||||
|
$scope.preventField[map] = true
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## !!! MUST BE CALLED AT THE END of the controller
|
||||||
|
initialize()
|
||||||
|
|
||||||
|
]
|
@ -144,30 +144,28 @@ class ProjectsController
|
|||||||
##
|
##
|
||||||
# Controller used on projects listing page
|
# Controller used on projects listing page
|
||||||
##
|
##
|
||||||
Application.Controllers.controller "projectsController", ["$scope", "$state", 'Project', 'Machine', 'Theme', 'Component', ($scope, $state, Project, Machine, Theme, Component) ->
|
Application.Controllers.controller "ProjectsController", ["$scope", "$state", 'Project', 'machinesPromise', 'themesPromise', 'componentsPromise'
|
||||||
|
, ($scope, $state, Project, machinesPromise, themesPromise, componentsPromise) ->
|
||||||
|
|
||||||
|
|
||||||
### PRIVATE STATIC CONSTANTS ###
|
### PRIVATE STATIC CONSTANTS ###
|
||||||
|
|
||||||
# Number of notifications added to the page when the user clicks on 'load next notifications'
|
# Number of notifications added to the page when the user clicks on 'load next notifications'
|
||||||
PROJECTS_PER_PAGE = 12
|
PROJECTS_PER_PAGE = 12
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### PUBLIC SCOPE ###
|
### PUBLIC SCOPE ###
|
||||||
|
$scope.search = { q: "", from: undefined, machine_id: undefined, component_id: undefined, theme_id: undefined }
|
||||||
|
|
||||||
## list of projects to display
|
## list of projects to display
|
||||||
$scope.projects = []
|
$scope.projects = []
|
||||||
|
|
||||||
## list of machines / used for filtering
|
## list of machines / used for filtering
|
||||||
$scope.machines = []
|
$scope.machines = machinesPromise
|
||||||
|
|
||||||
## list of themes / used for filtering
|
## list of themes / used for filtering
|
||||||
$scope.themes = Theme.query()
|
$scope.themes = themesPromise
|
||||||
|
|
||||||
## list of components / used for filtering
|
## list of components / used for filtering
|
||||||
$scope.components = Component.query()
|
$scope.components = componentsPromise
|
||||||
|
|
||||||
## By default, the pagination mode is activated to limit the page size
|
## By default, the pagination mode is activated to limit the page size
|
||||||
$scope.paginateActive = true
|
$scope.paginateActive = true
|
||||||
@ -175,19 +173,31 @@ Application.Controllers.controller "projectsController", ["$scope", "$state", 'P
|
|||||||
## The currently displayed page number
|
## The currently displayed page number
|
||||||
$scope.page = 1
|
$scope.page = 1
|
||||||
|
|
||||||
|
$scope.resetFilters = ->
|
||||||
|
$scope.search.q = ""
|
||||||
|
$scope.search.from = undefined
|
||||||
|
$scope.search.machine_id = undefined
|
||||||
|
$scope.search.component_id = undefined
|
||||||
|
$scope.search.theme_id = undefined
|
||||||
|
$scope.triggerSearch()
|
||||||
|
|
||||||
|
$scope.triggerSearch = ->
|
||||||
|
Project.search { search: $scope.search, page: 1 }, (projects)->
|
||||||
|
$scope.projects = projects
|
||||||
|
if projects.length < PROJECTS_PER_PAGE
|
||||||
|
$scope.paginateActive = false
|
||||||
|
else
|
||||||
|
$scope.paginateActive = true
|
||||||
|
$scope.page = 2
|
||||||
|
|
||||||
##
|
|
||||||
# Request the server to retrieve the next undisplayed projects and add them
|
|
||||||
# to the local projects list.
|
|
||||||
##
|
|
||||||
$scope.loadMoreProjects = ->
|
$scope.loadMoreProjects = ->
|
||||||
Project.query {page: $scope.page}, (projects) ->
|
# Project.query {page: $scope.page}, (projects) ->
|
||||||
$scope.projects = $scope.projects.concat projects
|
# $scope.projects = $scope.projects.concat projects
|
||||||
$scope.paginateActive = false if projects.length < PROJECTS_PER_PAGE
|
# $scope.paginateActive = false if projects.length < PROJECTS_PER_PAGE
|
||||||
|
Project.search { search: $scope.search, page: $scope.page }, (projects)->
|
||||||
$scope.page += 1
|
$scope.projects = $scope.projects.concat projects
|
||||||
|
$scope.paginateActive = false if projects.length < PROJECTS_PER_PAGE
|
||||||
|
$scope.page += 1
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
@ -199,37 +209,9 @@ Application.Controllers.controller "projectsController", ["$scope", "$state", 'P
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
##
|
## initialization
|
||||||
# Callback to delete the provided project. Then, the projects list page is refreshed (admins only)
|
$scope.triggerSearch()
|
||||||
##
|
|
||||||
$scope.delete = (project) ->
|
|
||||||
# check the permissions
|
|
||||||
if $scope.currentUser.role isnt 'admin'
|
|
||||||
console.error 'Unauthorized operation'
|
|
||||||
else
|
|
||||||
# delete the project then refresh the projects list
|
|
||||||
project.$delete ->
|
|
||||||
$state.go('app.public.projects_list', {}, {reload: true})
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### PRIVATE SCOPE ###
|
|
||||||
|
|
||||||
##
|
|
||||||
# Kind of constructor: these actions will be realized first when the controller is loaded
|
|
||||||
##
|
|
||||||
initialize = ->
|
|
||||||
Machine.query().$promise.then (data)->
|
|
||||||
$scope.machines = data.map (d) ->
|
|
||||||
id: d.id
|
|
||||||
name: d.name
|
|
||||||
|
|
||||||
$scope.loadMoreProjects()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## !!! MUST BE CALLED AT THE END of the controller
|
|
||||||
initialize()
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -237,7 +219,8 @@ Application.Controllers.controller "projectsController", ["$scope", "$state", 'P
|
|||||||
##
|
##
|
||||||
# Controller used in the project creation page
|
# Controller used in the project creation page
|
||||||
##
|
##
|
||||||
Application.Controllers.controller "newProjectController", ["$scope", "$state", 'Project', 'Machine', 'Member', 'Component', 'Theme', 'Licence', '$document', 'CSRF', ($scope, $state, Project, Machine, Member, Component, Theme, Licence, $document, CSRF) ->
|
Application.Controllers.controller "NewProjectController", ["$scope", "$state", 'Project', 'Machine', 'Member', 'Component', 'Theme', 'Licence', '$document', 'CSRF'
|
||||||
|
, ($scope, $state, Project, Machine, Member, Component, Theme, Licence, $document, CSRF) ->
|
||||||
CSRF.setMetaTags()
|
CSRF.setMetaTags()
|
||||||
|
|
||||||
## API URL where the form will be posted
|
## API URL where the form will be posted
|
||||||
@ -246,9 +229,6 @@ Application.Controllers.controller "newProjectController", ["$scope", "$state",
|
|||||||
## Form action on the above URL
|
## Form action on the above URL
|
||||||
$scope.method = 'post'
|
$scope.method = 'post'
|
||||||
|
|
||||||
## Button litteral text value
|
|
||||||
$scope.submitName = 'Enregistrer comme brouillon'
|
|
||||||
|
|
||||||
## Default project parameters
|
## Default project parameters
|
||||||
$scope.project =
|
$scope.project =
|
||||||
project_steps_attributes: []
|
project_steps_attributes: []
|
||||||
@ -271,7 +251,8 @@ Application.Controllers.controller "newProjectController", ["$scope", "$state",
|
|||||||
##
|
##
|
||||||
# Controller used in the project edition page
|
# Controller used in the project edition page
|
||||||
##
|
##
|
||||||
Application.Controllers.controller "editProjectController", ["$scope", "$state", '$stateParams', 'Project', 'Machine', 'Member', 'Component', 'Theme', 'Licence', '$document', 'CSRF', ($scope, $state, $stateParams, Project, Machine, Member, Component, Theme, Licence, $document, CSRF) ->
|
Application.Controllers.controller "EditProjectController", ["$scope", "$state", '$stateParams', 'Project', 'Machine', 'Member', 'Component', 'Theme', 'Licence', '$document', 'CSRF', 'projectPromise'
|
||||||
|
, ($scope, $state, $stateParams, Project, Machine, Member, Component, Theme, Licence, $document, CSRF, projectPromise) ->
|
||||||
CSRF.setMetaTags()
|
CSRF.setMetaTags()
|
||||||
|
|
||||||
## API URL where the form will be posted
|
## API URL where the form will be posted
|
||||||
@ -280,15 +261,8 @@ Application.Controllers.controller "editProjectController", ["$scope", "$state",
|
|||||||
## Form action on the above URL
|
## Form action on the above URL
|
||||||
$scope.method = 'put'
|
$scope.method = 'put'
|
||||||
|
|
||||||
## Button litteral text value
|
|
||||||
$scope.submitName = 'Enregistrer'
|
|
||||||
|
|
||||||
## Retrieve the project's details, if an error occured, redirect the user to the projects list page
|
## Retrieve the project's details, if an error occured, redirect the user to the projects list page
|
||||||
$scope.project = Project.get {id: $stateParams.id}
|
$scope.project = projectPromise
|
||||||
, -> # success
|
|
||||||
return
|
|
||||||
, -> # failed
|
|
||||||
$state.go('app.public.projects_list')
|
|
||||||
|
|
||||||
## Other members list (project collaborators)
|
## Other members list (project collaborators)
|
||||||
Member.query().$promise.then (data)->
|
Member.query().$promise.then (data)->
|
||||||
@ -307,18 +281,15 @@ Application.Controllers.controller "editProjectController", ["$scope", "$state",
|
|||||||
##
|
##
|
||||||
# Controller used in the public project's details page
|
# Controller used in the public project's details page
|
||||||
##
|
##
|
||||||
Application.Controllers.controller "showProjectController", ["$scope", "$state", "$stateParams", "Project", '$location', ($scope, $state, $stateParams, Project, $location) ->
|
Application.Controllers.controller "ShowProjectController", ["$scope", "$state", "projectPromise", '$location', '$uibModal', '_t'
|
||||||
|
, ($scope, $state, projectPromise, $location, $uibModal, _t) ->
|
||||||
|
|
||||||
|
|
||||||
### PUBLIC SCOPE ###
|
### PUBLIC SCOPE ###
|
||||||
|
|
||||||
## Will be set to true once the project details are loaded. Used to load the Disqus plugin at the right moment
|
|
||||||
$scope.contentLoaded = false
|
|
||||||
|
|
||||||
## Store the project's details
|
## Store the project's details
|
||||||
$scope.project = {}
|
$scope.project = projectPromise
|
||||||
|
$scope.projectUrl = $location.absUrl()
|
||||||
|
$scope.disqusShortname = Fablab.disqusShortname
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
@ -336,23 +307,63 @@ Application.Controllers.controller "showProjectController", ["$scope", "$state",
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
### PRIVATE SCOPE ###
|
##
|
||||||
|
# Test if the provided user has the deletion rights on the current project
|
||||||
|
# @param [user] {{id:number}} (optional) the user to check rights
|
||||||
|
# @returns boolean
|
||||||
|
##
|
||||||
|
$scope.projectDeletableBy = (user) ->
|
||||||
|
return false if not user?
|
||||||
|
return true if $scope.project.author_id == user.id
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
# Kind of constructor: these actions will be realized first when the controller is loaded
|
# Callback to delete the current project. Then, the user is redirected to the projects list page,
|
||||||
|
# which is refreshed. Admins and project owner only are allowed to delete a project
|
||||||
##
|
##
|
||||||
initialize = ->
|
$scope.deleteProject = ->
|
||||||
## Retrieve the project content
|
# check the permissions
|
||||||
$scope.project = Project.get {id: $stateParams.id}
|
if $scope.currentUser.role is 'admin' or $scope.projectDeletableBy($scope.currentUser)
|
||||||
, -> # success
|
# delete the project then refresh the projects list
|
||||||
$scope.contentLoaded = true
|
$scope.project.$delete ->
|
||||||
$scope.project_url = $location.absUrl()
|
$state.go('app.public.projects_list', {}, {reload: true})
|
||||||
return
|
else
|
||||||
, -> # failed, redirect the user to the projects listing
|
console.error _t('unauthorized_operation')
|
||||||
$state.go('app.public.projects_list')
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Open a modal box containg a form that allow the end-user to signal an abusive content
|
||||||
|
# @param e {Object} jQuery event
|
||||||
|
##
|
||||||
|
$scope.signalAbuse = (e) ->
|
||||||
|
e.preventDefault() if e
|
||||||
|
|
||||||
|
$uibModal.open
|
||||||
|
templateUrl: '<%= asset_path "shared/signalAbuseModal.html" %>'
|
||||||
|
size: 'md'
|
||||||
|
resolve:
|
||||||
|
project: -> $scope.project
|
||||||
|
controller: ['$scope', '$uibModalInstance', '_t', 'growl', 'Abuse', 'project', ($scope, $uibModalInstance, _t, growl, Abuse, project) ->
|
||||||
|
|
||||||
|
# signaler's profile & signalement infos
|
||||||
|
$scope.signaler = {
|
||||||
|
signaled_type: 'Project'
|
||||||
|
signaled_id: project.id
|
||||||
|
}
|
||||||
|
|
||||||
|
# callback for signaling cancellation
|
||||||
|
$scope.cancel = ->
|
||||||
|
$uibModalInstance.dismiss('cancel')
|
||||||
|
|
||||||
|
# callback for form validation
|
||||||
|
$scope.ok = ->
|
||||||
|
Abuse.save {}, {abuse: $scope.signaler}, (res) ->
|
||||||
|
# creation successful
|
||||||
|
growl.success(_t('your_report_was_successful_thanks'))
|
||||||
|
$uibModalInstance.close(res)
|
||||||
|
, (error) ->
|
||||||
|
# creation failed...
|
||||||
|
growl.error(_t('an_error_occured_while_sending_your_report'))
|
||||||
|
]
|
||||||
|
|
||||||
## !!! MUST BE CALLED AT THE END of the controller
|
|
||||||
initialize()
|
|
||||||
]
|
]
|
658
app/assets/javascripts/controllers/trainings.coffee.erb
Normal file
@ -0,0 +1,658 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
##
|
||||||
|
# Controller used in the training reservation agenda page.
|
||||||
|
# This controller is very similar to the machine reservation controller with one major difference: here, ONLY ONE
|
||||||
|
# training can be reserved during the reservation process (the shopping cart may contains only one training and a subscription).
|
||||||
|
##
|
||||||
|
|
||||||
|
Application.Controllers.controller "ReserveTrainingController", ["$scope", "$state", '$stateParams', "$uibModal", 'Auth', 'dialogs', '$timeout', 'Price', 'Availability', 'Slot', 'Member', 'Setting', 'CustomAsset', '$compile', 'availabilityTrainingsPromise', 'plansPromise', 'groupsPromise', 'growl', 'settingsPromise', '_t',
|
||||||
|
($scope, $state, $stateParams, $uibModal, Auth, dialogs, $timeout, Price, Availability, Slot, Member, Setting, CustomAsset, $compile, availabilityTrainingsPromise, plansPromise, groupsPromise, growl, settingsPromise, _t) ->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### PRIVATE STATIC CONSTANTS ###
|
||||||
|
|
||||||
|
# The calendar is divided in slots of 60 minutes
|
||||||
|
BASE_SLOT = '01:00:00'
|
||||||
|
|
||||||
|
# The calendar will be initialized positioned under 9:00 AM
|
||||||
|
DEFAULT_CALENDAR_POSITION = '09:00:00'
|
||||||
|
|
||||||
|
# The user is unable to modify his already booked reservation 1 day before it occurs
|
||||||
|
PREVENT_BOOKING_MODIFICATION_DELAY = 1
|
||||||
|
|
||||||
|
# Color of the selected event backgound
|
||||||
|
SELECTED_EVENT_BG_COLOR = '#ffdd00'
|
||||||
|
|
||||||
|
# Slot already booked by the current user
|
||||||
|
FREE_SLOT_BORDER_COLOR = '#bd7ae9'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### PUBLIC SCOPE ###
|
||||||
|
|
||||||
|
## after fullCalendar loads, provides access to its methods through $scope.calendar.fullCalendar()
|
||||||
|
$scope.calendar = null
|
||||||
|
|
||||||
|
## bind the trainings availabilities with full-Calendar events
|
||||||
|
$scope.eventSources = [ { events: availabilityTrainingsPromise, textColor: 'black' } ]
|
||||||
|
|
||||||
|
## the user to deal with, ie. the current user for non-admins
|
||||||
|
$scope.ctrl =
|
||||||
|
member: {}
|
||||||
|
|
||||||
|
## the full list of members, used by admin to select a user to interact with
|
||||||
|
$scope.members = []
|
||||||
|
|
||||||
|
## list of plans, classified by group
|
||||||
|
$scope.plansClassifiedByGroup = []
|
||||||
|
for group in groupsPromise
|
||||||
|
groupObj = { id: group.id, name: group.name, plans: [] }
|
||||||
|
for plan in plansPromise
|
||||||
|
groupObj.plans.push(plan) if plan.group_id == group.id
|
||||||
|
$scope.plansClassifiedByGroup.push(groupObj)
|
||||||
|
|
||||||
|
## indicates the state of the current view : calendar or plans informations
|
||||||
|
$scope.plansAreShown = false
|
||||||
|
|
||||||
|
## indicates if the selected training was validated (ie. added to the shopping cart)
|
||||||
|
$scope.trainingIsValid = false
|
||||||
|
|
||||||
|
## contains the selected training once it was payed, allows to display a firendly end-of-shopping message
|
||||||
|
$scope.paidTraining = null
|
||||||
|
|
||||||
|
## will store the user's plan if he choosed to buy one
|
||||||
|
$scope.selectedPlan = null
|
||||||
|
|
||||||
|
## fullCalendar event. Training slot that the user want to book
|
||||||
|
$scope.selectedTraining = null
|
||||||
|
|
||||||
|
## fullCalendar event. An already booked slot that the user want to modify
|
||||||
|
$scope.slotToModify = null
|
||||||
|
|
||||||
|
## Once a training reservation was modified, will contains {newReservedSlot:{}, oldReservedSlot:{}}
|
||||||
|
$scope.modifiedSlots = null
|
||||||
|
|
||||||
|
## fullCalendar (v2) configuration
|
||||||
|
$scope.calendarConfig =
|
||||||
|
timezone: Fablab.timezone
|
||||||
|
lang: Fablab.fullcalendar_locale
|
||||||
|
header:
|
||||||
|
left: 'month agendaWeek'
|
||||||
|
center: 'title'
|
||||||
|
right: 'today prev,next'
|
||||||
|
firstDay: 1 # Week start on monday (France)
|
||||||
|
scrollTime: DEFAULT_CALENDAR_POSITION
|
||||||
|
slotDuration: BASE_SLOT
|
||||||
|
allDayDefault: false
|
||||||
|
minTime: '00:00:00'
|
||||||
|
maxTime: '24:00:00'
|
||||||
|
height: 'auto'
|
||||||
|
buttonIcons:
|
||||||
|
prev: 'left-single-arrow'
|
||||||
|
next: 'right-single-arrow'
|
||||||
|
timeFormat:
|
||||||
|
agenda:'H:mm'
|
||||||
|
month: 'H(:mm)'
|
||||||
|
axisFormat: 'H:mm'
|
||||||
|
|
||||||
|
allDaySlot: false
|
||||||
|
defaultView: 'agendaWeek'
|
||||||
|
editable: false
|
||||||
|
eventClick: (event, jsEvent, view) ->
|
||||||
|
calendarEventClickCb(event, jsEvent, view)
|
||||||
|
eventAfterAllRender: (view)->
|
||||||
|
$scope.events = $scope.calendar.fullCalendar 'clientEvents'
|
||||||
|
eventRender: (event, element, view) ->
|
||||||
|
eventRenderCb(event, element, view)
|
||||||
|
|
||||||
|
## Custom settings
|
||||||
|
$scope.subscriptionExplicationsAlert = settingsPromise.subscription_explications_alert
|
||||||
|
$scope.trainingExplicationsAlert = settingsPromise.training_explications_alert
|
||||||
|
$scope.trainingInformationMessage = settingsPromise.training_information_message
|
||||||
|
$scope.enableBookingMove = (settingsPromise.booking_move_enable == "true")
|
||||||
|
$scope.moveBookingDelay = parseInt(settingsPromise.booking_move_delay)
|
||||||
|
$scope.enableBookingCancel = (settingsPromise.booking_cancel_enable == "true")
|
||||||
|
$scope.cancelBookingDelay = parseInt(settingsPromise.booking_cancel_delay)
|
||||||
|
$scope.calendarConfig.minTime = moment.duration(moment(settingsPromise.booking_window_start).format('HH:mm:ss'))
|
||||||
|
$scope.calendarConfig.maxTime = moment.duration(moment(settingsPromise.booking_window_end).format('HH:mm:ss'))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Callback to deal with the reservations of the user selected in the dropdown list instead of the current user's
|
||||||
|
# reservations. (admins only)
|
||||||
|
##
|
||||||
|
$scope.updateMember = ->
|
||||||
|
if $scope.ctrl.member
|
||||||
|
Availability.trainings {member_id: $scope.ctrl.member.id}, (trainings) ->
|
||||||
|
$scope.calendar.fullCalendar 'removeEvents'
|
||||||
|
$scope.eventSources.push
|
||||||
|
events: trainings
|
||||||
|
textColor: 'black'
|
||||||
|
$scope.trainingIsValid = false
|
||||||
|
$scope.paidTraining = null
|
||||||
|
$scope.plansAreShown = false
|
||||||
|
$scope.selectedPlan = null
|
||||||
|
$scope.selectedTraining = null
|
||||||
|
$scope.slotToModify = null
|
||||||
|
$scope.modifiedSlots = null
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Callback to mark the selected training as validated (add it to the shopping cart).
|
||||||
|
##
|
||||||
|
$scope.validTraining = ->
|
||||||
|
$scope.trainingIsValid = true
|
||||||
|
$scope.updatePrices()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Remove the training from the shopping cart
|
||||||
|
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
|
||||||
|
##
|
||||||
|
$scope.removeTraining = (e) ->
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
$scope.selectedTraining.backgroundColor = 'white'
|
||||||
|
$scope.selectedTraining = null
|
||||||
|
$scope.plansAreShown = false
|
||||||
|
$scope.selectedPlan = null
|
||||||
|
$scope.trainingIsValid = false
|
||||||
|
$timeout ->
|
||||||
|
$scope.calendar.fullCalendar 'refetchEvents'
|
||||||
|
$scope.calendar.fullCalendar 'rerenderEvents'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Validates the shopping chart and redirect the user to the payment step
|
||||||
|
##
|
||||||
|
$scope.payTraining = ->
|
||||||
|
|
||||||
|
# first, we check that a user was selected
|
||||||
|
if Object.keys($scope.ctrl.member).length > 0
|
||||||
|
reservation = mkReservation($scope.ctrl.member, $scope.selectedTraining, $scope.selectedPlan)
|
||||||
|
|
||||||
|
if $scope.currentUser.role isnt 'admin' and $scope.amountTotal > 0
|
||||||
|
payByStripe(reservation)
|
||||||
|
else
|
||||||
|
if $scope.currentUser.role is 'admin' or $scope.amountTotal is 0
|
||||||
|
payOnSite(reservation)
|
||||||
|
else
|
||||||
|
# otherwise we alert, this error musn't occur when the current user is not admin
|
||||||
|
growl.error(_t('please_select_a_member_first'))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Add the provided plan to the current shopping cart
|
||||||
|
# @param plan {Object} the plan to subscribe
|
||||||
|
##
|
||||||
|
$scope.selectPlan = (plan) ->
|
||||||
|
if $scope.isAuthenticated()
|
||||||
|
if $scope.selectedPlan != plan
|
||||||
|
$scope.selectedPlan = plan
|
||||||
|
$scope.updatePrices()
|
||||||
|
else
|
||||||
|
$scope.selectedPlan = null
|
||||||
|
$scope.updatePrices()
|
||||||
|
else
|
||||||
|
$scope.login null, ->
|
||||||
|
$scope.selectedPlan = plan
|
||||||
|
$scope.updatePrices()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Changes the user current view from the plan subsription screen to the machine reservation agenda
|
||||||
|
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
|
||||||
|
##
|
||||||
|
$scope.doNotSubscribePlan = (e)->
|
||||||
|
e.preventDefault()
|
||||||
|
$scope.plansAreShown = false
|
||||||
|
$scope.selectedPlan = null
|
||||||
|
$scope.updatePrices()
|
||||||
|
|
||||||
|
##
|
||||||
|
# Switch the user's view from the reservation agenda to the plan subscription
|
||||||
|
##
|
||||||
|
$scope.showPlans = ->
|
||||||
|
$scope.plansAreShown = true
|
||||||
|
|
||||||
|
##
|
||||||
|
# Cancel the current booking modification, removing the previously booked slot from the selection
|
||||||
|
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
|
||||||
|
##
|
||||||
|
$scope.removeSlotToModify = (e) ->
|
||||||
|
e.preventDefault()
|
||||||
|
if $scope.slotToPlace
|
||||||
|
$scope.slotToPlace.backgroundColor = 'white'
|
||||||
|
$scope.slotToPlace.title = $scope.slotToPlace.training.name
|
||||||
|
$scope.slotToPlace = null
|
||||||
|
$scope.slotToModify.title = if $scope.currentUser.role isnt 'admin' then $scope.slotToModify.training.name + " - " + _t('i_ve_reserved') else $scope.slotToModify.training.name
|
||||||
|
$scope.slotToModify.backgroundColor = 'white'
|
||||||
|
$scope.slotToModify = null
|
||||||
|
$scope.calendar.fullCalendar 'rerenderEvents'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# When modifying an already booked reservation, cancel the choice of the new slot
|
||||||
|
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
|
||||||
|
##
|
||||||
|
$scope.removeSlotToPlace = (e)->
|
||||||
|
e.preventDefault()
|
||||||
|
$scope.slotToPlace.backgroundColor = 'white'
|
||||||
|
$scope.slotToPlace.title = $scope.slotToPlace.training.name
|
||||||
|
$scope.slotToPlace = null
|
||||||
|
$scope.calendar.fullCalendar 'rerenderEvents'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# When modifying an already booked reservation, confirm the modification.
|
||||||
|
##
|
||||||
|
$scope.modifyTrainingSlot = ->
|
||||||
|
Slot.update {id: $scope.slotToModify.slot_id},
|
||||||
|
slot:
|
||||||
|
start_at: $scope.slotToPlace.start
|
||||||
|
end_at: $scope.slotToPlace.end
|
||||||
|
availability_id: $scope.slotToPlace.id
|
||||||
|
, -> # success
|
||||||
|
$scope.modifiedSlots =
|
||||||
|
newReservedSlot: $scope.slotToPlace
|
||||||
|
oldReservedSlot: $scope.slotToModify
|
||||||
|
$scope.slotToPlace.title = if $scope.currentUser.role isnt 'admin' then $scope.slotToPlace.training.name + " - " + _t('i_ve_reserved') else $scope.slotToPlace.training.name
|
||||||
|
$scope.slotToPlace.backgroundColor = 'white'
|
||||||
|
$scope.slotToPlace.borderColor = $scope.slotToModify.borderColor
|
||||||
|
$scope.slotToPlace.slot_id = $scope.slotToModify.slot_id
|
||||||
|
$scope.slotToPlace.is_reserved = true
|
||||||
|
$scope.slotToPlace.can_modify = true
|
||||||
|
$scope.slotToPlace = null
|
||||||
|
|
||||||
|
$scope.slotToModify.backgroundColor = 'white'
|
||||||
|
$scope.slotToModify.title = $scope.slotToModify.training.name
|
||||||
|
$scope.slotToModify.borderColor = FREE_SLOT_BORDER_COLOR
|
||||||
|
$scope.slotToModify.slot_id = null
|
||||||
|
$scope.slotToModify.is_reserved = false
|
||||||
|
$scope.slotToModify.can_modify = false
|
||||||
|
$scope.slotToModify.is_completed = false if $scope.slotToModify.is_completed
|
||||||
|
$scope.slotToModify = null
|
||||||
|
$scope.calendar.fullCalendar 'rerenderEvents'
|
||||||
|
, -> # failure
|
||||||
|
growl.error('an_error_occured_preventing_the_booked_slot_from_being_modified')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Cancel the current booking modification, reseting the whole process
|
||||||
|
##
|
||||||
|
$scope.cancelModifyMachineSlot = ->
|
||||||
|
$scope.slotToPlace.backgroundColor = 'white'
|
||||||
|
$scope.slotToPlace.title = $scope.slotToPlace.training.name
|
||||||
|
$scope.slotToPlace = null
|
||||||
|
$scope.slotToModify.title = if $scope.currentUser.role isnt 'admin' then $scope.slotToModify.training.name + " - " + _t('i_ve_reserved') else $scope.slotToModify.training.name
|
||||||
|
$scope.slotToModify.backgroundColor = 'white'
|
||||||
|
$scope.slotToModify = null
|
||||||
|
$scope.calendar.fullCalendar 'rerenderEvents'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Update the prices, based on the current selection
|
||||||
|
##
|
||||||
|
$scope.updatePrices = ->
|
||||||
|
if Object.keys($scope.ctrl.member).length > 0
|
||||||
|
r = mkReservation($scope.ctrl.member, $scope.selectedTraining, $scope.selectedPlan)
|
||||||
|
Price.compute {reservation: r}, (res) ->
|
||||||
|
$scope.amountTotal = res.price
|
||||||
|
else
|
||||||
|
$scope.amountTotal = null
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### PRIVATE SCOPE ###
|
||||||
|
|
||||||
|
##
|
||||||
|
# Kind of constructor: these actions will be realized first when the controller is loaded
|
||||||
|
##
|
||||||
|
initialize = ->
|
||||||
|
if $scope.currentUser.role isnt 'admin'
|
||||||
|
Member.get id: $scope.currentUser.id, (member) ->
|
||||||
|
$scope.ctrl.member = member
|
||||||
|
else
|
||||||
|
Member.query {requested_attributes:'[subscription,credits]'}, (members) ->
|
||||||
|
$scope.members = members
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Create an hash map implementing the Reservation specs
|
||||||
|
# @param member {Object} User as retreived from the API: current user / selected user if current is admin
|
||||||
|
# @param training {Object} fullCalendar event: training slot selected on the calendar
|
||||||
|
# @param [plan] {Object} Plan as retrived from the API: plan to buy with the current reservation
|
||||||
|
# @return {{user_id:Number, reservable_id:Number, reservable_type:String, slots_attributes:Array<Object>, plan_id:Number|null}}
|
||||||
|
##
|
||||||
|
mkReservation = (member, training, plan = null) ->
|
||||||
|
reservation =
|
||||||
|
user_id: member.id
|
||||||
|
reservable_id: training.training.id
|
||||||
|
reservable_type: 'Training'
|
||||||
|
slots_attributes: []
|
||||||
|
plan_id: (plan.id if plan)
|
||||||
|
|
||||||
|
reservation.slots_attributes.push
|
||||||
|
start_at: training.start
|
||||||
|
end_at: training.end
|
||||||
|
availability_id: training.id
|
||||||
|
offered: training.offered || false
|
||||||
|
|
||||||
|
reservation
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Triggered when the user clicks on a reservation slot in the agenda.
|
||||||
|
# Defines the behavior to adopt depending on the slot status (already booked, free, ready to be reserved ...),
|
||||||
|
# the user's subscription (current or about to be took) and the time (the user cannot modify a booked reservation
|
||||||
|
# if it's too late).
|
||||||
|
# @see http://fullcalendar.io/docs/mouse/eventClick/
|
||||||
|
##
|
||||||
|
calendarEventClickCb = (event, jsEvent, view) ->
|
||||||
|
if $scope.ctrl.member
|
||||||
|
# reserve a training if this training will not be reserved and is not about to move and not is completed
|
||||||
|
if !event.is_reserved && !$scope.slotToModify && !event.is_completed
|
||||||
|
if event != $scope.selectedTraining
|
||||||
|
$scope.selectedTraining = event
|
||||||
|
$scope.selectedTraining.offered = false
|
||||||
|
event.backgroundColor = SELECTED_EVENT_BG_COLOR
|
||||||
|
computeTrainingAmount($scope.selectedTraining)
|
||||||
|
else
|
||||||
|
$scope.selectedTraining = null
|
||||||
|
event.backgroundColor = 'white'
|
||||||
|
$scope.trainingIsValid = false
|
||||||
|
$scope.paidTraining = null
|
||||||
|
$scope.selectedPlan = null
|
||||||
|
$scope.modifiedSlots = null
|
||||||
|
# clean all others events background
|
||||||
|
angular.forEach $scope.events, (e)->
|
||||||
|
if event.id != e.id
|
||||||
|
e.backgroundColor = 'white'
|
||||||
|
$scope.calendar.fullCalendar 'rerenderEvents'
|
||||||
|
# two if below for move training reserved
|
||||||
|
# if training isnt reserved and have a training to modify and same training and not complete
|
||||||
|
else if !event.is_reserved && $scope.slotToModify && slotCanBePlaced(event)
|
||||||
|
if $scope.slotToPlace
|
||||||
|
$scope.slotToPlace.backgroundColor = 'white'
|
||||||
|
$scope.slotToPlace.title = event.training.name
|
||||||
|
$scope.slotToPlace = event
|
||||||
|
event.backgroundColor = '#bbb'
|
||||||
|
event.title = event.training.name + ' - ' + _t('i_shift')
|
||||||
|
$scope.calendar.fullCalendar 'rerenderEvents'
|
||||||
|
# if training reserved can modify
|
||||||
|
else if event.is_reserved and (slotCanBeModified(event) or slotCanBeCanceled(event)) and !$scope.slotToModify and !$scope.selectedTraining
|
||||||
|
event.movable = slotCanBeModified(event)
|
||||||
|
event.cancelable = slotCanBeCanceled(event)
|
||||||
|
if $scope.currentUser.role is 'admin'
|
||||||
|
event.user =
|
||||||
|
name: $scope.ctrl.member.name
|
||||||
|
dialogs.confirm
|
||||||
|
templateUrl: '<%= asset_path "shared/confirm_modify_slot_modal.html" %>'
|
||||||
|
resolve:
|
||||||
|
object: -> event
|
||||||
|
, (type) -> # success
|
||||||
|
if type == 'move'
|
||||||
|
$scope.modifiedSlots = null
|
||||||
|
$scope.slotToModify = event
|
||||||
|
event.backgroundColor = '#eee'
|
||||||
|
event.title = event.training.name + ' - ' + _t('i_change')
|
||||||
|
$scope.calendar.fullCalendar 'rerenderEvents'
|
||||||
|
else if type == 'cancel'
|
||||||
|
dialogs.confirm
|
||||||
|
resolve:
|
||||||
|
object: ->
|
||||||
|
title: _t('confirmation_required')
|
||||||
|
msg: _t('do_you_really_want_to_cancel_this_reservation')
|
||||||
|
, -> # cancel confirmed
|
||||||
|
Slot.cancel {id: event.slot_id}, -> # successfully canceled
|
||||||
|
growl.success _t('reservation_was_successfully_cancelled')
|
||||||
|
$scope.canceledSlot = event
|
||||||
|
$scope.canceledSlot.backgroundColor = 'white'
|
||||||
|
$scope.canceledSlot.title = event.training.name
|
||||||
|
$scope.canceledSlot.borderColor = FREE_SLOT_BORDER_COLOR
|
||||||
|
$scope.canceledSlot.slot_id = null
|
||||||
|
$scope.canceledSlot.is_reserved = false
|
||||||
|
$scope.canceledSlot.can_modify = false
|
||||||
|
$scope.canceledSlot.is_completed = false if event.is_completed
|
||||||
|
$scope.canceledSlot = null
|
||||||
|
$scope.calendar.fullCalendar 'rerenderEvents'
|
||||||
|
, -> # error while canceling
|
||||||
|
growl.error _t('cancellation_failed')
|
||||||
|
, -> # canceled
|
||||||
|
$scope.paidMachineSlots = null
|
||||||
|
$scope.selectedPlan = null
|
||||||
|
$scope.modifiedSlots = null
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# When events are rendered, adds attributes for popover and compile
|
||||||
|
# @see http://fullcalendar.io/docs/event_rendering/eventRender/
|
||||||
|
##
|
||||||
|
eventRenderCb = (event, element, view)->
|
||||||
|
element.attr(
|
||||||
|
'uib-popover': event.training.description
|
||||||
|
'popover-trigger': 'mouseenter'
|
||||||
|
'popover-append-to-body': true
|
||||||
|
)
|
||||||
|
$compile(element)($scope)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Open a modal window that allows the user to process a credit card payment for his current shopping cart.
|
||||||
|
##
|
||||||
|
payByStripe = (reservation) ->
|
||||||
|
|
||||||
|
$uibModal.open
|
||||||
|
templateUrl: '<%= asset_path "stripe/payment_modal.html" %>'
|
||||||
|
size: 'md'
|
||||||
|
resolve:
|
||||||
|
reservation: ->
|
||||||
|
reservation
|
||||||
|
price: ->
|
||||||
|
Price.compute({reservation: reservation}).$promise
|
||||||
|
cgv: ->
|
||||||
|
CustomAsset.get({name: 'cgv-file'}).$promise
|
||||||
|
controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'cgv', 'Auth', 'Reservation', ($scope, $uibModalInstance, $state, reservation, price, cgv, Auth, Reservation) ->
|
||||||
|
# Price
|
||||||
|
$scope.amount = price.price
|
||||||
|
|
||||||
|
# CGV
|
||||||
|
$scope.cgv = cgv.custom_asset
|
||||||
|
|
||||||
|
# Reservation
|
||||||
|
$scope.reservation = reservation
|
||||||
|
|
||||||
|
##
|
||||||
|
# Callback to process the payment with Stripe, triggered on button click
|
||||||
|
##
|
||||||
|
$scope.payment = (status, response) ->
|
||||||
|
if response.error
|
||||||
|
growl.error(response.error.message)
|
||||||
|
else
|
||||||
|
$scope.attempting = true
|
||||||
|
$scope.reservation.card_token = response.id
|
||||||
|
Reservation.save reservation: $scope.reservation, (reservation) ->
|
||||||
|
$uibModalInstance.close(reservation)
|
||||||
|
, (response)->
|
||||||
|
$scope.alerts = []
|
||||||
|
if response.data.card
|
||||||
|
$scope.alerts.push
|
||||||
|
msg: response.data.card[0]
|
||||||
|
type: 'danger'
|
||||||
|
else
|
||||||
|
$scope.alerts.push({msg: _t('a_problem_occured_during_the_payment_process_please_try_again_later'), type: 'danger' })
|
||||||
|
$scope.attempting = false
|
||||||
|
]
|
||||||
|
.result['finally'](null).then (reservation)->
|
||||||
|
afterPayment(reservation)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Open a modal window that allows the user to process a local payment for his current shopping cart (admin only).
|
||||||
|
##
|
||||||
|
payOnSite = (reservation) ->
|
||||||
|
|
||||||
|
$uibModal.open
|
||||||
|
templateUrl: '<%= asset_path "shared/valid_reservation_modal.html" %>'
|
||||||
|
size: 'sm'
|
||||||
|
resolve:
|
||||||
|
reservation: ->
|
||||||
|
reservation
|
||||||
|
price: ->
|
||||||
|
Price.compute({reservation: reservation}).$promise
|
||||||
|
controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'Auth', 'Reservation', ($scope, $uibModalInstance, $state, reservation, price, Auth, Reservation) ->
|
||||||
|
# Price
|
||||||
|
$scope.amount = price.price
|
||||||
|
|
||||||
|
# Reservation
|
||||||
|
$scope.reservation = reservation
|
||||||
|
|
||||||
|
# Button label
|
||||||
|
if $scope.amount > 0
|
||||||
|
$scope.validButtonName = _t('confirm_(payment_on_site)')
|
||||||
|
else
|
||||||
|
$scope.validButtonName = _t('confirm')
|
||||||
|
|
||||||
|
##
|
||||||
|
# Callback to process the local payment, triggered on button click
|
||||||
|
##
|
||||||
|
$scope.ok = ->
|
||||||
|
$scope.attempting = true
|
||||||
|
Reservation.save reservation: $scope.reservation, (reservation) ->
|
||||||
|
$uibModalInstance.close(reservation)
|
||||||
|
$scope.attempting = true
|
||||||
|
, (response)->
|
||||||
|
$scope.alerts = []
|
||||||
|
$scope.alerts.push({msg: _t('a_problem_occured_during_the_payment_process_please_try_again_later'), type: 'danger' })
|
||||||
|
$scope.attempting = false
|
||||||
|
$scope.cancel = ->
|
||||||
|
$uibModalInstance.dismiss('cancel')
|
||||||
|
]
|
||||||
|
.result['finally'](null).then (reservation)->
|
||||||
|
afterPayment(reservation)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Computes the training amount depending of the member's credit
|
||||||
|
# @param training {Object} training slot
|
||||||
|
##
|
||||||
|
computeTrainingAmount = (training)->
|
||||||
|
# first we check that a user was selected
|
||||||
|
if Object.keys($scope.ctrl.member).length > 0
|
||||||
|
r = mkReservation($scope.ctrl.member, training) # reservation without any Plan -> we get the training price
|
||||||
|
Price.compute {reservation: r}, (res) ->
|
||||||
|
$scope.selectedTrainingAmount = res.price
|
||||||
|
else
|
||||||
|
$scope.selectedTrainingAmount = null
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Once the reservation is booked (payment process successfully completed), change the event style
|
||||||
|
# in fullCalendar, update the user's subscription and free-credits if needed
|
||||||
|
# @param reservation {Object}
|
||||||
|
##
|
||||||
|
afterPayment = (reservation)->
|
||||||
|
$scope.paidTraining = $scope.selectedTraining
|
||||||
|
$scope.paidTraining.backgroundColor = 'white'
|
||||||
|
$scope.paidTraining.is_reserved = true
|
||||||
|
$scope.paidTraining.can_modify = true
|
||||||
|
updateTrainingSlotId($scope.paidTraining, reservation)
|
||||||
|
$scope.paidTraining.borderColor = '#b2e774'
|
||||||
|
$scope.paidTraining.title = $scope.paidTraining.training.name + " - " + _t('i_ve_reserved')
|
||||||
|
|
||||||
|
$scope.selectedTraining = null
|
||||||
|
$scope.trainingIsValid = false
|
||||||
|
|
||||||
|
if $scope.selectedPlan
|
||||||
|
$scope.ctrl.member.subscribed_plan = angular.copy($scope.selectedPlan)
|
||||||
|
Auth._currentUser.subscribed_plan = angular.copy($scope.selectedPlan)
|
||||||
|
$scope.plansAreShown = false
|
||||||
|
$scope.selectedPlan = null
|
||||||
|
$scope.ctrl.member.training_credits = angular.copy(reservation.user.training_credits)
|
||||||
|
$scope.ctrl.member.machine_credits = angular.copy(reservation.user.machine_credits)
|
||||||
|
Auth._currentUser.training_credits = angular.copy(reservation.user.training_credits)
|
||||||
|
Auth._currentUser.machine_credits = angular.copy(reservation.user.machine_credits)
|
||||||
|
|
||||||
|
$scope.calendar.fullCalendar 'refetchEvents'
|
||||||
|
$scope.calendar.fullCalendar 'rerenderEvents'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Determines if the provided booked slot is able to be modified by the user.
|
||||||
|
# @param slot {Object} fullCalendar event object
|
||||||
|
##
|
||||||
|
slotCanBeModified = (slot)->
|
||||||
|
return true if $scope.currentUser.role is 'admin'
|
||||||
|
slotStart = moment(slot.start)
|
||||||
|
now = moment(new Date())
|
||||||
|
if slot.can_modify and $scope.enableBookingMove and slotStart.diff(now, "hours") >= $scope.moveBookingDelay
|
||||||
|
return true
|
||||||
|
else
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Determines if the provided booked slot is able to be canceled by the user.
|
||||||
|
# @param slot {Object} fullCalendar event object
|
||||||
|
##
|
||||||
|
slotCanBeCanceled = (slot) ->
|
||||||
|
return true if $scope.currentUser.role is 'admin'
|
||||||
|
slotStart = moment(slot.start)
|
||||||
|
now = moment()
|
||||||
|
if slot.can_modify and $scope.enableBookingCancel and slotStart.diff(now, "hours") >= $scope.cancelBookingDelay
|
||||||
|
return true
|
||||||
|
else
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# For booking modifications, checks that the newly selected slot is valid
|
||||||
|
# @param slot {Object} fullCalendar event object
|
||||||
|
##
|
||||||
|
slotCanBePlaced = (slot)->
|
||||||
|
if slot.training.id == $scope.slotToModify.training.id and !slot.is_completed
|
||||||
|
return true
|
||||||
|
else
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# After payment, update the id of the newly reserved slot with the id returned by the server.
|
||||||
|
# This will allow the user to modify the reservation he just booked.
|
||||||
|
# @param slot {Object}
|
||||||
|
# @param reservation {Object}
|
||||||
|
##
|
||||||
|
updateTrainingSlotId = (slot, reservation)->
|
||||||
|
angular.forEach reservation.slots, (s)->
|
||||||
|
if slot.start_at == slot.start_at
|
||||||
|
slot.slot_id = s.id
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## !!! MUST BE CALLED AT THE END of the controller
|
||||||
|
initialize()
|
||||||
|
|
||||||
|
]
|
@ -28,7 +28,7 @@ Application.Directives.directive('bsJasnyFileinput', [function(){
|
|||||||
ngModelCtrl.$setValidity('filetype', true);
|
ngModelCtrl.$setValidity('filetype', true);
|
||||||
else
|
else
|
||||||
ngModelCtrl.$setValidity('filetype', false);
|
ngModelCtrl.$setValidity('filetype', false);
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
$scope.$apply();
|
$scope.$apply();
|
||||||
});
|
});
|
||||||
|
20
app/assets/javascripts/directives/confirmation_needed.coffee
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
Application.Directives.directive 'confirmationNeeded', [->
|
||||||
|
return {
|
||||||
|
priority: 1
|
||||||
|
terminal: true
|
||||||
|
link: (scope, element, attrs)->
|
||||||
|
msg = attrs.confirmationNeeded || "Are you sure?"
|
||||||
|
clickAction = attrs.ngClick
|
||||||
|
element.bind 'click', ->
|
||||||
|
if attrs.confirmationNeededIf?
|
||||||
|
confirmNeededIf = scope.$eval(attrs.confirmationNeededIf)
|
||||||
|
if confirmNeededIf == true
|
||||||
|
if ( window.confirm(msg) )
|
||||||
|
scope.$eval(clickAction)
|
||||||
|
else
|
||||||
|
scope.$eval(clickAction)
|
||||||
|
else
|
||||||
|
if ( window.confirm(msg) )
|
||||||
|
scope.$eval(clickAction)
|
||||||
|
}
|
||||||
|
]
|
@ -21,6 +21,8 @@ Application.Directives.directive 'bsHolder', [ ->
|
|||||||
{
|
{
|
||||||
link: (scope, element, attrs) ->
|
link: (scope, element, attrs) ->
|
||||||
Holder.addTheme("icon", { background: "white", foreground: "#e9e9e9", size: 80, font: "FontAwesome"})
|
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"})
|
.addTheme("avatar", { background: "#eeeeee", foreground: "#555555", size: 16, font: "FontAwesome"})
|
||||||
.run(element[0])
|
.run(element[0])
|
||||||
return
|
return
|
||||||
@ -66,3 +68,38 @@ Application.Directives.directive "disableAnimation", ($animate) ->
|
|||||||
attrs.$observe "disableAnimation", (value) ->
|
attrs.$observe "disableAnimation", (value) ->
|
||||||
$animate.enabled not value, elem
|
$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)
|
||||||
|
|
||||||
|
}
|
||||||
|
]
|
24
app/assets/javascripts/directives/stripe-angular.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
// https://github.com/gtramontina/stripe-angular
|
||||||
|
|
||||||
|
Application.Directives.directive('stripeForm', ['$window',
|
||||||
|
function($window) {
|
||||||
|
var directive = { restrict: 'A' };
|
||||||
|
directive.link = function(scope, element, attributes) {
|
||||||
|
var form = angular.element(element);
|
||||||
|
form.bind('submit', function() {
|
||||||
|
var button = form.find('button');
|
||||||
|
button.prop('disabled', true);
|
||||||
|
$window.Stripe.createToken(form[0], function() {
|
||||||
|
var args = arguments;
|
||||||
|
scope.$apply(function() {
|
||||||
|
scope[attributes.stripeForm].apply(scope, args);
|
||||||
|
});
|
||||||
|
//button.prop('disabled', false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return directive;
|
||||||
|
|
||||||
|
}]);
|
34
app/assets/javascripts/directives/validators.coffee
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
Application.Directives.directive 'url', [ ->
|
||||||
|
URL_REGEXP = /^(https?:\/\/)([\da-z\.-]+)\.([-a-z0-9\.]{2,30})([\/\w \.-]*)*\/?$/
|
||||||
|
{
|
||||||
|
require: 'ngModel'
|
||||||
|
link: (scope, element, attributes, ctrl) ->
|
||||||
|
ctrl.$validators.url = (modelValue, viewValue) ->
|
||||||
|
if ctrl.$isEmpty(modelValue)
|
||||||
|
return true
|
||||||
|
if URL_REGEXP.test(viewValue)
|
||||||
|
return true
|
||||||
|
|
||||||
|
# otherwise, this is invalid
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
Application.Directives.directive 'endpoint', [ ->
|
||||||
|
ENDPOINT_REGEXP = /^\/([-._~:?#\[\]@!$&'()*+,;=%\w]+\/?)*$/
|
||||||
|
{
|
||||||
|
require: 'ngModel'
|
||||||
|
link: (scope, element, attributes, ctrl) ->
|
||||||
|
ctrl.$validators.endpoint = (modelValue, viewValue) ->
|
||||||
|
if ctrl.$isEmpty(modelValue)
|
||||||
|
return true
|
||||||
|
if ENDPOINT_REGEXP.test(viewValue)
|
||||||
|
return true
|
||||||
|
|
||||||
|
# otherwise, this is invalid
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
]
|
@ -1,7 +1,19 @@
|
|||||||
'use strict'
|
'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
|
# filter for projects and trainings
|
||||||
Application.Controllers.filter "machineFilter", [ ->
|
Application.Filters.filter "machineFilter", [ ->
|
||||||
(elements, selectedMachine) ->
|
(elements, selectedMachine) ->
|
||||||
if !angular.isUndefined(elements) and !angular.isUndefined(selectedMachine) and elements? and selectedMachine?
|
if !angular.isUndefined(elements) and !angular.isUndefined(selectedMachine) and elements? and selectedMachine?
|
||||||
filteredElements = []
|
filteredElements = []
|
||||||
@ -13,7 +25,7 @@ Application.Controllers.filter "machineFilter", [ ->
|
|||||||
elements
|
elements
|
||||||
]
|
]
|
||||||
|
|
||||||
Application.Controllers.filter "projectMemberFilter", [ "Auth", (Auth)->
|
Application.Filters.filter "projectMemberFilter", [ "Auth", (Auth)->
|
||||||
(projects, selectedMember) ->
|
(projects, selectedMember) ->
|
||||||
if !angular.isUndefined(projects) and angular.isDefined(selectedMember) and projects? and selectedMember? and selectedMember != ""
|
if !angular.isUndefined(projects) and angular.isDefined(selectedMember) and projects? and selectedMember? and selectedMember != ""
|
||||||
filteredProject = []
|
filteredProject = []
|
||||||
@ -32,7 +44,7 @@ Application.Controllers.filter "projectMemberFilter", [ "Auth", (Auth)->
|
|||||||
projects
|
projects
|
||||||
]
|
]
|
||||||
|
|
||||||
Application.Controllers.filter "themeFilter", [ ->
|
Application.Filters.filter "themeFilter", [ ->
|
||||||
(projects, selectedTheme) ->
|
(projects, selectedTheme) ->
|
||||||
if !angular.isUndefined(projects) and !angular.isUndefined(selectedTheme) and projects? and selectedTheme?
|
if !angular.isUndefined(projects) and !angular.isUndefined(selectedTheme) and projects? and selectedTheme?
|
||||||
filteredProjects = []
|
filteredProjects = []
|
||||||
@ -44,7 +56,7 @@ Application.Controllers.filter "themeFilter", [ ->
|
|||||||
projects
|
projects
|
||||||
]
|
]
|
||||||
|
|
||||||
Application.Controllers.filter "componentFilter", [ ->
|
Application.Filters.filter "componentFilter", [ ->
|
||||||
(projects, selectedComponent) ->
|
(projects, selectedComponent) ->
|
||||||
if !angular.isUndefined(projects) and !angular.isUndefined(selectedComponent) and projects? and selectedComponent?
|
if !angular.isUndefined(projects) and !angular.isUndefined(selectedComponent) and projects? and selectedComponent?
|
||||||
filteredProjects = []
|
filteredProjects = []
|
||||||
@ -56,7 +68,7 @@ Application.Controllers.filter "componentFilter", [ ->
|
|||||||
projects
|
projects
|
||||||
]
|
]
|
||||||
|
|
||||||
Application.Controllers.filter "projectsByAuthor", [ ->
|
Application.Filters.filter "projectsByAuthor", [ ->
|
||||||
(projects, authorId) ->
|
(projects, authorId) ->
|
||||||
if !angular.isUndefined(projects) and angular.isDefined(authorId) and projects? and authorId? and authorId != ""
|
if !angular.isUndefined(projects) and angular.isDefined(authorId) and projects? and authorId? and authorId != ""
|
||||||
filteredProject = []
|
filteredProject = []
|
||||||
@ -68,7 +80,7 @@ Application.Controllers.filter "projectsByAuthor", [ ->
|
|||||||
projects
|
projects
|
||||||
]
|
]
|
||||||
|
|
||||||
Application.Controllers.filter "projectsCollabored", [ ->
|
Application.Filters.filter "projectsCollabored", [ ->
|
||||||
(projects, memberId) ->
|
(projects, memberId) ->
|
||||||
if !angular.isUndefined(projects) and angular.isDefined(memberId) and projects? and memberId? and memberId != ""
|
if !angular.isUndefined(projects) and angular.isDefined(memberId) and projects? and memberId? and memberId != ""
|
||||||
filteredProject = []
|
filteredProject = []
|
||||||
@ -81,24 +93,84 @@ Application.Controllers.filter "projectsCollabored", [ ->
|
|||||||
]
|
]
|
||||||
|
|
||||||
# depend on humanize.js lib in /vendor
|
# depend on humanize.js lib in /vendor
|
||||||
Application.Controllers.filter "humanize", [ ->
|
Application.Filters.filter "humanize", [ ->
|
||||||
(element, param) ->
|
(element, param) ->
|
||||||
Humanize.truncate(element, param, null)
|
Humanize.truncate(element, param, null)
|
||||||
]
|
]
|
||||||
|
|
||||||
Application.Controllers.filter "breakFilter", [ ->
|
Application.Filters.filter "breakFilter", [ ->
|
||||||
(text) ->
|
(text) ->
|
||||||
if text != undefined
|
if text != undefined
|
||||||
text.replace(/\n/g, '<br />')
|
text.replace(/\n/g, '<br />')
|
||||||
]
|
]
|
||||||
|
|
||||||
Application.Controllers.filter "toTrusted", [ "$sce", ($sce) ->
|
Application.Filters.filter "toTrusted", [ "$sce", ($sce) ->
|
||||||
(text) ->
|
(text) ->
|
||||||
$sce.trustAsHtml text
|
$sce.trustAsHtml text
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
Application.Controllers.filter "eventsFilter", [ ->
|
Application.Filters.filter "planIntervalFilter", [ ->
|
||||||
|
(interval, intervalCount) ->
|
||||||
|
if typeof intervalCount != 'number'
|
||||||
|
switch interval
|
||||||
|
when 'day' then return 'jour'
|
||||||
|
when 'week' then return 'semaine'
|
||||||
|
when 'month' then return 'mois'
|
||||||
|
when 'year' then return 'année'
|
||||||
|
else
|
||||||
|
if intervalCount == 1
|
||||||
|
switch interval
|
||||||
|
when 'day' then return 'un jour'
|
||||||
|
when 'week' then return 'une semaine'
|
||||||
|
when 'month' then return 'un mois'
|
||||||
|
when 'year' then return 'un an'
|
||||||
|
else
|
||||||
|
switch interval
|
||||||
|
when 'day' then return intervalCount+ ' jours'
|
||||||
|
when 'week' then return intervalCount+ ' semaines'
|
||||||
|
when 'month' then return intervalCount+ ' mois'
|
||||||
|
when 'year' then return intervalCount+ ' ans'
|
||||||
|
]
|
||||||
|
|
||||||
|
Application.Filters.filter "humanReadablePlanName", ['$filter', ($filter)->
|
||||||
|
(plan, groups, short) ->
|
||||||
|
if plan?
|
||||||
|
result = plan.base_name
|
||||||
|
if groups?
|
||||||
|
for group in groups
|
||||||
|
if group.id == plan.group_id
|
||||||
|
if short?
|
||||||
|
result += " - #{group.slug}"
|
||||||
|
else
|
||||||
|
result += " - #{group.name}"
|
||||||
|
result += " - #{$filter('planIntervalFilter')(plan.interval, plan.interval_count)}"
|
||||||
|
result
|
||||||
|
]
|
||||||
|
|
||||||
|
Application.Filters.filter "trainingReservationsFilter", [ ->
|
||||||
|
(elements, selectedScope) ->
|
||||||
|
if !angular.isUndefined(elements) and !angular.isUndefined(selectedScope) and elements? and selectedScope?
|
||||||
|
filteredElements = []
|
||||||
|
angular.forEach elements, (element)->
|
||||||
|
switch selectedScope
|
||||||
|
when "future"
|
||||||
|
if new Date(element.start_at) > new Date
|
||||||
|
filteredElements.push(element)
|
||||||
|
when "passed"
|
||||||
|
if new Date(element.start_at) <= new Date and !element.is_valid
|
||||||
|
filteredElements.push(element)
|
||||||
|
when "valided"
|
||||||
|
if new Date(element.start_at) <= new Date and element.is_valid
|
||||||
|
filteredElements.push(element)
|
||||||
|
else
|
||||||
|
return []
|
||||||
|
filteredElements
|
||||||
|
else
|
||||||
|
elements
|
||||||
|
]
|
||||||
|
|
||||||
|
Application.Filters.filter "eventsReservationsFilter", [ ->
|
||||||
(elements, selectedScope) ->
|
(elements, selectedScope) ->
|
||||||
if !angular.isUndefined(elements) and !angular.isUndefined(selectedScope) and elements? and selectedScope? and selectedScope != ""
|
if !angular.isUndefined(elements) and !angular.isUndefined(selectedScope) and elements? and selectedScope? and selectedScope != ""
|
||||||
filteredElements = []
|
filteredElements = []
|
||||||
@ -117,3 +189,43 @@ Application.Controllers.filter "eventsFilter", [ ->
|
|||||||
else
|
else
|
||||||
elements
|
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})
|
||||||
|
|
||||||
|
]
|
@ -1,222 +1,844 @@
|
|||||||
angular.module('application.router', ['ui.router']).
|
angular.module('application.router', ['ui.router']).
|
||||||
config ['$stateProvider', '$urlRouterProvider', '$locationProvider', ($stateProvider, $urlRouterProvider, $locationProvider) ->
|
config ['$stateProvider', '$urlRouterProvider', '$locationProvider', ($stateProvider, $urlRouterProvider, $locationProvider) ->
|
||||||
$locationProvider.hashPrefix('!')
|
$locationProvider.hashPrefix('!')
|
||||||
$urlRouterProvider.otherwise("/")
|
$urlRouterProvider.otherwise("/")
|
||||||
|
|
||||||
# abstract root parents states
|
# abstract root parents states
|
||||||
# these states controls the access rights to the various routes inherited from them
|
# these states controls the access rights to the various routes inherited from them
|
||||||
$stateProvider
|
$stateProvider
|
||||||
.state 'app',
|
.state 'app',
|
||||||
abstract: true
|
abstract: true
|
||||||
views:
|
views:
|
||||||
'header': { templateUrl: '<%= asset_path "shared/header.html" %>' }
|
'header':
|
||||||
'leftnav':
|
templateUrl: '<%= asset_path "shared/header.html" %>'
|
||||||
templateUrl: '<%= asset_path "shared/leftnav.html" %>'
|
'leftnav':
|
||||||
controller: 'mainNavController'
|
templateUrl: '<%= asset_path "shared/leftnav.html" %>'
|
||||||
'main':
|
controller: 'MainNavController'
|
||||||
templateUrl: '<%= asset_path "home.html" %>'
|
'main': {}
|
||||||
controller: 'homeController'
|
resolve:
|
||||||
.state 'app.public',
|
logoFile: ['CustomAsset', (CustomAsset) ->
|
||||||
abstract: true
|
CustomAsset.get({name: 'logo-file'}).$promise
|
||||||
.state 'app.logged',
|
]
|
||||||
abstract: true
|
logoBlackFile: ['CustomAsset', (CustomAsset) ->
|
||||||
data:
|
CustomAsset.get({name: 'logo-black-file'}).$promise
|
||||||
authorizedRoles: ['member', 'admin']
|
]
|
||||||
resolve:
|
commonTranslations: [ 'Translations', (Translations) ->
|
||||||
currentUser: ['Auth', (Auth)->
|
Translations.query(['app.public.common', 'app.shared.buttons', 'app.shared.elements']).$promise
|
||||||
Auth.currentUser()
|
]
|
||||||
]
|
onEnter: ['$rootScope', 'logoFile', 'logoBlackFile', ($rootScope, logoFile, logoBlackFile) ->
|
||||||
onEnter: ["currentUser", "$rootScope", (currentUser, $rootScope)->
|
## Application logo
|
||||||
$rootScope.currentUser = currentUser
|
$rootScope.logo = logoFile.custom_asset
|
||||||
]
|
$rootScope.logoBlack = logoBlackFile.custom_asset
|
||||||
.state 'app.admin',
|
]
|
||||||
abstract: true
|
.state 'app.public',
|
||||||
data:
|
abstract: true
|
||||||
authorizedRoles: ['admin']
|
.state 'app.logged',
|
||||||
resolve:
|
abstract: true
|
||||||
currentUser: ['Auth', (Auth)->
|
data:
|
||||||
Auth.currentUser()
|
authorizedRoles: ['member', 'admin']
|
||||||
]
|
resolve:
|
||||||
onEnter: ["currentUser", "$rootScope", (currentUser, $rootScope)->
|
currentUser: ['Auth', (Auth)->
|
||||||
$rootScope.currentUser = currentUser
|
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
|
# main pages
|
||||||
.state 'app.public.about',
|
.state 'app.public.about',
|
||||||
url: '/about'
|
url: '/about'
|
||||||
views:
|
views:
|
||||||
'content@': { templateUrl: '<%= asset_path "shared/about.html" %>' }
|
'content@':
|
||||||
.state 'app.public.home',
|
templateUrl: '<%= asset_path "shared/about.html" %>'
|
||||||
url: '/?reset_password_token'
|
controller: 'AboutController'
|
||||||
views:
|
resolve:
|
||||||
'main':
|
translations: [ 'Translations', (Translations) ->
|
||||||
templateUrl: '<%= asset_path "home.html" %>'
|
Translations.query('app.public.about').$promise
|
||||||
controller: 'homeController'
|
]
|
||||||
|
.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)
|
||||||
# dashboard
|
.state 'app.logged.profileCompletion',
|
||||||
.state 'app.logged.dashboard_profile',
|
url: '/profile_completion'
|
||||||
url: '/dashboard/profile'
|
views:
|
||||||
views:
|
'main@':
|
||||||
'main@':
|
templateUrl: '<%= asset_path "profile/complete.html"%>'
|
||||||
templateUrl: '<%= asset_path "dashboard/profile.html" %>'
|
controller: 'CompleteProfileController'
|
||||||
controller: 'editProfileController'
|
resolve:
|
||||||
.state 'app.logged.dashboard_projects',
|
settingsPromise: ['Setting', (Setting)->
|
||||||
url: '/dashboard/projects'
|
Setting.query(names: "['fablab_name', 'name_genre']").$promise
|
||||||
views:
|
]
|
||||||
'main@':
|
activeProviderPromise: ['AuthProvider', (AuthProvider) ->
|
||||||
templateUrl: '<%= asset_path "dashboard/projects.html" %>'
|
AuthProvider.active().$promise
|
||||||
controller: 'dashboardProjectsController'
|
]
|
||||||
|
groupsPromise: ['Group', (Group)->
|
||||||
|
Group.query().$promise
|
||||||
# members
|
]
|
||||||
.state 'app.logged.members_show',
|
cguFile: ['CustomAsset', (CustomAsset) ->
|
||||||
url: '/members/:id'
|
CustomAsset.get({name: 'cgu-file'}).$promise
|
||||||
views:
|
]
|
||||||
'main@':
|
memberPromise: ['Member', 'currentUser', (Member, currentUser)->
|
||||||
templateUrl: '<%= asset_path "members/show.html" %>'
|
Member.get(id: currentUser.id).$promise
|
||||||
controller: 'showProfileController'
|
]
|
||||||
.state 'app.logged.members',
|
translations: [ 'Translations', (Translations) ->
|
||||||
url: '/members'
|
Translations.query(['app.logged.profileCompletion', 'app.shared.user']).$promise
|
||||||
views:
|
]
|
||||||
'main@':
|
|
||||||
templateUrl: '<%= asset_path "members/index.html" %>'
|
|
||||||
controller: 'membersController'
|
|
||||||
|
|
||||||
|
|
||||||
# projects
|
|
||||||
.state 'app.public.projects_list',
|
|
||||||
url: '/projects'
|
|
||||||
views:
|
|
||||||
'main@':
|
|
||||||
templateUrl: '<%= asset_path "projects/index.html" %>'
|
|
||||||
controller: 'projectsController'
|
|
||||||
.state 'app.public.projects_show',
|
|
||||||
url: '/projects/:id'
|
|
||||||
views:
|
|
||||||
'main@':
|
|
||||||
templateUrl: '<%= asset_path "projects/show.html" %>'
|
|
||||||
controller: 'showProjectController'
|
|
||||||
.state 'app.logged.projects_new',
|
|
||||||
url: '/projects/new'
|
|
||||||
views:
|
|
||||||
'main@':
|
|
||||||
templateUrl: '<%= asset_path "projects/new.html" %>'
|
|
||||||
controller: 'newProjectController'
|
|
||||||
.state 'app.logged.projects_edit',
|
|
||||||
url: '/projects/:id/edit'
|
|
||||||
views:
|
|
||||||
'main@':
|
|
||||||
templateUrl: '<%= asset_path "projects/edit.html" %>'
|
|
||||||
controller: 'editProjectController'
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# machines
|
# dashboard
|
||||||
.state 'app.public.machines_list',
|
.state 'app.logged.dashboard',
|
||||||
url: '/machines'
|
abstract: true
|
||||||
views:
|
url: '/dashboard'
|
||||||
'main@':
|
resolve:
|
||||||
templateUrl: '<%= asset_path "machines/index.html" %>'
|
memberPromise: ['Member', 'currentUser', (Member, currentUser)->
|
||||||
controller: 'machinesController'
|
Member.get(id: currentUser.id).$promise
|
||||||
.state 'app.public.machines_show',
|
]
|
||||||
url: '/machines/:id'
|
.state 'app.logged.dashboard.profile',
|
||||||
views:
|
url: '/profile'
|
||||||
'main@':
|
views:
|
||||||
templateUrl: '<%= asset_path "machines/show.html" %>'
|
'main@':
|
||||||
controller: 'showMachineController'
|
templateUrl: '<%= asset_path "dashboard/profile.html" %>'
|
||||||
.state 'app.admin.machines_new',
|
controller: 'EditProfileController'
|
||||||
url: '/machines/new'
|
resolve:
|
||||||
views:
|
groups: ['Group', (Group)->
|
||||||
'main@':
|
Group.query().$promise
|
||||||
templateUrl: '<%= asset_path "machines/new.html" %>'
|
]
|
||||||
controller: 'newMachineController'
|
activeProviderPromise: ['AuthProvider', (AuthProvider) ->
|
||||||
.state 'app.admin.machines_edit',
|
AuthProvider.active().$promise
|
||||||
url: '/machines/:id/edit'
|
]
|
||||||
views:
|
translations: [ 'Translations', (Translations) ->
|
||||||
'main@':
|
Translations.query(['app.logged.dashboard.profile', 'app.shared.user']).$promise
|
||||||
templateUrl: '<%= asset_path "machines/edit.html" %>'
|
]
|
||||||
controller: 'editMachineController'
|
.state 'app.logged.dashboard.projects',
|
||||||
|
url: '/projects'
|
||||||
|
views:
|
||||||
|
'main@':
|
||||||
|
templateUrl: '<%= asset_path "dashboard/projects.html" %>'
|
||||||
|
controller: 'DashboardController'
|
||||||
|
resolve:
|
||||||
|
translations: [ 'Translations', (Translations) ->
|
||||||
|
Translations.query('app.logged.dashboard.projects').$promise
|
||||||
|
]
|
||||||
|
.state 'app.logged.dashboard.trainings',
|
||||||
|
url: '/trainings'
|
||||||
|
views:
|
||||||
|
'main@':
|
||||||
|
templateUrl: '<%= asset_path "dashboard/trainings.html" %>'
|
||||||
|
controller: 'DashboardController'
|
||||||
|
resolve:
|
||||||
|
translations: [ 'Translations', (Translations) ->
|
||||||
|
Translations.query('app.logged.dashboard.trainings').$promise
|
||||||
|
]
|
||||||
|
.state 'app.logged.dashboard.events',
|
||||||
|
url: '/events'
|
||||||
|
views:
|
||||||
|
'main@':
|
||||||
|
templateUrl: '<%= asset_path "dashboard/events.html" %>'
|
||||||
|
controller: 'DashboardController'
|
||||||
|
resolve:
|
||||||
|
translations: [ 'Translations', (Translations) ->
|
||||||
|
Translations.query('app.logged.dashboard.events').$promise
|
||||||
|
]
|
||||||
|
.state 'app.logged.dashboard.invoices',
|
||||||
|
url: '/invoices'
|
||||||
|
views:
|
||||||
|
'main@':
|
||||||
|
templateUrl: '<%= asset_path "dashboard/invoices.html" %>'
|
||||||
|
controller: 'DashboardController'
|
||||||
|
resolve:
|
||||||
|
translations: [ 'Translations', (Translations) ->
|
||||||
|
Translations.query('app.logged.dashboard.invoices').$promise
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
# notifications
|
# members
|
||||||
.state 'app.logged.notifications',
|
.state 'app.logged.members_show',
|
||||||
url: '/notifications'
|
url: '/members/:id'
|
||||||
views:
|
views:
|
||||||
'main@':
|
'main@':
|
||||||
templateUrl: '<%= asset_path "notifications/index.html" %>'
|
templateUrl: '<%= asset_path "members/show.html" %>'
|
||||||
controller: 'notificationsController'
|
controller: 'ShowProfileController'
|
||||||
|
resolve:
|
||||||
|
memberPromise: ['$stateParams', 'Member', ($stateParams, Member)->
|
||||||
|
Member.get(id: $stateParams.id).$promise
|
||||||
|
]
|
||||||
|
translations: [ 'Translations', (Translations) ->
|
||||||
|
Translations.query('app.logged.members_show').$promise
|
||||||
|
]
|
||||||
|
.state 'app.logged.members',
|
||||||
|
url: '/members'
|
||||||
|
views:
|
||||||
|
'main@':
|
||||||
|
templateUrl: '<%= asset_path "members/index.html" %>'
|
||||||
|
controller: 'MembersController'
|
||||||
|
resolve:
|
||||||
|
membersPromise: ['Member', (Member)->
|
||||||
|
Member.query({requested_attributes:'[profile]'}).$promise
|
||||||
|
]
|
||||||
|
translations: [ 'Translations', (Translations) ->
|
||||||
|
Translations.query('app.logged.members').$promise
|
||||||
|
]
|
||||||
|
|
||||||
|
# projects
|
||||||
|
.state 'app.public.projects_list',
|
||||||
|
url: '/projects'
|
||||||
|
views:
|
||||||
|
'main@':
|
||||||
|
templateUrl: '<%= asset_path "projects/index.html" %>'
|
||||||
|
controller: 'ProjectsController'
|
||||||
|
resolve:
|
||||||
|
themesPromise: ['Theme', (Theme)->
|
||||||
|
Theme.query().$promise
|
||||||
|
]
|
||||||
|
componentsPromise: ['Component', (Component)->
|
||||||
|
Component.query().$promise
|
||||||
|
]
|
||||||
|
machinesPromise: ['Machine', (Machine)->
|
||||||
|
Machine.query().$promise
|
||||||
|
]
|
||||||
|
translations: [ 'Translations', (Translations) ->
|
||||||
|
Translations.query('app.public.projects_list').$promise
|
||||||
|
]
|
||||||
|
.state 'app.logged.projects_new',
|
||||||
|
url: '/projects/new'
|
||||||
|
views:
|
||||||
|
'main@':
|
||||||
|
templateUrl: '<%= asset_path "projects/new.html" %>'
|
||||||
|
controller: 'NewProjectController'
|
||||||
|
resolve:
|
||||||
|
translations: [ 'Translations', (Translations) ->
|
||||||
|
Translations.query(['app.logged.projects_new', 'app.shared.project']).$promise
|
||||||
|
]
|
||||||
|
.state 'app.public.projects_show',
|
||||||
|
url: '/projects/:id'
|
||||||
|
views:
|
||||||
|
'main@':
|
||||||
|
templateUrl: '<%= asset_path "projects/show.html" %>'
|
||||||
|
controller: 'ShowProjectController'
|
||||||
|
resolve:
|
||||||
|
projectPromise: ['$stateParams', 'Project', ($stateParams, Project)->
|
||||||
|
Project.get(id: $stateParams.id).$promise
|
||||||
|
]
|
||||||
|
translations: [ 'Translations', (Translations) ->
|
||||||
|
Translations.query('app.public.projects_show').$promise
|
||||||
|
]
|
||||||
|
.state 'app.logged.projects_edit',
|
||||||
|
url: '/projects/:id/edit'
|
||||||
|
views:
|
||||||
|
'main@':
|
||||||
|
templateUrl: '<%= asset_path "projects/edit.html" %>'
|
||||||
|
controller: 'EditProjectController'
|
||||||
|
resolve:
|
||||||
|
projectPromise: ['$stateParams', 'Project', ($stateParams, Project)->
|
||||||
|
Project.get(id: $stateParams.id).$promise
|
||||||
|
]
|
||||||
|
translations: [ 'Translations', (Translations) ->
|
||||||
|
Translations.query(['app.logged.projects_edit', 'app.shared.project']).$promise
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
# events
|
# machines
|
||||||
.state 'app.public.events_list',
|
.state 'app.public.machines_list',
|
||||||
url: '/events'
|
url: '/machines'
|
||||||
views:
|
views:
|
||||||
'main@':
|
'main@':
|
||||||
templateUrl: '<%= asset_path "events/index.html" %>'
|
templateUrl: '<%= asset_path "machines/index.html" %>'
|
||||||
controller: 'eventsController'
|
controller: 'MachinesController'
|
||||||
.state 'app.public.events_show',
|
resolve:
|
||||||
url: '/events/:id'
|
machinesPromise: ['Machine', (Machine)->
|
||||||
views:
|
Machine.query().$promise
|
||||||
'main@':
|
]
|
||||||
templateUrl: '<%= asset_path "events/show.html" %>'
|
translations: [ 'Translations', (Translations) ->
|
||||||
controller: 'showEventController'
|
Translations.query(['app.public.machines_list', 'app.shared.training_reservation_modal', 'app.shared.request_training_modal']).$promise
|
||||||
|
]
|
||||||
|
.state 'app.admin.machines_new',
|
||||||
|
url: '/machines/new'
|
||||||
|
views:
|
||||||
|
'main@':
|
||||||
|
templateUrl: '<%= asset_path "machines/new.html" %>'
|
||||||
|
controller: 'NewMachineController'
|
||||||
|
resolve:
|
||||||
|
translations: [ 'Translations', (Translations) ->
|
||||||
|
Translations.query(['app.admin.machines_new', 'app.shared.machine']).$promise
|
||||||
|
]
|
||||||
|
.state 'app.public.machines_show',
|
||||||
|
url: '/machines/:id'
|
||||||
|
views:
|
||||||
|
'main@':
|
||||||
|
templateUrl: '<%= asset_path "machines/show.html" %>'
|
||||||
|
controller: 'ShowMachineController'
|
||||||
|
resolve:
|
||||||
|
machinePromise: ['Machine', '$stateParams', (Machine, $stateParams)->
|
||||||
|
Machine.get(id: $stateParams.id).$promise
|
||||||
|
]
|
||||||
|
translations: [ 'Translations', (Translations) ->
|
||||||
|
Translations.query(['app.public.machines_show', 'app.shared.training_reservation_modal', 'app.shared.request_training_modal']).$promise
|
||||||
|
]
|
||||||
|
.state 'app.logged.machines_reserve',
|
||||||
|
url: '/machines/:id/reserve'
|
||||||
|
views:
|
||||||
|
'main@':
|
||||||
|
templateUrl: '<%= asset_path "machines/reserve.html" %>'
|
||||||
|
controller: 'ReserveMachineController'
|
||||||
|
resolve:
|
||||||
|
plansPromise: ['Plan', (Plan)->
|
||||||
|
Plan.query(attributes_requested: "['machines_credits']").$promise
|
||||||
|
]
|
||||||
|
groupsPromise: ['Group', (Group)->
|
||||||
|
Group.query().$promise
|
||||||
|
]
|
||||||
|
settingsPromise: ['Setting', (Setting)->
|
||||||
|
Setting.query(names: "['machine_explications_alert',
|
||||||
|
'booking_window_start',
|
||||||
|
'booking_window_end',
|
||||||
|
'booking_move_enable',
|
||||||
|
'booking_move_delay',
|
||||||
|
'booking_cancel_enable',
|
||||||
|
'booking_cancel_delay',
|
||||||
|
'subscription_explications_alert']").$promise
|
||||||
|
]
|
||||||
|
translations: [ 'Translations', (Translations) ->
|
||||||
|
Translations.query(['app.logged.machines_reserve', 'app.shared.plan_subscribe', 'app.shared.member_select',
|
||||||
|
'app.shared.stripe', 'app.shared.valid_reservation_modal', 'app.shared.confirm_modify_slot_modal']).$promise
|
||||||
|
]
|
||||||
|
.state 'app.admin.machines_edit',
|
||||||
|
url: '/machines/:id/edit'
|
||||||
|
views:
|
||||||
|
'main@':
|
||||||
|
templateUrl: '<%= asset_path "machines/edit.html" %>'
|
||||||
|
controller: 'EditMachineController'
|
||||||
|
resolve:
|
||||||
|
machinePromise: ['Machine', '$stateParams', (Machine, $stateParams)->
|
||||||
|
Machine.get(id: $stateParams.id).$promise
|
||||||
|
]
|
||||||
|
translations: [ 'Translations', (Translations) ->
|
||||||
|
Translations.query(['app.admin.machines_edit', 'app.shared.machine']).$promise
|
||||||
|
]
|
||||||
|
|
||||||
|
# trainings
|
||||||
|
.state 'app.logged.trainings_reserve',
|
||||||
|
url: '/trainings/reserve'
|
||||||
|
views:
|
||||||
|
'main@':
|
||||||
|
templateUrl: '<%= asset_path "trainings/reserve.html" %>'
|
||||||
|
controller: 'ReserveTrainingController'
|
||||||
|
resolve:
|
||||||
|
explicationAlertPromise: ['Setting', (Setting)->
|
||||||
|
Setting.get(name: 'training_explications_alert').$promise
|
||||||
|
]
|
||||||
|
plansPromise: ['Plan', (Plan)->
|
||||||
|
Plan.query(attributes_requested: "['trainings_credits']").$promise
|
||||||
|
]
|
||||||
|
groupsPromise: ['Group', (Group)->
|
||||||
|
Group.query().$promise
|
||||||
|
]
|
||||||
|
availabilityTrainingsPromise: ['Availability', (Availability)->
|
||||||
|
Availability.trainings().$promise
|
||||||
|
]
|
||||||
|
settingsPromise: ['Setting', (Setting)->
|
||||||
|
Setting.query(names: "['booking_window_start',
|
||||||
|
'booking_window_end',
|
||||||
|
'booking_move_enable',
|
||||||
|
'booking_move_delay',
|
||||||
|
'booking_cancel_enable',
|
||||||
|
'booking_cancel_delay',
|
||||||
|
'subscription_explications_alert',
|
||||||
|
'training_explications_alert',
|
||||||
|
'training_information_message']").$promise
|
||||||
|
]
|
||||||
|
translations: [ 'Translations', (Translations) ->
|
||||||
|
Translations.query(['app.logged.trainings_reserve', 'app.shared.plan_subscribe', 'app.shared.member_select',
|
||||||
|
'app.shared.stripe', 'app.shared.valid_reservation_modal', 'app.shared.confirm_modify_slot_modal']).$promise
|
||||||
|
]
|
||||||
|
# notifications
|
||||||
|
.state 'app.logged.notifications',
|
||||||
|
url: '/notifications'
|
||||||
|
views:
|
||||||
|
'main@':
|
||||||
|
templateUrl: '<%= asset_path "notifications/index.html" %>'
|
||||||
|
controller: 'NotificationsController'
|
||||||
|
resolve:
|
||||||
|
translations: [ 'Translations', (Translations) ->
|
||||||
|
Translations.query('app.logged.notifications').$promise
|
||||||
|
]
|
||||||
|
|
||||||
|
# pricing
|
||||||
|
.state 'app.public.plans',
|
||||||
|
url: '/plans'
|
||||||
|
abstract: Fablab.withoutPlans
|
||||||
|
views:
|
||||||
|
'main@':
|
||||||
|
templateUrl: '<%= asset_path "plans/index.html" %>'
|
||||||
|
controller: 'PlansIndexController'
|
||||||
|
resolve:
|
||||||
|
subscriptionExplicationsPromise: ['Setting', (Setting)->
|
||||||
|
Setting.get(name: 'subscription_explications_alert').$promise
|
||||||
|
]
|
||||||
|
plansPromise: ['Plan', (Plan)->
|
||||||
|
Plan.query(shallow: true).$promise
|
||||||
|
]
|
||||||
|
groupsPromise: ['Group', (Group)->
|
||||||
|
Group.query().$promise
|
||||||
|
]
|
||||||
|
translations: [ 'Translations', (Translations) ->
|
||||||
|
Translations.query(['app.public.plans', 'app.shared.member_select', 'app.shared.stripe']).$promise
|
||||||
|
]
|
||||||
|
|
||||||
|
# events
|
||||||
|
.state 'app.public.events_list',
|
||||||
|
url: '/events'
|
||||||
|
views:
|
||||||
|
'main@':
|
||||||
|
templateUrl: '<%= asset_path "events/index.html" %>'
|
||||||
|
controller: 'EventsController'
|
||||||
|
resolve:
|
||||||
|
translations: [ 'Translations', (Translations) ->
|
||||||
|
Translations.query('app.public.events_list').$promise
|
||||||
|
]
|
||||||
|
.state 'app.public.events_show',
|
||||||
|
url: '/events/:id'
|
||||||
|
views:
|
||||||
|
'main@':
|
||||||
|
templateUrl: '<%= asset_path "events/show.html" %>'
|
||||||
|
controller: 'ShowEventController'
|
||||||
|
resolve:
|
||||||
|
eventPromise: ['Event', '$stateParams', (Event, $stateParams)->
|
||||||
|
Event.get(id: $stateParams.id).$promise
|
||||||
|
]
|
||||||
|
reducedAmountAlert: ['Setting', (Setting)->
|
||||||
|
Setting.get(name: 'event_reduced_amount_alert').$promise
|
||||||
|
]
|
||||||
|
translations: [ 'Translations', (Translations) ->
|
||||||
|
Translations.query(['app.public.events_show', 'app.shared.member_select', 'app.shared.stripe', 'app.shared.valid_reservation_modal']).$promise
|
||||||
|
]
|
||||||
|
|
||||||
|
# --- namespace /admin/... ---
|
||||||
|
# calendar
|
||||||
|
.state 'app.admin.calendar',
|
||||||
|
url: '/admin/calendar'
|
||||||
|
views:
|
||||||
|
'main@':
|
||||||
|
templateUrl: '<%= asset_path "admin/calendar/calendar.html" %>'
|
||||||
|
controller: 'AdminCalendarController'
|
||||||
|
resolve:
|
||||||
|
availabilitiesPromise: ['Availability', (Availability)->
|
||||||
|
Availability.query().$promise
|
||||||
|
]
|
||||||
|
bookingWindowStart: ['Setting', (Setting)->
|
||||||
|
Setting.get(name: 'booking_window_start').$promise
|
||||||
|
]
|
||||||
|
bookingWindowEnd: ['Setting', (Setting)->
|
||||||
|
Setting.get(name: 'booking_window_end').$promise
|
||||||
|
]
|
||||||
|
machinesPromise: ['Machine', (Machine) ->
|
||||||
|
Machine.query().$promise
|
||||||
|
]
|
||||||
|
translations: [ 'Translations', (Translations) ->
|
||||||
|
Translations.query('app.admin.calendar').$promise
|
||||||
|
]
|
||||||
|
|
||||||
|
# project's elements
|
||||||
|
.state 'app.admin.project_elements',
|
||||||
|
url: '/admin/project_elements'
|
||||||
|
views:
|
||||||
|
'main@':
|
||||||
|
templateUrl: '<%= asset_path "admin/project_elements/index.html" %>'
|
||||||
|
controller: 'ProjectElementsController'
|
||||||
|
resolve:
|
||||||
|
componentsPromise: ['Component', (Component)->
|
||||||
|
Component.query().$promise
|
||||||
|
]
|
||||||
|
licencesPromise: ['Licence', (Licence)->
|
||||||
|
Licence.query().$promise
|
||||||
|
]
|
||||||
|
themesPromise: ['Theme', (Theme)->
|
||||||
|
Theme.query().$promise
|
||||||
|
]
|
||||||
|
translations: [ 'Translations', (Translations) ->
|
||||||
|
Translations.query('app.admin.project_elements').$promise
|
||||||
|
]
|
||||||
|
|
||||||
|
# trainings
|
||||||
|
.state 'app.admin.trainings',
|
||||||
|
url: '/admin/trainings'
|
||||||
|
views:
|
||||||
|
'main@':
|
||||||
|
templateUrl: '<%= asset_path "admin/trainings/index.html" %>'
|
||||||
|
controller: 'TrainingsController'
|
||||||
|
resolve:
|
||||||
|
trainingsPromise: ['Training', (Training)->
|
||||||
|
Training.query().$promise
|
||||||
|
]
|
||||||
|
machinesPromise: ['Machine', (Machine)->
|
||||||
|
Machine.query().$promise
|
||||||
|
]
|
||||||
|
translations: [ 'Translations', (Translations) ->
|
||||||
|
Translations.query('app.admin.trainings').$promise
|
||||||
|
]
|
||||||
|
|
||||||
|
# events
|
||||||
|
.state 'app.admin.events',
|
||||||
|
url: '/admin/events'
|
||||||
|
views:
|
||||||
|
'main@':
|
||||||
|
templateUrl: '<%= asset_path "admin/events/index.html" %>'
|
||||||
|
controller: 'AdminEventsController'
|
||||||
|
resolve:
|
||||||
|
eventsPromise: ['Event', (Event)->
|
||||||
|
Event.query(page: 1).$promise
|
||||||
|
]
|
||||||
|
translations: [ 'Translations', (Translations) ->
|
||||||
|
Translations.query('app.admin.events').$promise
|
||||||
|
]
|
||||||
|
.state 'app.admin.events_new',
|
||||||
|
url: '/admin/events/new'
|
||||||
|
views:
|
||||||
|
'main@':
|
||||||
|
templateUrl: '<%= asset_path "events/new.html" %>'
|
||||||
|
controller: 'NewEventController'
|
||||||
|
resolve:
|
||||||
|
translations: [ 'Translations', (Translations) ->
|
||||||
|
Translations.query(['app.admin.events_new', 'app.shared.event']).$promise
|
||||||
|
]
|
||||||
|
.state 'app.admin.events_edit',
|
||||||
|
url: '/admin/events/:id/edit'
|
||||||
|
views:
|
||||||
|
'main@':
|
||||||
|
templateUrl: '<%= asset_path "events/edit.html" %>'
|
||||||
|
controller: 'EditEventController'
|
||||||
|
resolve:
|
||||||
|
eventPromise: ['Event', '$stateParams', (Event, $stateParams)->
|
||||||
|
Event.get(id: $stateParams.id).$promise
|
||||||
|
]
|
||||||
|
translations: [ 'Translations', (Translations) ->
|
||||||
|
Translations.query(['app.admin.events_edit', 'app.shared.event']).$promise
|
||||||
|
]
|
||||||
|
.state 'app.admin.event_reservations',
|
||||||
|
url: '/admin/events/:id/reservations'
|
||||||
|
views:
|
||||||
|
'main@':
|
||||||
|
templateUrl: '<%= asset_path "admin/events/reservations.html" %>'
|
||||||
|
controller: 'ShowEventReservationsController'
|
||||||
|
resolve:
|
||||||
|
eventPromise: ['Event', '$stateParams', (Event, $stateParams)->
|
||||||
|
Event.get(id: $stateParams.id).$promise
|
||||||
|
]
|
||||||
|
reservationsPromise: ['Reservation', '$stateParams', (Reservation, $stateParams)->
|
||||||
|
Reservation.query(reservable_id: $stateParams.id, reservable_type: 'Event').$promise
|
||||||
|
]
|
||||||
|
translations: [ 'Translations', (Translations) ->
|
||||||
|
Translations.query('app.admin.event_reservations').$promise
|
||||||
|
]
|
||||||
|
|
||||||
|
# pricing
|
||||||
|
.state 'app.admin.pricing',
|
||||||
|
url: '/admin/pricing'
|
||||||
|
views:
|
||||||
|
'main@':
|
||||||
|
templateUrl: '<%= asset_path "admin/pricing/index.html" %>'
|
||||||
|
controller: 'EditPricingController'
|
||||||
|
resolve:
|
||||||
|
plans: ['Plan', (Plan) ->
|
||||||
|
Plan.query().$promise
|
||||||
|
]
|
||||||
|
groups: ['Group', (Group) ->
|
||||||
|
Group.query().$promise
|
||||||
|
]
|
||||||
|
machinesPricesPromise: ['Price', (Price)->
|
||||||
|
Price.query(priceable_type: 'Machine', plan_id: 'null').$promise
|
||||||
|
]
|
||||||
|
trainingsPricingsPromise: ['TrainingsPricing', (TrainingsPricing)->
|
||||||
|
TrainingsPricing.query().$promise
|
||||||
|
]
|
||||||
|
translations: [ 'Translations', (Translations) ->
|
||||||
|
Translations.query('app.admin.pricing').$promise
|
||||||
|
]
|
||||||
|
|
||||||
|
# plans
|
||||||
|
.state 'app.admin.plans',
|
||||||
|
abstract: true
|
||||||
|
resolve:
|
||||||
|
prices: ['Pricing', (Pricing) ->
|
||||||
|
Pricing.query().$promise
|
||||||
|
]
|
||||||
|
machines: ['Machine', (Machine) ->
|
||||||
|
Machine.query().$promise
|
||||||
|
]
|
||||||
|
groups: ['Group', (Group) ->
|
||||||
|
Group.query().$promise
|
||||||
|
]
|
||||||
|
plans: ['Plan', (Plan) ->
|
||||||
|
Plan.query().$promise
|
||||||
|
]
|
||||||
|
partners: ['User', (User) ->
|
||||||
|
User.query({role: 'partner'}).$promise
|
||||||
|
]
|
||||||
|
.state 'app.admin.plans.new',
|
||||||
|
url: '/admin/plans/new'
|
||||||
|
views:
|
||||||
|
'main@':
|
||||||
|
templateUrl: '<%= asset_path "admin/plans/new.html" %>'
|
||||||
|
controller: 'NewPlanController'
|
||||||
|
resolve:
|
||||||
|
translations: [ 'Translations', (Translations) ->
|
||||||
|
Translations.query(['app.admin.plans.new', 'app.shared.plan']).$promise
|
||||||
|
]
|
||||||
|
.state 'app.admin.plans.edit',
|
||||||
|
url: '/admin/plans/:id/edit'
|
||||||
|
views:
|
||||||
|
'main@':
|
||||||
|
templateUrl: '<%= asset_path "admin/plans/edit.html" %>'
|
||||||
|
controller: 'EditPlanController'
|
||||||
|
resolve:
|
||||||
|
planPromise: ['Plan', '$stateParams', (Plan, $stateParams) ->
|
||||||
|
Plan.get({id: $stateParams.id}).$promise
|
||||||
|
]
|
||||||
|
translations: [ 'Translations', (Translations) ->
|
||||||
|
Translations.query(['app.admin.plans.edit', 'app.shared.plan']).$promise
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
# --- namespace /admin/... ---
|
|
||||||
|
|
||||||
|
|
||||||
# project's elements
|
# invoices
|
||||||
.state 'app.admin.project_elements',
|
.state 'app.admin.invoices',
|
||||||
url: '/admin/project_elements'
|
url: '/admin/invoices'
|
||||||
views:
|
views:
|
||||||
'main@':
|
'main@':
|
||||||
templateUrl: '<%= asset_path "admin/project_elements/index.html" %>'
|
templateUrl: '<%= asset_path "admin/invoices/index.html" %>'
|
||||||
controller: 'projectElementsController'
|
controller: 'InvoicesController'
|
||||||
|
resolve:
|
||||||
|
settings: ['Setting', (Setting)->
|
||||||
|
Setting.query(names: "[
|
||||||
|
'invoice_legals',
|
||||||
|
'invoice_text',
|
||||||
|
'invoice_VAT-rate',
|
||||||
|
'invoice_VAT-active',
|
||||||
|
'invoice_order-nb',
|
||||||
|
'invoice_code-value',
|
||||||
|
'invoice_code-active',
|
||||||
|
'invoice_reference',
|
||||||
|
'invoice_logo'
|
||||||
|
]").$promise
|
||||||
|
]
|
||||||
|
translations: [ 'Translations', (Translations) ->
|
||||||
|
Translations.query('app.admin.invoices').$promise
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
# events
|
# members
|
||||||
.state 'app.admin.events',
|
.state 'app.admin.members',
|
||||||
url: '/admin/events'
|
url: '/admin/members'
|
||||||
views:
|
views:
|
||||||
'main@':
|
'main@':
|
||||||
templateUrl: '<%= asset_path "admin/events/index.html" %>'
|
templateUrl: '<%= asset_path "admin/members/index.html" %>'
|
||||||
controller: 'adminEventsController'
|
controller: 'AdminMembersController'
|
||||||
.state 'app.admin.events_new',
|
'groups@app.admin.members':
|
||||||
url: '/admin/events/new'
|
templateUrl: '<%= asset_path "admin/groups/index.html" %>'
|
||||||
views:
|
controller: 'GroupsController'
|
||||||
'main@':
|
'tags@app.admin.members':
|
||||||
templateUrl: '<%= asset_path "events/new.html" %>'
|
templateUrl: '<%= asset_path "admin/tags/index.html" %>'
|
||||||
controller: 'newEventController'
|
controller: 'TagsController'
|
||||||
.state 'app.admin.events_edit',
|
'authentification@app.admin.members':
|
||||||
url: '/admin/events/:id/edit'
|
templateUrl: '<%= asset_path "admin/authentications/index.html" %>'
|
||||||
views:
|
controller: 'AuthentificationController'
|
||||||
'main@':
|
resolve:
|
||||||
templateUrl: '<%= asset_path "events/edit.html" %>'
|
membersPromise: ['Member', (Member)->
|
||||||
controller: 'editEventController'
|
Member.query({requested_attributes:'[profile,group,subscription]'}).$promise
|
||||||
|
]
|
||||||
|
adminsPromise: ['Admin', (Admin)->
|
||||||
|
Admin.query().$promise
|
||||||
|
]
|
||||||
|
groupsPromise: ['Group', (Group)->
|
||||||
|
Group.query().$promise
|
||||||
|
]
|
||||||
|
tagsPromise: ['Tag', (Tag)->
|
||||||
|
Tag.query().$promise
|
||||||
|
]
|
||||||
|
authProvidersPromise: ['AuthProvider', (AuthProvider)->
|
||||||
|
AuthProvider.query().$promise
|
||||||
|
]
|
||||||
|
translations: [ 'Translations', (Translations) ->
|
||||||
|
Translations.query('app.admin.members').$promise
|
||||||
|
]
|
||||||
|
.state 'app.admin.members_new',
|
||||||
|
url: '/admin/members/new'
|
||||||
|
views:
|
||||||
|
'main@':
|
||||||
|
templateUrl: '<%= asset_path "admin/members/new.html" %>'
|
||||||
|
controller: 'NewMemberController'
|
||||||
|
resolve:
|
||||||
|
translations: [ 'Translations', (Translations) ->
|
||||||
|
Translations.query(['app.admin.members_new', 'app.shared.user', 'app.shared.user_admin']).$promise
|
||||||
|
]
|
||||||
|
.state 'app.admin.members_edit',
|
||||||
|
url: '/admin/members/:id/edit'
|
||||||
|
views:
|
||||||
|
'main@':
|
||||||
|
templateUrl: '<%= asset_path "admin/members/edit.html" %>'
|
||||||
|
controller: 'EditMemberController'
|
||||||
|
resolve:
|
||||||
|
memberPromise: ['Member', '$stateParams', (Member, $stateParams)->
|
||||||
|
Member.get(id: $stateParams.id).$promise
|
||||||
|
]
|
||||||
|
tagsPromise: ['Tag', (Tag)->
|
||||||
|
Tag.query().$promise
|
||||||
|
]
|
||||||
|
translations: [ 'Translations', (Translations) ->
|
||||||
|
Translations.query(['app.admin.members_edit', 'app.shared.user', 'app.shared.user_admin']).$promise
|
||||||
|
]
|
||||||
|
.state 'app.admin.admins_new',
|
||||||
|
url: '/admin/admins/new'
|
||||||
|
views:
|
||||||
|
'main@':
|
||||||
|
templateUrl: '<%= asset_path "admin/admins/new.html" %>'
|
||||||
|
controller: 'NewAdminController'
|
||||||
|
resolve:
|
||||||
|
translations: [ 'Translations', (Translations) ->
|
||||||
|
Translations.query('app.admin.admins_new').$promise
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
# members
|
# authentification providers
|
||||||
.state 'app.admin.members',
|
.state 'app.admin.authentication_new',
|
||||||
url: '/admin/members'
|
url: '/admin/authentications/new'
|
||||||
views:
|
views:
|
||||||
'main@':
|
'main@':
|
||||||
templateUrl: '<%= asset_path "admin/members/index.html" %>'
|
templateUrl: '<%= asset_path "admin/authentications/new.html" %>'
|
||||||
controller: 'membersController'
|
controller: 'NewAuthenticationController'
|
||||||
.state 'app.admin.members_new',
|
resolve:
|
||||||
url: '/admin/members/new'
|
mappingFieldsPromise: ['AuthProvider', (AuthProvider)->
|
||||||
views:
|
AuthProvider.mapping_fields().$promise
|
||||||
'main@':
|
]
|
||||||
templateUrl: '<%= asset_path "admin/members/new.html" %>'
|
authProvidersPromise: ['AuthProvider', (AuthProvider)->
|
||||||
controller: 'newMemberController'
|
AuthProvider.query().$promise
|
||||||
.state 'app.admin.members_edit',
|
]
|
||||||
url: '/admin/members/:id/edit'
|
translations: [ 'Translations', (Translations) ->
|
||||||
views:
|
Translations.query(['app.admin.authentication_new', 'app.shared.authentication', 'app.shared.oauth2']).$promise
|
||||||
'main@':
|
]
|
||||||
templateUrl: '<%= asset_path "admin/members/edit.html" %>'
|
.state 'app.admin.authentication_edit',
|
||||||
controller: 'editMemberController'
|
url: '/admin/authentications/:id/edit'
|
||||||
|
views:
|
||||||
|
'main@':
|
||||||
|
templateUrl: '<%= asset_path "admin/authentications/edit.html" %>'
|
||||||
|
controller: 'EditAuthenticationController'
|
||||||
|
resolve:
|
||||||
|
providerPromise: ['AuthProvider', '$stateParams', (AuthProvider, $stateParams)->
|
||||||
|
AuthProvider.get(id: $stateParams.id).$promise
|
||||||
|
]
|
||||||
|
mappingFieldsPromise: ['AuthProvider', (AuthProvider)->
|
||||||
|
AuthProvider.mapping_fields().$promise
|
||||||
|
]
|
||||||
|
translations: [ 'Translations', (Translations) ->
|
||||||
|
Translations.query(['app.admin.authentication_edit', 'app.shared.authentication', 'app.shared.oauth2']).$promise
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# statistics
|
||||||
|
.state 'app.admin.statistics',
|
||||||
|
url: '/admin/statistics'
|
||||||
|
views:
|
||||||
|
'main@':
|
||||||
|
templateUrl: '<%= asset_path "admin/statistics/index.html" %>'
|
||||||
|
controller: 'StatisticsController'
|
||||||
|
resolve:
|
||||||
|
translations: [ 'Translations', (Translations) ->
|
||||||
|
Translations.query('app.admin.statistics').$promise
|
||||||
|
]
|
||||||
|
.state 'app.admin.stats_graphs',
|
||||||
|
url: '/admin/statistics/evolution'
|
||||||
|
views:
|
||||||
|
'main@':
|
||||||
|
templateUrl: '<%= asset_path "admin/statistics/graphs.html" %>'
|
||||||
|
controller: 'GraphsController'
|
||||||
|
resolve:
|
||||||
|
translations: [ 'Translations', (Translations) ->
|
||||||
|
Translations.query('app.admin.stats_graphs').$promise
|
||||||
|
]
|
||||||
|
|
||||||
|
# configurations
|
||||||
|
.state 'app.admin.settings',
|
||||||
|
url: '/admin/settings'
|
||||||
|
views:
|
||||||
|
'main@':
|
||||||
|
templateUrl: '<%= asset_path "admin/settings/index.html" %>'
|
||||||
|
controller: 'SettingsController'
|
||||||
|
resolve:
|
||||||
|
settingsPromise: ['Setting', (Setting)->
|
||||||
|
Setting.query(names: "[
|
||||||
|
'twitter_name',
|
||||||
|
'about_title',
|
||||||
|
'about_body',
|
||||||
|
'about_contacts',
|
||||||
|
'home_blogpost',
|
||||||
|
'machine_explications_alert',
|
||||||
|
'training_explications_alert',
|
||||||
|
'training_information_message',
|
||||||
|
'subscription_explications_alert',
|
||||||
|
'event_reduced_amount_alert',
|
||||||
|
'booking_window_start',
|
||||||
|
'booking_window_end',
|
||||||
|
'booking_move_enable',
|
||||||
|
'booking_move_delay',
|
||||||
|
'booking_cancel_enable',
|
||||||
|
'booking_cancel_delay',
|
||||||
|
'main_color',
|
||||||
|
'secondary_color',
|
||||||
|
'fablab_name',
|
||||||
|
'name_genre'
|
||||||
|
]").$promise
|
||||||
|
]
|
||||||
|
cguFile: ['CustomAsset', (CustomAsset) ->
|
||||||
|
CustomAsset.get({name: 'cgu-file'}).$promise
|
||||||
|
]
|
||||||
|
cgvFile: ['CustomAsset', (CustomAsset) ->
|
||||||
|
CustomAsset.get({name: 'cgv-file'}).$promise
|
||||||
|
]
|
||||||
|
faviconFile: ['CustomAsset', (CustomAsset) ->
|
||||||
|
CustomAsset.get({name: 'favicon-file'}).$promise
|
||||||
|
]
|
||||||
|
translations: [ 'Translations', (Translations) ->
|
||||||
|
Translations.query('app.admin.settings').$promise
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
]
|
]
|
||||||
|
6
app/assets/javascripts/services/_t.coffee
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
Application.Services.factory '_t', ["$filter", ($filter)->
|
||||||
|
(key, interpolation = undefined, options = undefined) ->
|
||||||
|
$filter('translate')(key, interpolation, options)
|
||||||
|
]
|
8
app/assets/javascripts/services/abuse.coffee
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
Application.Services.factory 'Abuse', ["$resource", ($resource)->
|
||||||
|
$resource "/api/abuses/:id",
|
||||||
|
{id: "@id"},
|
||||||
|
update:
|
||||||
|
method: 'PUT'
|
||||||
|
]
|
8
app/assets/javascripts/services/admin.coffee
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
Application.Services.factory 'Admin', ["$resource", ($resource)->
|
||||||
|
$resource "/api/admins/:id",
|
||||||
|
{id: "@id"},
|
||||||
|
query:
|
||||||
|
isArray: false
|
||||||
|
]
|
14
app/assets/javascripts/services/authProvider.coffee
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
Application.Services.factory 'AuthProvider', ["$resource", ($resource)->
|
||||||
|
$resource "/api/auth_providers/:id",
|
||||||
|
{id: "@id"},
|
||||||
|
update:
|
||||||
|
method: 'PUT'
|
||||||
|
mapping_fields:
|
||||||
|
method: 'GET'
|
||||||
|
url: '/api/auth_providers/mapping_fields'
|
||||||
|
active:
|
||||||
|
method: 'GET'
|
||||||
|
url: '/api/auth_providers/active'
|
||||||
|
]
|
21
app/assets/javascripts/services/availability.coffee
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
Application.Services.factory 'Availability', ["$resource", ($resource)->
|
||||||
|
$resource "/api/availabilities/:id",
|
||||||
|
{id: "@id"},
|
||||||
|
machine:
|
||||||
|
method: 'GET'
|
||||||
|
url: '/api/availabilities/machines/:machineId'
|
||||||
|
params: {machineId: "@machineId"}
|
||||||
|
isArray: true
|
||||||
|
reservations:
|
||||||
|
method: 'GET'
|
||||||
|
url: '/api/availabilities/:id/reservations'
|
||||||
|
isArray: true
|
||||||
|
trainings:
|
||||||
|
method: 'GET'
|
||||||
|
url: '/api/availabilities/trainings'
|
||||||
|
isArray: true
|
||||||
|
update:
|
||||||
|
method: 'PUT'
|
||||||
|
]
|
8
app/assets/javascripts/services/credit.coffee
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
Application.Services.factory 'Credit', ["$resource", ($resource)->
|
||||||
|
$resource "/api/credits/:id",
|
||||||
|
{id: "@id"},
|
||||||
|
update:
|
||||||
|
method: 'PUT'
|
||||||
|
]
|
6
app/assets/javascripts/services/customAsset.coffee
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
Application.Services.factory 'CustomAsset', ["$resource", ($resource)->
|
||||||
|
$resource "/api/custom_assets/:name",
|
||||||
|
{name: "@name"}
|
||||||
|
]
|
@ -1,6 +1,6 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
Application.Services.factory 'dialogs', ["$modal", ($modal) ->
|
Application.Services.factory 'dialogs', ["$uibModal", ($uibModal) ->
|
||||||
confirm: (options, success, error)->
|
confirm: (options, success, error)->
|
||||||
defaultOpts =
|
defaultOpts =
|
||||||
templateUrl: '<%= asset_path "shared/confirm_modal.html" %>'
|
templateUrl: '<%= asset_path "shared/confirm_modal.html" %>'
|
||||||
@ -8,20 +8,20 @@ Application.Services.factory 'dialogs', ["$modal", ($modal) ->
|
|||||||
resolve:
|
resolve:
|
||||||
object: ->
|
object: ->
|
||||||
title: 'Titre de confirmation'
|
title: 'Titre de confirmation'
|
||||||
msg: 'Message de confiramtion'
|
msg: 'Message de confirmation'
|
||||||
controller: ['$scope', '$modalInstance', '$state', 'object', ($scope, $modalInstance, $state, object) ->
|
controller: ['$scope', '$uibModalInstance', '$state', 'object', ($scope, $uibModalInstance, $state, object) ->
|
||||||
$scope.object = object
|
$scope.object = object
|
||||||
$scope.ok = ->
|
$scope.ok = (info) ->
|
||||||
$modalInstance.close()
|
$uibModalInstance.close( info )
|
||||||
$scope.cancel = ->
|
$scope.cancel = ->
|
||||||
$modalInstance.dismiss('cancel')
|
$uibModalInstance.dismiss('cancel')
|
||||||
]
|
]
|
||||||
angular.extend(defaultOpts, options) if angular.isObject options
|
angular.extend(defaultOpts, options) if angular.isObject options
|
||||||
$modal.open defaultOpts
|
$uibModal.open defaultOpts
|
||||||
.result['finally'](null).then ->
|
.result['finally'](null).then (info)->
|
||||||
if angular.isFunction(success)
|
if angular.isFunction(success)
|
||||||
success()
|
success(info)
|
||||||
, ->
|
, (reason)->
|
||||||
if angular.isFunction(error)
|
if angular.isFunction(error)
|
||||||
error()
|
error(reason)
|
||||||
]
|
]
|
||||||
|
3
app/assets/javascripts/services/elastic.js.erb
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
Application.Services.service('es', function (esFactory) {
|
||||||
|
return esFactory({ host: window.location.origin });
|
||||||
|
});
|
@ -2,5 +2,7 @@
|
|||||||
|
|
||||||
Application.Services.factory 'Group', ["$resource", ($resource)->
|
Application.Services.factory 'Group', ["$resource", ($resource)->
|
||||||
$resource "/api/groups/:id",
|
$resource "/api/groups/:id",
|
||||||
{id: "@id"}
|
{id: "@id"},
|
||||||
|
update:
|
||||||
|
method: 'PUT'
|
||||||
]
|
]
|
||||||
|
8
app/assets/javascripts/services/invoice.coffee
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
Application.Services.factory 'Invoice', ["$resource", ($resource)->
|
||||||
|
$resource "/api/invoices/:id",
|
||||||
|
{id: "@id"},
|
||||||
|
update:
|
||||||
|
method: 'PUT'
|
||||||
|
]
|
@ -3,9 +3,14 @@
|
|||||||
Application.Services.factory 'Member', ["$resource", ($resource)->
|
Application.Services.factory 'Member', ["$resource", ($resource)->
|
||||||
$resource "/api/members/:id",
|
$resource "/api/members/:id",
|
||||||
{id: "@id"},
|
{id: "@id"},
|
||||||
|
update:
|
||||||
|
method: 'PUT'
|
||||||
lastSubscribed:
|
lastSubscribed:
|
||||||
method: 'GET'
|
method: 'GET'
|
||||||
url: '/api/last_subscribed/:limit'
|
url: '/api/last_subscribed/:limit'
|
||||||
params: {limit: "@limit"}
|
params: {limit: "@limit"}
|
||||||
isArray: true
|
isArray: true
|
||||||
|
merge:
|
||||||
|
method: 'PUT'
|
||||||
|
url: '/api/members/:id/merge'
|
||||||
]
|
]
|
||||||
|
8
app/assets/javascripts/services/plan.coffee
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
Application.Services.factory 'Plan', ["$resource", ($resource)->
|
||||||
|
$resource "/api/plans/:id",
|
||||||
|
{id: "@id"},
|
||||||
|
update:
|
||||||
|
method: 'PUT'
|
||||||
|
]
|
14
app/assets/javascripts/services/price.coffee
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
Application.Services.factory 'Price', ["$resource", ($resource)->
|
||||||
|
$resource "/api/prices/:id",
|
||||||
|
{},
|
||||||
|
query:
|
||||||
|
isArray: false
|
||||||
|
update:
|
||||||
|
method: 'PUT'
|
||||||
|
compute:
|
||||||
|
method: 'POST'
|
||||||
|
url: '/api/prices/compute'
|
||||||
|
isArray: false
|
||||||
|
]
|
8
app/assets/javascripts/services/pricing.coffee
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
Application.Services.factory 'Pricing', ["$resource", ($resource)->
|
||||||
|
$resource "/api/pricing",
|
||||||
|
{},
|
||||||
|
update:
|
||||||
|
method: 'PUT'
|
||||||
|
]
|
@ -7,4 +7,8 @@ Application.Services.factory 'Project', ["$resource", ($resource)->
|
|||||||
method: 'GET'
|
method: 'GET'
|
||||||
url: '/api/projects/last_published'
|
url: '/api/projects/last_published'
|
||||||
isArray: true
|
isArray: true
|
||||||
|
search:
|
||||||
|
method: 'GET'
|
||||||
|
url: '/api/projects/search'
|
||||||
|
isArray: true
|
||||||
]
|
]
|
||||||
|
8
app/assets/javascripts/services/reservation.coffee
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
Application.Services.factory 'Reservation', ["$resource", ($resource)->
|
||||||
|
$resource "/api/reservations/:id",
|
||||||
|
{id: "@id"},
|
||||||
|
update:
|
||||||
|
method: 'PUT'
|
||||||
|
]
|
10
app/assets/javascripts/services/setting.coffee
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
Application.Services.factory 'Setting', ["$resource", ($resource)->
|
||||||
|
$resource "/api/settings/:name",
|
||||||
|
{name: "@name"},
|
||||||
|
update:
|
||||||
|
method: 'PUT'
|
||||||
|
query:
|
||||||
|
isArray: false
|
||||||
|
]
|
11
app/assets/javascripts/services/slot.coffee
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
Application.Services.factory 'Slot', ["$resource", ($resource)->
|
||||||
|
$resource "/api/slots/:id",
|
||||||
|
{id: "@id"},
|
||||||
|
update:
|
||||||
|
method: 'PUT'
|
||||||
|
cancel:
|
||||||
|
method: 'PUT'
|
||||||
|
url: '/api/slots/:id/cancel'
|
||||||
|
]
|
5
app/assets/javascripts/services/statistics.coffee
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
Application.Services.factory 'Statistics', ["$resource", ($resource)->
|
||||||
|
$resource "/api/statistics"
|
||||||
|
]
|
8
app/assets/javascripts/services/subscription.coffee
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
Application.Services.factory 'Subscription', ["$resource", ($resource)->
|
||||||
|
$resource "/api/subscriptions/:id",
|
||||||
|
{id: "@id"},
|
||||||
|
update:
|
||||||
|
method: 'PUT'
|
||||||
|
]
|
8
app/assets/javascripts/services/tag.coffee
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
Application.Services.factory 'Tag', ["$resource", ($resource)->
|
||||||
|
$resource "/api/tags/:id",
|
||||||
|
{id: "@id"},
|
||||||
|
update:
|
||||||
|
method: 'PUT'
|
||||||
|
]
|
8
app/assets/javascripts/services/training.coffee
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
Application.Services.factory 'Training', ["$resource", ($resource)->
|
||||||
|
$resource "/api/trainings/:id",
|
||||||
|
{id: "@id"},
|
||||||
|
update:
|
||||||
|
method: 'PUT'
|
||||||
|
]
|
8
app/assets/javascripts/services/trainings_pricing.coffee
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
Application.Services.factory 'TrainingsPricing', ["$resource", ($resource)->
|
||||||
|
$resource "/api/trainings_pricings/:id",
|
||||||
|
{},
|
||||||
|
update:
|
||||||
|
method: 'PUT'
|
||||||
|
]
|
13
app/assets/javascripts/services/translations.coffee
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
Application.Services.factory 'Translations', ["$translatePartialLoader", "$translate", ($translatePartialLoader, $translate)->
|
||||||
|
return {
|
||||||
|
query: (stateName) ->
|
||||||
|
if angular.isArray (stateName)
|
||||||
|
angular.forEach stateName, (state) ->
|
||||||
|
$translatePartialLoader.addPart(state)
|
||||||
|
else
|
||||||
|
$translatePartialLoader.addPart(stateName)
|
||||||
|
$translate.refresh()
|
||||||
|
}
|
||||||
|
]
|
8
app/assets/javascripts/services/user.coffee
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
Application.Services.factory 'User', ["$resource", ($resource)->
|
||||||
|
$resource "/api/users",
|
||||||
|
{},
|
||||||
|
query:
|
||||||
|
isArray: false
|
||||||
|
]
|
@ -16,7 +16,7 @@ h1, .page-title {
|
|||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
}
|
}
|
||||||
h2 {
|
h2 {
|
||||||
color: $red;
|
//color: $red;
|
||||||
line-height: rem-calc(24);
|
line-height: rem-calc(24);
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
}
|
}
|
||||||
@ -26,7 +26,7 @@ h5 {
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: relative;
|
position: relative;
|
||||||
line-height: rem-calc(18);
|
line-height: rem-calc(18);
|
||||||
color: $red;
|
//color: $red;
|
||||||
font-size: rem-calc(16);
|
font-size: rem-calc(16);
|
||||||
&:after {
|
&:after {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -35,7 +35,7 @@ h5 {
|
|||||||
content: '';
|
content: '';
|
||||||
width: 35%;
|
width: 35%;
|
||||||
height: 1px;
|
height: 1px;
|
||||||
background-color: $red;
|
//background-color: $red;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,12 +43,12 @@ h5 {
|
|||||||
// -------------------------
|
// -------------------------
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: $link-color;
|
//color: $link-color;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
a:hover,
|
a:hover,
|
||||||
a:focus {
|
a:focus {
|
||||||
color: $link-hover-color;
|
//color: $link-hover-color;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,7 +127,8 @@ dd {
|
|||||||
// transition:0.5s linear all;
|
// transition:0.5s linear all;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
[ui-view].ng-enter, [ui-view].ng-leave {
|
// only for main content
|
||||||
|
#content-main.ng-enter, #content-main.ng-leave {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
@ -17,8 +17,8 @@
|
|||||||
.btn-warning-full {
|
.btn-warning-full {
|
||||||
outline: 0;
|
outline: 0;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
border: 3px solid $yellow;
|
//border: 3px solid $yellow;
|
||||||
background-color: $yellow;
|
//background-color: $yellow;
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
}
|
}
|
||||||
@ -41,4 +41,18 @@
|
|||||||
.btn-inactive{
|
.btn-inactive{
|
||||||
-webkit-box-shadow: none !important;
|
-webkit-box-shadow: none !important;
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-loading:after {
|
||||||
|
margin-left: 1em;
|
||||||
|
display: inline-block;
|
||||||
|
content: "\f110";
|
||||||
|
font-family: FontAwesome;
|
||||||
|
-webkit-animation:spin 4s linear infinite;
|
||||||
|
-moz-animation:spin 4s linear infinite;
|
||||||
|
animation:spin 4s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-moz-keyframes spin { 100% { -moz-transform: rotate(360deg); } }
|
||||||
|
@-webkit-keyframes spin { 100% { -webkit-transform: rotate(360deg); } }
|
||||||
|
@keyframes spin { 100% { -webkit-transform: rotate(360deg); transform:rotate(360deg); } }
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
|
|
||||||
.bg-light { background-color: $brand-light; }
|
.bg-light { background-color: $brand-light; }
|
||||||
.bg-red { background-color: $red; color: white; }
|
//.bg-red { background-color: $red; color: white; }
|
||||||
.bg-red-dark { background-color: $red-dark; }
|
//.bg-red-dark { background-color: $red-dark; }
|
||||||
.bg-yellow { background-color: $yellow !important; }
|
//.bg-yellow { background-color: $yellow !important; }
|
||||||
|
.bg-token { background-color: rgba(230, 208, 137, 0.49); }
|
||||||
.bg-machine { background-color: $beige; }
|
.bg-machine { background-color: $beige; }
|
||||||
.bg-formation { background-color: $violet; }
|
.bg-formation { background-color: $violet; }
|
||||||
.bg-atelier { background-color: $blue; }
|
.bg-atelier { background-color: $blue; }
|
||||||
@ -30,7 +31,7 @@
|
|||||||
.text-black-light { color: #424242 !important; }
|
.text-black-light { color: #424242 !important; }
|
||||||
.text-gray { color: #5a5a5a !important; }
|
.text-gray { color: #5a5a5a !important; }
|
||||||
.text-white { color: #fff !important; }
|
.text-white { color: #fff !important; }
|
||||||
.text-yellow { color: $yellow !important; }
|
//.text-yellow { color: $yellow !important; }
|
||||||
.text-blue { color: $blue; }
|
.text-blue { color: $blue; }
|
||||||
.text-muted { color: $text-muted; }
|
.text-muted { color: $text-muted; }
|
||||||
.text-danger, .red { color: $red !important; }
|
.text-danger, .red { color: $red !important; }
|
||||||
|
@ -7,11 +7,11 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
h1 {
|
h1 {
|
||||||
font-size: rem-calc(16); text-transform: uppercase;
|
font-size: rem-calc(16); text-transform: uppercase;
|
||||||
}
|
}
|
||||||
h2 { font-weight: bold; }
|
h2 { font-weight: bold; }
|
||||||
h3 { color: $red; }
|
//h3 { color: $red; }
|
||||||
h4 {
|
h4 {
|
||||||
font-size: rem-calc(12);
|
font-size: rem-calc(12);
|
||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
@ -50,13 +50,14 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
max-height: 44px;
|
||||||
}
|
}
|
||||||
h1 {
|
h1 {
|
||||||
margin: 25px 0 20px 0;
|
margin: 25px 0 20px 0;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: $red;
|
//color: $red;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -129,14 +130,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.article-thumbnail {
|
.article-thumbnail {
|
||||||
max-height: 400px;
|
// max-height: 400px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.label-staging {
|
.label-staging {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50px;
|
top: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification-open {
|
.notification-open {
|
||||||
@ -197,6 +198,7 @@
|
|||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
font-size: rem-calc(16);
|
font-size: rem-calc(16);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
color: black;
|
||||||
}
|
}
|
||||||
.content {
|
.content {
|
||||||
padding: 15px 0;
|
padding: 15px 0;
|
||||||
@ -206,7 +208,7 @@
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
background: white;
|
background: white;
|
||||||
@include border-radius(50%);
|
@include border-radius(50%);
|
||||||
border: 3px solid $yellow;
|
border: 3px solid;// $yellow;
|
||||||
}
|
}
|
||||||
.price {
|
.price {
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -238,15 +240,15 @@
|
|||||||
background-color: white;
|
background-color: white;
|
||||||
padding-left: 30px;
|
padding-left: 30px;
|
||||||
padding-right: 30px;
|
padding-right: 30px;
|
||||||
&:hover { background-color: $yellow; }
|
//&:hover { background-color: $yellow; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.well {
|
.well {
|
||||||
&.well-warning {
|
&.well-warning {
|
||||||
border-color: #ffdc4e;
|
//border-color: #ffdc4e;
|
||||||
background-color: #ffdc4e;
|
//background-color: #ffdc4e;
|
||||||
@include border-radius(3px);
|
@include border-radius(3px);
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
}
|
}
|
||||||
@ -324,10 +326,10 @@
|
|||||||
|
|
||||||
.block-link {
|
.block-link {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
&:hover { background-color: $yellow; }
|
//&:hover { background-color: $yellow; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-control.form-control-ui-select .select2-choices .select2-search-choice {
|
.form-control .ui-select-choices, .form-control .ui-select-match {
|
||||||
font-size: 85% !important;
|
font-size: 85% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -351,16 +353,15 @@
|
|||||||
.about-picture {
|
.about-picture {
|
||||||
padding: 70px 0;
|
padding: 70px 0;
|
||||||
height: 326px;
|
height: 326px;
|
||||||
background: white asset-url("about-fablab.jpg") no-repeat;
|
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
}
|
}
|
||||||
.about-title {
|
.about-title, .about-title p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: rem-calc(50);
|
font-size: rem-calc(50);
|
||||||
line-height: rem-calc(48);
|
line-height: rem-calc(48);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-weight: 900; //black
|
font-weight: 900; //black
|
||||||
}
|
}
|
||||||
|
|
||||||
.about-title-aside {
|
.about-title-aside {
|
||||||
@ -393,7 +394,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.event:hover {
|
.event:hover {
|
||||||
background-color: #cb1117;
|
//background-color: #cb1117;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -441,3 +442,25 @@ padding: 10px;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// angular-bootstrap accordions (enlightened version)
|
||||||
|
.light-accordion > .panel-heading {
|
||||||
|
padding-top: 0.2em;
|
||||||
|
padding-bottom: 0.2em;
|
||||||
|
}
|
||||||
|
.light-accordion > .panel-heading > .panel-title {
|
||||||
|
font-size: 12pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-generator {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0; right: 10px;
|
||||||
|
z-index: 100;
|
||||||
|
padding: 3px 15px;
|
||||||
|
border: 1px solid $border-color;
|
||||||
|
border-top-left-radius: 8px;
|
||||||
|
background: $bg-gray;
|
||||||
|
@media only screen and (max-width: 768px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -76,11 +76,17 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: black;
|
color: black;
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $yellow;
|
//background-color: $yellow;
|
||||||
}
|
}
|
||||||
i:before { content: "\f177"; }
|
i:before { content: "\f177"; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.heading-icon {
|
||||||
|
width: 100%;
|
||||||
|
padding: 35px 40%;
|
||||||
|
display: inline-block;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
.heading-title {
|
.heading-title {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: 94px;
|
height: 94px;
|
||||||
@ -341,4 +347,131 @@ body.container{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.customMenuButton {
|
||||||
|
min-width: 15em;
|
||||||
|
max-width: 15em;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customMenuInput {
|
||||||
|
width:100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.reservation-canceled {
|
||||||
|
color: #606060;
|
||||||
|
border-radius: 0.2em;
|
||||||
|
background-color: #e4e4e4;
|
||||||
|
padding: 0.7em 0.7em;
|
||||||
|
font-size: 90%;
|
||||||
|
display:inline-block;
|
||||||
|
vertical-align:middle;
|
||||||
|
|
||||||
|
.reservation-time {
|
||||||
|
color: #606060;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: "Annulée";
|
||||||
|
display: inline-block;
|
||||||
|
background-color: #c44242;
|
||||||
|
border-radius: 0.25em;
|
||||||
|
padding: 0.1em 0.5em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #fff;
|
||||||
|
float: left;
|
||||||
|
margin-right: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-logo-container {
|
||||||
|
max-width: 240px;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.custom-logo {
|
||||||
|
height: 100px;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
background-size: cover;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
border: 1px dashed #c4c4c4;
|
||||||
|
border-radius: 0.7em;
|
||||||
|
padding: 1.6em;
|
||||||
|
margin-left: 1em;
|
||||||
|
|
||||||
|
img {
|
||||||
|
display: block;
|
||||||
|
width: auto;
|
||||||
|
max-height: 44px;
|
||||||
|
max-width: 100%;
|
||||||
|
margin:auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .tools-box {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tools-box {
|
||||||
|
opacity: 0;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 10px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-dark {
|
||||||
|
background-color: #000;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-favicon-container {
|
||||||
|
max-width: 70px;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.custom-favicon {
|
||||||
|
height: 70px;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
background-size: cover;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
border: 1px dashed #c4c4c4;
|
||||||
|
border-radius: 0.7em;
|
||||||
|
padding: 1.6em;
|
||||||
|
margin-left: 1em;
|
||||||
|
|
||||||
|
img {
|
||||||
|
display: block;
|
||||||
|
width: auto;
|
||||||
|
max-height: 16px;
|
||||||
|
max-width: 16px;
|
||||||
|
margin:auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .tools-box {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tools-box {
|
||||||
|
opacity: 0;
|
||||||
|
position: absolute;
|
||||||
|
bottom: -7px;
|
||||||
|
left: 51px;
|
||||||
|
right: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.flash-message {
|
||||||
|
position: absolute;
|
||||||
|
top: 1%;
|
||||||
|
z-index: 1001;
|
||||||
|
width: 33%;
|
||||||
|
left: 33%;
|
||||||
}
|
}
|
@ -425,7 +425,7 @@
|
|||||||
#nav {
|
#nav {
|
||||||
// border-right: 1px solid $red-dark;
|
// border-right: 1px solid $red-dark;
|
||||||
.nav {
|
.nav {
|
||||||
background-color: $red;
|
//background-color: $red;
|
||||||
> li {
|
> li {
|
||||||
> a {
|
> a {
|
||||||
padding: 13px 17px;
|
padding: 13px 17px;
|
||||||
@ -433,11 +433,11 @@
|
|||||||
color: white;
|
color: white;
|
||||||
&:hover,
|
&:hover,
|
||||||
&:focus, &.active {
|
&:focus, &.active {
|
||||||
background-color: $red-light;
|
//background-color: $red-light;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
&.active {
|
&.active {
|
||||||
border-left: 3px solid #870003;
|
border-left: 3px solid;// #870003;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,37 +1,108 @@
|
|||||||
|
// medium editor placeholder
|
||||||
|
.medium-editor-placeholder {
|
||||||
|
min-height: 30px; // fix for firefox
|
||||||
|
}
|
||||||
|
|
||||||
|
//xeditable
|
||||||
|
.editable-buttons{
|
||||||
|
button[type=submit].btn-primary{
|
||||||
|
@extend .btn-warning;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//summernote
|
//summernote
|
||||||
|
|
||||||
.note-editor .note-editable {
|
.note-editor .note-editable {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Growl
|
// Growl
|
||||||
.growl {
|
.growl {
|
||||||
top: 90px;
|
top: 90px;
|
||||||
z-index: 1100;
|
z-index: 1100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fullcalendar
|
||||||
|
|
||||||
|
.fc-view-container .fc-body tr {
|
||||||
// UI Select
|
height: 40px !important;
|
||||||
|
|
||||||
.form-control {
|
|
||||||
&.form-control-ui-select {
|
|
||||||
height: auto;
|
|
||||||
.select2-choices {
|
|
||||||
border: none;
|
|
||||||
background: transparent;
|
|
||||||
.select2-search-choice {
|
|
||||||
@extend .label;
|
|
||||||
padding-left: .9em;
|
|
||||||
font-size: 100%;
|
|
||||||
font-weight: normal;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fc-toolbar {
|
||||||
|
height: 40px;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-toolbar .fc-button {
|
||||||
|
background: #F2F2F2;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
text-shadow: none;
|
||||||
|
margin: 0;
|
||||||
|
height: 40px;
|
||||||
|
line-height: 18px;
|
||||||
|
padding: 10px;
|
||||||
|
//&:hover, &:active, &.fc-state-active { background-color: $yellow; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-toolbar h2 {
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 40px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-view-container .fc-widget-header,
|
||||||
|
.fc-view-container .fc-widget-content {
|
||||||
|
border-color: #e8e8e8;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-content-skeleton .fc-event {
|
||||||
|
padding: 2px;
|
||||||
|
border-left: solid 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-event {
|
||||||
|
-webkit-box-sizing: content-box;
|
||||||
|
-moz-box-sizing: content-box;
|
||||||
|
box-sizing: content-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-event .fc-time span, .fc-event .fc-title {
|
||||||
|
font-size: rem-calc(10);
|
||||||
|
line-height: rem-calc(12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-event .fc-time span.label {
|
||||||
|
font-size: rem-calc(8);
|
||||||
|
margin-left: 0.7em;
|
||||||
|
}
|
||||||
|
|
||||||
|
// croix de suppression pour un créneau de disponibilité
|
||||||
|
.remove-event {
|
||||||
|
position: absolute;
|
||||||
|
float: right;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 11px;
|
||||||
|
color: black;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 9999;
|
||||||
|
text-align: right;
|
||||||
|
.training-reserve &, .machine-reserve & { display: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-v-event.fc-end {
|
||||||
|
border-bottom-width: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-divider {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -59,6 +130,7 @@
|
|||||||
line-height: rem-calc(24);
|
line-height: rem-calc(24);
|
||||||
color: white;
|
color: white;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
|
text-align: center;
|
||||||
a {
|
a {
|
||||||
color: white;
|
color: white;
|
||||||
&:hover { color: $yellow; }
|
&:hover { color: $yellow; }
|
||||||
@ -77,7 +149,7 @@
|
|||||||
background: none;
|
background: none;
|
||||||
@include border-radius($border-radius-base);
|
@include border-radius($border-radius-base);
|
||||||
&:hover, &:focus {
|
&:hover, &:focus {
|
||||||
color: $yellow;
|
//color: $yellow;
|
||||||
}
|
}
|
||||||
|
|
||||||
.glyphicon-chevron-left {
|
.glyphicon-chevron-left {
|
||||||
@ -99,6 +171,31 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// .carousel-control {
|
||||||
|
// // position: absolute;
|
||||||
|
// display: block;
|
||||||
|
// margin-bottom: -20px;
|
||||||
|
// padding: 20px;
|
||||||
|
// color: white;
|
||||||
|
// width: 58px;
|
||||||
|
// height: 58px;
|
||||||
|
// border: 3px solid white;
|
||||||
|
// border-radius: 50%;
|
||||||
|
|
||||||
|
// .glyphicon-chevron-right:before {
|
||||||
|
// // //Reset the icon
|
||||||
|
// // content: " ";
|
||||||
|
// // //Give layout
|
||||||
|
// // display:block;
|
||||||
|
// // //Your image as background
|
||||||
|
// // background:url('http://yourarrow.png') no-repeat;
|
||||||
|
// // //To show full image set the dimensions
|
||||||
|
// // width:30px;
|
||||||
|
// // height:30px;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.banner { }
|
.banner { }
|
||||||
|
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
/*
|
/*
|
||||||
* Require here your print media stylesheets
|
*= require fullcalendar/dist/fullcalendar.print
|
||||||
*/
|
*/
|
@ -111,7 +111,7 @@ p, .widget p {
|
|||||||
.b{border: 1px solid rgba(0, 0, 0, 0.05)}
|
.b{border: 1px solid rgba(0, 0, 0, 0.05)}
|
||||||
.b-a{border: 1px solid $border-color}
|
.b-a{border: 1px solid $border-color}
|
||||||
.b-t{border-top: 1px solid $border-color}
|
.b-t{border-top: 1px solid $border-color}
|
||||||
.b-r{border-right: 1px solid $border-color}
|
.b-r{border-right: 1px solid $border-color !important;}
|
||||||
.b-b{border-bottom: 1px solid $border-color}
|
.b-b{border-bottom: 1px solid $border-color}
|
||||||
.b-l{border-left: 1px solid $border-color}
|
.b-l{border-left: 1px solid $border-color}
|
||||||
.b-light{border-color: darken($brand-light, 5%)}
|
.b-light{border-color: darken($brand-light, 5%)}
|
||||||
|
@ -1,17 +1,23 @@
|
|||||||
/*
|
/*
|
||||||
*= require_self
|
*= require_self
|
||||||
*= require select2/select2
|
*= require angular-ui-select/dist/select
|
||||||
|
*= require fullcalendar/dist/fullcalendar
|
||||||
*= require jasny-bootstrap/dist/css/jasny-bootstrap
|
*= require jasny-bootstrap/dist/css/jasny-bootstrap
|
||||||
*= require angular-growl/build/angular-growl.min.css
|
*= require angular-growl-v2/build/angular-growl
|
||||||
*= require angular-xeditable/dist/css/xeditable
|
*= require angular-xeditable/dist/css/xeditable
|
||||||
*= require angular-loading-bar/src/loading-bar
|
*= require angular-loading-bar/src/loading-bar
|
||||||
|
*= require nvd3/build/nv.d3
|
||||||
*= require font-awesome
|
*= require font-awesome
|
||||||
|
*= require medium-editor/dist/css/medium-editor
|
||||||
|
*= require medium-editor/dist/css/themes/default
|
||||||
|
*= require bootstrap-switch/dist/css/bootstrap3/bootstrap-switch.min
|
||||||
*= require summernote/dist/summernote
|
*= require summernote/dist/summernote
|
||||||
|
*= require jquery-minicolors/jquery.minicolors.css
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@import "app.functions";
|
@import "app.functions";
|
||||||
|
@import "bootstrap-compass";
|
||||||
|
@import "bootstrap-sprockets";
|
||||||
@import "compass";
|
@import "compass";
|
||||||
@import "bootstrap_and_overrides";
|
@import "bootstrap_and_overrides";
|
||||||
|
|
||||||
@ -25,5 +31,6 @@
|
|||||||
@import "app.buttons";
|
@import "app.buttons";
|
||||||
@import "app.components";
|
@import "app.components";
|
||||||
@import "app.plugins";
|
@import "app.plugins";
|
||||||
|
@import "modules/invoice";
|
||||||
|
|
||||||
@import "app.responsive";
|
@import "app.responsive";
|
||||||
|
@ -78,10 +78,10 @@ $link-hover-decoration: underline;
|
|||||||
|
|
||||||
// Semibold = 600, Bold = 700, ExtraB = 800
|
// Semibold = 600, Bold = 700, ExtraB = 800
|
||||||
|
|
||||||
$font-family-sans-serif: "Open Sans", Helvetica, Arial, sans-serif !default;
|
$font-family-sans-serif: 'proxima-nova', 'Open Sans', Helvetica, Arial, sans-serif !default;
|
||||||
$font-proxima-condensed: "Open Sans Condensed", Helvetica, Arial, sans-serif !default;
|
$font-proxima-condensed: 'proxima-nova-condensed', 'Open Sans Condensed', Helvetica, Arial, sans-serif !default;
|
||||||
$font-family-serif: Georgia, "Times New Roman", Times, serif !default;
|
$font-family-serif: Georgia, 'Times New Roman', Times, serif !default;
|
||||||
$font-felt: "Loved by the King", sans-serif;
|
$font-felt: 'felt-tip-roman', 'Loved by the King', cursive, sans-serif;
|
||||||
|
|
||||||
//** Default monospace fonts for `<code>`, `<kbd>`, and `<pre>`.
|
//** Default monospace fonts for `<code>`, `<kbd>`, and `<pre>`.
|
||||||
// $font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace !default;
|
// $font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace !default;
|
||||||
@ -952,16 +952,16 @@ $hr-border: $gray-lighter !default;
|
|||||||
@import "bootstrap/input-groups";
|
@import "bootstrap/input-groups";
|
||||||
@import "bootstrap/navs";
|
@import "bootstrap/navs";
|
||||||
@import "bootstrap/navbar";
|
@import "bootstrap/navbar";
|
||||||
// @import "bootstrap/breadcrumbs";
|
@import "bootstrap/breadcrumbs";
|
||||||
@import "bootstrap/pagination";
|
@import "bootstrap/pagination";
|
||||||
// @import "bootstrap/pager";
|
@import "bootstrap/pager";
|
||||||
@import "bootstrap/labels";
|
@import "bootstrap/labels";
|
||||||
@import "bootstrap/badges";
|
@import "bootstrap/badges";
|
||||||
//@import "bootstrap/jumbotron";
|
@import "bootstrap/jumbotron";
|
||||||
@import "bootstrap/thumbnails";
|
@import "bootstrap/thumbnails";
|
||||||
@import "bootstrap/alerts";
|
@import "bootstrap/alerts";
|
||||||
@import "bootstrap/progress-bars";
|
@import "bootstrap/progress-bars";
|
||||||
// @import "bootstrap/media";
|
@import "bootstrap/media";
|
||||||
@import "bootstrap/list-group";
|
@import "bootstrap/list-group";
|
||||||
@import "bootstrap/panels";
|
@import "bootstrap/panels";
|
||||||
@import "bootstrap/responsive-embed";
|
@import "bootstrap/responsive-embed";
|
||||||
|
181
app/assets/stylesheets/modules/invoice.scss
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
|
||||||
|
// admin invoices
|
||||||
|
|
||||||
|
.invoice-placeholder {
|
||||||
|
width: 80%;
|
||||||
|
max-width: 800px;
|
||||||
|
height: 100%;
|
||||||
|
margin: auto;
|
||||||
|
margin-top: 2em;
|
||||||
|
border-width: 1px;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: #aeaeae #979797 #7b7b7b;
|
||||||
|
box-shadow: 2px 3px 6px 0 #898989,
|
||||||
|
-2px 3px 6px 0 #898989;
|
||||||
|
padding: 2em;
|
||||||
|
|
||||||
|
.invoice-buyer-infos {
|
||||||
|
float: right;
|
||||||
|
text-align: right;
|
||||||
|
margin-left: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-logo {
|
||||||
|
height: 6em;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
background-size: cover;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
|
||||||
|
img {
|
||||||
|
display: block;
|
||||||
|
width: auto;
|
||||||
|
max-height: 100%;
|
||||||
|
max-width: 60%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .tools-box {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tools-box {
|
||||||
|
opacity: 0;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 10px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-object, .invoice-data, .invoice-data p, .invoice-text, .invoice-legals {
|
||||||
|
margin-top: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-data table {
|
||||||
|
width: 100%
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-data tr, .invoice-data th, .invoice-data td {
|
||||||
|
border: 1px solid;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-text {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-legals {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-editable:hover {
|
||||||
|
background-color: $yellow;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-activable {
|
||||||
|
font-style: italic;
|
||||||
|
color: #c4c4c4;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-activable:hover {
|
||||||
|
background-color: $yellow;
|
||||||
|
border: 1px dashed #c4c4c4;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
content:"";
|
||||||
|
display:block;
|
||||||
|
margin-top:30%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vat-line {
|
||||||
|
background-color: #e4e4e4;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bold {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.italic {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-invoice {
|
||||||
|
.modal-header {
|
||||||
|
@extend .modal-header;
|
||||||
|
// padding-left: 4em;
|
||||||
|
text-align: center;
|
||||||
|
background-color: #e6e6e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
|
||||||
|
.elements ul {
|
||||||
|
@extend .list-unstyled;
|
||||||
|
}
|
||||||
|
|
||||||
|
.elements li {
|
||||||
|
@extend .btn;
|
||||||
|
@extend .btn-default;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.invoice-element-legend {
|
||||||
|
min-width: 15em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-element-legend tr, .invoice-element-legend th, .invoice-element-legend td {
|
||||||
|
border: 1px solid;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-notes {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.partial-avoir-table tr {
|
||||||
|
float:left;
|
||||||
|
|
||||||
|
.input-col { min-width: 2em; }
|
||||||
|
.label-col { min-width: 18em; }
|
||||||
|
.amount-col { min-width: 6em; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.partial-avoir-selected-item {
|
||||||
|
background-color: $yellow;
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
content:"Rembourser";
|
||||||
|
display:inline-block;
|
||||||
|
position: absolute;
|
||||||
|
right: 0.8em;
|
||||||
|
top: 1.8em;
|
||||||
|
color: $red;
|
||||||
|
width: 6.7em;
|
||||||
|
transform: rotate(-22.5deg);
|
||||||
|
overflow: visible;
|
||||||
|
font-weight: bold;
|
||||||
|
border: 1px solid;
|
||||||
|
padding: 0 4px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: small;
|
||||||
|
}
|
||||||
|
}
|