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
|
||||
|
||||
# Ignore all logfiles and tempfiles.
|
||||
/log/*
|
||||
!/log/.keep
|
||||
/log/*.log
|
||||
/tmp
|
||||
|
||||
# uploads and public assets
|
||||
/public/uploads
|
||||
/public/assets
|
||||
|
||||
# MacOS and IDE files
|
||||
# Ignore application configuration
|
||||
/config/application.yml
|
||||
|
||||
*.DS_Store
|
||||
.idea
|
||||
|
||||
# PDF invoices
|
||||
/invoices/*
|
||||
|
||||
/config/database.yml
|
||||
/config/application.yml
|
||||
|
||||
.DS_Store
|
||||
|
||||
# machine specific database config
|
||||
/config/database.yml
|
||||
.vagrant
|
||||
|
@ -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'
|
||||
|
||||
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
|
||||
gem 'rails', '4.2.1'
|
||||
gem 'rails', '4.2.5'
|
||||
# Use SCSS for stylesheets
|
||||
gem 'sass-rails', '5.0.1'
|
||||
gem 'compass-rails', '2.0.4'
|
||||
@ -11,21 +11,22 @@ gem 'uglifier', '>= 1.3.0'
|
||||
# Use CoffeeScript for .js.coffee assets and views
|
||||
gem 'coffee-rails', '~> 4.1.0'
|
||||
# See https://github.com/sstephenson/execjs#readme for more supported runtimes
|
||||
gem 'therubyracer', platforms: :ruby
|
||||
gem 'therubyracer', '= 0.12.0', platforms: :ruby
|
||||
|
||||
# Use jquery as the JavaScript library
|
||||
gem 'jquery-rails'
|
||||
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
|
||||
gem 'jbuilder', '~> 2.0'
|
||||
# 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 'responders', '~> 2.0'
|
||||
|
||||
group :development, :test do
|
||||
# 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
|
||||
gem 'web-console', '~> 2.0'
|
||||
@ -38,6 +39,8 @@ group :development, :test do
|
||||
gem 'spring-commands-rspec'
|
||||
|
||||
gem 'guard-rspec', require: false
|
||||
|
||||
gem 'railroady'
|
||||
end
|
||||
|
||||
group :development do
|
||||
@ -51,6 +54,9 @@ group :development do
|
||||
gem 'capistrano'
|
||||
gem 'rvm-capistrano', require: false
|
||||
gem 'capistrano-sidekiq', require: false
|
||||
gem 'capistrano-maintenance', '0.0.5', require: false
|
||||
|
||||
gem 'active_record_query_trace'
|
||||
end
|
||||
|
||||
group :test do
|
||||
@ -70,6 +76,9 @@ gem 'pg'
|
||||
gem 'devise'
|
||||
gem 'devise-async'
|
||||
|
||||
gem 'omniauth'
|
||||
gem 'omniauth-oauth2'
|
||||
|
||||
gem 'rolify'
|
||||
|
||||
gem 'kaminari'
|
||||
@ -79,7 +88,8 @@ gem 'figaro'
|
||||
gem 'bootstrap-sass'
|
||||
gem 'font-awesome-rails'
|
||||
|
||||
gem 'angularjs-rails'
|
||||
#using bower instead
|
||||
#gem 'angularjs-rails'
|
||||
|
||||
# Image processing ruby wrapper for ImageMagick
|
||||
gem 'mini_magick'
|
||||
@ -101,13 +111,32 @@ gem 'sinatra', require: false
|
||||
# Recurring jobs for Sidekiq
|
||||
gem 'sidekiq-cron'
|
||||
|
||||
gem 'stripe', '1.30.2'
|
||||
|
||||
gem 'recurrence'
|
||||
|
||||
# Fork de la gem avec support Attachments
|
||||
gem 'mandrill_dm', github: 'AbleTech/mandrill_dm'
|
||||
gem 'newrelic_rpm'
|
||||
|
||||
gem 'disqus_api'
|
||||
# PDF
|
||||
gem 'prawn'
|
||||
gem 'prawn-table'
|
||||
|
||||
gem 'elasticsearch-rails'
|
||||
gem 'elasticsearch-model'
|
||||
gem 'elasticsearch-persistence'
|
||||
|
||||
gem 'notify_with'
|
||||
|
||||
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
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
aasm (4.1.0)
|
||||
actionmailer (4.2.1)
|
||||
actionpack (= 4.2.1)
|
||||
actionview (= 4.2.1)
|
||||
activejob (= 4.2.1)
|
||||
actionmailer (4.2.5)
|
||||
actionpack (= 4.2.5)
|
||||
actionview (= 4.2.5)
|
||||
activejob (= 4.2.5)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
rails-dom-testing (~> 1.0, >= 1.0.5)
|
||||
actionpack (4.2.1)
|
||||
actionview (= 4.2.1)
|
||||
activesupport (= 4.2.1)
|
||||
actionpack (4.2.5)
|
||||
actionview (= 4.2.5)
|
||||
activesupport (= 4.2.5)
|
||||
rack (~> 1.6)
|
||||
rack-test (~> 0.6.2)
|
||||
rails-dom-testing (~> 1.0, >= 1.0.5)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.0.1)
|
||||
actionview (4.2.1)
|
||||
activesupport (= 4.2.1)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
||||
actionpack-page_caching (1.0.2)
|
||||
actionpack (>= 4.0.0, < 5)
|
||||
actionview (4.2.5)
|
||||
activesupport (= 4.2.5)
|
||||
builder (~> 3.1)
|
||||
erubis (~> 2.7.0)
|
||||
rails-dom-testing (~> 1.0, >= 1.0.5)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.0.1)
|
||||
activejob (4.2.1)
|
||||
activesupport (= 4.2.1)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
||||
active_record_query_trace (1.4)
|
||||
activejob (4.2.5)
|
||||
activesupport (= 4.2.5)
|
||||
globalid (>= 0.3.0)
|
||||
activemodel (4.2.1)
|
||||
activesupport (= 4.2.1)
|
||||
activemodel (4.2.5)
|
||||
activesupport (= 4.2.5)
|
||||
builder (~> 3.1)
|
||||
activerecord (4.2.1)
|
||||
activemodel (= 4.2.1)
|
||||
activesupport (= 4.2.1)
|
||||
activerecord (4.2.5)
|
||||
activemodel (= 4.2.5)
|
||||
activesupport (= 4.2.5)
|
||||
arel (~> 6.0)
|
||||
activesupport (4.2.1)
|
||||
activesupport (4.2.5)
|
||||
i18n (~> 0.7)
|
||||
json (~> 1.7, >= 1.7.7)
|
||||
minitest (~> 5.1)
|
||||
thread_safe (~> 0.3, >= 0.3.4)
|
||||
tzinfo (~> 1.1)
|
||||
addressable (2.3.8)
|
||||
angularjs-rails (1.3.15)
|
||||
arel (6.0.0)
|
||||
arel (6.0.3)
|
||||
autoprefixer-rails (5.1.8)
|
||||
execjs
|
||||
json
|
||||
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)
|
||||
binding_of_caller (0.7.2)
|
||||
debug_inspector (>= 0.0.1)
|
||||
@ -59,14 +58,15 @@ GEM
|
||||
sass (>= 3.2.19)
|
||||
buftok (0.2.0)
|
||||
builder (3.2.2)
|
||||
byebug (4.0.4)
|
||||
columnize (= 0.9.0)
|
||||
camertron-eprun (1.1.0)
|
||||
capistrano (2.15.5)
|
||||
highline
|
||||
net-scp (>= 1.0.0)
|
||||
net-sftp (>= 2.0.0)
|
||||
net-ssh (>= 2.0.14)
|
||||
net-ssh-gateway (>= 1.1.0)
|
||||
capistrano-maintenance (0.0.5)
|
||||
capistrano (~> 2.0)
|
||||
capistrano-sidekiq (0.5.2)
|
||||
capistrano
|
||||
sidekiq
|
||||
@ -77,8 +77,12 @@ GEM
|
||||
mime-types (>= 1.16)
|
||||
celluloid (0.16.0)
|
||||
timers (~> 4.0.0)
|
||||
chroma (0.0.1)
|
||||
chunky_png (1.3.4)
|
||||
cldr-plurals-runtime-rb (1.0.1)
|
||||
coderay (1.1.0)
|
||||
coercible (1.0.0)
|
||||
descendants_tracker (~> 0.0.1)
|
||||
coffee-rails (4.1.0)
|
||||
coffee-script (>= 2.2.0)
|
||||
railties (>= 4.0.0, < 5.0)
|
||||
@ -86,7 +90,6 @@ GEM
|
||||
coffee-script-source
|
||||
execjs
|
||||
coffee-script-source (1.9.1)
|
||||
columnize (0.9.0)
|
||||
compass (1.0.3)
|
||||
chunky_png (~> 1.2)
|
||||
compass-core (~> 1.0.2)
|
||||
@ -103,9 +106,11 @@ GEM
|
||||
compass (~> 1.0.0)
|
||||
sass-rails (<= 5.0.1)
|
||||
sprockets (< 2.13)
|
||||
connection_pool (2.1.3)
|
||||
connection_pool (2.2.0)
|
||||
database_cleaner (1.4.1)
|
||||
debug_inspector (0.0.2)
|
||||
descendants_tracker (0.0.4)
|
||||
thread_safe (~> 0.3, >= 0.3.1)
|
||||
devise (3.4.1)
|
||||
bcrypt (~> 3.0)
|
||||
orm_adapter (~> 0.1)
|
||||
@ -116,13 +121,30 @@ GEM
|
||||
devise-async (0.9.0)
|
||||
devise (~> 3.2)
|
||||
diff-lcs (1.2.5)
|
||||
disqus_api (0.0.4)
|
||||
activesupport (>= 3.0.0)
|
||||
faraday (>= 0.8)
|
||||
faraday_middleware (>= 0.9)
|
||||
domain_name (0.5.25)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
elasticsearch (1.0.12)
|
||||
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)
|
||||
erubis (2.7.0)
|
||||
excon (0.45.1)
|
||||
execjs (2.4.0)
|
||||
factory_girl (4.5.0)
|
||||
activesupport (>= 3.0.0)
|
||||
@ -133,8 +155,6 @@ GEM
|
||||
i18n (~> 0.5)
|
||||
faraday (0.9.1)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
faraday_middleware (0.9.1)
|
||||
faraday (>= 0.7.4, < 0.10)
|
||||
ffi (1.9.8)
|
||||
figaro (1.1.0)
|
||||
thor (~> 0.14)
|
||||
@ -146,7 +166,7 @@ GEM
|
||||
formatador (0.2.5)
|
||||
friendly_id (5.1.0)
|
||||
activerecord (>= 4.0.0)
|
||||
globalid (0.3.3)
|
||||
globalid (0.3.6)
|
||||
activesupport (>= 4.1.0)
|
||||
guard (2.12.5)
|
||||
formatador (>= 0.2.4)
|
||||
@ -162,13 +182,17 @@ GEM
|
||||
guard (~> 2.1)
|
||||
guard-compat (~> 1.1)
|
||||
rspec (>= 2.99.0, < 4.0)
|
||||
hashie (3.4.2)
|
||||
highline (1.7.1)
|
||||
hike (1.2.3)
|
||||
hitimes (1.2.2)
|
||||
http (0.6.4)
|
||||
http_parser.rb (~> 0.6.0)
|
||||
http-cookie (1.0.2)
|
||||
domain_name (~> 0.5)
|
||||
http_parser.rb (0.6.0)
|
||||
i18n (0.7.0)
|
||||
ice_nine (0.11.1)
|
||||
jbuilder (2.2.12)
|
||||
activesupport (>= 3.0.0, < 5)
|
||||
multi_json (~> 1.2)
|
||||
@ -176,7 +200,8 @@ GEM
|
||||
rails-dom-testing (~> 1.0)
|
||||
railties (>= 4.2.0)
|
||||
thor (>= 0.14, < 2.0)
|
||||
json (1.8.2)
|
||||
json (1.8.3)
|
||||
jwt (1.5.1)
|
||||
kaminari (0.16.3)
|
||||
actionpack (>= 3.0.0)
|
||||
activesupport (>= 3.0.0)
|
||||
@ -185,27 +210,27 @@ GEM
|
||||
addressable (~> 2.3)
|
||||
letter_opener (1.3.0)
|
||||
launchy (~> 2.2)
|
||||
libv8 (3.16.14.7)
|
||||
libv8 (3.16.14.11)
|
||||
listen (2.10.0)
|
||||
celluloid (~> 0.16.0)
|
||||
rb-fsevent (>= 0.9.3)
|
||||
rb-inotify (>= 0.9)
|
||||
loofah (2.0.1)
|
||||
loofah (2.0.3)
|
||||
nokogiri (>= 1.5.9)
|
||||
lumberjack (1.0.9)
|
||||
mail (2.6.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)
|
||||
thread_safe (~> 0.3, >= 0.3.1)
|
||||
message_format (0.0.3)
|
||||
twitter_cldr (~> 3.1)
|
||||
method_source (0.8.2)
|
||||
mime-types (2.4.3)
|
||||
mime-types (2.99)
|
||||
mini_magick (4.2.0)
|
||||
mini_portile (0.6.2)
|
||||
minitest (5.5.1)
|
||||
multi_json (1.11.0)
|
||||
minitest (5.8.3)
|
||||
multi_json (1.11.2)
|
||||
multi_xml (0.5.5)
|
||||
multipart-post (2.0.0)
|
||||
naught (1.0.0)
|
||||
nenv (0.2.0)
|
||||
@ -216,7 +241,9 @@ GEM
|
||||
net-ssh (2.9.2)
|
||||
net-ssh-gateway (1.2.0)
|
||||
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)
|
||||
notiffany (0.0.6)
|
||||
nenv (~> 0.1)
|
||||
@ -225,8 +252,28 @@ GEM
|
||||
jbuilder (~> 2.0)
|
||||
rails (>= 4.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)
|
||||
pdf-core (0.5.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)
|
||||
coderay (~> 1.1.0)
|
||||
method_source (~> 0.8.1)
|
||||
@ -235,38 +282,41 @@ GEM
|
||||
rack (>= 1.1, < 2.0)
|
||||
pundit (1.0.0)
|
||||
activesupport (>= 3.0.0)
|
||||
rack (1.6.0)
|
||||
rack (1.6.4)
|
||||
rack-protection (1.5.3)
|
||||
rack
|
||||
rack-test (0.6.3)
|
||||
rack (>= 1.0)
|
||||
rails (4.2.1)
|
||||
actionmailer (= 4.2.1)
|
||||
actionpack (= 4.2.1)
|
||||
actionview (= 4.2.1)
|
||||
activejob (= 4.2.1)
|
||||
activemodel (= 4.2.1)
|
||||
activerecord (= 4.2.1)
|
||||
activesupport (= 4.2.1)
|
||||
railroady (1.4.0)
|
||||
rails (4.2.5)
|
||||
actionmailer (= 4.2.5)
|
||||
actionpack (= 4.2.5)
|
||||
actionview (= 4.2.5)
|
||||
activejob (= 4.2.5)
|
||||
activemodel (= 4.2.5)
|
||||
activerecord (= 4.2.5)
|
||||
activesupport (= 4.2.5)
|
||||
bundler (>= 1.3.0, < 2.0)
|
||||
railties (= 4.2.1)
|
||||
railties (= 4.2.5)
|
||||
sprockets-rails
|
||||
rails-deprecated_sanitizer (1.0.3)
|
||||
activesupport (>= 4.2.0.alpha)
|
||||
rails-dom-testing (1.0.6)
|
||||
rails-dom-testing (1.0.7)
|
||||
activesupport (>= 4.2.0.beta, < 5.0)
|
||||
nokogiri (~> 1.6.0)
|
||||
rails-deprecated_sanitizer (>= 1.0.1)
|
||||
rails-html-sanitizer (1.0.2)
|
||||
loofah (~> 2.0)
|
||||
rails-observers (0.1.2)
|
||||
activemodel (~> 4.0)
|
||||
rails_12factor (0.0.3)
|
||||
rails_serve_static_assets
|
||||
rails_stdout_logging
|
||||
rails_serve_static_assets (0.0.4)
|
||||
rails_stdout_logging (0.0.3)
|
||||
railties (4.2.1)
|
||||
actionpack (= 4.2.1)
|
||||
activesupport (= 4.2.1)
|
||||
railties (4.2.5)
|
||||
actionpack (= 4.2.5)
|
||||
activesupport (= 4.2.5)
|
||||
rake (>= 0.8.7)
|
||||
thor (>= 0.18.1, < 2.0)
|
||||
raindrops (0.13.0)
|
||||
@ -281,9 +331,13 @@ GEM
|
||||
redis (3.2.1)
|
||||
redis-namespace (1.5.2)
|
||||
redis (~> 3.0, >= 3.0.4)
|
||||
ref (1.0.5)
|
||||
ref (2.0.0)
|
||||
responders (2.1.0)
|
||||
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)
|
||||
rspec (3.2.0)
|
||||
rspec-core (~> 3.2.0)
|
||||
@ -324,7 +378,7 @@ GEM
|
||||
activerecord (~> 4)
|
||||
activesupport (~> 4)
|
||||
shellany (0.0.1)
|
||||
sidekiq (3.3.3)
|
||||
sidekiq (3.3.4)
|
||||
celluloid (>= 0.16.0)
|
||||
connection_pool (>= 2.1.1)
|
||||
json
|
||||
@ -340,19 +394,22 @@ GEM
|
||||
rack-protection (~> 1.4)
|
||||
tilt (>= 1.3, < 3)
|
||||
slop (3.6.0)
|
||||
spring (1.3.3)
|
||||
spring (1.3.5)
|
||||
spring-commands-rspec (1.0.4)
|
||||
spring (>= 0.9.1)
|
||||
sprockets (2.12.3)
|
||||
sprockets (2.12.4)
|
||||
hike (~> 1.2)
|
||||
multi_json (~> 1.0)
|
||||
rack (~> 1.0)
|
||||
tilt (~> 1.1, != 1.3.0)
|
||||
sprockets-rails (2.2.4)
|
||||
sprockets-rails (2.3.3)
|
||||
actionpack (>= 3.0)
|
||||
activesupport (>= 3.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)
|
||||
ref
|
||||
thor (0.19.1)
|
||||
@ -360,6 +417,7 @@ GEM
|
||||
tilt (1.4.1)
|
||||
timers (4.0.1)
|
||||
hitimes
|
||||
ttfunk (1.4.0)
|
||||
twitter (5.14.0)
|
||||
addressable (~> 2.3)
|
||||
buftok (~> 0.2.0)
|
||||
@ -373,6 +431,11 @@ GEM
|
||||
simple_oauth (~> 0.3.0)
|
||||
twitter-text (1.11.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)
|
||||
thread_safe (~> 0.1)
|
||||
uglifier (2.7.1)
|
||||
@ -385,6 +448,11 @@ GEM
|
||||
kgio (~> 2.6)
|
||||
rack
|
||||
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)
|
||||
rack (>= 1.0)
|
||||
web-console (2.1.2)
|
||||
@ -398,19 +466,23 @@ PLATFORMS
|
||||
|
||||
DEPENDENCIES
|
||||
aasm
|
||||
angularjs-rails
|
||||
actionpack-page_caching
|
||||
active_record_query_trace
|
||||
awesome_print
|
||||
bootstrap-sass
|
||||
byebug
|
||||
capistrano
|
||||
capistrano-maintenance (= 0.0.5)
|
||||
capistrano-sidekiq
|
||||
carrierwave
|
||||
chroma
|
||||
coffee-rails (~> 4.1.0)
|
||||
compass-rails (= 2.0.4)
|
||||
database_cleaner
|
||||
devise
|
||||
devise-async
|
||||
disqus_api
|
||||
elasticsearch-model
|
||||
elasticsearch-persistence
|
||||
elasticsearch-rails
|
||||
factory_girl_rails
|
||||
faker
|
||||
figaro
|
||||
@ -423,13 +495,22 @@ DEPENDENCIES
|
||||
jquery-rails
|
||||
kaminari
|
||||
letter_opener
|
||||
mandrill_dm!
|
||||
message_format
|
||||
mini_magick
|
||||
newrelic_rpm
|
||||
notify_with
|
||||
oj
|
||||
omniauth
|
||||
omniauth-oauth2
|
||||
pg
|
||||
prawn
|
||||
prawn-table
|
||||
protected_attributes
|
||||
puma
|
||||
pundit
|
||||
rails (= 4.2.1)
|
||||
railroady
|
||||
rails (= 4.2.5)
|
||||
rails-observers
|
||||
rails_12factor
|
||||
recurrence
|
||||
responders (~> 2.0)
|
||||
@ -444,9 +525,13 @@ DEPENDENCIES
|
||||
sinatra
|
||||
spring
|
||||
spring-commands-rspec
|
||||
therubyracer
|
||||
stripe (= 1.30.2)
|
||||
therubyracer (= 0.12.0)
|
||||
twitter
|
||||
twitter-text
|
||||
uglifier (>= 1.3.0)
|
||||
unicorn
|
||||
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
|
||||
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
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
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/>.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
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
|
||||
|
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.
|
||||
|
||||
The underlying technologies are:
|
||||
- `Ruby on Rails` for the backend application (server RESTful API)
|
||||
- `AngularJS` for the frontend application (web-based graphical user interface)
|
||||
##### Table of Contents
|
||||
1. [Software stack](#software-stack)
|
||||
2. [Contributing](#contributing)
|
||||
3. [Setup a development environment](#setup-a-development-environment)
|
||||
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
|
||||
- `mandrill` -> change this if you're using a different mailing system
|
||||
|
||||
- config/environments/staging.rb
|
||||
- `config.action_mailer.default_url_options` -> change the URL according to the staging deployment url
|
||||
- `mandrill` -> change this if you're using a different mailing system
|
||||
- Ubuntu/Debian
|
||||
- Ruby 2.2.3
|
||||
- Git 1.9.1+
|
||||
- Redis 2.8.4+
|
||||
- Sidekiq 3.3.4+
|
||||
- Elasticsearch 1.7
|
||||
- PostgreSQL 9.4
|
||||
|
||||
- config/application.yml
|
||||
- `DEVISE_KEY` -> generate any secret phrase to secure the Devise authentication. You can use the `$ rake secret` command for this purpose.
|
||||
- `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)
|
||||
<a name="contributing"></a>
|
||||
## Contributing
|
||||
|
||||
- config/mandrill.rb
|
||||
You may change this if you don't want to use mandrill as your production mailing system
|
||||
Contributions are welcome. Please read [the contribution guidelines](CONTRIBUTING.md) for more information about the contribution process.
|
||||
|
||||
- config/database.yml.default
|
||||
Copy/Paste this file to `config/database.yml` and modify the configuration according to your postgreSQL configuration
|
||||
**IMPORTANT**: **do not** update Arshaw/fullCalendar.js as it contains a hack for the remove-event cross.
|
||||
|
||||
- config/disqus_api.yml
|
||||
Insert here your identifiers for the Disqus API
|
||||
<a name="setup-a-development-environment"></a>
|
||||
## Setup a development environment
|
||||
|
||||
<a name="general-guidelines"></a>
|
||||
### General Guidelines
|
||||
|
||||
|
||||
## 2. Setup a development environment
|
||||
|
||||
1. Install RVM with latest ruby version
|
||||
See http://rvm.io/rvm/install
|
||||
1. Install RVM with the ruby version specified in the [.ruby-version file](.ruby-version).
|
||||
For more details about the process, Please read the [official RVM documentation](http://rvm.io/rvm/install).
|
||||
|
||||
2. Retrieve the project from Git
|
||||
`$ git clone git@github.com:LaCasemate/fab-manager.git`
|
||||
|
||||
3. Install the dependencies
|
||||
- Ubuntu: `$ sudo apt-get install libpq-dev postgresql redis-server imagemagick`
|
||||
- MacOS: `$ brew install postgresql redis imagemagick`
|
||||
```bash
|
||||
git clone https://github.com/LaCasemate/fab-manager.git
|
||||
```
|
||||
|
||||
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
|
||||
```
|
||||
$ cd fab-manager
|
||||
$ rvm current
|
||||
```
|
||||
```bash
|
||||
brew install postgresql redis imagemagick
|
||||
```
|
||||
|
||||
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
|
||||
`$ bundle install`
|
||||
5. Install bundler in the current RVM gemset
|
||||
|
||||
```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`
|
||||
`$ rake db:setup`
|
||||
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.
|
||||
|
||||
```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`
|
||||
`$ mkdir -p tmp/pids`
|
||||
8. Create the pids folder used by Sidekiq. If you want to use a different location, you can configure it in `config/sidekiq.yml`
|
||||
|
||||
```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`
|
||||
|
||||
9. Start the development web server
|
||||
`$ foreman s -p 3000`
|
||||
10. Start the development web server
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
$ ln -sfv /usr/local/opt/postgresql/*.plist ~/Library/LaunchAgents
|
||||
$ launchctl load ~/Library/LaunchAgents/homebrew.mxcl.postgresql.plist
|
||||
|
||||
The first command will start postgresql at login with launchd. The second will load postgresql now.
|
||||
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)
|
||||
- `deb http://apt.postgresql.org/pub/repos/apt/ jessie-pgdg main` (Debian 8 Jessie)
|
||||
|
||||
|
||||
### 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
|
||||
`$ sudo -i -u postgres`
|
||||
|
||||
2. Run the postgreSQL administration command line interface
|
||||
`$ psql`
|
||||
```bash
|
||||
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")
|
||||
`# CREATE USER sleede;`
|
||||
3. Create a new user in PostgreSQL (in this example, the user will be named `sleede`)
|
||||
|
||||
```sql
|
||||
CREATE USER sleede;
|
||||
```
|
||||
|
||||
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
|
||||
`# CREATE DATABASE fabmanager_development OWNER sleede;`
|
||||
5. Then, create the fablab_development and fablab_test databases
|
||||
|
||||
```sql
|
||||
CREATE DATABASE fablab_development OWNER sleede;
|
||||
CREATE DATABASE fablab_test OWNER sleede;
|
||||
```
|
||||
|
||||
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
|
||||
Uncaught exception: FATAL: authentification peer échouée pour l'utilisateur « USERNAME »
|
||||
Exiting
|
||||
.rvm/gems/ruby-2.2.1@fabmanager/gems/activerecord-4.2.1/lib/active_record/connection_adapters/postgresql_adapter.rb:651:in `initialize'
|
||||
...
|
||||
```
|
||||
|
||||
To solve this issue, edit your `/etc/postgresql/9.4/main/pg_hba.conf` as root and replace the following:
|
||||
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.
|
||||
|
||||
In FabManager, it is used for the admin's statistics module and to perform searches in projects.
|
||||
|
||||
<a name="elasticsearch-on-debian"></a>
|
||||
### Install ElasticSearch on Ubuntu/Debian
|
||||
|
||||
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
|
||||
# comment over or replace...
|
||||
local all all peer
|
||||
# ...by the following:
|
||||
local all all trust
|
||||
sudo apt-get install openjdk-8-jre
|
||||
```
|
||||
|
||||
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
|
||||
- Angular-Bootstrap: http://angular-ui.github.io/bootstrap/
|
||||
<a name="i18n-apply"></a>
|
||||
#### 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
|
||||
- French translation is available on the branches [master](../../tree/master) and [dev](../../tree/dev)
|
||||
- English translation is available on the branch [english](../../tree/english)
|
||||
<a name="known-issues"></a>
|
||||
## Known issues
|
||||
|
||||
- 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',
|
||||
'ngUpload', 'duScroll', 'application.filters','application.services', 'application.directives',
|
||||
'application.constants', 'application.controllers', 'application.router', 'ui.select2', 'angularMoment',
|
||||
'Devise', 'DeviseModal', 'angular-growl', 'xeditable', 'checklist-model', 'unsavedChanges', 'angular-loading-bar',
|
||||
'ngTouch', 'angular-google-analytics', 'angularUtils.directives.dirDisqus', 'summernote']).
|
||||
config(['$locationProvider', '$httpProvider', 'AuthProvider', "growlProvider", "unsavedWarningsConfigProvider", "AnalyticsProvider", "datepickerPopupConfig",
|
||||
function($locationProvider, $httpProvider, AuthProvider, growlProvider, unsavedWarningsConfigProvider, AnalyticsProvider, datepickerPopupConfig) {
|
||||
'frapontillo.bootstrap-switch', 'application.constants', 'application.controllers', 'application.router',
|
||||
'ui.select', 'ui.calendar', 'angularMoment', 'Devise', 'DeviseModal', 'angular-growl', 'xeditable',
|
||||
'checklist-model', 'unsavedChanges', 'angular-loading-bar', 'ngTouch', 'angular-google-analytics',
|
||||
'angularUtils.directives.dirDisqus', 'summernote', 'elasticsearch', 'angular-medium-editor', 'naif.base64',
|
||||
'minicolors', 'pascalprecht.translate']).
|
||||
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 %>
|
||||
AnalyticsProvider.setAccount('<%= ENV["GOOGLE_ANALYTICS_ACCOUNT"] %>');
|
||||
// Google analytics
|
||||
<% if Rails.env.production? %>
|
||||
AnalyticsProvider.setAccount(Fablab.gaId);
|
||||
// track all routes (or not)
|
||||
AnalyticsProvider.trackPages(true);
|
||||
AnalyticsProvider.setDomainName('<%= ENV["APPLICATION_ROOT_URL"] %>');
|
||||
AnalyticsProvider.setDomainName(Fablab.defaultHost);
|
||||
AnalyticsProvider.useAnalytics(true);
|
||||
AnalyticsProvider.setPageEvent('$stateChangeSuccess');
|
||||
<% else %>
|
||||
AnalyticsProvider.setAccount('DISABLED');
|
||||
AnalyticsProvider.setAccount('DISABLED');
|
||||
<% end %>
|
||||
|
||||
datepickerPopupConfig.closeText = "Fermer";
|
||||
datepickerPopupConfig.cleartext = "Effacer";
|
||||
datepickerPopupConfig.currentText = "Aujourd'hui";
|
||||
// Custom messages for the date-picker widget
|
||||
uibDatepickerPopupConfig.closeText = Fablab.translations.app.shared.buttons.close;
|
||||
uibDatepickerPopupConfig.clearText = Fablab.translations.app.shared.buttons.clear;
|
||||
uibDatepickerPopupConfig.currentText = Fablab.translations.app.shared.buttons.today;
|
||||
|
||||
// custom message for angular-unsavedChanges
|
||||
unsavedWarningsConfigProvider.navigateMessage = "Vous perdrez les modifications non enregistrées si vous quittez cette page";
|
||||
unsavedWarningsConfigProvider.reloadMessage = "Vous perdrez les modifications non enregistrées si vous rechargez cette page";
|
||||
// Custom messages for angular-unsavedChanges
|
||||
unsavedWarningsConfigProvider.navigateMessage = Fablab.translations.app.shared.messages.you_will_lose_any_unsaved_modification_if_you_quit_this_page;
|
||||
unsavedWarningsConfigProvider.reloadMessage = Fablab.translations.app.shared.messages.you_will_lose_any_unsaved_modification_if_you_reload_this_page;
|
||||
|
||||
// Set how long the popup messages (growl) will remain
|
||||
growlProvider.globalTimeToLive(5000);
|
||||
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';
|
||||
|
||||
// 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){
|
||||
$state.prevState = fromState;
|
||||
$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){
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@ -67,8 +92,9 @@ config(['$locationProvider', '$httpProvider', 'AuthProvider', "growlProvider", "
|
||||
$state.go($state.prevState, $state.prevParams);
|
||||
};
|
||||
|
||||
// Configuration of the summernote editor (used in project edition)
|
||||
$rootScope.summernoteOpts = {
|
||||
lang: 'fr-FR',
|
||||
lang: Fablab.summernote_locale,
|
||||
height: 200,
|
||||
toolbar: [
|
||||
['style', ['style']],
|
||||
@ -85,25 +111,16 @@ config(['$locationProvider', '$httpProvider', 'AuthProvider', "growlProvider", "
|
||||
maximumImageFileSize: 4096
|
||||
};
|
||||
|
||||
}]).filter('array', function() {
|
||||
return function(arrayLength) {
|
||||
if (arrayLength) {
|
||||
arrayLength = Math.ceil(arrayLength);
|
||||
var arr = new Array(arrayLength), i = 0;
|
||||
for (; i < arrayLength; i++) {
|
||||
arr[i] = i;
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
};
|
||||
}).directive('datepickerPopup', function (){
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
});
|
||||
// Prevent the usage of the application for members with incomplete profiles: they will be redirected to
|
||||
// the 'profile completion' page. This is especially useful for user's accounts imported through SSO.
|
||||
$rootScope.$on('$stateChangeStart', function (event, toState) {
|
||||
Auth.currentUser().then(function(currentUser) {
|
||||
if (currentUser.need_completion && toState.name != 'app.logged.profileCompletion') {
|
||||
$state.go('app.logged.profileCompletion');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}]).constant('angularMomentConfig', {
|
||||
timezone: Fablab.timezone
|
||||
});
|
||||
|
@ -20,7 +20,6 @@
|
||||
//= require jquery-ui/ui/jquery.ui.droppable
|
||||
//= require jquery-ui/ui/jquery.ui.resizable
|
||||
//= require angular
|
||||
//= require angular-i18n/angular-locale_fr-fr.js
|
||||
//= require angular-cookies
|
||||
//= require angular-resource
|
||||
//= require angular-sanitize
|
||||
@ -29,18 +28,18 @@
|
||||
//= require angular-touch
|
||||
//= require angular-ui-router/release/angular-ui-router
|
||||
//= require angular-bootstrap/ui-bootstrap-tpls
|
||||
//= require select2/select2
|
||||
//= require select2/select2_locale_fr
|
||||
//= require angular-ui-select2/src/select2
|
||||
//= require angular-ui-select/dist/select
|
||||
//= 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 ngUpload/ng-upload
|
||||
//= require jasny-bootstrap/js/fileinput
|
||||
//= require holderjs/holder
|
||||
//= require angular-devise/lib/devise
|
||||
//= require devise-modal
|
||||
//= require angular-growl/build/angular-growl
|
||||
//= require angular-growl-v2/build/angular-growl
|
||||
//= require angular-xeditable/dist/js/xeditable
|
||||
//= require checklist-model/checklist-model
|
||||
//= require angular-unsavedChanges/src/unsavedChanges
|
||||
@ -50,13 +49,25 @@
|
||||
//= require dirDisqus
|
||||
//= require humanize
|
||||
//= require underscore/underscore
|
||||
//= require elasticsearch/elasticsearch.angular
|
||||
//= require d3/d3
|
||||
//= require nvd3/build/nv.d3.js
|
||||
//= require app
|
||||
//= 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 ./services
|
||||
//= require_tree ./directives
|
||||
//= 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.deleteFile(file)
|
||||
# - $scope.fileinputClass(v)
|
||||
# - $scope.openStartDatePicker($event)
|
||||
# - $scope.openEndDatePicker($event)
|
||||
# - $scope.toggleStartDatePicker($event)
|
||||
# - $scope.toggleEndDatePicker($event)
|
||||
# - $scope.toggleRecurrenceEnd(e)
|
||||
#
|
||||
# Requires :
|
||||
@ -23,7 +23,7 @@
|
||||
# - $state (Ui-Router) [ 'app.public.events_list' ]
|
||||
##
|
||||
class EventsController
|
||||
constructor: ($scope, $state, Event, Category) ->
|
||||
constructor: ($scope, $state, $locale, Event, Category) ->
|
||||
|
||||
## Retrieve the list of categories from the server (stage, atelier, ...)
|
||||
Category.query().$promise.then (data)->
|
||||
@ -33,12 +33,12 @@ class EventsController
|
||||
|
||||
## default parameters for AngularUI-Bootstrap datepicker
|
||||
$scope.datePicker =
|
||||
format: 'dd/MM/yyyy'
|
||||
format: $locale.DATETIME_FORMATS.shortDate
|
||||
startOpened: false # default: datePicker is not shown
|
||||
endOpened: false
|
||||
recurrenceEndOpened: false
|
||||
options:
|
||||
startingDay: 1 # France: the week starts on monday
|
||||
startingDay: Fablab.weekStartingDay
|
||||
|
||||
|
||||
|
||||
@ -136,21 +136,20 @@ class EventsController
|
||||
##
|
||||
# Controller used in the events listing page (admin view)
|
||||
##
|
||||
Application.Controllers.controller "adminEventsController", ["$scope", "$state", 'Event', ($scope, $state, Event) ->
|
||||
Application.Controllers.controller "AdminEventsController", ["$scope", "$state", 'Event', 'eventsPromise', ($scope, $state, Event, eventsPromise) ->
|
||||
|
||||
|
||||
|
||||
### PUBLIC SCOPE ###
|
||||
|
||||
## The events displayed on the page
|
||||
$scope.events = []
|
||||
|
||||
## By default, the pagination mode is activated to limit the page size
|
||||
$scope.paginateActive = true
|
||||
|
||||
## The currently displayed page number
|
||||
$scope.page = 1
|
||||
## The events displayed on the page
|
||||
$scope.events = eventsPromise
|
||||
|
||||
## Current virtual page
|
||||
$scope.page = 2
|
||||
|
||||
##
|
||||
# Adds a bucket of events to the bottom of the page, grouped by month
|
||||
@ -158,10 +157,7 @@ Application.Controllers.controller "adminEventsController", ["$scope", "$state",
|
||||
$scope.loadMoreEvents = ->
|
||||
Event.query {page: $scope.page}, (data)->
|
||||
$scope.events = $scope.events.concat data
|
||||
if data.length
|
||||
$scope.paginateActive = false if $scope.events.length >= data[0].nb_total_events
|
||||
else
|
||||
$scope.paginateActive = false
|
||||
paginationCheck(data, $scope.events)
|
||||
$scope.page += 1
|
||||
|
||||
|
||||
@ -172,10 +168,40 @@ Application.Controllers.controller "adminEventsController", ["$scope", "$state",
|
||||
# Kind of constructor: these actions will be realized first when the controller is loaded
|
||||
##
|
||||
initialize = ->
|
||||
$scope.loadMoreEvents()
|
||||
paginationCheck(eventsPromise, $scope.events)
|
||||
|
||||
## !!! MUST BE CALLED AT THE END of the controller
|
||||
|
||||
##
|
||||
# Check if all events are already displayed OR if the button 'load more events'
|
||||
# is required
|
||||
# @param lastEvents {Array} last events loaded onto the diplay (ie. last "page")
|
||||
# @param events {Array} full list of events displayed on the page (not only the last retrieved)
|
||||
##
|
||||
paginationCheck = (lastEvents, events)->
|
||||
if lastEvents.length > 0
|
||||
$scope.paginateActive = false if events.length >= lastEvents[0].nb_total_events
|
||||
else
|
||||
$scope.paginateActive = false
|
||||
|
||||
|
||||
|
||||
# init the controller (call at the end !)
|
||||
initialize()
|
||||
|
||||
]
|
||||
|
||||
|
||||
|
||||
##
|
||||
# Controller used in the reservations listing page for a specific event
|
||||
##
|
||||
Application.Controllers.controller "ShowEventReservationsController", ["$scope", 'eventPromise', 'reservationsPromise', ($scope, eventPromise, reservationsPromise) ->
|
||||
|
||||
## retrieve the event from the ID provided in the current URL
|
||||
$scope.event = eventPromise
|
||||
|
||||
## list of reservations for the current event
|
||||
$scope.reservations = reservationsPromise
|
||||
]
|
||||
|
||||
|
||||
@ -183,7 +209,8 @@ Application.Controllers.controller "adminEventsController", ["$scope", "$state",
|
||||
##
|
||||
# Controller used in the event creation page
|
||||
##
|
||||
Application.Controllers.controller "newEventController", ["$scope", "$state", 'Event', 'Category', 'CSRF', ($scope, $state, Event, Category, CSRF) ->
|
||||
Application.Controllers.controller "NewEventController", ["$scope", "$state", "$locale", 'Event', 'Category', 'CSRF', '_t'
|
||||
, ($scope, $state, $locale, Event, Category, CSRF, _t) ->
|
||||
CSRF.setMetaTags()
|
||||
|
||||
## API URL where the form will be posted
|
||||
@ -204,15 +231,18 @@ Application.Controllers.controller "newEventController", ["$scope", "$state", 'E
|
||||
|
||||
## Possible types of recurrences for an event
|
||||
$scope.recurrenceTypes = [
|
||||
{label: 'Aucune', value: 'none'},
|
||||
{label: 'Tous les jours', value: 'day'},
|
||||
{label: 'Chaque semaine', value: 'week'},
|
||||
{label: 'Chaque mois', value: 'month'},
|
||||
{label: 'Chaque année', value: 'year'}
|
||||
{label: _t('none'), value: 'none'},
|
||||
{label: _t('every_days'), value: 'day'},
|
||||
{label: _t('every_week'), value: 'week'},
|
||||
{label: _t('every_month'), value: 'month'},
|
||||
{label: _t('every_year'), value: 'year'}
|
||||
]
|
||||
|
||||
## currency symbol for the current locale (cf. angular-i18n)
|
||||
$scope.currencySymbol = $locale.NUMBER_FORMATS.CURRENCY_SYM;
|
||||
|
||||
## Using the EventsController
|
||||
new EventsController($scope, $state, Event, Category)
|
||||
new EventsController($scope, $state, $locale, Event, Category)
|
||||
]
|
||||
|
||||
|
||||
@ -220,8 +250,12 @@ Application.Controllers.controller "newEventController", ["$scope", "$state", 'E
|
||||
##
|
||||
# Controller used in the events edition page
|
||||
##
|
||||
Application.Controllers.controller "editEventController", ["$scope", "$state", "$stateParams", 'Event', 'Category', 'CSRF', ($scope, $state, $stateParams, Event, Category, CSRF) ->
|
||||
CSRF.setMetaTags()
|
||||
Application.Controllers.controller "EditEventController", ["$scope", "$state", "$stateParams", "$locale", 'Event', 'Category', 'CSRF', 'eventPromise'
|
||||
, ($scope, $state, $stateParams, $locale, Event, Category, CSRF, eventPromise) ->
|
||||
|
||||
### PUBLIC SCOPE ###
|
||||
|
||||
|
||||
|
||||
## API URL where the form will be posted
|
||||
$scope.actionUrl = "/api/events/" + $stateParams.id
|
||||
@ -230,13 +264,32 @@ Application.Controllers.controller "editEventController", ["$scope", "$state", "
|
||||
$scope.method = 'put'
|
||||
|
||||
## Retrieve the event details, in case of error the user is redirected to the events listing
|
||||
Event.get {id: $stateParams.id}
|
||||
, (event)->
|
||||
$scope.event = event
|
||||
return
|
||||
, ->
|
||||
$state.go('app.public.events_list')
|
||||
$scope.event = eventPromise
|
||||
|
||||
## Using the EventsController
|
||||
new EventsController($scope, $state, Event, Category)
|
||||
## currency symbol for the current locale (cf. angular-i18n)
|
||||
$scope.currencySymbol = $locale.NUMBER_FORMATS.CURRENCY_SYM;
|
||||
|
||||
|
||||
|
||||
### PRIVATE SCOPE ###
|
||||
|
||||
|
||||
|
||||
##
|
||||
# Kind of constructor: these actions will be realized first when the controller is loaded
|
||||
##
|
||||
initialize = ->
|
||||
CSRF.setMetaTags()
|
||||
|
||||
# init the dates to JS objects
|
||||
$scope.event.start_date = moment($scope.event.start_date).toDate()
|
||||
$scope.event.end_date = moment($scope.event.end_date).toDate()
|
||||
|
||||
## Using the EventsController
|
||||
new EventsController($scope, $state, $locale, Event, Category)
|
||||
|
||||
|
||||
|
||||
## !!! MUST BE CALLED AT THE END of the controller
|
||||
initialize()
|
||||
]
|
||||
|
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'
|
||||
|
||||
Application.Controllers.controller "projectElementsController", ["$scope", "$state", 'Component', 'Licence', 'Theme', ($scope, $state, Component, Licence, Theme) ->
|
||||
Application.Controllers.controller "ProjectElementsController", ["$scope", "$state", 'Component', 'Licence', 'Theme', 'componentsPromise', 'licencesPromise', 'themesPromise'
|
||||
, ($scope, $state, Component, Licence, Theme, componentsPromise, licencesPromise, themesPromise) ->
|
||||
|
||||
## Materials list (plastic, wood ...)
|
||||
$scope.components = Component.query()
|
||||
$scope.components = componentsPromise
|
||||
|
||||
## Licences list (Creative Common ...)
|
||||
$scope.licences = Licence.query()
|
||||
$scope.licences = licencesPromise
|
||||
|
||||
## Themes list (cooking, sport ...)
|
||||
$scope.themes = Theme.query()
|
||||
|
||||
|
||||
$scope.themes = themesPromise
|
||||
|
||||
##
|
||||
# Saves a new component / Update an existing material to the server (form validation callback)
|
||||
@ -153,5 +152,3 @@ Application.Controllers.controller "projectElementsController", ["$scope", "$sta
|
||||
else
|
||||
$scope.licences.splice(index, 1)
|
||||
]
|
||||
|
||||
|
||||
|
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'
|
||||
|
||||
Application.Controllers.controller 'ApplicationController', ["$rootScope", "$scope", "Session", "AuthService", "Auth", "$modal", "$state", 'growl', 'Notification', '$interval', ($rootScope, $scope, Session, AuthService, Auth, $modal, $state, growl, Notification, $interval) ->
|
||||
Application.Controllers.controller 'ApplicationController', ["$rootScope", "$scope", "$window", "Session", "AuthService", "Auth", "$uibModal", "$state", 'growl', 'Notification', '$interval', "Setting", '$locale', '_t'
|
||||
, ($rootScope, $scope, $window, Session, AuthService, Auth, $uibModal, $state, growl, Notification, $interval, Setting, $locale, _t) ->
|
||||
|
||||
|
||||
|
||||
@ -18,14 +19,14 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
|
||||
# @param user {Object} Rails/Devise user
|
||||
##
|
||||
$scope.setCurrentUser = (user) ->
|
||||
$scope.currentUser = user
|
||||
$rootScope.currentUser = user
|
||||
Session.create(user);
|
||||
getNotifications()
|
||||
|
||||
|
||||
##
|
||||
# Login callback
|
||||
# @param e {Object} jQuery event
|
||||
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
|
||||
# @param callback {function}
|
||||
##
|
||||
$scope.login = (e, callback) ->
|
||||
@ -36,14 +37,14 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
|
||||
|
||||
##
|
||||
# Logout callback
|
||||
# @param e {Object} jQuery event
|
||||
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
|
||||
##
|
||||
$scope.logout = (e) ->
|
||||
e.preventDefault()
|
||||
Auth.logout().then (oldUser) ->
|
||||
# console.log(oldUser.name + " you're signed out now.");
|
||||
Session.destroy()
|
||||
$scope.currentUser = null
|
||||
$rootScope.currentUser = null
|
||||
$rootScope.toCheckNotifications = false
|
||||
$scope.notifications = []
|
||||
$state.go('app.public.home')
|
||||
@ -54,21 +55,21 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
|
||||
|
||||
##
|
||||
# Open the modal window allowing the user to create an account.
|
||||
# @param e {Object} jQuery event
|
||||
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
|
||||
##
|
||||
$scope.signup = (e) ->
|
||||
e.preventDefault() if e
|
||||
|
||||
$modal.open
|
||||
$uibModal.open
|
||||
templateUrl: '<%= asset_path "shared/signupModal.html" %>'
|
||||
size: 'md'
|
||||
controller: ['$scope', '$modalInstance', 'Group', ($scope, $modalInstance, Group) ->
|
||||
controller: ['$scope', '$uibModalInstance', 'Group', 'CustomAsset', ($scope, $uibModalInstance, Group, CustomAsset) ->
|
||||
# default parameters for the date picker in the account creation modal
|
||||
$scope.datePicker =
|
||||
format: 'dd/MM/yyyy'
|
||||
format: $locale.DATETIME_FORMATS.shortDate
|
||||
opened: false
|
||||
options:
|
||||
startingDay: 1
|
||||
startingDay: Fablab.weekStartingDay
|
||||
|
||||
# callback to open the date picker (account creation modal)
|
||||
$scope.openDatePicker = ($event) ->
|
||||
@ -80,6 +81,10 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
|
||||
Group.query (groups) ->
|
||||
$scope.groups = groups
|
||||
|
||||
# retrieve the CGU
|
||||
CustomAsset.get {name: 'cgu-file'}, (cgu) ->
|
||||
$scope.cgu = cgu.custom_asset
|
||||
|
||||
# default user's parameters
|
||||
$scope.user =
|
||||
is_allow_contact: true
|
||||
@ -95,7 +100,7 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
|
||||
$scope.alerts = []
|
||||
Auth.register($scope.user).then (user) ->
|
||||
# creation successful
|
||||
$modalInstance.close(user)
|
||||
$uibModalInstance.close(user)
|
||||
, (error) ->
|
||||
# creation failed...
|
||||
angular.forEach error.data.errors, (v, k) ->
|
||||
@ -114,10 +119,10 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
|
||||
# @param token {string} security token for password changing. The user should have recieved it by mail
|
||||
##
|
||||
$scope.editPassword = (token) ->
|
||||
$modal.open
|
||||
$uibModal.open
|
||||
templateUrl: '<%= asset_path "shared/passwordEditModal.html" %>'
|
||||
size: 'md'
|
||||
controller: ['$scope', '$modalInstance', '$http', ($scope, $modalInstance, $http) ->
|
||||
controller: ['$scope', '$uibModalInstance', '$http', '_t', ($scope, $uibModalInstance, $http, _t) ->
|
||||
$scope.user =
|
||||
reset_password_token: token
|
||||
$scope.alerts = []
|
||||
@ -127,7 +132,7 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
|
||||
$scope.changePassword = ->
|
||||
$scope.alerts = []
|
||||
$http.put('/users/password.json', {user: $scope.user}).success (data) ->
|
||||
$modalInstance.close()
|
||||
$uibModalInstance.close()
|
||||
.error (data) ->
|
||||
angular.forEach data.errors, (v, k) ->
|
||||
angular.forEach v, (err) ->
|
||||
@ -136,20 +141,20 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
|
||||
type: 'danger'
|
||||
]
|
||||
.result['finally'](null).then (user) ->
|
||||
growl.addInfoMessage('Votre mot de passe a bien été modifié.')
|
||||
growl.success(_t('your_password_was_successfully_changed'))
|
||||
Auth.login().then (user) ->
|
||||
$scope.setCurrentUser(user)
|
||||
, (error) ->
|
||||
# Authentication failed...
|
||||
|
||||
|
||||
|
||||
##
|
||||
# Compact/Expend the width of the left navigation bar
|
||||
# @param e {Object} jQuery event object
|
||||
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
|
||||
##
|
||||
$scope.toggleNavSize = (event) ->
|
||||
if typeof event == 'undefined'
|
||||
console.error '[applicationController::toggleNavSize] Missing event parameter'
|
||||
console.error '[ApplicationController::toggleNavSize] Missing event parameter'
|
||||
return
|
||||
|
||||
toggler = $(event.target)
|
||||
@ -184,14 +189,16 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
|
||||
|
||||
|
||||
### PRIVATE SCOPE ###
|
||||
|
||||
##
|
||||
# Kind of constructor: these actions will be realized first when the controller is loaded
|
||||
##
|
||||
initialize = ->
|
||||
|
||||
# try to retrieve any currently logged user
|
||||
Auth.login().then (user) ->
|
||||
$scope.setCurrentUser(user)
|
||||
if user.need_completion
|
||||
$state.transitionTo('app.logged.profileCompletion')
|
||||
, (error) ->
|
||||
# Authentication failed...
|
||||
$rootScope.toCheckNotifications = false
|
||||
@ -205,11 +212,17 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
|
||||
event.preventDefault()
|
||||
if AuthService.isAuthenticated()
|
||||
# user is not allowed
|
||||
console.log('user is not allowed')
|
||||
console.error('[ApplicationController::initialize] user is not allowed')
|
||||
else
|
||||
# user is not logged in
|
||||
openLoginModal(toState, toParams)
|
||||
|
||||
Setting.get { name: 'fablab_name' }, (data)->
|
||||
$scope.fablabName = data.setting.value
|
||||
Setting.get { name: 'name_genre' }, (data)->
|
||||
$scope.nameGenre = data.setting.value
|
||||
|
||||
|
||||
|
||||
# shorthands
|
||||
$scope.isAuthenticated = Auth.isAuthenticated;
|
||||
@ -223,7 +236,7 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
|
||||
##
|
||||
getNotifications = ->
|
||||
$rootScope.toCheckNotifications = true
|
||||
unless $rootScope.checkNotificationsIsInit or !$scope.currentUser
|
||||
unless $rootScope.checkNotificationsIsInit or !$rootScope.currentUser
|
||||
$scope.notifications = Notification.query {is_read: false}
|
||||
$scope.$watch 'notifications', (newValue, oldValue) ->
|
||||
diff = []
|
||||
@ -239,7 +252,7 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
|
||||
|
||||
|
||||
angular.forEach diff, (notification, key) ->
|
||||
growl.addInfoMessage(notification.message.description)
|
||||
growl.info(notification.message.description)
|
||||
|
||||
, true
|
||||
|
||||
@ -257,35 +270,39 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
|
||||
# Open the modal window allowing the user to log in.
|
||||
##
|
||||
openLoginModal = (toState, toParams, callback) ->
|
||||
$modal.open
|
||||
<% active_provider = AuthProvider.active %>
|
||||
<% if active_provider.providable_type != DatabaseProvider.name %>
|
||||
$window.location.href = '<%=user_omniauth_authorize_path(AuthProvider.active.strategy_name.to_sym)%>'
|
||||
<% else %>
|
||||
$uibModal.open
|
||||
templateUrl: '<%= asset_path "shared/deviseModal.html" %>'
|
||||
size: 'sm'
|
||||
controller: ['$scope', '$modalInstance', ($scope, $modalInstance) ->
|
||||
controller: ['$scope', '$uibModalInstance', '_t', ($scope, $uibModalInstance, _t) ->
|
||||
user = $scope.user = {}
|
||||
$scope.login = () ->
|
||||
Auth.login(user).then (user) ->
|
||||
# Authentification succeeded ...
|
||||
$modalInstance.close(user)
|
||||
$uibModalInstance.close(user)
|
||||
if callback and typeof callback is "function"
|
||||
callback(user)
|
||||
, (error) ->
|
||||
# Authentication failed...
|
||||
$scope.alerts = []
|
||||
$scope.alerts.push
|
||||
msg: 'E-mail ou mot de passe incorrect.'
|
||||
msg: _t('wrong_email_or_password')
|
||||
type: 'danger'
|
||||
|
||||
# handle modal behaviors. The provided reason will be used to define the following actions
|
||||
$scope.dismiss = ->
|
||||
$modalInstance.dismiss('cancel')
|
||||
$uibModalInstance.dismiss('cancel')
|
||||
|
||||
$scope.openSignup = (e) ->
|
||||
e.preventDefault()
|
||||
$modalInstance.dismiss('signup')
|
||||
$uibModalInstance.dismiss('signup')
|
||||
|
||||
$scope.openResetPassword = (e) ->
|
||||
e.preventDefault()
|
||||
$modalInstance.dismiss('resetPassword')
|
||||
$uibModalInstance.dismiss('resetPassword')
|
||||
]
|
||||
|
||||
# what to do when the modal is closed
|
||||
@ -303,25 +320,26 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
|
||||
$scope.signup()
|
||||
else if reason is 'resetPassword'
|
||||
# open the 'reset password' modal
|
||||
$modal.open
|
||||
$uibModal.open
|
||||
templateUrl: '<%= asset_path "shared/passwordNewModal.html" %>'
|
||||
size: 'sm'
|
||||
controller: ['$scope', '$modalInstance', '$http', ($scope, $modalInstance, $http) ->
|
||||
controller: ['$scope', '$uibModalInstance', '$http', ($scope, $uibModalInstance, $http) ->
|
||||
$scope.user = {email: ''}
|
||||
$scope.sendReset = () ->
|
||||
$scope.alerts = []
|
||||
$http.post('/users/password.json', {user: $scope.user}).success ->
|
||||
$modalInstance.close()
|
||||
$uibModalInstance.close()
|
||||
.error ->
|
||||
$scope.alerts.push
|
||||
msg: "Votre adresse email n'existe pas."
|
||||
msg: _t('your_email_address_is_unknown')
|
||||
type: 'danger'
|
||||
|
||||
]
|
||||
.result['finally'](null).then ->
|
||||
growl.addInfoMessage('Vous allez recevoir sous quelques minutes un e-mail vous indiquant comment réinitialiser votre mot de passe.')
|
||||
growl.info(_t('you_will_receive_in_a_moment_an_email_with_instructions_to_reset_your_password'))
|
||||
|
||||
# otherwise the user just closed the modal
|
||||
<% end %>
|
||||
|
||||
|
||||
|
||||
|
@ -1,43 +1,7 @@
|
||||
'use strict'
|
||||
|
||||
##
|
||||
# Controller used on the private projects listing page (my dashboard/projects)
|
||||
##
|
||||
Application.Controllers.controller "dashboardProjectsController", ["$scope", 'Member', ($scope, Member) ->
|
||||
Application.Controllers.controller "DashboardController", ["$scope", 'memberPromise', ($scope, memberPromise) ->
|
||||
|
||||
## Current user's profile
|
||||
$scope.user = Member.get {id: $scope.currentUser.id}
|
||||
]
|
||||
|
||||
|
||||
|
||||
##
|
||||
# Controller used on the personal trainings page (my dashboard/trainings)
|
||||
##
|
||||
Application.Controllers.controller "dashboardTrainingsController", ["$scope", 'Member', ($scope, Member) ->
|
||||
|
||||
## Current user's profile
|
||||
$scope.user = Member.get {id: $scope.currentUser.id}
|
||||
]
|
||||
|
||||
|
||||
|
||||
##
|
||||
# Controller used on the private events page (my dashboard/events)
|
||||
##
|
||||
Application.Controllers.controller "dashboardEventsController", ["$scope", 'Member', ($scope, Member) ->
|
||||
|
||||
## Current user's profile
|
||||
$scope.user = Member.get {id: $scope.currentUser.id}
|
||||
]
|
||||
|
||||
|
||||
|
||||
##
|
||||
# Controller used on the personal invoices listing page (my dashboard/invoices)
|
||||
##
|
||||
Application.Controllers.controller "dashboardInvoicesController", ["$scope", 'Member', ($scope, Member) ->
|
||||
|
||||
## Current user's profile
|
||||
$scope.user = Member.get {id: $scope.currentUser.id}
|
||||
## Current user's profile
|
||||
$scope.user = memberPromise
|
||||
]
|
||||
|
@ -1,71 +1,72 @@
|
||||
'use strict'
|
||||
|
||||
Application.Controllers.controller "eventsController", ["$scope", "$state", 'Event', ($scope, $state, Event) ->
|
||||
Application.Controllers.controller "EventsController", ["$scope", "$state", 'Event', ($scope, $state, Event) ->
|
||||
|
||||
|
||||
|
||||
### PRIVATE STATIC CONSTANTS ###
|
||||
### PRIVATE STATIC CONSTANTS ###
|
||||
|
||||
# Number of events added to the page when the user clicks on 'load next events'
|
||||
EVENTS_PER_PAGE = 12
|
||||
# Number of events added to the page when the user clicks on 'load next events'
|
||||
EVENTS_PER_PAGE = 12
|
||||
|
||||
|
||||
|
||||
### PUBLIC SCOPE ###
|
||||
### PUBLIC SCOPE ###
|
||||
|
||||
## The events displayed on the page
|
||||
$scope.events = []
|
||||
## The events displayed on the page
|
||||
$scope.events = []
|
||||
|
||||
## By default, the pagination mode is activated to limit the page size
|
||||
$scope.paginateActive = true
|
||||
## By default, the pagination mode is activated to limit the page size
|
||||
$scope.paginateActive = true
|
||||
|
||||
## The currently displayed page number
|
||||
$scope.page = 1
|
||||
## The currently displayed page number
|
||||
$scope.page = 1
|
||||
|
||||
##
|
||||
# Adds EVENTS_PER_PAGE events to the bottom of the page, grouped by month
|
||||
##
|
||||
$scope.loadMoreEvents = ->
|
||||
Event.query {page: $scope.page}, (data) ->
|
||||
$scope.events = $scope.events.concat data
|
||||
if data.length > 0
|
||||
$scope.paginateActive = false if ($scope.page-2)*EVENTS_PER_PAGE+data.length >= data[0].nb_total_events
|
||||
##
|
||||
# Adds EVENTS_PER_PAGE events to the bottom of the page, grouped by month
|
||||
##
|
||||
$scope.loadMoreEvents = ->
|
||||
Event.query {page: $scope.page}, (data) ->
|
||||
$scope.events = $scope.events.concat data
|
||||
if data.length > 0
|
||||
$scope.paginateActive = false if ($scope.page-2)*EVENTS_PER_PAGE+data.length >= data[0].nb_total_events
|
||||
|
||||
$scope.eventsGroupByMonth = _.groupBy($scope.events, (obj) ->
|
||||
_.map ['month', 'year'], (key, value) -> obj[key]
|
||||
)
|
||||
$scope.monthOrder = _.sortBy _.keys($scope.eventsGroupByMonth), (k)->
|
||||
monthYearArray = k.split(',')
|
||||
date = new Date()
|
||||
date.setMonth(monthYearArray[0])
|
||||
date.setYear(monthYearArray[1])
|
||||
return -date.getTime()
|
||||
else
|
||||
$scope.paginateActive = false
|
||||
$scope.page += 1
|
||||
$scope.eventsGroupByMonth = _.groupBy($scope.events, (obj) ->
|
||||
_.map ['month', 'year'], (key, value) -> obj[key]
|
||||
)
|
||||
$scope.monthOrder = _.sortBy _.keys($scope.eventsGroupByMonth), (k)->
|
||||
monthYearArray = k.split(',')
|
||||
date = new Date()
|
||||
date.setMonth(monthYearArray[0])
|
||||
date.setYear(monthYearArray[1])
|
||||
return -date.getTime()
|
||||
else
|
||||
$scope.paginateActive = false
|
||||
$scope.page += 1
|
||||
|
||||
|
||||
|
||||
##
|
||||
# Callback to redirect the user to the specified event page
|
||||
# @param event {{id:number}}
|
||||
##
|
||||
$scope.showEvent = (event) ->
|
||||
$state.go('app.public.events_show', {id: event.id})
|
||||
##
|
||||
# Callback to redirect the user to the specified event page
|
||||
# @param event {{id:number}}
|
||||
##
|
||||
$scope.showEvent = (event) ->
|
||||
$state.go('app.public.events_show', {id: event.id})
|
||||
|
||||
|
||||
|
||||
### PRIVATE SCOPE ###
|
||||
### PRIVATE SCOPE ###
|
||||
|
||||
##
|
||||
# Kind of constructor: these actions will be realized first when the controller is loaded
|
||||
##
|
||||
initialize = ->
|
||||
$scope.loadMoreEvents()
|
||||
##
|
||||
# Kind of constructor: these actions will be realized first when the controller is loaded
|
||||
##
|
||||
initialize = ->
|
||||
$scope.loadMoreEvents()
|
||||
|
||||
|
||||
## !!! MUST BE CALLED AT THE END of the controller
|
||||
initialize()
|
||||
|
||||
## !!! MUST BE CALLED AT THE END of the controller
|
||||
initialize()
|
||||
]
|
||||
|
||||
|
||||
@ -74,47 +75,440 @@ Application.Controllers.controller "eventsController", ["$scope", "$state", 'Eve
|
||||
|
||||
|
||||
|
||||
Application.Controllers.controller "showEventController", ["$scope", "$state", "$stateParams", "Event", '$modal', 'Member', ($scope, $state, $stateParams, Event, $modal, Member) ->
|
||||
Application.Controllers.controller "ShowEventController", ["$scope", "$state", "$stateParams", "Event", '$uibModal', 'Member', 'Reservation', 'Price', 'CustomAsset', 'eventPromise', 'reducedAmountAlert', 'growl', '_t'
|
||||
($scope, $state, $stateParams, Event, $uibModal, Member, Reservation, Price, CustomAsset, eventPromise, reducedAmountAlert, growl, _t) ->
|
||||
|
||||
|
||||
|
||||
### PUBLIC SCOPE ###
|
||||
### PUBLIC SCOPE ###
|
||||
$scope.reducedAmountAlert = reducedAmountAlert.setting.value
|
||||
|
||||
## current event details
|
||||
$scope.event = {}
|
||||
## reservations for the currently shown event
|
||||
$scope.reservations = []
|
||||
|
||||
## user to deal with
|
||||
$scope.ctrl =
|
||||
member: {}
|
||||
|
||||
## parameters for a new reservation
|
||||
$scope.reserve =
|
||||
nbPlaces: []
|
||||
nbReducedPlaces: []
|
||||
nbReservePlaces: 0
|
||||
nbReserveReducedPlaces: 0
|
||||
toReserve: false
|
||||
amountTotal : 0
|
||||
|
||||
|
||||
|
||||
##
|
||||
# Callback to delete the provided event (admins only)
|
||||
# @param event {$resource} angular's Event $resource
|
||||
##
|
||||
$scope.deleteEvent = (event) ->
|
||||
event.$delete ->
|
||||
$state.go('app.public.events_list')
|
||||
# get the details for the current event (event's id is recovered from the current URL)
|
||||
$scope.event = eventPromise
|
||||
|
||||
|
||||
|
||||
### PRIVATE SCOPE ###
|
||||
|
||||
##
|
||||
# Kind of constructor: these actions will be realized first when the controller is loaded
|
||||
##
|
||||
initialize = ->
|
||||
|
||||
# get the details for the current event (event's id is recovered from the current URL)
|
||||
Event.get {id: $stateParams.id}
|
||||
, (data) ->
|
||||
$scope.event = data
|
||||
if !$scope.event.reduced_amount
|
||||
$scope.event.reduced_amount = 0
|
||||
return
|
||||
, ->
|
||||
$state.go('app.public.events_list')
|
||||
##
|
||||
# Callback to delete the provided event (admins only)
|
||||
# @param event {$resource} angular's Event $resource
|
||||
##
|
||||
$scope.deleteEvent = (event) ->
|
||||
event.$delete ->
|
||||
$state.go('app.public.events_list')
|
||||
|
||||
|
||||
|
||||
## !!! MUST BE CALLED AT THE END of the controller
|
||||
initialize()
|
||||
##
|
||||
# Callback to call when the number of places change in the current booking
|
||||
##
|
||||
$scope.changeNbPlaces = ->
|
||||
reste = $scope.event.nb_free_places - $scope.reserve.nbReservePlaces
|
||||
$scope.reserve.nbReducedPlaces = [0..reste]
|
||||
$scope.computeEventAmount()
|
||||
|
||||
|
||||
|
||||
##
|
||||
# Callback to call when the number of discounted places change in the current booking
|
||||
##
|
||||
$scope.changeNbReducedPlaces = ->
|
||||
reste = $scope.event.nb_free_places - $scope.reserve.nbReserveReducedPlaces
|
||||
$scope.reserve.nbPlaces = [0..reste]
|
||||
$scope.computeEventAmount()
|
||||
|
||||
|
||||
|
||||
##
|
||||
# Callback to reset the current reservation parameters
|
||||
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
|
||||
##
|
||||
$scope.cancelReserve = (e)->
|
||||
e.preventDefault()
|
||||
resetEventReserve()
|
||||
|
||||
|
||||
|
||||
##
|
||||
# Callback to allow the user to set the details for his reservation
|
||||
##
|
||||
$scope.reserveEvent = ->
|
||||
if $scope.event.nb_total_places > 0
|
||||
$scope.reserveSuccess = false
|
||||
if !$scope.isAuthenticated()
|
||||
$scope.login null, (user)->
|
||||
$scope.reserve.toReserve = !$scope.reserve.toReserve
|
||||
if user.role isnt 'admin'
|
||||
$scope.ctrl.member = user
|
||||
else
|
||||
Member.query (members) ->
|
||||
$scope.members = members
|
||||
else
|
||||
$scope.reserve.toReserve = !$scope.reserve.toReserve
|
||||
|
||||
|
||||
|
||||
##
|
||||
# Callback to deal with the reservations of the user selected in the dropdown list instead of the current user's
|
||||
# reservations. (admins only)
|
||||
##
|
||||
$scope.updateMember = ->
|
||||
resetEventReserve()
|
||||
$scope.reserveSuccess = false
|
||||
if $scope.ctrl.member
|
||||
getReservations($scope.event.id, 'Event', $scope.ctrl.member.id)
|
||||
|
||||
|
||||
|
||||
##
|
||||
# Callback to trigger the payment process of the current reservation
|
||||
##
|
||||
$scope.payEvent = ->
|
||||
|
||||
# first, we check that a user was selected
|
||||
if Object.keys($scope.ctrl.member).length > 0
|
||||
reservation = mkReservation($scope.ctrl.member, $scope.reserve, $scope.event)
|
||||
|
||||
if $scope.currentUser.role isnt 'admin' and $scope.reserve.amountTotal > 0
|
||||
payByStripe(reservation)
|
||||
else
|
||||
if $scope.currentUser.role is 'admin' or $scope.reserve.amountTotal is 0
|
||||
payOnSite(reservation)
|
||||
else
|
||||
# otherwise we alert, this error musn't occur when the current user is not admin
|
||||
growl.error(_t('please_select_a_member_first'))
|
||||
|
||||
|
||||
|
||||
##
|
||||
# Callback to validate the booking of a free event
|
||||
##
|
||||
$scope.validReserveEvent = ->
|
||||
reservation =
|
||||
user_id: $scope.ctrl.member.id
|
||||
reservable_id: $scope.event.id
|
||||
reservable_type: 'Event'
|
||||
slots_attributes: []
|
||||
nb_reserve_places: $scope.reserve.nbReservePlaces
|
||||
nb_reserve_reduced_places: $scope.reserve.nbReserveReducedPlaces
|
||||
reservation.slots_attributes.push
|
||||
start_at: $scope.event.start_date
|
||||
end_at: $scope.event.end_date
|
||||
availability_id: $scope.event.availability.id
|
||||
$scope.attempting = true
|
||||
Reservation.save reservation: reservation, (reservation) ->
|
||||
afterPayment(reservation)
|
||||
$scope.attempting = false
|
||||
, (response)->
|
||||
$scope.alerts = []
|
||||
$scope.alerts.push
|
||||
msg: response.data.card[0]
|
||||
type: 'danger'
|
||||
$scope.attempting = false
|
||||
|
||||
|
||||
|
||||
##
|
||||
# Callback to alter an already booked reservation date. A modal window will be opened to allow the user to choose
|
||||
# a new date for his reservation (if any available)
|
||||
# @param reservation {{id:number, reservable_id:number, nb_reserve_places:number, nb_reserve_reduced_places:number}}
|
||||
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
|
||||
##
|
||||
$scope.modifyReservation = (reservation, e)->
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
index = $scope.reservations.indexOf(reservation)
|
||||
$uibModal.open
|
||||
templateUrl: '<%= asset_path "events/modify_event_reservation_modal.html" %>'
|
||||
resolve:
|
||||
event: -> $scope.event
|
||||
reservation: -> reservation
|
||||
controller: ['$scope', '$uibModalInstance', 'event', 'reservation', 'Reservation', ($scope, $uibModalInstance, event, reservation, Reservation) ->
|
||||
# we copy the controller's resolved parameters into the scope
|
||||
$scope.event = event
|
||||
$scope.reservation = angular.copy reservation
|
||||
|
||||
# set the reservable_id to the first available event
|
||||
for e in event.recurrence_events
|
||||
if e.nb_free_places > (reservation.nb_reserve_places + reservation.nb_reserve_reduced_places)
|
||||
$scope.reservation.reservable_id = e.id
|
||||
break
|
||||
|
||||
# Callback to validate the new reservation's date
|
||||
$scope.ok = ->
|
||||
eventToPlace = null
|
||||
angular.forEach event.recurrence_events, (e)->
|
||||
if e.id is parseInt($scope.reservation.reservable_id, 10)
|
||||
eventToPlace = e
|
||||
$scope.reservation.slots[0].start_at = eventToPlace.start_date
|
||||
$scope.reservation.slots[0].end_at = eventToPlace.end_date
|
||||
$scope.reservation.slots[0].availability_id = eventToPlace.availability_id
|
||||
$scope.reservation.slots_attributes = $scope.reservation.slots
|
||||
$scope.attempting = true
|
||||
Reservation.update {id: reservation.id}, {reservation: $scope.reservation}, (reservation) ->
|
||||
$uibModalInstance.close(reservation)
|
||||
$scope.attempting = true
|
||||
, (response)->
|
||||
$scope.alerts = []
|
||||
angular.forEach response, (v, k)->
|
||||
angular.forEach v, (err)->
|
||||
$scope.alerts.push({msg: k+': '+err, type: 'danger'})
|
||||
$scope.attempting = false
|
||||
|
||||
# Callback to cancel the modification
|
||||
$scope.cancel = ->
|
||||
$uibModalInstance.dismiss('cancel')
|
||||
]
|
||||
.result['finally'](null).then (reservation)->
|
||||
$scope.reservations.splice(index, 1)
|
||||
$scope.event.nb_free_places = $scope.event.nb_free_places + reservation.nb_reserve_places + reservation.nb_reserve_reduced_places
|
||||
angular.forEach $scope.event.recurrence_events, (e)->
|
||||
if e.id is parseInt(reservation.reservable_id, 10)
|
||||
e.nb_free_places = e.nb_free_places - reservation.nb_reserve_places - reservation.nb_reserve_reduced_places
|
||||
|
||||
|
||||
|
||||
##
|
||||
# Checks if the provided reservation is able to be modified
|
||||
# @param reservation {{nb_reserve_places:number, nb_reserve_reduced_places:number}}
|
||||
##
|
||||
$scope.reservationCanModify = (reservation)->
|
||||
isAble = false
|
||||
angular.forEach $scope.event.recurrence_events, (e)->
|
||||
isAble = true if e.nb_free_places > (reservation.nb_reserve_places + reservation.nb_reserve_reduced_places)
|
||||
isAble
|
||||
|
||||
|
||||
|
||||
##
|
||||
# Compute the total amount for the current reservation according to the previously set parameters
|
||||
# and assign the result in $scope.reserve.amountTotal
|
||||
##
|
||||
$scope.computeEventAmount = ->
|
||||
# first we check that a user was selected
|
||||
if Object.keys($scope.ctrl.member).length > 0
|
||||
r = mkReservation($scope.ctrl.member, $scope.reserve, $scope.event)
|
||||
Price.compute {reservation: r}, (res) ->
|
||||
$scope.reserve.amountTotal = res.price
|
||||
else
|
||||
$scope.reserve.amountTotal = null
|
||||
|
||||
|
||||
|
||||
### PRIVATE SCOPE ###
|
||||
|
||||
##
|
||||
# Kind of constructor: these actions will be realized first when the controller is loaded
|
||||
##
|
||||
initialize = ->
|
||||
# gather the current user or the list of users if the current user is an admin
|
||||
if $scope.currentUser
|
||||
if $scope.currentUser.role isnt 'admin'
|
||||
$scope.ctrl.member = $scope.currentUser
|
||||
else
|
||||
Member.query (members) ->
|
||||
$scope.members = members
|
||||
|
||||
# check that the event's reduced rate is initialized
|
||||
if !$scope.event.reduced_amount
|
||||
$scope.event.reduced_amount = 0
|
||||
|
||||
# initialize the "reserve" object with the event's data
|
||||
$scope.reserve.nbPlaces = [0..$scope.event.nb_free_places]
|
||||
$scope.reserve.nbReducedPlaces = [0..$scope.event.nb_free_places]
|
||||
|
||||
# if non-admin, get the current user's reservations into $scope.reservations
|
||||
if $scope.currentUser
|
||||
getReservations($scope.event.id, 'Event', $scope.currentUser.id)
|
||||
|
||||
|
||||
|
||||
##
|
||||
# Retrieve the reservations for the couple event / user
|
||||
# @param reservable_id {number} the current event id
|
||||
# @param reservable_type {string} 'Event'
|
||||
# @param user_id {number} the user's id (current or managed)
|
||||
##
|
||||
getReservations = (reservable_id, reservable_type, user_id)->
|
||||
Reservation.query(reservable_id: reservable_id, reservable_type: reservable_type, user_id: user_id).$promise.then (reservations)->
|
||||
$scope.reservations = reservations
|
||||
|
||||
|
||||
|
||||
##
|
||||
# Create an hash map implementing the Reservation specs
|
||||
# @param member {Object} User as retreived from the API: current user / selected user if current is admin
|
||||
# @param reserve {Object} Reservation parameters (places...)
|
||||
# @param event {Object} Current event (Atelier/Stage)
|
||||
# @return {{user_id:Number, reservable_id:Number, reservable_type:String, slots_attributes:Array<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'
|
||||
|
||||
Application.Controllers.controller "homeController", ['$scope', '$stateParams', 'Member', 'Twitter', 'Project', 'Event', ($scope, $stateParams, Member, Twitter, Project, Event) ->
|
||||
|
||||
|
||||
|
||||
### PRIVATE STATIC CONSTANTS ###
|
||||
|
||||
# The 4 last users will be displayed on the home page
|
||||
LAST_MEMBERS_LIMIT = 4
|
||||
|
||||
# Only the last tweet is shown
|
||||
LAST_TWEETS_LIMIT = 1
|
||||
|
||||
# The 3 closest events are shown
|
||||
LAST_EVENTS_LIMIT = 3
|
||||
|
||||
|
||||
Application.Controllers.controller "HomeController", ['$scope', '$stateParams', 'Twitter', 'lastMembersPromise', 'lastProjectsPromise', 'upcomingEventsPromise', 'homeBlogpostPromise', 'twitterNamePromise', ($scope, $stateParams, Twitter, lastMembersPromise, lastProjectsPromise, upcomingEventsPromise, homeBlogpostPromise, twitterNamePromise)->
|
||||
|
||||
### PUBLIC SCOPE ###
|
||||
|
||||
## The last registered members who confirmed their addresses
|
||||
$scope.last_members = []
|
||||
$scope.lastMembers = lastMembersPromise
|
||||
|
||||
## The last tweets from the Fablab official twitter account
|
||||
$scope.last_tweets = []
|
||||
$scope.lastTweets = []
|
||||
|
||||
## The last projects published/documented on the plateform
|
||||
$scope.last_projects = []
|
||||
$scope.lastProjects = lastProjectsPromise
|
||||
|
||||
## The closest upcoming events
|
||||
$scope.upcoming_events = []
|
||||
$scope.upcomingEvents = upcomingEventsPromise
|
||||
|
||||
## The admin blogpost
|
||||
$scope.homeBlogpost = homeBlogpostPromise.setting.value
|
||||
|
||||
## Twitter username
|
||||
$scope.twitterName = twitterNamePromise.setting.value
|
||||
|
||||
##
|
||||
# Test if the provided event run on a single day or not
|
||||
# @param event {Object} single event from the $scope.upcomingEvents array
|
||||
# @returns {boolean} false if the event runs on more that 1 day
|
||||
##
|
||||
$scope.isOneDayEvent = (event) ->
|
||||
moment(event.start_date).isSame(event.end_date, 'day')
|
||||
|
||||
|
||||
|
||||
@ -39,20 +38,15 @@ Application.Controllers.controller "homeController", ['$scope', '$stateParams',
|
||||
# Kind of constructor: these actions will be realized first when the controller is loaded
|
||||
##
|
||||
initialize = ->
|
||||
# display the reset password dialog if the parameter was provided
|
||||
# we retrieve tweets from here instead of ui-router's promise because, if adblock stop the API request,
|
||||
# this prevent the whole home page to be blocked
|
||||
$scope.lastTweets = Twitter.query(limit: 1)
|
||||
|
||||
# if we recieve a token to reset the password as GET parameter, trigger the
|
||||
# changePassword modal from the parent controller
|
||||
if $stateParams.reset_password_token
|
||||
$scope.$parent.editPassword($stateParams.reset_password_token)
|
||||
|
||||
# initialize the homepage data
|
||||
Member.lastSubscribed {limit: LAST_MEMBERS_LIMIT}, (members) ->
|
||||
$scope.last_members = members
|
||||
Twitter.query {limit: LAST_TWEETS_LIMIT}, (tweets) ->
|
||||
$scope.last_tweets = tweets
|
||||
Project.lastPublished (projects) ->
|
||||
$scope.last_projects = projects
|
||||
Event.upcoming {limit: LAST_EVENTS_LIMIT}, (events) ->
|
||||
$scope.upcoming_events = events
|
||||
|
||||
|
||||
|
||||
## !!! MUST BE CALLED AT THE END of the controller
|
||||
|
@ -74,19 +74,96 @@ class MachinesController
|
||||
|
||||
|
||||
|
||||
##
|
||||
# Manages the transition when a user clicks on the reservation button.
|
||||
# According to the status of user currently logged into the system, redirect him to the reservation page,
|
||||
# or display a modal window asking him to complete a training before he can book a machine reservation.
|
||||
# @param machine {{id:number}} An object containg the id of the machine to book,
|
||||
# the object will be completed before the fonction returns.
|
||||
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
|
||||
##
|
||||
_reserveMachine = (machine, e) ->
|
||||
_this = this
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
# retrieve the full machine object
|
||||
machine = _this.Machine.get {id: machine.id}, ->
|
||||
|
||||
# if the currently logged'in user has completed the training for this machine, or this machine does not require
|
||||
# a prior training, just redirect him to the machine's booking page
|
||||
if machine.current_user_is_training or machine.trainings.length == 0
|
||||
_this.$state.go('app.logged.machines_reserve', {id: machine.id})
|
||||
else
|
||||
# otherwise, if a user is authenticated ...
|
||||
if _this.$scope.isAuthenticated()
|
||||
# ... and have booked a training for this machine, tell him that he must wait for an admin to validate
|
||||
# the training before he can book the reservation
|
||||
if machine.current_user_training_reservation
|
||||
_this.$uibModal.open
|
||||
templateUrl: '<%= asset_path "machines/training_reservation_modal.html" %>'
|
||||
controller: ['$scope', '$uibModalInstance', '$state', ($scope, $uibModalInstance, $state) ->
|
||||
$scope.machine = machine
|
||||
$scope.cancel = ->
|
||||
$uibModalInstance.dismiss('cancel')
|
||||
]
|
||||
# ... but does not have booked the training, tell him to register for a training session first
|
||||
else
|
||||
_this.$uibModal.open
|
||||
templateUrl: '<%= asset_path "machines/request_training_modal.html" %>'
|
||||
controller: ['$scope', '$uibModalInstance', '$state', ($scope, $uibModalInstance, $state) ->
|
||||
$scope.machine = machine
|
||||
$scope.member = _this.$scope.currentUser
|
||||
|
||||
# transform the name of the trainings associated with the machine to integrate them in a sentence
|
||||
$scope.humanizeTrainings = ->
|
||||
text = ''
|
||||
angular.forEach $scope.machine.trainings, (training) ->
|
||||
if text.length > 0
|
||||
text += _this._t('_or_the_')
|
||||
text += training.name.substr(0,1).toLowerCase() + training.name.substr(1)
|
||||
text
|
||||
|
||||
# modal is close with validation
|
||||
$scope.ok = ->
|
||||
$state.go('app.logged.trainings_reserve')
|
||||
$uibModalInstance.close(machine)
|
||||
|
||||
# modal is closed with escaping
|
||||
$scope.cancel = (e)->
|
||||
e.preventDefault()
|
||||
$uibModalInstance.dismiss('cancel')
|
||||
]
|
||||
# if the user is not logged, open the login modal window
|
||||
else
|
||||
_this.$scope.login()
|
||||
|
||||
|
||||
|
||||
|
||||
##
|
||||
# Controller used in the public listing page, allowing everyone to see the list of machines
|
||||
##
|
||||
Application.Controllers.controller "machinesController", ["$scope", "$state", 'Machine', '$modal', ($scope, $state, Machine, $modal) ->
|
||||
Application.Controllers.controller "MachinesController", ["$scope", "$state", '_t', 'Machine', '$uibModal', 'machinesPromise', ($scope, $state, _t, Machine, $uibModal, machinesPromise) ->
|
||||
|
||||
## Retrieve the list of machines
|
||||
$scope.machines = Machine.query()
|
||||
## Retrieve the list of machines
|
||||
$scope.machines = machinesPromise
|
||||
|
||||
##
|
||||
# Redirect the user to the machine details page
|
||||
##
|
||||
$scope.showMachine = (machine) ->
|
||||
$state.go('app.public.machines_show', {id: machine.slug})
|
||||
|
||||
##
|
||||
# Callback to book a reservation for the current machine
|
||||
##
|
||||
$scope.reserveMachine = _reserveMachine.bind
|
||||
$scope: $scope
|
||||
$state: $state
|
||||
_t: _t
|
||||
$uibModal: $uibModal
|
||||
Machine: Machine
|
||||
]
|
||||
|
||||
|
||||
@ -94,7 +171,7 @@ Application.Controllers.controller "machinesController", ["$scope", "$state", 'M
|
||||
##
|
||||
# Controller used in the machine creation page (admin)
|
||||
##
|
||||
Application.Controllers.controller "newMachineController", ["$scope", "$state", 'CSRF', ($scope, $state, CSRF) ->
|
||||
Application.Controllers.controller "NewMachineController", ["$scope", "$state", 'CSRF',($scope, $state, CSRF) ->
|
||||
CSRF.setMetaTags()
|
||||
|
||||
## API URL where the form will be posted
|
||||
@ -116,8 +193,11 @@ Application.Controllers.controller "newMachineController", ["$scope", "$state",
|
||||
##
|
||||
# Controller used in the machine edition page (admin)
|
||||
##
|
||||
Application.Controllers.controller "editMachineController", ["$scope", "$state", '$stateParams', 'Machine', 'CSRF', ($scope, $state, $stateParams, Machine, CSRF) ->
|
||||
CSRF.setMetaTags()
|
||||
Application.Controllers.controller "EditMachineController", ["$scope", '$state', '$stateParams', 'machinePromise', 'CSRF', ($scope, $state, $stateParams, machinePromise, CSRF) ->
|
||||
|
||||
|
||||
|
||||
### PUBLIC SCOPE ###
|
||||
|
||||
## API URL where the form will be posted
|
||||
$scope.actionUrl = "/api/machines/" + $stateParams.id
|
||||
@ -126,14 +206,24 @@ Application.Controllers.controller "editMachineController", ["$scope", "$state",
|
||||
$scope.method = "put"
|
||||
|
||||
## Retrieve the details for the machine id in the URL, if an error occurs redirect the user to the machines list
|
||||
$scope.machine = Machine.get {id: $stateParams.id}
|
||||
, ->
|
||||
return
|
||||
, ->
|
||||
$state.go('app.public.machines_list')
|
||||
$scope.machine = machinePromise
|
||||
|
||||
## Using the MachinesController
|
||||
new MachinesController($scope, $state)
|
||||
|
||||
|
||||
### PRIVATE SCOPE ###
|
||||
|
||||
##
|
||||
# Kind of constructor: these actions will be realized first when the controller is loaded
|
||||
##
|
||||
initialize = ->
|
||||
CSRF.setMetaTags()
|
||||
|
||||
## Using the MachinesController
|
||||
new MachinesController($scope, $state)
|
||||
|
||||
|
||||
## !!! MUST BE CALLED AT THE END of the controller
|
||||
initialize()
|
||||
]
|
||||
|
||||
|
||||
@ -141,14 +231,11 @@ Application.Controllers.controller "editMachineController", ["$scope", "$state",
|
||||
##
|
||||
# Controller used in the machine details page (public)
|
||||
##
|
||||
Application.Controllers.controller "showMachineController", ['$scope', '$state', '$modal', '$stateParams', 'Machine', ($scope, $state, $modal, $stateParams, Machine) ->
|
||||
Application.Controllers.controller "ShowMachineController", ['$scope', '$state', '$uibModal', '$stateParams', '_t', 'Machine', 'growl', 'machinePromise'
|
||||
, ($scope, $state, $uibModal, $stateParams, _t, Machine, growl, machinePromise) ->
|
||||
|
||||
## Retrieve the details for the machine id in the URL, if an error occurs redirect the user to the machines list
|
||||
$scope.machine = Machine.get {id: $stateParams.id}
|
||||
, ->
|
||||
return
|
||||
, ->
|
||||
$state.go('app.public.machines_list')
|
||||
## Retrieve the details for the machine id in the URL, if an error occurs redirect the user to the machines list
|
||||
$scope.machine = machinePromise
|
||||
|
||||
##
|
||||
# Callback to delete the current machine (admins only)
|
||||
@ -156,9 +243,730 @@ Application.Controllers.controller "showMachineController", ['$scope', '$state',
|
||||
$scope.delete = (machine) ->
|
||||
# check the permissions
|
||||
if $scope.currentUser.role isnt 'admin'
|
||||
console.error 'Unauthorized operation'
|
||||
console.error _t('unauthorized_operation')
|
||||
else
|
||||
# delete the machine then redirect to the machines listing
|
||||
machine.$delete ->
|
||||
$state.go('app.public.machines_list')
|
||||
, (error)->
|
||||
growl.warning(_t('the_machine_cant_be_deleted_because_it_is_already_reserved_by_some_users'))
|
||||
##
|
||||
# Callback to book a reservation for the current machine
|
||||
##
|
||||
$scope.reserveMachine = _reserveMachine.bind
|
||||
$scope: $scope
|
||||
$state: $state
|
||||
_t: _t
|
||||
$uibModal: $uibModal
|
||||
Machine: Machine
|
||||
]
|
||||
|
||||
|
||||
|
||||
##
|
||||
# Controller used in the machine reservation page (for logged users who have completed the training and admins).
|
||||
# This controller workflow is pretty similar to the trainings reservation controller.
|
||||
##
|
||||
|
||||
Application.Controllers.controller "ReserveMachineController", ["$scope", "$state", '$stateParams', "$uibModal", '_t', "moment", 'Machine', 'Auth', 'dialogs', '$timeout', 'Price', 'Member', 'Availability', 'Slot', 'Setting', 'CustomAsset', 'plansPromise', 'groupsPromise', 'growl', 'settingsPromise',
|
||||
($scope, $state, $stateParams, $uibModal, _t, moment, Machine, Auth, dialogs, $timeout, Price, Member, Availability, Slot, Setting, CustomAsset, plansPromise, groupsPromise, growl, settingsPromise) ->
|
||||
|
||||
|
||||
|
||||
### PRIVATE STATIC CONSTANTS ###
|
||||
|
||||
# The calendar is divided in slots of 60 minutes
|
||||
BASE_SLOT = '01:00:00'
|
||||
|
||||
# The calendar will be initialized positioned under 9:00 AM
|
||||
DEFAULT_CALENDAR_POSITION = '09:00:00'
|
||||
|
||||
# The user is unable to modify his already booked reservation 1 day before it occurs
|
||||
PREVENT_BOOKING_MODIFICATION_DELAY = 1
|
||||
|
||||
# Slot already booked by the current user
|
||||
FREE_SLOT_BORDER_COLOR = '#e4cd78'
|
||||
|
||||
# Slot already booked by another user
|
||||
UNAVAILABLE_SLOT_BORDER_COLOR = '#1d98ec'
|
||||
|
||||
# Slot free to be booked
|
||||
BOOKED_SLOT_BORDER_COLOR = '#b2e774'
|
||||
|
||||
|
||||
|
||||
### PUBLIC SCOPE ###
|
||||
|
||||
## after fullCalendar loads, provides access to its methods through $scope.calendar.fullCalendar()
|
||||
$scope.calendar = null
|
||||
|
||||
## bind the machine availabilities with full-Calendar events
|
||||
$scope.eventSources = []
|
||||
|
||||
## fullCalendar event. The last selected slot that the user want to book
|
||||
$scope.slotToPlace = null
|
||||
|
||||
## fullCalendar event. An already booked slot that the user want to modify
|
||||
$scope.slotToModify = null
|
||||
|
||||
## indicates the state of the current view : calendar or plans informations
|
||||
$scope.plansAreShown = false
|
||||
|
||||
## will store the user's plan if he choosed to buy one
|
||||
$scope.selectedPlan = null
|
||||
|
||||
## array of fullCalendar events. Slots where the user want to book
|
||||
$scope.eventsReserved = []
|
||||
|
||||
## total amount of the bill to pay
|
||||
$scope.amountTotal = 0
|
||||
|
||||
## is the user allowed to change the date of his booking
|
||||
$scope.enableBookingMove = true
|
||||
|
||||
## how many hours before the reservation, the user is still allowed to change his booking
|
||||
$scope.moveBookingDelay = 24
|
||||
|
||||
## list of plans, classified by group
|
||||
$scope.plansClassifiedByGroup = []
|
||||
for group in groupsPromise
|
||||
groupObj = { id: group.id, name: group.name, plans: [] }
|
||||
for plan in plansPromise
|
||||
groupObj.plans.push(plan) if plan.group_id == group.id
|
||||
$scope.plansClassifiedByGroup.push(groupObj)
|
||||
|
||||
## the user to deal with, ie. the current user for non-admins
|
||||
$scope.ctrl =
|
||||
member: {}
|
||||
|
||||
## fablab users list
|
||||
$scope.members = []
|
||||
|
||||
## current machine to reserve
|
||||
$scope.machine = {}
|
||||
|
||||
## fullCalendar (v2) configuration
|
||||
$scope.calendarConfig =
|
||||
timezone: Fablab.timezone
|
||||
lang: Fablab.fullcalendar_locale
|
||||
header:
|
||||
left: 'month agendaWeek'
|
||||
center: 'title'
|
||||
right: 'today prev,next'
|
||||
firstDay: 1 # Week start on monday (France)
|
||||
scrollTime: DEFAULT_CALENDAR_POSITION
|
||||
slotDuration: BASE_SLOT
|
||||
allDayDefault: false
|
||||
minTime: '00:00:00'
|
||||
maxTime: '24:00:00'
|
||||
height: 'auto'
|
||||
buttonIcons:
|
||||
prev: 'left-single-arrow'
|
||||
next: 'right-single-arrow'
|
||||
timeFormat:
|
||||
agenda:'H:mm'
|
||||
month: 'H(:mm)'
|
||||
axisFormat: 'H:mm'
|
||||
|
||||
allDaySlot: false
|
||||
defaultView: 'agendaWeek'
|
||||
editable: false
|
||||
eventClick: (event, jsEvent, view) ->
|
||||
calendarEventClickCb(event, jsEvent, view)
|
||||
eventRender: (event, element, view) ->
|
||||
eventRenderCb(event, element)
|
||||
|
||||
## Global config: message to the end user concerning the subscriptions rules
|
||||
$scope.subscriptionExplicationsAlert = settingsPromise.subscription_explications_alert
|
||||
|
||||
## Gloabl config: message to the end user concerning the machine bookings
|
||||
$scope.machineExplicationsAlert = settingsPromise.machine_explications_alert
|
||||
|
||||
## Global config: is the user authorized to change his bookings slots?
|
||||
$scope.enableBookingMove = (settingsPromise.booking_move_enable == "true")
|
||||
|
||||
## Global config: delay in hours before a booking while changing the booking slot is forbidden
|
||||
$scope.moveBookingDelay = parseInt(settingsPromise.booking_move_delay)
|
||||
|
||||
## Global config: is the user authorized to cancel his bookings?
|
||||
$scope.enableBookingCancel = (settingsPromise.booking_cancel_enable == "true")
|
||||
|
||||
## Global config: delay in hours before a booking while the cancellation is forbidden
|
||||
$scope.cancelBookingDelay = parseInt(settingsPromise.booking_cancel_delay)
|
||||
|
||||
## Global config: calendar window in the morning
|
||||
$scope.calendarConfig.minTime = moment.duration(moment(settingsPromise.booking_window_start).format('HH:mm:ss'))
|
||||
|
||||
## Global config: calendar window in the evening
|
||||
$scope.calendarConfig.maxTime = moment.duration(moment(settingsPromise.booking_window_end).format('HH:mm:ss'))
|
||||
|
||||
|
||||
|
||||
##
|
||||
# Cancel the current booking modification, removing the previously booked slot from the selection
|
||||
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
|
||||
##
|
||||
$scope.removeSlotToModify = (e) ->
|
||||
e.preventDefault()
|
||||
if $scope.slotToPlace
|
||||
$scope.slotToPlace.backgroundColor = 'white'
|
||||
$scope.slotToPlace.title = ''
|
||||
$scope.slotToPlace = null
|
||||
$scope.slotToModify.title = if $scope.currentUser.role isnt 'admin' then _t('i_ve_reserved') else _t('not_available')
|
||||
$scope.slotToModify.backgroundColor = 'white'
|
||||
$scope.slotToModify = null
|
||||
$scope.calendar.fullCalendar 'rerenderEvents'
|
||||
|
||||
|
||||
|
||||
##
|
||||
# When modifying an already booked reservation, cancel the choice of the new slot
|
||||
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
|
||||
##
|
||||
$scope.removeSlotToPlace = (e)->
|
||||
e.preventDefault()
|
||||
$scope.slotToPlace.backgroundColor = 'white'
|
||||
$scope.slotToPlace.title = ''
|
||||
$scope.slotToPlace = null
|
||||
$scope.calendar.fullCalendar 'rerenderEvents'
|
||||
|
||||
|
||||
|
||||
##
|
||||
# When modifying an already booked reservation, confirm the modification.
|
||||
##
|
||||
$scope.modifyMachineSlot = ->
|
||||
Slot.update {id: $scope.slotToModify.id},
|
||||
slot:
|
||||
start_at: $scope.slotToPlace.start
|
||||
end_at: $scope.slotToPlace.end
|
||||
availability_id: $scope.slotToPlace.availability_id
|
||||
, -> # success
|
||||
$scope.modifiedSlots =
|
||||
newReservedSlot: $scope.slotToPlace
|
||||
oldReservedSlot: $scope.slotToModify
|
||||
$scope.slotToPlace.title = if $scope.currentUser.role isnt 'admin' then _t('i_ve_reserved') else _t('not_available')
|
||||
$scope.slotToPlace.backgroundColor = 'white'
|
||||
$scope.slotToPlace.borderColor = $scope.slotToModify.borderColor
|
||||
$scope.slotToPlace.id = $scope.slotToModify.id
|
||||
$scope.slotToPlace.is_reserved = true
|
||||
$scope.slotToPlace.can_modify = true
|
||||
$scope.slotToPlace = null
|
||||
|
||||
$scope.slotToModify.backgroundColor = 'white'
|
||||
$scope.slotToModify.title = ''
|
||||
$scope.slotToModify.borderColor = FREE_SLOT_BORDER_COLOR
|
||||
$scope.slotToModify.id = null
|
||||
$scope.slotToModify.is_reserved = false
|
||||
$scope.slotToModify.can_modify = false
|
||||
$scope.slotToModify = null
|
||||
|
||||
$scope.calendar.fullCalendar 'rerenderEvents'
|
||||
, (err) -> # failure
|
||||
growl.error(_t('unable_to_change_the_reservation'))
|
||||
console.error(err)
|
||||
|
||||
|
||||
|
||||
##
|
||||
# Cancel the current booking modification, reseting the whole process
|
||||
##
|
||||
$scope.cancelModifyMachineSlot = ->
|
||||
$scope.slotToPlace.backgroundColor = 'white'
|
||||
$scope.slotToPlace.title = ''
|
||||
$scope.slotToPlace = null
|
||||
$scope.slotToModify.title = if $scope.currentUser.role isnt 'admin' then _t('i_ve_reserved') else _t('not_available')
|
||||
$scope.slotToModify.backgroundColor = 'white'
|
||||
$scope.slotToModify = null
|
||||
|
||||
$scope.calendar.fullCalendar 'rerenderEvents'
|
||||
|
||||
|
||||
|
||||
##
|
||||
# Callback to deal with the reservations of the user selected in the dropdown list instead of the current user's
|
||||
# reservations. (admins only)
|
||||
##
|
||||
$scope.updateMember = ->
|
||||
$scope.paidMachineSlots = null
|
||||
$scope.plansAreShown = false
|
||||
$scope.selectedPlan = null
|
||||
updateCartPrice()
|
||||
|
||||
|
||||
|
||||
##
|
||||
# Add the provided slot to the shopping cart (state transition from free to 'about to be reserved')
|
||||
# and increment the total amount of the cart if needed.
|
||||
# @param machineSlot {Object} fullCalendar event object
|
||||
##
|
||||
$scope.validMachineSlot = (machineSlot)->
|
||||
machineSlot.isValid = true
|
||||
updateCartPrice()
|
||||
|
||||
|
||||
|
||||
##
|
||||
# Remove the provided slot from the shopping cart (state transition from 'about to be reserved' to free)
|
||||
# and decrement the total amount of the cart if needed.
|
||||
# @param machineSlot {Object} fullCalendar event object
|
||||
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
|
||||
##
|
||||
$scope.removeMachineSlot = (machineSlot, e)->
|
||||
e.preventDefault() if e
|
||||
machineSlot.backgroundColor = 'white'
|
||||
machineSlot.borderColor = FREE_SLOT_BORDER_COLOR
|
||||
machineSlot.title = ''
|
||||
machineSlot.isValid = false
|
||||
|
||||
if machineSlot.machine.is_reduced_amount
|
||||
angular.forEach $scope.ctrl.member.machine_credits, (credit)->
|
||||
if credit.machine_id = machineSlot.machine.id
|
||||
credit.hours_used--
|
||||
machineSlot.machine.is_reduced_amount = false
|
||||
|
||||
index = $scope.eventsReserved.indexOf(machineSlot)
|
||||
$scope.eventsReserved.splice(index, 1)
|
||||
if $scope.eventsReserved.length == 0
|
||||
if $scope.plansAreShown
|
||||
$scope.selectedPlan = null
|
||||
$scope.plansAreShown = false
|
||||
updateCartPrice()
|
||||
$timeout ->
|
||||
$scope.calendar.fullCalendar 'refetchEvents'
|
||||
$scope.calendar.fullCalendar 'rerenderEvents'
|
||||
|
||||
|
||||
|
||||
##
|
||||
# Checks that every selected slots were added to the shopping cart. Ie. will return false if
|
||||
# any checked slot was not validated by the user.
|
||||
##
|
||||
$scope.machineSlotsValid = ->
|
||||
isValid = true
|
||||
angular.forEach $scope.eventsReserved, (m)->
|
||||
isValid = false if !m.isValid
|
||||
isValid
|
||||
|
||||
|
||||
|
||||
##
|
||||
# Changes the user current view from the plan subsription screen to the machine reservation agenda
|
||||
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
|
||||
##
|
||||
$scope.doNotSubscribePlan = (e)->
|
||||
e.preventDefault()
|
||||
$scope.plansAreShown = false
|
||||
$scope.selectPlan($scope.selectedPlan)
|
||||
|
||||
|
||||
|
||||
##
|
||||
# Validates the shopping chart and redirect the user to the payment step
|
||||
##
|
||||
$scope.payMachine = ->
|
||||
|
||||
# first, we check that a user was selected
|
||||
if Object.keys($scope.ctrl.member).length > 0
|
||||
reservation = mkReservation($scope.ctrl.member, $scope.eventsReserved, $scope.selectedPlan)
|
||||
|
||||
if $scope.currentUser.role isnt 'admin' and $scope.amountTotal > 0
|
||||
payByStripe(reservation)
|
||||
else
|
||||
if $scope.currentUser.role is 'admin' or $scope.amountTotal is 0
|
||||
payOnSite(reservation)
|
||||
else
|
||||
# otherwise we alert, this error musn't occur when the current user is not admin
|
||||
growl.error(_t('please_select_a_member_first'))
|
||||
|
||||
|
||||
##
|
||||
# Switch the user's view from the reservation agenda to the plan subscription
|
||||
##
|
||||
$scope.showPlans = ->
|
||||
$scope.plansAreShown = true
|
||||
|
||||
|
||||
|
||||
##
|
||||
# Add the provided plan to the current shopping cart
|
||||
# @param plan {Object} the plan to subscribe
|
||||
##
|
||||
$scope.selectPlan = (plan) ->
|
||||
if $scope.isAuthenticated()
|
||||
angular.forEach $scope.eventsReserved, (machineSlot)->
|
||||
angular.forEach $scope.ctrl.member.machine_credits, (credit)->
|
||||
if credit.machine_id = machineSlot.machine.id
|
||||
credit.hours_used = 0
|
||||
machineSlot.machine.is_reduced_amount = false
|
||||
|
||||
if $scope.selectedPlan != plan
|
||||
$scope.selectedPlan = plan
|
||||
else
|
||||
$scope.selectedPlan = null
|
||||
updateCartPrice()
|
||||
else
|
||||
$scope.login null, ->
|
||||
$scope.selectedPlan = plan
|
||||
updateCartPrice()
|
||||
|
||||
|
||||
|
||||
##
|
||||
# Checks if $scope.slotToModify and $scope.slotToPlace have tag incompatibilities
|
||||
# @returns {boolean} true in case of incompatibility
|
||||
##
|
||||
$scope.tagMissmatch = ->
|
||||
for tag in $scope.slotToModify.tags
|
||||
if tag.id not in $scope.slotToPlace.tag_ids
|
||||
return true
|
||||
false
|
||||
|
||||
|
||||
|
||||
### PRIVATE SCOPE ###
|
||||
|
||||
##
|
||||
# Kind of constructor: these actions will be realized first when the controller is loaded
|
||||
##
|
||||
initialize = ->
|
||||
Availability.machine {machineId: $stateParams.id}, (availabilities) ->
|
||||
$scope.eventSources.push
|
||||
events: availabilities
|
||||
textColor: 'black'
|
||||
|
||||
if $scope.currentUser.role isnt 'admin'
|
||||
$scope.ctrl.member = $scope.currentUser
|
||||
else
|
||||
Member.query {requested_attributes:'[subscription,credits]'}, (members) ->
|
||||
$scope.members = members
|
||||
|
||||
$scope.machine = Machine.get {id: $stateParams.id}
|
||||
, ->
|
||||
return
|
||||
, ->
|
||||
$state.go('app.public.machines_list')
|
||||
|
||||
|
||||
|
||||
##
|
||||
# Create an hash map implementing the Reservation specs
|
||||
# @param member {Object} User as retreived from the API: current user / selected user if current is admin
|
||||
# @param slots {Array<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.
|
||||
##
|
||||
Application.Controllers.controller "mainNavController", ["$scope", "$location", "$cookies", ($scope, $location, $cookies) ->
|
||||
Application.Controllers.controller "MainNavController", ["$scope", "$location", "$cookies", ($scope, $location, $cookies) ->
|
||||
|
||||
## Common links (public application)
|
||||
## Common links (public application)
|
||||
$scope.navLinks = [
|
||||
{
|
||||
state: 'app.public.home'
|
||||
linkText: 'Accueil'
|
||||
linkText: 'home'
|
||||
linkIcon: 'home'
|
||||
}
|
||||
|
||||
{
|
||||
state: 'app.public.machines_list'
|
||||
linkText: 'Liste des machines'
|
||||
linkIcon: 'gears'
|
||||
linkText: 'reserve_a_machine'
|
||||
linkIcon: 'calendar'
|
||||
}
|
||||
{
|
||||
state: 'app.logged.trainings_reserve'
|
||||
linkText: 'trainings_registrations'
|
||||
linkIcon: 'graduation-cap'
|
||||
}
|
||||
{
|
||||
state: 'app.public.events_list'
|
||||
linkText: 'Liste des stages et ateliers'
|
||||
linkText: 'courses_and_workshops_registrations'
|
||||
linkIcon: 'tags'
|
||||
}
|
||||
{
|
||||
state: 'app.public.projects_list'
|
||||
linkText: 'Galerie de projets'
|
||||
linkText: 'projects_gallery'
|
||||
linkIcon: 'th'
|
||||
}
|
||||
|
||||
]
|
||||
|
||||
## Admin links (backoffice application)
|
||||
unless Fablab.withoutPlans
|
||||
$scope.navLinks.push({
|
||||
state: 'app.public.plans'
|
||||
linkText: 'subscriptions'
|
||||
linkIcon: 'credit-card'
|
||||
})
|
||||
|
||||
|
||||
$scope.adminNavLinks = [
|
||||
{
|
||||
state: 'app.admin.trainings'
|
||||
linkText: 'trainings_monitoring'
|
||||
linkIcon: 'graduation-cap'
|
||||
}
|
||||
{
|
||||
state: 'app.admin.calendar'
|
||||
linkText: 'manage_the_calendar'
|
||||
linkIcon: 'calendar'
|
||||
}
|
||||
{
|
||||
state: 'app.admin.members'
|
||||
linkText: 'Suivi utilisateurs'
|
||||
linkText: 'manage_the_users'
|
||||
linkIcon: 'users'
|
||||
}
|
||||
{
|
||||
state: 'app.admin.invoices'
|
||||
linkText: 'manage_the_invoices'
|
||||
linkIcon: 'file-pdf-o'
|
||||
}
|
||||
{
|
||||
state: 'app.admin.pricing'
|
||||
linkText: 'subscriptions_and_prices'
|
||||
linkIcon: 'money'
|
||||
}
|
||||
{
|
||||
state: 'app.admin.events'
|
||||
linkText: 'Suivi stages et ateliers'
|
||||
linkText: 'courses_and_workshops_monitoring'
|
||||
linkIcon: 'tags'
|
||||
}
|
||||
{
|
||||
state: 'app.public.machines_list'
|
||||
linkText: 'Gérer les machines'
|
||||
linkText: 'manage_the_machines'
|
||||
linkIcon: 'cogs'
|
||||
}
|
||||
{
|
||||
state: 'app.admin.project_elements'
|
||||
linkText: 'Gérer les éléments Projets'
|
||||
linkText: 'manage_the_projects_elements'
|
||||
linkIcon: 'tasks'
|
||||
}
|
||||
{
|
||||
state: 'app.admin.statistics'
|
||||
linkText: 'statistics'
|
||||
linkIcon: 'bar-chart-o'
|
||||
}
|
||||
{
|
||||
state: 'app.admin.settings'
|
||||
linkText: 'customization'
|
||||
linkIcon: 'gear'
|
||||
}
|
||||
]
|
||||
|
||||
]
|
||||
|
@ -3,23 +3,11 @@
|
||||
##
|
||||
# Controller used in the members listing page
|
||||
##
|
||||
Application.Controllers.controller "membersController", ["$scope", "$state", 'Member', ($scope, $state, Member) ->
|
||||
Application.Controllers.controller "MembersController", ["$scope", 'membersPromise', ($scope, membersPromise) ->
|
||||
|
||||
## members list
|
||||
$scope.members = Member.query()
|
||||
$scope.members = membersPromise
|
||||
|
||||
## Merbers ordering/sorting. Default: not sorted
|
||||
$scope.orderMember = null
|
||||
|
||||
##
|
||||
# Change the members ordering criterion to the one provided
|
||||
# @param orderBy {string} ordering criterion
|
||||
##
|
||||
$scope.setOrderMember = (orderBy)->
|
||||
if $scope.orderMember == orderBy
|
||||
$scope.orderMember = '-'+orderBy
|
||||
else
|
||||
$scope.orderMember = orderBy
|
||||
]
|
||||
|
||||
|
||||
@ -27,24 +15,71 @@ Application.Controllers.controller "membersController", ["$scope", "$state", 'Me
|
||||
##
|
||||
# Controller used when editing the current user's profile
|
||||
##
|
||||
Application.Controllers.controller "editProfileController", ["$scope", "$state", "Member", "Auth", 'growl', 'dialogs', 'CSRF', ($scope, $state, Member, Auth, growl, dialogs, CSRF) ->
|
||||
CSRF.setMetaTags()
|
||||
Application.Controllers.controller "EditProfileController", ["$scope", "$rootScope", "$state", "$window", '$locale', "Member", "Auth", "Session", "activeProviderPromise", 'growl', 'dialogs', 'CSRF', 'memberPromise', 'groups', '_t'
|
||||
, ($scope, $rootScope, $state, $window, $locale, Member, Auth, Session, activeProviderPromise, growl, dialogs, CSRF, memberPromise, groups, _t) ->
|
||||
|
||||
|
||||
|
||||
### PUBLIC SCOPE ###
|
||||
|
||||
## API URL where the form will be posted
|
||||
$scope.actionUrl = "/api/members/" + $scope.currentUser.id
|
||||
|
||||
## list of groups
|
||||
$scope.groups = groups
|
||||
|
||||
## Form action on the above URL
|
||||
$scope.method = 'patch'
|
||||
|
||||
## Current user's profile
|
||||
$scope.user = Member.get {id: $scope.currentUser.id}
|
||||
$scope.user = memberPromise
|
||||
|
||||
## default : do not show the group changing form
|
||||
$scope.group =
|
||||
change: false
|
||||
|
||||
## group ID of the current/selected user
|
||||
$scope.userGroup = memberPromise.group_id
|
||||
|
||||
## active authentication provider parameters
|
||||
$scope.activeProvider = activeProviderPromise
|
||||
|
||||
## allow the user to change his password except if he connect from an SSO
|
||||
$scope.preventPassword = false
|
||||
|
||||
## mapping of fields to disable
|
||||
$scope.preventField = {}
|
||||
|
||||
## Angular-Bootstrap datepicker configuration for birthday
|
||||
$scope.datePicker =
|
||||
format: 'dd/MM/yyyy'
|
||||
format: $locale.DATETIME_FORMATS.shortDate
|
||||
opened: false # default: datePicker is not shown
|
||||
options:
|
||||
startingDay: 1 # France: the week starts on monday
|
||||
startingDay: Fablab.weekStartingDay
|
||||
|
||||
|
||||
|
||||
##
|
||||
# Return the group object, identified by the ID set in $scope.userGroup
|
||||
##
|
||||
$scope.getUserGroup = ->
|
||||
for group in $scope.groups
|
||||
if group.id == $scope.userGroup
|
||||
return group
|
||||
|
||||
|
||||
|
||||
##
|
||||
# Change the group of the current user to the one set in $scope.userGroup
|
||||
##
|
||||
$scope.selectGroup = ->
|
||||
Member.update {id: $scope.user.id}, {user: {group_id: $scope.userGroup}}, (user) ->
|
||||
$scope.user = user
|
||||
$scope.group.change = false
|
||||
growl.success(_t('your_group_has_been_successfully_changed'))
|
||||
, (err) ->
|
||||
growl.error(_t('an_unexpected_error_prevented_your_group_from_being_changed'))
|
||||
console.error(err)
|
||||
|
||||
|
||||
|
||||
@ -81,10 +116,32 @@ Application.Controllers.controller "editProfileController", ["$scope", "$state",
|
||||
Auth._currentUser.name = content.name
|
||||
$scope.currentUser = content
|
||||
Auth._currentUser = content
|
||||
$rootScope.currentUser = content
|
||||
$state.go('app.public.home')
|
||||
|
||||
|
||||
|
||||
##
|
||||
# Ask for confirmation then delete the current user's account
|
||||
# @param user {Object} the current user (to delete)
|
||||
##
|
||||
$scope.deleteUser = (user)->
|
||||
dialogs.confirm
|
||||
resolve:
|
||||
object: ->
|
||||
title: _t('confirmation_required')
|
||||
msg: _t('do_you_really_want_to_delete_your_account')+' '+_t('all_data_relative_to_your_projects_will_be_lost')
|
||||
, -> # cancel confirmed
|
||||
Member.remove { id: user.id }, ->
|
||||
Auth.logout().then ->
|
||||
$state.go('app.public.home')
|
||||
growl.success(_t('your_user_account_has_been_successfully_deleted_goodbye'))
|
||||
, (error)->
|
||||
console.log(error)
|
||||
growl.error(_t('an_error_occured_preventing_your_account_from_being_deleted'))
|
||||
|
||||
|
||||
|
||||
##
|
||||
# For use with 'ng-class', returns the CSS class name for the uploads previews.
|
||||
# The preview may show a placeholder or the content of the file depending on the upload state.
|
||||
@ -95,6 +152,52 @@ Application.Controllers.controller "editProfileController", ["$scope", "$state",
|
||||
'fileinput-exists'
|
||||
else
|
||||
'fileinput-new'
|
||||
|
||||
|
||||
##
|
||||
# Check if the of the properties editable by the user are linked to the SSO
|
||||
# @return {boolean} true if some editable fields are mapped with the SSO, false otherwise
|
||||
##
|
||||
$scope.hasSsoFields = ->
|
||||
# if check if keys > 1 because there's a minimum of 1 mapping (id <-> provider-uid)
|
||||
# so the user may want to edit his profile on the SSO if at least 2 mappings exists
|
||||
Object.keys($scope.preventField).length > 1
|
||||
|
||||
|
||||
##
|
||||
# Disconnect and re-connect the user to the SSO to force the synchronisation of the profile's data
|
||||
##
|
||||
$scope.syncProfile = ->
|
||||
Auth.logout().then (oldUser) ->
|
||||
Session.destroy()
|
||||
$rootScope.currentUser = null
|
||||
$rootScope.toCheckNotifications = false
|
||||
$scope.notifications = []
|
||||
$window.location.href = $scope.activeProvider.link_to_sso_connect
|
||||
|
||||
|
||||
### PRIVATE SCOPE ###
|
||||
|
||||
##
|
||||
# Kind of constructor: these actions will be realized first when the controller is loaded
|
||||
##
|
||||
initialize = ->
|
||||
CSRF.setMetaTags()
|
||||
|
||||
# init the birth date to JS object
|
||||
$scope.user.profile.birthday = moment($scope.user.profile.birthday).toDate()
|
||||
|
||||
if $scope.activeProvider.providable_type != 'DatabaseProvider'
|
||||
$scope.preventPassword = true
|
||||
# bind fields protection with sso fields
|
||||
angular.forEach activeProviderPromise.mapping, (map) ->
|
||||
$scope.preventField[map] = true
|
||||
|
||||
|
||||
|
||||
|
||||
## !!! MUST BE CALLED AT THE END of the controller
|
||||
initialize()
|
||||
]
|
||||
|
||||
|
||||
@ -102,8 +205,8 @@ Application.Controllers.controller "editProfileController", ["$scope", "$state",
|
||||
##
|
||||
# Controller used on the public user's profile page (seeing another user's profile)
|
||||
##
|
||||
Application.Controllers.controller "showProfileController", ["$scope", "$stateParams", 'Member', ($scope, $stateParams, Member) ->
|
||||
Application.Controllers.controller "ShowProfileController", ["$scope", "$stateParams", 'Member', 'memberPromise', ($scope, $stateParams, Member, memberPromise) ->
|
||||
|
||||
## Selected user's profile (id from the current URL)
|
||||
$scope.user = Member.get {id: $stateParams.id}
|
||||
$scope.user = memberPromise
|
||||
]
|
||||
|
@ -4,7 +4,7 @@
|
||||
# Controller used in notifications page
|
||||
# inherits $scope.$parent.notifications (unread notifications) from ApplicationController
|
||||
##
|
||||
Application.Controllers.controller "notificationsController", ["$scope", 'Notification', ($scope, Notification) ->
|
||||
Application.Controllers.controller "NotificationsController", ["$scope", 'Notification', ($scope, Notification) ->
|
||||
|
||||
|
||||
|
||||
@ -32,7 +32,7 @@ Application.Controllers.controller "notificationsController", ["$scope", 'Notifi
|
||||
# Mark the provided notification as read, updating its status on the server and moving it
|
||||
# to the already read notifications list.
|
||||
# @param notification {{id:number}} the notification to mark as read
|
||||
# @param e {Object} jQuery event object
|
||||
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
|
||||
##
|
||||
$scope.markAsRead = (notification, e) ->
|
||||
e.preventDefault()
|
||||
|
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
|
||||
##
|
||||
Application.Controllers.controller "projectsController", ["$scope", "$state", 'Project', 'Machine', 'Theme', 'Component', ($scope, $state, Project, Machine, Theme, Component) ->
|
||||
|
||||
|
||||
Application.Controllers.controller "ProjectsController", ["$scope", "$state", 'Project', 'machinesPromise', 'themesPromise', 'componentsPromise'
|
||||
, ($scope, $state, Project, machinesPromise, themesPromise, componentsPromise) ->
|
||||
|
||||
### PRIVATE STATIC CONSTANTS ###
|
||||
|
||||
# Number of notifications added to the page when the user clicks on 'load next notifications'
|
||||
PROJECTS_PER_PAGE = 12
|
||||
|
||||
|
||||
|
||||
### PUBLIC SCOPE ###
|
||||
$scope.search = { q: "", from: undefined, machine_id: undefined, component_id: undefined, theme_id: undefined }
|
||||
|
||||
## list of projects to display
|
||||
$scope.projects = []
|
||||
|
||||
## list of machines / used for filtering
|
||||
$scope.machines = []
|
||||
$scope.machines = machinesPromise
|
||||
|
||||
## list of themes / used for filtering
|
||||
$scope.themes = Theme.query()
|
||||
$scope.themes = themesPromise
|
||||
|
||||
## list of components / used for filtering
|
||||
$scope.components = Component.query()
|
||||
$scope.components = componentsPromise
|
||||
|
||||
## By default, the pagination mode is activated to limit the page size
|
||||
$scope.paginateActive = true
|
||||
@ -175,19 +173,31 @@ Application.Controllers.controller "projectsController", ["$scope", "$state", 'P
|
||||
## The currently displayed page number
|
||||
$scope.page = 1
|
||||
|
||||
$scope.resetFilters = ->
|
||||
$scope.search.q = ""
|
||||
$scope.search.from = undefined
|
||||
$scope.search.machine_id = undefined
|
||||
$scope.search.component_id = undefined
|
||||
$scope.search.theme_id = undefined
|
||||
$scope.triggerSearch()
|
||||
|
||||
$scope.triggerSearch = ->
|
||||
Project.search { search: $scope.search, page: 1 }, (projects)->
|
||||
$scope.projects = projects
|
||||
if projects.length < PROJECTS_PER_PAGE
|
||||
$scope.paginateActive = false
|
||||
else
|
||||
$scope.paginateActive = true
|
||||
$scope.page = 2
|
||||
|
||||
##
|
||||
# Request the server to retrieve the next undisplayed projects and add them
|
||||
# to the local projects list.
|
||||
##
|
||||
$scope.loadMoreProjects = ->
|
||||
Project.query {page: $scope.page}, (projects) ->
|
||||
$scope.projects = $scope.projects.concat projects
|
||||
$scope.paginateActive = false if projects.length < PROJECTS_PER_PAGE
|
||||
|
||||
$scope.page += 1
|
||||
|
||||
# Project.query {page: $scope.page}, (projects) ->
|
||||
# $scope.projects = $scope.projects.concat projects
|
||||
# $scope.paginateActive = false if projects.length < PROJECTS_PER_PAGE
|
||||
Project.search { search: $scope.search, page: $scope.page }, (projects)->
|
||||
$scope.projects = $scope.projects.concat projects
|
||||
$scope.paginateActive = false if projects.length < PROJECTS_PER_PAGE
|
||||
$scope.page += 1
|
||||
|
||||
|
||||
##
|
||||
@ -199,37 +209,9 @@ Application.Controllers.controller "projectsController", ["$scope", "$state", 'P
|
||||
|
||||
|
||||
|
||||
##
|
||||
# Callback to delete the provided project. Then, the projects list page is refreshed (admins only)
|
||||
##
|
||||
$scope.delete = (project) ->
|
||||
# check the permissions
|
||||
if $scope.currentUser.role isnt 'admin'
|
||||
console.error 'Unauthorized operation'
|
||||
else
|
||||
# delete the project then refresh the projects list
|
||||
project.$delete ->
|
||||
$state.go('app.public.projects_list', {}, {reload: true})
|
||||
## initialization
|
||||
$scope.triggerSearch()
|
||||
|
||||
|
||||
|
||||
### PRIVATE SCOPE ###
|
||||
|
||||
##
|
||||
# Kind of constructor: these actions will be realized first when the controller is loaded
|
||||
##
|
||||
initialize = ->
|
||||
Machine.query().$promise.then (data)->
|
||||
$scope.machines = data.map (d) ->
|
||||
id: d.id
|
||||
name: d.name
|
||||
|
||||
$scope.loadMoreProjects()
|
||||
|
||||
|
||||
|
||||
## !!! MUST BE CALLED AT THE END of the controller
|
||||
initialize()
|
||||
]
|
||||
|
||||
|
||||
@ -237,7 +219,8 @@ Application.Controllers.controller "projectsController", ["$scope", "$state", 'P
|
||||
##
|
||||
# Controller used in the project creation page
|
||||
##
|
||||
Application.Controllers.controller "newProjectController", ["$scope", "$state", 'Project', 'Machine', 'Member', 'Component', 'Theme', 'Licence', '$document', 'CSRF', ($scope, $state, Project, Machine, Member, Component, Theme, Licence, $document, CSRF) ->
|
||||
Application.Controllers.controller "NewProjectController", ["$scope", "$state", 'Project', 'Machine', 'Member', 'Component', 'Theme', 'Licence', '$document', 'CSRF'
|
||||
, ($scope, $state, Project, Machine, Member, Component, Theme, Licence, $document, CSRF) ->
|
||||
CSRF.setMetaTags()
|
||||
|
||||
## API URL where the form will be posted
|
||||
@ -246,9 +229,6 @@ Application.Controllers.controller "newProjectController", ["$scope", "$state",
|
||||
## Form action on the above URL
|
||||
$scope.method = 'post'
|
||||
|
||||
## Button litteral text value
|
||||
$scope.submitName = 'Enregistrer comme brouillon'
|
||||
|
||||
## Default project parameters
|
||||
$scope.project =
|
||||
project_steps_attributes: []
|
||||
@ -271,7 +251,8 @@ Application.Controllers.controller "newProjectController", ["$scope", "$state",
|
||||
##
|
||||
# Controller used in the project edition page
|
||||
##
|
||||
Application.Controllers.controller "editProjectController", ["$scope", "$state", '$stateParams', 'Project', 'Machine', 'Member', 'Component', 'Theme', 'Licence', '$document', 'CSRF', ($scope, $state, $stateParams, Project, Machine, Member, Component, Theme, Licence, $document, CSRF) ->
|
||||
Application.Controllers.controller "EditProjectController", ["$scope", "$state", '$stateParams', 'Project', 'Machine', 'Member', 'Component', 'Theme', 'Licence', '$document', 'CSRF', 'projectPromise'
|
||||
, ($scope, $state, $stateParams, Project, Machine, Member, Component, Theme, Licence, $document, CSRF, projectPromise) ->
|
||||
CSRF.setMetaTags()
|
||||
|
||||
## API URL where the form will be posted
|
||||
@ -280,15 +261,8 @@ Application.Controllers.controller "editProjectController", ["$scope", "$state",
|
||||
## Form action on the above URL
|
||||
$scope.method = 'put'
|
||||
|
||||
## Button litteral text value
|
||||
$scope.submitName = 'Enregistrer'
|
||||
|
||||
## Retrieve the project's details, if an error occured, redirect the user to the projects list page
|
||||
$scope.project = Project.get {id: $stateParams.id}
|
||||
, -> # success
|
||||
return
|
||||
, -> # failed
|
||||
$state.go('app.public.projects_list')
|
||||
$scope.project = projectPromise
|
||||
|
||||
## Other members list (project collaborators)
|
||||
Member.query().$promise.then (data)->
|
||||
@ -307,18 +281,15 @@ Application.Controllers.controller "editProjectController", ["$scope", "$state",
|
||||
##
|
||||
# Controller used in the public project's details page
|
||||
##
|
||||
Application.Controllers.controller "showProjectController", ["$scope", "$state", "$stateParams", "Project", '$location', ($scope, $state, $stateParams, Project, $location) ->
|
||||
|
||||
|
||||
Application.Controllers.controller "ShowProjectController", ["$scope", "$state", "projectPromise", '$location', '$uibModal', '_t'
|
||||
, ($scope, $state, projectPromise, $location, $uibModal, _t) ->
|
||||
|
||||
### PUBLIC SCOPE ###
|
||||
|
||||
## Will be set to true once the project details are loaded. Used to load the Disqus plugin at the right moment
|
||||
$scope.contentLoaded = false
|
||||
|
||||
## Store the project's details
|
||||
$scope.project = {}
|
||||
|
||||
$scope.project = projectPromise
|
||||
$scope.projectUrl = $location.absUrl()
|
||||
$scope.disqusShortname = Fablab.disqusShortname
|
||||
|
||||
|
||||
##
|
||||
@ -336,23 +307,63 @@ Application.Controllers.controller "showProjectController", ["$scope", "$state",
|
||||
|
||||
|
||||
|
||||
### PRIVATE SCOPE ###
|
||||
##
|
||||
# Test if the provided user has the deletion rights on the current project
|
||||
# @param [user] {{id:number}} (optional) the user to check rights
|
||||
# @returns boolean
|
||||
##
|
||||
$scope.projectDeletableBy = (user) ->
|
||||
return false if not user?
|
||||
return true if $scope.project.author_id == user.id
|
||||
|
||||
|
||||
|
||||
##
|
||||
# Kind of constructor: these actions will be realized first when the controller is loaded
|
||||
# Callback to delete the current project. Then, the user is redirected to the projects list page,
|
||||
# which is refreshed. Admins and project owner only are allowed to delete a project
|
||||
##
|
||||
initialize = ->
|
||||
## Retrieve the project content
|
||||
$scope.project = Project.get {id: $stateParams.id}
|
||||
, -> # success
|
||||
$scope.contentLoaded = true
|
||||
$scope.project_url = $location.absUrl()
|
||||
return
|
||||
, -> # failed, redirect the user to the projects listing
|
||||
$state.go('app.public.projects_list')
|
||||
$scope.deleteProject = ->
|
||||
# check the permissions
|
||||
if $scope.currentUser.role is 'admin' or $scope.projectDeletableBy($scope.currentUser)
|
||||
# delete the project then refresh the projects list
|
||||
$scope.project.$delete ->
|
||||
$state.go('app.public.projects_list', {}, {reload: true})
|
||||
else
|
||||
console.error _t('unauthorized_operation')
|
||||
|
||||
##
|
||||
# Open a modal box containg a form that allow the end-user to signal an abusive content
|
||||
# @param e {Object} jQuery event
|
||||
##
|
||||
$scope.signalAbuse = (e) ->
|
||||
e.preventDefault() if e
|
||||
|
||||
$uibModal.open
|
||||
templateUrl: '<%= asset_path "shared/signalAbuseModal.html" %>'
|
||||
size: 'md'
|
||||
resolve:
|
||||
project: -> $scope.project
|
||||
controller: ['$scope', '$uibModalInstance', '_t', 'growl', 'Abuse', 'project', ($scope, $uibModalInstance, _t, growl, Abuse, project) ->
|
||||
|
||||
# signaler's profile & signalement infos
|
||||
$scope.signaler = {
|
||||
signaled_type: 'Project'
|
||||
signaled_id: project.id
|
||||
}
|
||||
|
||||
# callback for signaling cancellation
|
||||
$scope.cancel = ->
|
||||
$uibModalInstance.dismiss('cancel')
|
||||
|
||||
# callback for form validation
|
||||
$scope.ok = ->
|
||||
Abuse.save {}, {abuse: $scope.signaler}, (res) ->
|
||||
# creation successful
|
||||
growl.success(_t('your_report_was_successful_thanks'))
|
||||
$uibModalInstance.close(res)
|
||||
, (error) ->
|
||||
# creation failed...
|
||||
growl.error(_t('an_error_occured_while_sending_your_report'))
|
||||
]
|
||||
|
||||
## !!! MUST BE CALLED AT THE END of the controller
|
||||
initialize()
|
||||
]
|
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);
|
||||
else
|
||||
ngModelCtrl.$setValidity('filetype', false);
|
||||
}
|
||||
};
|
||||
}
|
||||
$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) ->
|
||||
Holder.addTheme("icon", { background: "white", foreground: "#e9e9e9", size: 80, font: "FontAwesome"})
|
||||
.addTheme("icon-xs", { background: "white", foreground: "#e0e0e0", size: 20, font: "FontAwesome"})
|
||||
.addTheme("icon-black-xs", { background: "black", foreground: "white", size: 20, font: "FontAwesome"})
|
||||
.addTheme("avatar", { background: "#eeeeee", foreground: "#555555", size: 16, font: "FontAwesome"})
|
||||
.run(element[0])
|
||||
return
|
||||
@ -66,3 +68,38 @@ Application.Directives.directive "disableAnimation", ($animate) ->
|
||||
attrs.$observe "disableAnimation", (value) ->
|
||||
$animate.enabled not value, elem
|
||||
|
||||
|
||||
##
|
||||
# Isolate a form's scope from its parent : no nested validation
|
||||
##
|
||||
Application.Directives.directive 'isolateForm', [ ->
|
||||
{
|
||||
restrict: 'A',
|
||||
require: '?form'
|
||||
link: (scope, elm, attrs, ctrl) ->
|
||||
return unless ctrl
|
||||
|
||||
# Do a copy of the controller
|
||||
ctrlCopy = {}
|
||||
angular.copy(ctrl, ctrlCopy)
|
||||
|
||||
# Get the form's parent
|
||||
parent = elm.parent().controller('form')
|
||||
# Remove parent link to the controller
|
||||
parent.$removeControl(ctrl)
|
||||
|
||||
# Replace form controller with a "isolated form"
|
||||
isolatedFormCtrl =
|
||||
$setValidity: (validationToken, isValid, control) ->
|
||||
ctrlCopy.$setValidity(validationToken, isValid, control);
|
||||
parent.$setValidity(validationToken, true, ctrl);
|
||||
|
||||
$setDirty: ->
|
||||
elm.removeClass('ng-pristine').addClass('ng-dirty');
|
||||
ctrl.$dirty = true;
|
||||
ctrl.$pristine = false;
|
||||
|
||||
angular.extend(ctrl, isolatedFormCtrl)
|
||||
|
||||
}
|
||||
]
|
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'
|
||||
|
||||
Application.Filters.filter 'array', [ ->
|
||||
(arrayLength) ->
|
||||
if (arrayLength)
|
||||
arrayLength = Math.ceil(arrayLength)
|
||||
arr = new Array(arrayLength)
|
||||
|
||||
for i in [0 ... arrayLength]
|
||||
arr[i] = i
|
||||
|
||||
arr
|
||||
]
|
||||
|
||||
# filter for projects and trainings
|
||||
Application.Controllers.filter "machineFilter", [ ->
|
||||
Application.Filters.filter "machineFilter", [ ->
|
||||
(elements, selectedMachine) ->
|
||||
if !angular.isUndefined(elements) and !angular.isUndefined(selectedMachine) and elements? and selectedMachine?
|
||||
filteredElements = []
|
||||
@ -13,7 +25,7 @@ Application.Controllers.filter "machineFilter", [ ->
|
||||
elements
|
||||
]
|
||||
|
||||
Application.Controllers.filter "projectMemberFilter", [ "Auth", (Auth)->
|
||||
Application.Filters.filter "projectMemberFilter", [ "Auth", (Auth)->
|
||||
(projects, selectedMember) ->
|
||||
if !angular.isUndefined(projects) and angular.isDefined(selectedMember) and projects? and selectedMember? and selectedMember != ""
|
||||
filteredProject = []
|
||||
@ -32,7 +44,7 @@ Application.Controllers.filter "projectMemberFilter", [ "Auth", (Auth)->
|
||||
projects
|
||||
]
|
||||
|
||||
Application.Controllers.filter "themeFilter", [ ->
|
||||
Application.Filters.filter "themeFilter", [ ->
|
||||
(projects, selectedTheme) ->
|
||||
if !angular.isUndefined(projects) and !angular.isUndefined(selectedTheme) and projects? and selectedTheme?
|
||||
filteredProjects = []
|
||||
@ -44,7 +56,7 @@ Application.Controllers.filter "themeFilter", [ ->
|
||||
projects
|
||||
]
|
||||
|
||||
Application.Controllers.filter "componentFilter", [ ->
|
||||
Application.Filters.filter "componentFilter", [ ->
|
||||
(projects, selectedComponent) ->
|
||||
if !angular.isUndefined(projects) and !angular.isUndefined(selectedComponent) and projects? and selectedComponent?
|
||||
filteredProjects = []
|
||||
@ -56,7 +68,7 @@ Application.Controllers.filter "componentFilter", [ ->
|
||||
projects
|
||||
]
|
||||
|
||||
Application.Controllers.filter "projectsByAuthor", [ ->
|
||||
Application.Filters.filter "projectsByAuthor", [ ->
|
||||
(projects, authorId) ->
|
||||
if !angular.isUndefined(projects) and angular.isDefined(authorId) and projects? and authorId? and authorId != ""
|
||||
filteredProject = []
|
||||
@ -68,7 +80,7 @@ Application.Controllers.filter "projectsByAuthor", [ ->
|
||||
projects
|
||||
]
|
||||
|
||||
Application.Controllers.filter "projectsCollabored", [ ->
|
||||
Application.Filters.filter "projectsCollabored", [ ->
|
||||
(projects, memberId) ->
|
||||
if !angular.isUndefined(projects) and angular.isDefined(memberId) and projects? and memberId? and memberId != ""
|
||||
filteredProject = []
|
||||
@ -81,24 +93,84 @@ Application.Controllers.filter "projectsCollabored", [ ->
|
||||
]
|
||||
|
||||
# depend on humanize.js lib in /vendor
|
||||
Application.Controllers.filter "humanize", [ ->
|
||||
Application.Filters.filter "humanize", [ ->
|
||||
(element, param) ->
|
||||
Humanize.truncate(element, param, null)
|
||||
]
|
||||
|
||||
Application.Controllers.filter "breakFilter", [ ->
|
||||
Application.Filters.filter "breakFilter", [ ->
|
||||
(text) ->
|
||||
if text != undefined
|
||||
text.replace(/\n/g, '<br />')
|
||||
]
|
||||
|
||||
Application.Controllers.filter "toTrusted", [ "$sce", ($sce) ->
|
||||
Application.Filters.filter "toTrusted", [ "$sce", ($sce) ->
|
||||
(text) ->
|
||||
$sce.trustAsHtml text
|
||||
]
|
||||
|
||||
|
||||
Application.Controllers.filter "eventsFilter", [ ->
|
||||
Application.Filters.filter "planIntervalFilter", [ ->
|
||||
(interval, intervalCount) ->
|
||||
if typeof intervalCount != 'number'
|
||||
switch interval
|
||||
when 'day' then return 'jour'
|
||||
when 'week' then return 'semaine'
|
||||
when 'month' then return 'mois'
|
||||
when 'year' then return 'année'
|
||||
else
|
||||
if intervalCount == 1
|
||||
switch interval
|
||||
when 'day' then return 'un jour'
|
||||
when 'week' then return 'une semaine'
|
||||
when 'month' then return 'un mois'
|
||||
when 'year' then return 'un an'
|
||||
else
|
||||
switch interval
|
||||
when 'day' then return intervalCount+ ' jours'
|
||||
when 'week' then return intervalCount+ ' semaines'
|
||||
when 'month' then return intervalCount+ ' mois'
|
||||
when 'year' then return intervalCount+ ' ans'
|
||||
]
|
||||
|
||||
Application.Filters.filter "humanReadablePlanName", ['$filter', ($filter)->
|
||||
(plan, groups, short) ->
|
||||
if plan?
|
||||
result = plan.base_name
|
||||
if groups?
|
||||
for group in groups
|
||||
if group.id == plan.group_id
|
||||
if short?
|
||||
result += " - #{group.slug}"
|
||||
else
|
||||
result += " - #{group.name}"
|
||||
result += " - #{$filter('planIntervalFilter')(plan.interval, plan.interval_count)}"
|
||||
result
|
||||
]
|
||||
|
||||
Application.Filters.filter "trainingReservationsFilter", [ ->
|
||||
(elements, selectedScope) ->
|
||||
if !angular.isUndefined(elements) and !angular.isUndefined(selectedScope) and elements? and selectedScope?
|
||||
filteredElements = []
|
||||
angular.forEach elements, (element)->
|
||||
switch selectedScope
|
||||
when "future"
|
||||
if new Date(element.start_at) > new Date
|
||||
filteredElements.push(element)
|
||||
when "passed"
|
||||
if new Date(element.start_at) <= new Date and !element.is_valid
|
||||
filteredElements.push(element)
|
||||
when "valided"
|
||||
if new Date(element.start_at) <= new Date and element.is_valid
|
||||
filteredElements.push(element)
|
||||
else
|
||||
return []
|
||||
filteredElements
|
||||
else
|
||||
elements
|
||||
]
|
||||
|
||||
Application.Filters.filter "eventsReservationsFilter", [ ->
|
||||
(elements, selectedScope) ->
|
||||
if !angular.isUndefined(elements) and !angular.isUndefined(selectedScope) and elements? and selectedScope? and selectedScope != ""
|
||||
filteredElements = []
|
||||
@ -117,3 +189,43 @@ Application.Controllers.filter "eventsFilter", [ ->
|
||||
else
|
||||
elements
|
||||
]
|
||||
|
||||
Application.Filters.filter "groupFilter", [ ->
|
||||
(elements, member) ->
|
||||
if !angular.isUndefined(elements) and !angular.isUndefined(member) and elements? and member?
|
||||
filteredElements = []
|
||||
angular.forEach elements, (element)->
|
||||
if member.group_id == element.id
|
||||
filteredElements.push(element)
|
||||
filteredElements
|
||||
else
|
||||
elements
|
||||
]
|
||||
|
||||
Application.Filters.filter "groupByFilter", [ ->
|
||||
_.memoize (elements, field)->
|
||||
_.groupBy(elements, field)
|
||||
]
|
||||
|
||||
Application.Filters.filter "capitalize", [->
|
||||
(text)->
|
||||
"#{text.charAt(0).toUpperCase()}#{text.slice(1).toLowerCase()}"
|
||||
]
|
||||
|
||||
|
||||
Application.Filters.filter 'reverse', [ ->
|
||||
(items) ->
|
||||
unless angular.isArray(items)
|
||||
return items
|
||||
|
||||
items.slice().reverse()
|
||||
]
|
||||
|
||||
Application.Filters.filter 'toArray', [ ->
|
||||
(obj) ->
|
||||
return obj unless (obj instanceof Object)
|
||||
_.map obj, (val, key) ->
|
||||
if angular.isObject(val)
|
||||
Object.defineProperty(val, '$key', {__proto__: null, value: key})
|
||||
|
||||
]
|
@ -1,222 +1,844 @@
|
||||
angular.module('application.router', ['ui.router']).
|
||||
config ['$stateProvider', '$urlRouterProvider', '$locationProvider', ($stateProvider, $urlRouterProvider, $locationProvider) ->
|
||||
$locationProvider.hashPrefix('!')
|
||||
$urlRouterProvider.otherwise("/")
|
||||
config ['$stateProvider', '$urlRouterProvider', '$locationProvider', ($stateProvider, $urlRouterProvider, $locationProvider) ->
|
||||
$locationProvider.hashPrefix('!')
|
||||
$urlRouterProvider.otherwise("/")
|
||||
|
||||
# abstract root parents states
|
||||
# these states controls the access rights to the various routes inherited from them
|
||||
$stateProvider
|
||||
.state 'app',
|
||||
abstract: true
|
||||
views:
|
||||
'header': { templateUrl: '<%= asset_path "shared/header.html" %>' }
|
||||
'leftnav':
|
||||
templateUrl: '<%= asset_path "shared/leftnav.html" %>'
|
||||
controller: 'mainNavController'
|
||||
'main':
|
||||
templateUrl: '<%= asset_path "home.html" %>'
|
||||
controller: 'homeController'
|
||||
.state 'app.public',
|
||||
abstract: true
|
||||
.state 'app.logged',
|
||||
abstract: true
|
||||
data:
|
||||
authorizedRoles: ['member', 'admin']
|
||||
resolve:
|
||||
currentUser: ['Auth', (Auth)->
|
||||
Auth.currentUser()
|
||||
]
|
||||
onEnter: ["currentUser", "$rootScope", (currentUser, $rootScope)->
|
||||
$rootScope.currentUser = currentUser
|
||||
]
|
||||
.state 'app.admin',
|
||||
abstract: true
|
||||
data:
|
||||
authorizedRoles: ['admin']
|
||||
resolve:
|
||||
currentUser: ['Auth', (Auth)->
|
||||
Auth.currentUser()
|
||||
]
|
||||
onEnter: ["currentUser", "$rootScope", (currentUser, $rootScope)->
|
||||
$rootScope.currentUser = currentUser
|
||||
]
|
||||
# abstract root parents states
|
||||
# these states controls the access rights to the various routes inherited from them
|
||||
$stateProvider
|
||||
.state 'app',
|
||||
abstract: true
|
||||
views:
|
||||
'header':
|
||||
templateUrl: '<%= asset_path "shared/header.html" %>'
|
||||
'leftnav':
|
||||
templateUrl: '<%= asset_path "shared/leftnav.html" %>'
|
||||
controller: 'MainNavController'
|
||||
'main': {}
|
||||
resolve:
|
||||
logoFile: ['CustomAsset', (CustomAsset) ->
|
||||
CustomAsset.get({name: 'logo-file'}).$promise
|
||||
]
|
||||
logoBlackFile: ['CustomAsset', (CustomAsset) ->
|
||||
CustomAsset.get({name: 'logo-black-file'}).$promise
|
||||
]
|
||||
commonTranslations: [ 'Translations', (Translations) ->
|
||||
Translations.query(['app.public.common', 'app.shared.buttons', 'app.shared.elements']).$promise
|
||||
]
|
||||
onEnter: ['$rootScope', 'logoFile', 'logoBlackFile', ($rootScope, logoFile, logoBlackFile) ->
|
||||
## Application logo
|
||||
$rootScope.logo = logoFile.custom_asset
|
||||
$rootScope.logoBlack = logoBlackFile.custom_asset
|
||||
]
|
||||
.state 'app.public',
|
||||
abstract: true
|
||||
.state 'app.logged',
|
||||
abstract: true
|
||||
data:
|
||||
authorizedRoles: ['member', 'admin']
|
||||
resolve:
|
||||
currentUser: ['Auth', (Auth)->
|
||||
Auth.currentUser()
|
||||
]
|
||||
onEnter: ["$state", "$timeout", "currentUser", "$rootScope", ($state, $timeout, currentUser, $rootScope)->
|
||||
$rootScope.currentUser = currentUser
|
||||
]
|
||||
.state 'app.admin',
|
||||
abstract: true
|
||||
data:
|
||||
authorizedRoles: ['admin']
|
||||
resolve:
|
||||
currentUser: ['Auth', (Auth)->
|
||||
Auth.currentUser()
|
||||
]
|
||||
onEnter: ["$state", "$timeout", "currentUser", "$rootScope", ($state, $timeout, currentUser, $rootScope)->
|
||||
$rootScope.currentUser = currentUser
|
||||
]
|
||||
|
||||
|
||||
|
||||
# main pages
|
||||
.state 'app.public.about',
|
||||
url: '/about'
|
||||
views:
|
||||
'content@': { templateUrl: '<%= asset_path "shared/about.html" %>' }
|
||||
.state 'app.public.home',
|
||||
url: '/?reset_password_token'
|
||||
views:
|
||||
'main':
|
||||
templateUrl: '<%= asset_path "home.html" %>'
|
||||
controller: 'homeController'
|
||||
# main pages
|
||||
.state 'app.public.about',
|
||||
url: '/about'
|
||||
views:
|
||||
'content@':
|
||||
templateUrl: '<%= asset_path "shared/about.html" %>'
|
||||
controller: 'AboutController'
|
||||
resolve:
|
||||
translations: [ 'Translations', (Translations) ->
|
||||
Translations.query('app.public.about').$promise
|
||||
]
|
||||
.state 'app.public.home',
|
||||
url: '/?reset_password_token'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "home.html" %>'
|
||||
controller: 'HomeController'
|
||||
resolve:
|
||||
lastMembersPromise: ['Member', (Member)->
|
||||
Member.lastSubscribed(limit: 4).$promise
|
||||
]
|
||||
lastProjectsPromise: ['Project', (Project)->
|
||||
Project.lastPublished().$promise
|
||||
]
|
||||
upcomingEventsPromise: ['Event', (Event)->
|
||||
Event.upcoming(limit: 3).$promise
|
||||
]
|
||||
homeBlogpostPromise: ['Setting', (Setting)->
|
||||
Setting.get(name: 'home_blogpost').$promise
|
||||
]
|
||||
twitterNamePromise: ['Setting', (Setting)->
|
||||
Setting.get(name: 'twitter_name').$promise
|
||||
]
|
||||
translations: [ 'Translations', (Translations) ->
|
||||
Translations.query('app.public.home').$promise
|
||||
]
|
||||
|
||||
|
||||
# dashboard
|
||||
.state 'app.logged.dashboard_profile',
|
||||
url: '/dashboard/profile'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "dashboard/profile.html" %>'
|
||||
controller: 'editProfileController'
|
||||
.state 'app.logged.dashboard_projects',
|
||||
url: '/dashboard/projects'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "dashboard/projects.html" %>'
|
||||
controller: 'dashboardProjectsController'
|
||||
|
||||
|
||||
# members
|
||||
.state 'app.logged.members_show',
|
||||
url: '/members/:id'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "members/show.html" %>'
|
||||
controller: 'showProfileController'
|
||||
.state 'app.logged.members',
|
||||
url: '/members'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "members/index.html" %>'
|
||||
controller: 'membersController'
|
||||
|
||||
|
||||
# projects
|
||||
.state 'app.public.projects_list',
|
||||
url: '/projects'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "projects/index.html" %>'
|
||||
controller: 'projectsController'
|
||||
.state 'app.public.projects_show',
|
||||
url: '/projects/:id'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "projects/show.html" %>'
|
||||
controller: 'showProjectController'
|
||||
.state 'app.logged.projects_new',
|
||||
url: '/projects/new'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "projects/new.html" %>'
|
||||
controller: 'newProjectController'
|
||||
.state 'app.logged.projects_edit',
|
||||
url: '/projects/:id/edit'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "projects/edit.html" %>'
|
||||
controller: 'editProjectController'
|
||||
# profile completion (SSO import passage point)
|
||||
.state 'app.logged.profileCompletion',
|
||||
url: '/profile_completion'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "profile/complete.html"%>'
|
||||
controller: 'CompleteProfileController'
|
||||
resolve:
|
||||
settingsPromise: ['Setting', (Setting)->
|
||||
Setting.query(names: "['fablab_name', 'name_genre']").$promise
|
||||
]
|
||||
activeProviderPromise: ['AuthProvider', (AuthProvider) ->
|
||||
AuthProvider.active().$promise
|
||||
]
|
||||
groupsPromise: ['Group', (Group)->
|
||||
Group.query().$promise
|
||||
]
|
||||
cguFile: ['CustomAsset', (CustomAsset) ->
|
||||
CustomAsset.get({name: 'cgu-file'}).$promise
|
||||
]
|
||||
memberPromise: ['Member', 'currentUser', (Member, currentUser)->
|
||||
Member.get(id: currentUser.id).$promise
|
||||
]
|
||||
translations: [ 'Translations', (Translations) ->
|
||||
Translations.query(['app.logged.profileCompletion', 'app.shared.user']).$promise
|
||||
]
|
||||
|
||||
|
||||
|
||||
# machines
|
||||
.state 'app.public.machines_list',
|
||||
url: '/machines'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "machines/index.html" %>'
|
||||
controller: 'machinesController'
|
||||
.state 'app.public.machines_show',
|
||||
url: '/machines/:id'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "machines/show.html" %>'
|
||||
controller: 'showMachineController'
|
||||
.state 'app.admin.machines_new',
|
||||
url: '/machines/new'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "machines/new.html" %>'
|
||||
controller: 'newMachineController'
|
||||
.state 'app.admin.machines_edit',
|
||||
url: '/machines/:id/edit'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "machines/edit.html" %>'
|
||||
controller: 'editMachineController'
|
||||
# dashboard
|
||||
.state 'app.logged.dashboard',
|
||||
abstract: true
|
||||
url: '/dashboard'
|
||||
resolve:
|
||||
memberPromise: ['Member', 'currentUser', (Member, currentUser)->
|
||||
Member.get(id: currentUser.id).$promise
|
||||
]
|
||||
.state 'app.logged.dashboard.profile',
|
||||
url: '/profile'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "dashboard/profile.html" %>'
|
||||
controller: 'EditProfileController'
|
||||
resolve:
|
||||
groups: ['Group', (Group)->
|
||||
Group.query().$promise
|
||||
]
|
||||
activeProviderPromise: ['AuthProvider', (AuthProvider) ->
|
||||
AuthProvider.active().$promise
|
||||
]
|
||||
translations: [ 'Translations', (Translations) ->
|
||||
Translations.query(['app.logged.dashboard.profile', 'app.shared.user']).$promise
|
||||
]
|
||||
.state 'app.logged.dashboard.projects',
|
||||
url: '/projects'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "dashboard/projects.html" %>'
|
||||
controller: 'DashboardController'
|
||||
resolve:
|
||||
translations: [ 'Translations', (Translations) ->
|
||||
Translations.query('app.logged.dashboard.projects').$promise
|
||||
]
|
||||
.state 'app.logged.dashboard.trainings',
|
||||
url: '/trainings'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "dashboard/trainings.html" %>'
|
||||
controller: 'DashboardController'
|
||||
resolve:
|
||||
translations: [ 'Translations', (Translations) ->
|
||||
Translations.query('app.logged.dashboard.trainings').$promise
|
||||
]
|
||||
.state 'app.logged.dashboard.events',
|
||||
url: '/events'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "dashboard/events.html" %>'
|
||||
controller: 'DashboardController'
|
||||
resolve:
|
||||
translations: [ 'Translations', (Translations) ->
|
||||
Translations.query('app.logged.dashboard.events').$promise
|
||||
]
|
||||
.state 'app.logged.dashboard.invoices',
|
||||
url: '/invoices'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "dashboard/invoices.html" %>'
|
||||
controller: 'DashboardController'
|
||||
resolve:
|
||||
translations: [ 'Translations', (Translations) ->
|
||||
Translations.query('app.logged.dashboard.invoices').$promise
|
||||
]
|
||||
|
||||
|
||||
# notifications
|
||||
.state 'app.logged.notifications',
|
||||
url: '/notifications'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "notifications/index.html" %>'
|
||||
controller: 'notificationsController'
|
||||
# members
|
||||
.state 'app.logged.members_show',
|
||||
url: '/members/:id'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "members/show.html" %>'
|
||||
controller: 'ShowProfileController'
|
||||
resolve:
|
||||
memberPromise: ['$stateParams', 'Member', ($stateParams, Member)->
|
||||
Member.get(id: $stateParams.id).$promise
|
||||
]
|
||||
translations: [ 'Translations', (Translations) ->
|
||||
Translations.query('app.logged.members_show').$promise
|
||||
]
|
||||
.state 'app.logged.members',
|
||||
url: '/members'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "members/index.html" %>'
|
||||
controller: 'MembersController'
|
||||
resolve:
|
||||
membersPromise: ['Member', (Member)->
|
||||
Member.query({requested_attributes:'[profile]'}).$promise
|
||||
]
|
||||
translations: [ 'Translations', (Translations) ->
|
||||
Translations.query('app.logged.members').$promise
|
||||
]
|
||||
|
||||
# projects
|
||||
.state 'app.public.projects_list',
|
||||
url: '/projects'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "projects/index.html" %>'
|
||||
controller: 'ProjectsController'
|
||||
resolve:
|
||||
themesPromise: ['Theme', (Theme)->
|
||||
Theme.query().$promise
|
||||
]
|
||||
componentsPromise: ['Component', (Component)->
|
||||
Component.query().$promise
|
||||
]
|
||||
machinesPromise: ['Machine', (Machine)->
|
||||
Machine.query().$promise
|
||||
]
|
||||
translations: [ 'Translations', (Translations) ->
|
||||
Translations.query('app.public.projects_list').$promise
|
||||
]
|
||||
.state 'app.logged.projects_new',
|
||||
url: '/projects/new'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "projects/new.html" %>'
|
||||
controller: 'NewProjectController'
|
||||
resolve:
|
||||
translations: [ 'Translations', (Translations) ->
|
||||
Translations.query(['app.logged.projects_new', 'app.shared.project']).$promise
|
||||
]
|
||||
.state 'app.public.projects_show',
|
||||
url: '/projects/:id'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "projects/show.html" %>'
|
||||
controller: 'ShowProjectController'
|
||||
resolve:
|
||||
projectPromise: ['$stateParams', 'Project', ($stateParams, Project)->
|
||||
Project.get(id: $stateParams.id).$promise
|
||||
]
|
||||
translations: [ 'Translations', (Translations) ->
|
||||
Translations.query('app.public.projects_show').$promise
|
||||
]
|
||||
.state 'app.logged.projects_edit',
|
||||
url: '/projects/:id/edit'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "projects/edit.html" %>'
|
||||
controller: 'EditProjectController'
|
||||
resolve:
|
||||
projectPromise: ['$stateParams', 'Project', ($stateParams, Project)->
|
||||
Project.get(id: $stateParams.id).$promise
|
||||
]
|
||||
translations: [ 'Translations', (Translations) ->
|
||||
Translations.query(['app.logged.projects_edit', 'app.shared.project']).$promise
|
||||
]
|
||||
|
||||
|
||||
# events
|
||||
.state 'app.public.events_list',
|
||||
url: '/events'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "events/index.html" %>'
|
||||
controller: 'eventsController'
|
||||
.state 'app.public.events_show',
|
||||
url: '/events/:id'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "events/show.html" %>'
|
||||
controller: 'showEventController'
|
||||
# machines
|
||||
.state 'app.public.machines_list',
|
||||
url: '/machines'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "machines/index.html" %>'
|
||||
controller: 'MachinesController'
|
||||
resolve:
|
||||
machinesPromise: ['Machine', (Machine)->
|
||||
Machine.query().$promise
|
||||
]
|
||||
translations: [ 'Translations', (Translations) ->
|
||||
Translations.query(['app.public.machines_list', 'app.shared.training_reservation_modal', 'app.shared.request_training_modal']).$promise
|
||||
]
|
||||
.state 'app.admin.machines_new',
|
||||
url: '/machines/new'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "machines/new.html" %>'
|
||||
controller: 'NewMachineController'
|
||||
resolve:
|
||||
translations: [ 'Translations', (Translations) ->
|
||||
Translations.query(['app.admin.machines_new', 'app.shared.machine']).$promise
|
||||
]
|
||||
.state 'app.public.machines_show',
|
||||
url: '/machines/:id'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "machines/show.html" %>'
|
||||
controller: 'ShowMachineController'
|
||||
resolve:
|
||||
machinePromise: ['Machine', '$stateParams', (Machine, $stateParams)->
|
||||
Machine.get(id: $stateParams.id).$promise
|
||||
]
|
||||
translations: [ 'Translations', (Translations) ->
|
||||
Translations.query(['app.public.machines_show', 'app.shared.training_reservation_modal', 'app.shared.request_training_modal']).$promise
|
||||
]
|
||||
.state 'app.logged.machines_reserve',
|
||||
url: '/machines/:id/reserve'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "machines/reserve.html" %>'
|
||||
controller: 'ReserveMachineController'
|
||||
resolve:
|
||||
plansPromise: ['Plan', (Plan)->
|
||||
Plan.query(attributes_requested: "['machines_credits']").$promise
|
||||
]
|
||||
groupsPromise: ['Group', (Group)->
|
||||
Group.query().$promise
|
||||
]
|
||||
settingsPromise: ['Setting', (Setting)->
|
||||
Setting.query(names: "['machine_explications_alert',
|
||||
'booking_window_start',
|
||||
'booking_window_end',
|
||||
'booking_move_enable',
|
||||
'booking_move_delay',
|
||||
'booking_cancel_enable',
|
||||
'booking_cancel_delay',
|
||||
'subscription_explications_alert']").$promise
|
||||
]
|
||||
translations: [ 'Translations', (Translations) ->
|
||||
Translations.query(['app.logged.machines_reserve', 'app.shared.plan_subscribe', 'app.shared.member_select',
|
||||
'app.shared.stripe', 'app.shared.valid_reservation_modal', 'app.shared.confirm_modify_slot_modal']).$promise
|
||||
]
|
||||
.state 'app.admin.machines_edit',
|
||||
url: '/machines/:id/edit'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "machines/edit.html" %>'
|
||||
controller: 'EditMachineController'
|
||||
resolve:
|
||||
machinePromise: ['Machine', '$stateParams', (Machine, $stateParams)->
|
||||
Machine.get(id: $stateParams.id).$promise
|
||||
]
|
||||
translations: [ 'Translations', (Translations) ->
|
||||
Translations.query(['app.admin.machines_edit', 'app.shared.machine']).$promise
|
||||
]
|
||||
|
||||
# trainings
|
||||
.state 'app.logged.trainings_reserve',
|
||||
url: '/trainings/reserve'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "trainings/reserve.html" %>'
|
||||
controller: 'ReserveTrainingController'
|
||||
resolve:
|
||||
explicationAlertPromise: ['Setting', (Setting)->
|
||||
Setting.get(name: 'training_explications_alert').$promise
|
||||
]
|
||||
plansPromise: ['Plan', (Plan)->
|
||||
Plan.query(attributes_requested: "['trainings_credits']").$promise
|
||||
]
|
||||
groupsPromise: ['Group', (Group)->
|
||||
Group.query().$promise
|
||||
]
|
||||
availabilityTrainingsPromise: ['Availability', (Availability)->
|
||||
Availability.trainings().$promise
|
||||
]
|
||||
settingsPromise: ['Setting', (Setting)->
|
||||
Setting.query(names: "['booking_window_start',
|
||||
'booking_window_end',
|
||||
'booking_move_enable',
|
||||
'booking_move_delay',
|
||||
'booking_cancel_enable',
|
||||
'booking_cancel_delay',
|
||||
'subscription_explications_alert',
|
||||
'training_explications_alert',
|
||||
'training_information_message']").$promise
|
||||
]
|
||||
translations: [ 'Translations', (Translations) ->
|
||||
Translations.query(['app.logged.trainings_reserve', 'app.shared.plan_subscribe', 'app.shared.member_select',
|
||||
'app.shared.stripe', 'app.shared.valid_reservation_modal', 'app.shared.confirm_modify_slot_modal']).$promise
|
||||
]
|
||||
# notifications
|
||||
.state 'app.logged.notifications',
|
||||
url: '/notifications'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "notifications/index.html" %>'
|
||||
controller: 'NotificationsController'
|
||||
resolve:
|
||||
translations: [ 'Translations', (Translations) ->
|
||||
Translations.query('app.logged.notifications').$promise
|
||||
]
|
||||
|
||||
# pricing
|
||||
.state 'app.public.plans',
|
||||
url: '/plans'
|
||||
abstract: Fablab.withoutPlans
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "plans/index.html" %>'
|
||||
controller: 'PlansIndexController'
|
||||
resolve:
|
||||
subscriptionExplicationsPromise: ['Setting', (Setting)->
|
||||
Setting.get(name: 'subscription_explications_alert').$promise
|
||||
]
|
||||
plansPromise: ['Plan', (Plan)->
|
||||
Plan.query(shallow: true).$promise
|
||||
]
|
||||
groupsPromise: ['Group', (Group)->
|
||||
Group.query().$promise
|
||||
]
|
||||
translations: [ 'Translations', (Translations) ->
|
||||
Translations.query(['app.public.plans', 'app.shared.member_select', 'app.shared.stripe']).$promise
|
||||
]
|
||||
|
||||
# events
|
||||
.state 'app.public.events_list',
|
||||
url: '/events'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "events/index.html" %>'
|
||||
controller: 'EventsController'
|
||||
resolve:
|
||||
translations: [ 'Translations', (Translations) ->
|
||||
Translations.query('app.public.events_list').$promise
|
||||
]
|
||||
.state 'app.public.events_show',
|
||||
url: '/events/:id'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "events/show.html" %>'
|
||||
controller: 'ShowEventController'
|
||||
resolve:
|
||||
eventPromise: ['Event', '$stateParams', (Event, $stateParams)->
|
||||
Event.get(id: $stateParams.id).$promise
|
||||
]
|
||||
reducedAmountAlert: ['Setting', (Setting)->
|
||||
Setting.get(name: 'event_reduced_amount_alert').$promise
|
||||
]
|
||||
translations: [ 'Translations', (Translations) ->
|
||||
Translations.query(['app.public.events_show', 'app.shared.member_select', 'app.shared.stripe', 'app.shared.valid_reservation_modal']).$promise
|
||||
]
|
||||
|
||||
# --- namespace /admin/... ---
|
||||
# calendar
|
||||
.state 'app.admin.calendar',
|
||||
url: '/admin/calendar'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "admin/calendar/calendar.html" %>'
|
||||
controller: 'AdminCalendarController'
|
||||
resolve:
|
||||
availabilitiesPromise: ['Availability', (Availability)->
|
||||
Availability.query().$promise
|
||||
]
|
||||
bookingWindowStart: ['Setting', (Setting)->
|
||||
Setting.get(name: 'booking_window_start').$promise
|
||||
]
|
||||
bookingWindowEnd: ['Setting', (Setting)->
|
||||
Setting.get(name: 'booking_window_end').$promise
|
||||
]
|
||||
machinesPromise: ['Machine', (Machine) ->
|
||||
Machine.query().$promise
|
||||
]
|
||||
translations: [ 'Translations', (Translations) ->
|
||||
Translations.query('app.admin.calendar').$promise
|
||||
]
|
||||
|
||||
# project's elements
|
||||
.state 'app.admin.project_elements',
|
||||
url: '/admin/project_elements'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "admin/project_elements/index.html" %>'
|
||||
controller: 'ProjectElementsController'
|
||||
resolve:
|
||||
componentsPromise: ['Component', (Component)->
|
||||
Component.query().$promise
|
||||
]
|
||||
licencesPromise: ['Licence', (Licence)->
|
||||
Licence.query().$promise
|
||||
]
|
||||
themesPromise: ['Theme', (Theme)->
|
||||
Theme.query().$promise
|
||||
]
|
||||
translations: [ 'Translations', (Translations) ->
|
||||
Translations.query('app.admin.project_elements').$promise
|
||||
]
|
||||
|
||||
# trainings
|
||||
.state 'app.admin.trainings',
|
||||
url: '/admin/trainings'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "admin/trainings/index.html" %>'
|
||||
controller: 'TrainingsController'
|
||||
resolve:
|
||||
trainingsPromise: ['Training', (Training)->
|
||||
Training.query().$promise
|
||||
]
|
||||
machinesPromise: ['Machine', (Machine)->
|
||||
Machine.query().$promise
|
||||
]
|
||||
translations: [ 'Translations', (Translations) ->
|
||||
Translations.query('app.admin.trainings').$promise
|
||||
]
|
||||
|
||||
# events
|
||||
.state 'app.admin.events',
|
||||
url: '/admin/events'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "admin/events/index.html" %>'
|
||||
controller: 'AdminEventsController'
|
||||
resolve:
|
||||
eventsPromise: ['Event', (Event)->
|
||||
Event.query(page: 1).$promise
|
||||
]
|
||||
translations: [ 'Translations', (Translations) ->
|
||||
Translations.query('app.admin.events').$promise
|
||||
]
|
||||
.state 'app.admin.events_new',
|
||||
url: '/admin/events/new'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "events/new.html" %>'
|
||||
controller: 'NewEventController'
|
||||
resolve:
|
||||
translations: [ 'Translations', (Translations) ->
|
||||
Translations.query(['app.admin.events_new', 'app.shared.event']).$promise
|
||||
]
|
||||
.state 'app.admin.events_edit',
|
||||
url: '/admin/events/:id/edit'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "events/edit.html" %>'
|
||||
controller: 'EditEventController'
|
||||
resolve:
|
||||
eventPromise: ['Event', '$stateParams', (Event, $stateParams)->
|
||||
Event.get(id: $stateParams.id).$promise
|
||||
]
|
||||
translations: [ 'Translations', (Translations) ->
|
||||
Translations.query(['app.admin.events_edit', 'app.shared.event']).$promise
|
||||
]
|
||||
.state 'app.admin.event_reservations',
|
||||
url: '/admin/events/:id/reservations'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "admin/events/reservations.html" %>'
|
||||
controller: 'ShowEventReservationsController'
|
||||
resolve:
|
||||
eventPromise: ['Event', '$stateParams', (Event, $stateParams)->
|
||||
Event.get(id: $stateParams.id).$promise
|
||||
]
|
||||
reservationsPromise: ['Reservation', '$stateParams', (Reservation, $stateParams)->
|
||||
Reservation.query(reservable_id: $stateParams.id, reservable_type: 'Event').$promise
|
||||
]
|
||||
translations: [ 'Translations', (Translations) ->
|
||||
Translations.query('app.admin.event_reservations').$promise
|
||||
]
|
||||
|
||||
# pricing
|
||||
.state 'app.admin.pricing',
|
||||
url: '/admin/pricing'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "admin/pricing/index.html" %>'
|
||||
controller: 'EditPricingController'
|
||||
resolve:
|
||||
plans: ['Plan', (Plan) ->
|
||||
Plan.query().$promise
|
||||
]
|
||||
groups: ['Group', (Group) ->
|
||||
Group.query().$promise
|
||||
]
|
||||
machinesPricesPromise: ['Price', (Price)->
|
||||
Price.query(priceable_type: 'Machine', plan_id: 'null').$promise
|
||||
]
|
||||
trainingsPricingsPromise: ['TrainingsPricing', (TrainingsPricing)->
|
||||
TrainingsPricing.query().$promise
|
||||
]
|
||||
translations: [ 'Translations', (Translations) ->
|
||||
Translations.query('app.admin.pricing').$promise
|
||||
]
|
||||
|
||||
# plans
|
||||
.state 'app.admin.plans',
|
||||
abstract: true
|
||||
resolve:
|
||||
prices: ['Pricing', (Pricing) ->
|
||||
Pricing.query().$promise
|
||||
]
|
||||
machines: ['Machine', (Machine) ->
|
||||
Machine.query().$promise
|
||||
]
|
||||
groups: ['Group', (Group) ->
|
||||
Group.query().$promise
|
||||
]
|
||||
plans: ['Plan', (Plan) ->
|
||||
Plan.query().$promise
|
||||
]
|
||||
partners: ['User', (User) ->
|
||||
User.query({role: 'partner'}).$promise
|
||||
]
|
||||
.state 'app.admin.plans.new',
|
||||
url: '/admin/plans/new'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "admin/plans/new.html" %>'
|
||||
controller: 'NewPlanController'
|
||||
resolve:
|
||||
translations: [ 'Translations', (Translations) ->
|
||||
Translations.query(['app.admin.plans.new', 'app.shared.plan']).$promise
|
||||
]
|
||||
.state 'app.admin.plans.edit',
|
||||
url: '/admin/plans/:id/edit'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "admin/plans/edit.html" %>'
|
||||
controller: 'EditPlanController'
|
||||
resolve:
|
||||
planPromise: ['Plan', '$stateParams', (Plan, $stateParams) ->
|
||||
Plan.get({id: $stateParams.id}).$promise
|
||||
]
|
||||
translations: [ 'Translations', (Translations) ->
|
||||
Translations.query(['app.admin.plans.edit', 'app.shared.plan']).$promise
|
||||
]
|
||||
|
||||
|
||||
# --- namespace /admin/... ---
|
||||
|
||||
|
||||
# project's elements
|
||||
.state 'app.admin.project_elements',
|
||||
url: '/admin/project_elements'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "admin/project_elements/index.html" %>'
|
||||
controller: 'projectElementsController'
|
||||
# invoices
|
||||
.state 'app.admin.invoices',
|
||||
url: '/admin/invoices'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "admin/invoices/index.html" %>'
|
||||
controller: 'InvoicesController'
|
||||
resolve:
|
||||
settings: ['Setting', (Setting)->
|
||||
Setting.query(names: "[
|
||||
'invoice_legals',
|
||||
'invoice_text',
|
||||
'invoice_VAT-rate',
|
||||
'invoice_VAT-active',
|
||||
'invoice_order-nb',
|
||||
'invoice_code-value',
|
||||
'invoice_code-active',
|
||||
'invoice_reference',
|
||||
'invoice_logo'
|
||||
]").$promise
|
||||
]
|
||||
translations: [ 'Translations', (Translations) ->
|
||||
Translations.query('app.admin.invoices').$promise
|
||||
]
|
||||
|
||||
|
||||
# events
|
||||
.state 'app.admin.events',
|
||||
url: '/admin/events'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "admin/events/index.html" %>'
|
||||
controller: 'adminEventsController'
|
||||
.state 'app.admin.events_new',
|
||||
url: '/admin/events/new'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "events/new.html" %>'
|
||||
controller: 'newEventController'
|
||||
.state 'app.admin.events_edit',
|
||||
url: '/admin/events/:id/edit'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "events/edit.html" %>'
|
||||
controller: 'editEventController'
|
||||
# members
|
||||
.state 'app.admin.members',
|
||||
url: '/admin/members'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "admin/members/index.html" %>'
|
||||
controller: 'AdminMembersController'
|
||||
'groups@app.admin.members':
|
||||
templateUrl: '<%= asset_path "admin/groups/index.html" %>'
|
||||
controller: 'GroupsController'
|
||||
'tags@app.admin.members':
|
||||
templateUrl: '<%= asset_path "admin/tags/index.html" %>'
|
||||
controller: 'TagsController'
|
||||
'authentification@app.admin.members':
|
||||
templateUrl: '<%= asset_path "admin/authentications/index.html" %>'
|
||||
controller: 'AuthentificationController'
|
||||
resolve:
|
||||
membersPromise: ['Member', (Member)->
|
||||
Member.query({requested_attributes:'[profile,group,subscription]'}).$promise
|
||||
]
|
||||
adminsPromise: ['Admin', (Admin)->
|
||||
Admin.query().$promise
|
||||
]
|
||||
groupsPromise: ['Group', (Group)->
|
||||
Group.query().$promise
|
||||
]
|
||||
tagsPromise: ['Tag', (Tag)->
|
||||
Tag.query().$promise
|
||||
]
|
||||
authProvidersPromise: ['AuthProvider', (AuthProvider)->
|
||||
AuthProvider.query().$promise
|
||||
]
|
||||
translations: [ 'Translations', (Translations) ->
|
||||
Translations.query('app.admin.members').$promise
|
||||
]
|
||||
.state 'app.admin.members_new',
|
||||
url: '/admin/members/new'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "admin/members/new.html" %>'
|
||||
controller: 'NewMemberController'
|
||||
resolve:
|
||||
translations: [ 'Translations', (Translations) ->
|
||||
Translations.query(['app.admin.members_new', 'app.shared.user', 'app.shared.user_admin']).$promise
|
||||
]
|
||||
.state 'app.admin.members_edit',
|
||||
url: '/admin/members/:id/edit'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "admin/members/edit.html" %>'
|
||||
controller: 'EditMemberController'
|
||||
resolve:
|
||||
memberPromise: ['Member', '$stateParams', (Member, $stateParams)->
|
||||
Member.get(id: $stateParams.id).$promise
|
||||
]
|
||||
tagsPromise: ['Tag', (Tag)->
|
||||
Tag.query().$promise
|
||||
]
|
||||
translations: [ 'Translations', (Translations) ->
|
||||
Translations.query(['app.admin.members_edit', 'app.shared.user', 'app.shared.user_admin']).$promise
|
||||
]
|
||||
.state 'app.admin.admins_new',
|
||||
url: '/admin/admins/new'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "admin/admins/new.html" %>'
|
||||
controller: 'NewAdminController'
|
||||
resolve:
|
||||
translations: [ 'Translations', (Translations) ->
|
||||
Translations.query('app.admin.admins_new').$promise
|
||||
]
|
||||
|
||||
|
||||
# members
|
||||
.state 'app.admin.members',
|
||||
url: '/admin/members'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "admin/members/index.html" %>'
|
||||
controller: 'membersController'
|
||||
.state 'app.admin.members_new',
|
||||
url: '/admin/members/new'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "admin/members/new.html" %>'
|
||||
controller: 'newMemberController'
|
||||
.state 'app.admin.members_edit',
|
||||
url: '/admin/members/:id/edit'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "admin/members/edit.html" %>'
|
||||
controller: 'editMemberController'
|
||||
# authentification providers
|
||||
.state 'app.admin.authentication_new',
|
||||
url: '/admin/authentications/new'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "admin/authentications/new.html" %>'
|
||||
controller: 'NewAuthenticationController'
|
||||
resolve:
|
||||
mappingFieldsPromise: ['AuthProvider', (AuthProvider)->
|
||||
AuthProvider.mapping_fields().$promise
|
||||
]
|
||||
authProvidersPromise: ['AuthProvider', (AuthProvider)->
|
||||
AuthProvider.query().$promise
|
||||
]
|
||||
translations: [ 'Translations', (Translations) ->
|
||||
Translations.query(['app.admin.authentication_new', 'app.shared.authentication', 'app.shared.oauth2']).$promise
|
||||
]
|
||||
.state 'app.admin.authentication_edit',
|
||||
url: '/admin/authentications/:id/edit'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "admin/authentications/edit.html" %>'
|
||||
controller: 'EditAuthenticationController'
|
||||
resolve:
|
||||
providerPromise: ['AuthProvider', '$stateParams', (AuthProvider, $stateParams)->
|
||||
AuthProvider.get(id: $stateParams.id).$promise
|
||||
]
|
||||
mappingFieldsPromise: ['AuthProvider', (AuthProvider)->
|
||||
AuthProvider.mapping_fields().$promise
|
||||
]
|
||||
translations: [ 'Translations', (Translations) ->
|
||||
Translations.query(['app.admin.authentication_edit', 'app.shared.authentication', 'app.shared.oauth2']).$promise
|
||||
]
|
||||
|
||||
|
||||
|
||||
# statistics
|
||||
.state 'app.admin.statistics',
|
||||
url: '/admin/statistics'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "admin/statistics/index.html" %>'
|
||||
controller: 'StatisticsController'
|
||||
resolve:
|
||||
translations: [ 'Translations', (Translations) ->
|
||||
Translations.query('app.admin.statistics').$promise
|
||||
]
|
||||
.state 'app.admin.stats_graphs',
|
||||
url: '/admin/statistics/evolution'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "admin/statistics/graphs.html" %>'
|
||||
controller: 'GraphsController'
|
||||
resolve:
|
||||
translations: [ 'Translations', (Translations) ->
|
||||
Translations.query('app.admin.stats_graphs').$promise
|
||||
]
|
||||
|
||||
# configurations
|
||||
.state 'app.admin.settings',
|
||||
url: '/admin/settings'
|
||||
views:
|
||||
'main@':
|
||||
templateUrl: '<%= asset_path "admin/settings/index.html" %>'
|
||||
controller: 'SettingsController'
|
||||
resolve:
|
||||
settingsPromise: ['Setting', (Setting)->
|
||||
Setting.query(names: "[
|
||||
'twitter_name',
|
||||
'about_title',
|
||||
'about_body',
|
||||
'about_contacts',
|
||||
'home_blogpost',
|
||||
'machine_explications_alert',
|
||||
'training_explications_alert',
|
||||
'training_information_message',
|
||||
'subscription_explications_alert',
|
||||
'event_reduced_amount_alert',
|
||||
'booking_window_start',
|
||||
'booking_window_end',
|
||||
'booking_move_enable',
|
||||
'booking_move_delay',
|
||||
'booking_cancel_enable',
|
||||
'booking_cancel_delay',
|
||||
'main_color',
|
||||
'secondary_color',
|
||||
'fablab_name',
|
||||
'name_genre'
|
||||
]").$promise
|
||||
]
|
||||
cguFile: ['CustomAsset', (CustomAsset) ->
|
||||
CustomAsset.get({name: 'cgu-file'}).$promise
|
||||
]
|
||||
cgvFile: ['CustomAsset', (CustomAsset) ->
|
||||
CustomAsset.get({name: 'cgv-file'}).$promise
|
||||
]
|
||||
faviconFile: ['CustomAsset', (CustomAsset) ->
|
||||
CustomAsset.get({name: 'favicon-file'}).$promise
|
||||
]
|
||||
translations: [ 'Translations', (Translations) ->
|
||||
Translations.query('app.admin.settings').$promise
|
||||
]
|
||||
|
||||
|
||||
]
|
||||
|
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'
|
||||
|
||||
Application.Services.factory 'dialogs', ["$modal", ($modal) ->
|
||||
Application.Services.factory 'dialogs', ["$uibModal", ($uibModal) ->
|
||||
confirm: (options, success, error)->
|
||||
defaultOpts =
|
||||
templateUrl: '<%= asset_path "shared/confirm_modal.html" %>'
|
||||
@ -8,20 +8,20 @@ Application.Services.factory 'dialogs', ["$modal", ($modal) ->
|
||||
resolve:
|
||||
object: ->
|
||||
title: 'Titre de confirmation'
|
||||
msg: 'Message de confiramtion'
|
||||
controller: ['$scope', '$modalInstance', '$state', 'object', ($scope, $modalInstance, $state, object) ->
|
||||
msg: 'Message de confirmation'
|
||||
controller: ['$scope', '$uibModalInstance', '$state', 'object', ($scope, $uibModalInstance, $state, object) ->
|
||||
$scope.object = object
|
||||
$scope.ok = ->
|
||||
$modalInstance.close()
|
||||
$scope.ok = (info) ->
|
||||
$uibModalInstance.close( info )
|
||||
$scope.cancel = ->
|
||||
$modalInstance.dismiss('cancel')
|
||||
$uibModalInstance.dismiss('cancel')
|
||||
]
|
||||
angular.extend(defaultOpts, options) if angular.isObject options
|
||||
$modal.open defaultOpts
|
||||
.result['finally'](null).then ->
|
||||
$uibModal.open defaultOpts
|
||||
.result['finally'](null).then (info)->
|
||||
if angular.isFunction(success)
|
||||
success()
|
||||
, ->
|
||||
success(info)
|
||||
, (reason)->
|
||||
if angular.isFunction(error)
|
||||
error()
|
||||
error(reason)
|
||||
]
|
||||
|
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)->
|
||||
$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)->
|
||||
$resource "/api/members/:id",
|
||||
{id: "@id"},
|
||||
update:
|
||||
method: 'PUT'
|
||||
lastSubscribed:
|
||||
method: 'GET'
|
||||
url: '/api/last_subscribed/:limit'
|
||||
params: {limit: "@limit"}
|
||||
isArray: true
|
||||
merge:
|
||||
method: 'PUT'
|
||||
url: '/api/members/:id/merge'
|
||||
]
|
||||
|
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'
|
||||
url: '/api/projects/last_published'
|
||||
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;
|
||||
}
|
||||
h2 {
|
||||
color: $red;
|
||||
//color: $red;
|
||||
line-height: rem-calc(24);
|
||||
font-weight: 900;
|
||||
}
|
||||
@ -26,7 +26,7 @@ h5 {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
line-height: rem-calc(18);
|
||||
color: $red;
|
||||
//color: $red;
|
||||
font-size: rem-calc(16);
|
||||
&:after {
|
||||
position: absolute;
|
||||
@ -35,7 +35,7 @@ h5 {
|
||||
content: '';
|
||||
width: 35%;
|
||||
height: 1px;
|
||||
background-color: $red;
|
||||
//background-color: $red;
|
||||
}
|
||||
}
|
||||
|
||||
@ -43,12 +43,12 @@ h5 {
|
||||
// -------------------------
|
||||
|
||||
a {
|
||||
color: $link-color;
|
||||
//color: $link-color;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover,
|
||||
a:focus {
|
||||
color: $link-hover-color;
|
||||
//color: $link-hover-color;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@ -127,7 +127,8 @@ dd {
|
||||
// transition:0.5s linear all;
|
||||
// }
|
||||
|
||||
[ui-view].ng-enter, [ui-view].ng-leave {
|
||||
// only for main content
|
||||
#content-main.ng-enter, #content-main.ng-leave {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
@ -17,8 +17,8 @@
|
||||
.btn-warning-full {
|
||||
outline: 0;
|
||||
text-transform: uppercase;
|
||||
border: 3px solid $yellow;
|
||||
background-color: $yellow;
|
||||
//border: 3px solid $yellow;
|
||||
//background-color: $yellow;
|
||||
&:hover {
|
||||
background-color: white;
|
||||
}
|
||||
@ -41,4 +41,18 @@
|
||||
.btn-inactive{
|
||||
-webkit-box-shadow: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.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-red { background-color: $red; color: white; }
|
||||
.bg-red-dark { background-color: $red-dark; }
|
||||
.bg-yellow { background-color: $yellow !important; }
|
||||
//.bg-red { background-color: $red; color: white; }
|
||||
//.bg-red-dark { background-color: $red-dark; }
|
||||
//.bg-yellow { background-color: $yellow !important; }
|
||||
.bg-token { background-color: rgba(230, 208, 137, 0.49); }
|
||||
.bg-machine { background-color: $beige; }
|
||||
.bg-formation { background-color: $violet; }
|
||||
.bg-atelier { background-color: $blue; }
|
||||
@ -30,7 +31,7 @@
|
||||
.text-black-light { color: #424242 !important; }
|
||||
.text-gray { color: #5a5a5a !important; }
|
||||
.text-white { color: #fff !important; }
|
||||
.text-yellow { color: $yellow !important; }
|
||||
//.text-yellow { color: $yellow !important; }
|
||||
.text-blue { color: $blue; }
|
||||
.text-muted { color: $text-muted; }
|
||||
.text-danger, .red { color: $red !important; }
|
||||
|
@ -7,11 +7,11 @@
|
||||
font-weight: 600;
|
||||
color: black;
|
||||
}
|
||||
h1 {
|
||||
font-size: rem-calc(16); text-transform: uppercase;
|
||||
h1 {
|
||||
font-size: rem-calc(16); text-transform: uppercase;
|
||||
}
|
||||
h2 { font-weight: bold; }
|
||||
h3 { color: $red; }
|
||||
//h3 { color: $red; }
|
||||
h4 {
|
||||
font-size: rem-calc(12);
|
||||
margin: 8px 0;
|
||||
@ -50,13 +50,14 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: 0 auto;
|
||||
max-height: 44px;
|
||||
}
|
||||
h1 {
|
||||
margin: 25px 0 20px 0;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
color: $red;
|
||||
//color: $red;
|
||||
}
|
||||
}
|
||||
|
||||
@ -129,14 +130,14 @@
|
||||
}
|
||||
|
||||
.article-thumbnail {
|
||||
max-height: 400px;
|
||||
// max-height: 400px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.label-staging {
|
||||
position: absolute;
|
||||
top: 50px;
|
||||
top: 50px;
|
||||
}
|
||||
|
||||
.notification-open {
|
||||
@ -197,6 +198,7 @@
|
||||
margin: 10px 0;
|
||||
font-size: rem-calc(16);
|
||||
text-transform: uppercase;
|
||||
color: black;
|
||||
}
|
||||
.content {
|
||||
padding: 15px 0;
|
||||
@ -206,7 +208,7 @@
|
||||
display: inline-block;
|
||||
background: white;
|
||||
@include border-radius(50%);
|
||||
border: 3px solid $yellow;
|
||||
border: 3px solid;// $yellow;
|
||||
}
|
||||
.price {
|
||||
position: relative;
|
||||
@ -238,15 +240,15 @@
|
||||
background-color: white;
|
||||
padding-left: 30px;
|
||||
padding-right: 30px;
|
||||
&:hover { background-color: $yellow; }
|
||||
//&:hover { background-color: $yellow; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.well {
|
||||
&.well-warning {
|
||||
border-color: #ffdc4e;
|
||||
background-color: #ffdc4e;
|
||||
//border-color: #ffdc4e;
|
||||
//background-color: #ffdc4e;
|
||||
@include border-radius(3px);
|
||||
padding: 5px 10px;
|
||||
}
|
||||
@ -324,10 +326,10 @@
|
||||
|
||||
.block-link {
|
||||
cursor: pointer;
|
||||
&:hover { background-color: $yellow; }
|
||||
//&:hover { background-color: $yellow; }
|
||||
}
|
||||
|
||||
.form-control.form-control-ui-select .select2-choices .select2-search-choice {
|
||||
.form-control .ui-select-choices, .form-control .ui-select-match {
|
||||
font-size: 85% !important;
|
||||
}
|
||||
|
||||
@ -351,16 +353,15 @@
|
||||
.about-picture {
|
||||
padding: 70px 0;
|
||||
height: 326px;
|
||||
background: white asset-url("about-fablab.jpg") no-repeat;
|
||||
background-size: cover;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.about-title {
|
||||
.about-title, .about-title p {
|
||||
margin: 0;
|
||||
font-size: rem-calc(50);
|
||||
line-height: rem-calc(48);
|
||||
color: #fff;
|
||||
font-weight: 900; //black
|
||||
font-weight: 900; //black
|
||||
}
|
||||
|
||||
.about-title-aside {
|
||||
@ -393,7 +394,7 @@
|
||||
}
|
||||
|
||||
.event:hover {
|
||||
background-color: #cb1117;
|
||||
//background-color: #cb1117;
|
||||
color: white;
|
||||
}
|
||||
|
||||
@ -441,3 +442,25 @@ padding: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// angular-bootstrap accordions (enlightened version)
|
||||
.light-accordion > .panel-heading {
|
||||
padding-top: 0.2em;
|
||||
padding-bottom: 0.2em;
|
||||
}
|
||||
.light-accordion > .panel-heading > .panel-title {
|
||||
font-size: 12pt;
|
||||
}
|
||||
|
||||
.app-generator {
|
||||
position: absolute;
|
||||
bottom: 0; right: 10px;
|
||||
z-index: 100;
|
||||
padding: 3px 15px;
|
||||
border: 1px solid $border-color;
|
||||
border-top-left-radius: 8px;
|
||||
background: $bg-gray;
|
||||
@media only screen and (max-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
@ -76,11 +76,17 @@
|
||||
cursor: pointer;
|
||||
color: black;
|
||||
&:hover {
|
||||
background-color: $yellow;
|
||||
//background-color: $yellow;
|
||||
}
|
||||
i:before { content: "\f177"; }
|
||||
}
|
||||
}
|
||||
.heading-icon {
|
||||
width: 100%;
|
||||
padding: 35px 40%;
|
||||
display: inline-block;
|
||||
color: black;
|
||||
}
|
||||
.heading-title {
|
||||
overflow: hidden;
|
||||
height: 94px;
|
||||
@ -341,4 +347,131 @@ body.container{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.customMenuButton {
|
||||
min-width: 15em;
|
||||
max-width: 15em;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.customMenuInput {
|
||||
width:100% !important;
|
||||
}
|
||||
|
||||
|
||||
.reservation-canceled {
|
||||
color: #606060;
|
||||
border-radius: 0.2em;
|
||||
background-color: #e4e4e4;
|
||||
padding: 0.7em 0.7em;
|
||||
font-size: 90%;
|
||||
display:inline-block;
|
||||
vertical-align:middle;
|
||||
|
||||
.reservation-time {
|
||||
color: #606060;
|
||||
}
|
||||
|
||||
&:before {
|
||||
content: "Annulée";
|
||||
display: inline-block;
|
||||
background-color: #c44242;
|
||||
border-radius: 0.25em;
|
||||
padding: 0.1em 0.5em;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
float: left;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.custom-logo-container {
|
||||
max-width: 240px;
|
||||
height: 100%;
|
||||
|
||||
.custom-logo {
|
||||
height: 100px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
border: 1px dashed #c4c4c4;
|
||||
border-radius: 0.7em;
|
||||
padding: 1.6em;
|
||||
margin-left: 1em;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
width: auto;
|
||||
max-height: 44px;
|
||||
max-width: 100%;
|
||||
margin:auto;
|
||||
}
|
||||
|
||||
&:hover .tools-box {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tools-box {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.bg-dark {
|
||||
background-color: #000;
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-favicon-container {
|
||||
max-width: 70px;
|
||||
height: 100%;
|
||||
|
||||
.custom-favicon {
|
||||
height: 70px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
border: 1px dashed #c4c4c4;
|
||||
border-radius: 0.7em;
|
||||
padding: 1.6em;
|
||||
margin-left: 1em;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
width: auto;
|
||||
max-height: 16px;
|
||||
max-width: 16px;
|
||||
margin:auto;
|
||||
}
|
||||
|
||||
&:hover .tools-box {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tools-box {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
bottom: -7px;
|
||||
left: 51px;
|
||||
right: 0;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.flash-message {
|
||||
position: absolute;
|
||||
top: 1%;
|
||||
z-index: 1001;
|
||||
width: 33%;
|
||||
left: 33%;
|
||||
}
|
@ -425,7 +425,7 @@
|
||||
#nav {
|
||||
// border-right: 1px solid $red-dark;
|
||||
.nav {
|
||||
background-color: $red;
|
||||
//background-color: $red;
|
||||
> li {
|
||||
> a {
|
||||
padding: 13px 17px;
|
||||
@ -433,11 +433,11 @@
|
||||
color: white;
|
||||
&:hover,
|
||||
&:focus, &.active {
|
||||
background-color: $red-light;
|
||||
//background-color: $red-light;
|
||||
color: white;
|
||||
}
|
||||
&.active {
|
||||
border-left: 3px solid #870003;
|
||||
border-left: 3px solid;// #870003;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,37 +1,108 @@
|
||||
// medium editor placeholder
|
||||
.medium-editor-placeholder {
|
||||
min-height: 30px; // fix for firefox
|
||||
}
|
||||
|
||||
//xeditable
|
||||
.editable-buttons{
|
||||
button[type=submit].btn-primary{
|
||||
@extend .btn-warning;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//summernote
|
||||
|
||||
.note-editor .note-editable {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
|
||||
// Growl
|
||||
.growl {
|
||||
top: 90px;
|
||||
z-index: 1100;
|
||||
}
|
||||
|
||||
// fullcalendar
|
||||
|
||||
|
||||
// UI Select
|
||||
|
||||
.form-control {
|
||||
&.form-control-ui-select {
|
||||
height: auto;
|
||||
.select2-choices {
|
||||
border: none;
|
||||
background: transparent;
|
||||
.select2-search-choice {
|
||||
@extend .label;
|
||||
padding-left: .9em;
|
||||
font-size: 100%;
|
||||
font-weight: normal;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
.fc-view-container .fc-body tr {
|
||||
height: 40px !important;
|
||||
}
|
||||
|
||||
.fc-toolbar {
|
||||
height: 40px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.fc-toolbar .fc-button {
|
||||
background: #F2F2F2;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
text-shadow: none;
|
||||
margin: 0;
|
||||
height: 40px;
|
||||
line-height: 18px;
|
||||
padding: 10px;
|
||||
//&:hover, &:active, &.fc-state-active { background-color: $yellow; }
|
||||
}
|
||||
|
||||
.fc-toolbar h2 {
|
||||
font-size: 15px;
|
||||
line-height: 40px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.fc-view-container .fc-widget-header,
|
||||
.fc-view-container .fc-widget-content {
|
||||
border-color: #e8e8e8;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.fc-content-skeleton .fc-event {
|
||||
padding: 2px;
|
||||
border-left: solid 3px;
|
||||
}
|
||||
|
||||
.fc-event {
|
||||
-webkit-box-sizing: content-box;
|
||||
-moz-box-sizing: content-box;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
.fc-event .fc-time span, .fc-event .fc-title {
|
||||
font-size: rem-calc(10);
|
||||
line-height: rem-calc(12);
|
||||
}
|
||||
|
||||
.fc-event .fc-time span.label {
|
||||
font-size: rem-calc(8);
|
||||
margin-left: 0.7em;
|
||||
}
|
||||
|
||||
// croix de suppression pour un créneau de disponibilité
|
||||
.remove-event {
|
||||
position: absolute;
|
||||
float: right;
|
||||
right: 0;
|
||||
top: 0;
|
||||
padding: 0;
|
||||
font-size: 11px;
|
||||
color: black;
|
||||
cursor: pointer;
|
||||
z-index: 9999;
|
||||
text-align: right;
|
||||
.training-reserve &, .machine-reserve & { display: none; }
|
||||
}
|
||||
|
||||
.fc-v-event.fc-end {
|
||||
border-bottom-width: 2px;
|
||||
}
|
||||
|
||||
.fc-divider {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -59,6 +130,7 @@
|
||||
line-height: rem-calc(24);
|
||||
color: white;
|
||||
font-weight: 800;
|
||||
text-align: center;
|
||||
a {
|
||||
color: white;
|
||||
&:hover { color: $yellow; }
|
||||
@ -77,7 +149,7 @@
|
||||
background: none;
|
||||
@include border-radius($border-radius-base);
|
||||
&:hover, &:focus {
|
||||
color: $yellow;
|
||||
//color: $yellow;
|
||||
}
|
||||
|
||||
.glyphicon-chevron-left {
|
||||
@ -99,6 +171,31 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
// .carousel-control {
|
||||
// // position: absolute;
|
||||
// display: block;
|
||||
// margin-bottom: -20px;
|
||||
// padding: 20px;
|
||||
// color: white;
|
||||
// width: 58px;
|
||||
// height: 58px;
|
||||
// border: 3px solid white;
|
||||
// border-radius: 50%;
|
||||
|
||||
// .glyphicon-chevron-right:before {
|
||||
// // //Reset the icon
|
||||
// // content: " ";
|
||||
// // //Give layout
|
||||
// // display:block;
|
||||
// // //Your image as background
|
||||
// // background:url('http://yourarrow.png') no-repeat;
|
||||
// // //To show full image set the dimensions
|
||||
// // width:30px;
|
||||
// // height:30px;
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
|
||||
.banner { }
|
||||
|
||||
|
@ -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-a{border: 1px solid $border-color}
|
||||
.b-t{border-top: 1px solid $border-color}
|
||||
.b-r{border-right: 1px solid $border-color}
|
||||
.b-r{border-right: 1px solid $border-color !important;}
|
||||
.b-b{border-bottom: 1px solid $border-color}
|
||||
.b-l{border-left: 1px solid $border-color}
|
||||
.b-light{border-color: darken($brand-light, 5%)}
|
||||
|
@ -1,17 +1,23 @@
|
||||
/*
|
||||
*= require_self
|
||||
*= require select2/select2
|
||||
*= require angular-ui-select/dist/select
|
||||
*= require fullcalendar/dist/fullcalendar
|
||||
*= require jasny-bootstrap/dist/css/jasny-bootstrap
|
||||
*= require angular-growl/build/angular-growl.min.css
|
||||
*= require angular-growl-v2/build/angular-growl
|
||||
*= require angular-xeditable/dist/css/xeditable
|
||||
*= require angular-loading-bar/src/loading-bar
|
||||
*= require nvd3/build/nv.d3
|
||||
*= require font-awesome
|
||||
*= require medium-editor/dist/css/medium-editor
|
||||
*= require medium-editor/dist/css/themes/default
|
||||
*= require bootstrap-switch/dist/css/bootstrap3/bootstrap-switch.min
|
||||
*= require summernote/dist/summernote
|
||||
*= require jquery-minicolors/jquery.minicolors.css
|
||||
*/
|
||||
|
||||
|
||||
|
||||
@import "app.functions";
|
||||
@import "bootstrap-compass";
|
||||
@import "bootstrap-sprockets";
|
||||
@import "compass";
|
||||
@import "bootstrap_and_overrides";
|
||||
|
||||
@ -25,5 +31,6 @@
|
||||
@import "app.buttons";
|
||||
@import "app.components";
|
||||
@import "app.plugins";
|
||||
@import "modules/invoice";
|
||||
|
||||
@import "app.responsive";
|
||||
|
@ -78,10 +78,10 @@ $link-hover-decoration: underline;
|
||||
|
||||
// Semibold = 600, Bold = 700, ExtraB = 800
|
||||
|
||||
$font-family-sans-serif: "Open Sans", Helvetica, Arial, sans-serif !default;
|
||||
$font-proxima-condensed: "Open Sans Condensed", Helvetica, Arial, sans-serif !default;
|
||||
$font-family-serif: Georgia, "Times New Roman", Times, serif !default;
|
||||
$font-felt: "Loved by the King", sans-serif;
|
||||
$font-family-sans-serif: 'proxima-nova', 'Open Sans', Helvetica, Arial, sans-serif !default;
|
||||
$font-proxima-condensed: 'proxima-nova-condensed', 'Open Sans Condensed', Helvetica, Arial, sans-serif !default;
|
||||
$font-family-serif: Georgia, 'Times New Roman', Times, serif !default;
|
||||
$font-felt: 'felt-tip-roman', 'Loved by the King', cursive, sans-serif;
|
||||
|
||||
//** Default monospace fonts for `<code>`, `<kbd>`, and `<pre>`.
|
||||
// $font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace !default;
|
||||
@ -952,16 +952,16 @@ $hr-border: $gray-lighter !default;
|
||||
@import "bootstrap/input-groups";
|
||||
@import "bootstrap/navs";
|
||||
@import "bootstrap/navbar";
|
||||
// @import "bootstrap/breadcrumbs";
|
||||
@import "bootstrap/breadcrumbs";
|
||||
@import "bootstrap/pagination";
|
||||
// @import "bootstrap/pager";
|
||||
@import "bootstrap/pager";
|
||||
@import "bootstrap/labels";
|
||||
@import "bootstrap/badges";
|
||||
//@import "bootstrap/jumbotron";
|
||||
@import "bootstrap/jumbotron";
|
||||
@import "bootstrap/thumbnails";
|
||||
@import "bootstrap/alerts";
|
||||
@import "bootstrap/progress-bars";
|
||||
// @import "bootstrap/media";
|
||||
@import "bootstrap/media";
|
||||
@import "bootstrap/list-group";
|
||||
@import "bootstrap/panels";
|
||||
@import "bootstrap/responsive-embed";
|
||||
|
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;
|
||||
}
|
||||
}
|