1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2024-11-29 10:24:20 +01:00

Merge branch 'dev'

This commit is contained in:
Sylvain 2016-10-04 16:58:48 +02:00
commit dff155831a
458 changed files with 22487 additions and 5764 deletions

1
.fabmanager-version Normal file
View File

@ -0,0 +1 @@
2.4.0

5
.gitignore vendored
View File

@ -30,9 +30,14 @@
# PDF invoices
/invoices/*
# XLSX exports
/exports/*
.DS_Store
.vagrant
.docker
# Plugins are versioned is their own repository
/plugins/*

View File

@ -1,5 +1,73 @@
# Changelog Fab Manager
## v2.4.0 2016 October 4
- RSS feeds to follow new projects and events published
- Use slugs in projects URL opened from notifications
- Ask for confirmation on machine deletion from the public view
- Ability to delete a training from the public view for an admin
- Project images will show in full-size on a click
- Add a checkbox "I accept to receive informations from the FabLab" on Sign-up dialog and user's profile
- Share project with Facebook/Twitter
- Display fab-manager's version in "Powered by" label, when logged as admin
- Load translation locales from subdirectories
- Add wallet to user, client can pay total/partial reservation or subscription by wallet
- Public calendar for show all trainings/machines/events
- Display 'draft' badge on drafts in project galleries
- Add a 'new project' button in dashboard/my projects
- Open Projects: show the platform of origin even for local projects
- Ability to use HTML in machine specs and description
- Ability to manage project steps order
- Trainings are associated with a picture and an HTML textual description
- Public gallery of trainings with ability to view details or to book a training on its own calendar
- Ability to switch back to all trainings booking view
- Rename "Courses and Workshops" to "Events"
- Admin: Events can be associated with a theme and an age range
- Admin: Event categories, themes and age ranges can be customized
- Filter events by category, theme and age range in public view
- Ability to customise price's categories for the events
- Events can be associated with many custom price's categories, instead of only one "reduced price"
- Statistics views can trigger and display custom aggregations from ElasticSearch
- Machine hours/Trainings statistics: display number of tickets/hours available for booking
- Statistics will include informations abouts events category, theme and age range
- Ability to export the current statistics table to an Excel file
- Ability to export every statistics on a given dates range to an Excel file
- More fields in members exports
- Unified members, subscriptions and reservations exports with the new statistics exports
- Excel exports are now asynchronously generated and cached on the server for future identical requests
- Users have the ability to create an organizational profile when creating an account
- Organization informations will be used in invoices generation, if present
- Admins can create and enable/disable coupons. They can also notify an user about details of a coupon
- Users and admins can apply coupons's discounts to their shopping cart
- Send an email reminder and system notification some hours before a reservation happens
- Admins can toggle reminders on/off and customize the delay
- More file types allowed as project CAD attachements
- Project CAD attachements are now checked by MIME type in addition of extension check
- Project CAD attachement allowed are now configured in environment variables
- Project CAD attachement extensions allowed are shown next to input field
- Display strategy's name in SSO providers list
- SSO: documentation improved with an usage example
- SSO: mapped fields display their data type. Integers, booleans and dates allow some transformations.
- Fix a bug: project drafts are shown on public profiles
- Fix a bug: event category disappear when editing the event
- Fix a bug: machine name is not shown in plan edition
- Fix a bug: machine slots with tags are not displayed correctly on reservation calendar
- Fix a bug: avatar, address and organization details mapping from SSO were broken
- Fix a bug: in SSO configuration some valid endpoints were recognized as erroneous
- Fix a bug: clicking on the text in stripe's payment modal, does not validate the checkbox
- Fix a bug: move event reservation is not limited by admin settings (prior-delay & disable)
- Fix a bug: UI issues on small devices (dashboard + admin views)
- Fix a bug: embedded video not working in training/machine description
- Fix a bug: reordering project's steps trigger the unsaved-warning dialog
- Fix a bug: unable to compile assets in Docker with CoffeeScript error
- Fix a bug: do not force HTTPS for URLs in production environments
- [TODO DEPLOY] `rake fablab:es_build_availabilities_index`
- [TODO DEPLOY] `rake fablab:es_add_event_filters`
- [TODO DEPLOY] `rake db:migrate`
- [TODO DEPLOY] `bundle install`
- [TODO DEPLOY] add `EXCEL_DATE_FORMAT`, `ALLOWED_EXTENSIONS` and `ALLOWED_MIME_TYPES` environment variable in `application.yml`
- [OPTIONAL] `rake fablab:fix:assign_category_to_uncategorized_events` (will put every non-categorized events into a new category called "No Category", to ease re-categorization)
## v2.3.1 2016 September 26
- Fix a bug: group cache filename too long
@ -19,7 +87,7 @@
## v2.2.2 2016 June 23
- Fix some bugs: users with uncompleted account (sso imported) won't appear in statistics, in listings and in searches. Moreover, they won't block statistics generation
- Fix a bug: unable to display next results in statistics tables
- Admin: Category is mandatory when creating a course/workshop (event)
- Admin: Category is mandatory when creating an event
## v2.2.1 2016 June 22
- Fix a bug: field User.merged_at should not be allowed to be mapped in SSO

View File

@ -1,18 +1,11 @@
FROM ruby:2.3
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
@ -28,20 +21,11 @@ RUN bundle install --binstubs
# 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/exports
RUN mkdir -p /usr/src/app/log
RUN mkdir -p /usr/src/app/public/uploads
RUN mkdir -p /usr/src/app/public/assets
@ -56,13 +40,15 @@ COPY . /usr/src/app
# Volumes
VOLUME /usr/src/app/invoices
VOLUME /usr/src/app/exports
VOLUME /usr/src/app/public
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
# Expose port 3000 to the Docker host, so we can access it
# from the outside.
EXPOSE 80 443
EXPOSE 3000
# The main command to run when the container starts. Also
# tell the Rails dev server to bind to all interfaces by

View File

@ -144,3 +144,8 @@ gem 'openlab_ruby'
gem 'api-pagination'
gem 'has_secure_token'
gem 'apipie-rails'
# XLS files generation
gem 'rubyzip', '~> 1.1.0'
gem 'axlsx', '2.1.0.pre'
gem 'axlsx_rails'

View File

@ -54,6 +54,13 @@ GEM
descendants_tracker (~> 0.0.4)
ice_nine (~> 0.11.0)
thread_safe (~> 0.3, >= 0.3.1)
axlsx (2.1.0.pre)
htmlentities (~> 4.3.1)
nokogiri (>= 1.4.1)
rubyzip (~> 1.1.7)
axlsx_rails (0.4.0)
axlsx (>= 2.0.1)
rails (>= 3.1)
bcrypt (3.1.10)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
@ -87,13 +94,13 @@ GEM
cldr-plurals-runtime-rb (1.0.1)
coercible (1.0.0)
descendants_tracker (~> 0.0.1)
coffee-rails (4.1.0)
coffee-rails (4.1.1)
coffee-script (>= 2.2.0)
railties (>= 4.0.0, < 5.0)
coffee-script (2.3.0)
railties (>= 4.0.0, < 5.1.x)
coffee-script (2.4.1)
coffee-script-source
execjs
coffee-script-source (1.9.1)
coffee-script-source (1.10.0)
compass (1.0.3)
chunky_png (~> 1.2)
compass-core (~> 1.0.2)
@ -150,7 +157,7 @@ GEM
multi_json
equalizer (0.0.11)
erubis (2.7.0)
execjs (2.4.0)
execjs (2.7.0)
faker (1.4.3)
i18n (~> 0.5)
faraday (0.9.1)
@ -174,6 +181,7 @@ GEM
highline (1.7.1)
hike (1.2.3)
hitimes (1.2.2)
htmlentities (4.3.4)
http (0.6.4)
http_parser.rb (~> 0.6.0)
http-cookie (1.0.2)
@ -214,8 +222,8 @@ GEM
twitter_cldr (~> 3.1)
mime-types (2.99)
mini_magick (4.2.0)
mini_portile2 (2.0.0)
minitest (5.9.0)
mini_portile2 (2.1.0)
minitest (5.9.1)
minitest-reporters (1.1.8)
ansi
builder
@ -233,8 +241,9 @@ GEM
net-ssh-gateway (1.2.0)
net-ssh (>= 2.6.5)
netrc (0.10.3)
nokogiri (1.6.7.2)
mini_portile2 (~> 2.0.0.rc2)
nokogiri (1.6.8)
mini_portile2 (~> 2.1.0)
pkg-config (~> 1.1.7)
notify_with (0.0.2)
jbuilder (~> 2.0)
rails (>= 4.2.0)
@ -257,6 +266,7 @@ GEM
orm_adapter (0.5.0)
pdf-core (0.5.1)
pg (0.18.1)
pkg-config (1.1.7)
prawn (2.0.1)
pdf-core (~> 0.5.1)
ttfunk (~> 1.4.0)
@ -305,7 +315,7 @@ GEM
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
raindrops (0.13.0)
rake (11.1.2)
rake (11.3.0)
rb-fsevent (0.9.4)
rb-inotify (0.9.5)
ffi (>= 0.5.0)
@ -325,6 +335,7 @@ GEM
netrc (~> 0.7)
rolify (4.0.0)
ruby-progressbar (1.7.5)
rubyzip (1.1.7)
rufus-scheduler (3.0.9)
tzinfo
rvm-capistrano (1.5.6)
@ -440,6 +451,8 @@ DEPENDENCIES
api-pagination
apipie-rails
awesome_print
axlsx (= 2.1.0.pre)
axlsx_rails
bootstrap-sass
byebug
capistrano
@ -488,6 +501,7 @@ DEPENDENCIES
recurrence
responders (~> 2.0)
rolify
rubyzip (~> 1.1.0)
rvm-capistrano
sass-rails (= 5.0.1)
sdoc (~> 0.4.0)

184
README.md
View File

@ -6,14 +6,15 @@ FabManager is the FabLab management solution. It is web-based, open-source and t
##### Table of Contents
1. [Software stack](#software-stack)
2. [Contributing](#contributing)
3. [Setup a production environment with Docker and CoreOS](#setup-a-production-environment)
3. [Setup a production environment](#setup-a-production-environment)
4. [Setup a development environment](#setup-a-development-environment)<br/>
4.1. [General Guidelines](#general-guidelines)<br/>
4.2. [Environment Configuration](#environment-configuration)
5. [PostgreSQL](#postgresql)<br/>
5.1. [Install PostgreSQL 9.4 on Ubuntu/Debian](#postgresql-on-debian)<br/>
5.2. [Install and launch PostgreSQL on MacOS X](#postgresql-on-macosx)<br/>
5.3. [Setup the FabManager database in PostgreSQL](#setup-fabmanager-in-postgresql)
5.3. [Setup the FabManager database in PostgreSQL](#setup-fabmanager-in-postgresql)<br/>
5.4. [PostgreSQL Limitations](#postgresql-limitations)
6. [ElasticSearch](#elasticsearch)<br/>
6.1. [Install ElasticSearch on Ubuntu/Debian](#elasticsearch-on-debian)<br/>
6.2. [Install ElasticSearch on MacOS X](#elasticsearch-on-macosx)<br/>
@ -28,8 +29,9 @@ FabManager is the FabLab management solution. It is web-based, open-source and t
7.2.2. [Applying changes](#i18n-apply)
8. [Open Projects](#open-projects)
9. [Plugins](#plugins)
10. [Known issues](#known-issues)
11. [Related Documentation](#related-documentation)
10. [Single Sign-On](#sso)
11. [Known issues](#known-issues)
12. [Related Documentation](#related-documentation)
@ -38,7 +40,7 @@ FabManager is the FabLab management solution. It is web-based, open-source and t
FabManager is a Ruby on Rails / AngularJS web application that runs on the following software:
- Ubuntu/Debian
- Ubuntu LTS 14.04+ / Debian 8+
- Ruby 2.3
- Git 1.9.1+
- Redis 2.8.4+
@ -54,13 +56,16 @@ Contributions are welcome. Please read [the contribution guidelines](CONTRIBUTIN
**IMPORTANT**: **do not** update Arshaw/fullCalendar.js as it contains a hack for the remove-event cross.
<a name="setup-a-production-environment"></a>
## Setup a production environment with Docker and CoreOS
## Setup a production environment
[Docker Readme](docker/README.md)
To run fab-manager as a production application, this is highly recommended to use [Docker](https://www.docker.com/).
The procedure to follow is described in the [docker readme](docker/README.md).
<a name="setup-a-development-environment"></a>
## Setup a development environment
In you only intend to run fab-manager on your local machine for testing purposes or to contribute to the project development, you can set it up with the following procedure.
<a name="general-guidelines"></a>
### General Guidelines
@ -74,15 +79,17 @@ Contributions are welcome. Please read [the contribution guidelines](CONTRIBUTIN
```
3. Install the software dependencies.
First install [PostgreSQL](#postgresql) and [ElasticSearch](#elasticsearch) as specified in their respective documentations.
Then install the other dependencies:
- For Ubuntu/Debian:
```bash
sudo apt-get install libpq-dev postgresql-9.4 redis-server imagemagick
sudo apt-get install libpq-dev redis-server imagemagick
```
- For MacOS X:
```bash
brew install postgresql redis imagemagick
brew install redis imagemagick
```
4. Init the RVM instance and check it was correctly configured
@ -114,7 +121,7 @@ Contributions are welcome. Please read [the contribution guidelines](CONTRIBUTIN
# or use your favorite text editor instead of vi (nano, ne...)
```
8. 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.
8. Build the database. You may have to follow the steps described in [the PostgreSQL configuration chapter](#setup-fabmanager-in-postgresql) before, if you don't already had done it.
```bash
rake db:setup
@ -147,11 +154,13 @@ If you are in a development environment, your can keep the default values, other
POSTGRES_HOST
DNS name or IP address of the server hosting the PostgreSQL database of the application (see [PostgreSQL](#postgresql)).
This value is only used when deploying with Docker, otherwise this is configured in `config/database.yml`.
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.
This value is only used when deploying with Docker, otherwise this is configured in `config/database.yml`.
REDIS_HOST
@ -217,13 +226,42 @@ See https://help.disqus.com/customer/portal/articles/466208-what-s-a-shortname-
TWITTER_NAME
Identifier of the Twitter account, for witch the last tweet will be displayed on the home page.
Identifier of the Twitter account, from witch the last tweet will be fetched and displayed on the home page.
It will also be used for [Twitter Card analytics](https://dev.twitter.com/cards/analytics).
TWITTER_CONSUMER_KEY, TWITTER_CONSUMER_SECRET, TWITTER_ACCESS_TOKEN & TWITTER_ACCESS_TOKEN_SECRET
Keys and secrets to access the twitter API.
Retrieve them from https://apps.twitter.com
FACEBOOK_APP_ID
This is optional. You can follow [this guide to get your personal App ID](https://developers.facebook.com/docs/apps/register).
If you do so, you'll be able to customize and get statistics about project shares on Facebook.
LOG_LEVEL
This parameter configures the logs verbosity.
Available log levels can be found [here](http://guides.rubyonrails.org/debugging_rails_applications.html#log-levels).
ALLOWED_EXTENSIONS
Exhaustive list of file's extensions available for public upload as project's CAO attachements.
Each item in the list must be separated from the others by a space char.
You will probably want to check that this list match the `ALLOWED_MIME_TYPES` values below.
Please consider that allowing file archives (eg. ZIP) or binary executable (eg. EXE) may result in a **dangerous** security issue and must be avoided in any cases.
ALLOWED_MIME_TYPES
Exhaustive list of file's mime-types available for public upload as project's CAO attachements.
Each item in the list must be separated from the others by a space char.
You will probably want to check that this list match the `ALLOWED_EXTENSIONS` values above.
Please consider that allowing file archives (eg. application/zip) or binary executable (eg. application/exe) may result in a **dangerous** security issue and must be avoided in any cases.
Settings related to Open Projects
See the [Open Projects](#open-projects) section for a detailed description of these parameters.
Settings related to i18n
See the [Settings](#i18n-settings) section of the [Internationalization (i18n)](#i18n) paragraph for a detailed description of these parameters.
@ -237,6 +275,7 @@ See the [Settings](#i18n-settings) section of the [Internationalization (i18n)](
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/ xenial-pgdg main` (Ubuntu 16.04 Xenial)
- `deb http://apt.postgresql.org/pub/repos/apt/ jessie-pgdg main` (Debian 8 Jessie)
@ -264,7 +303,7 @@ Otherwise, please follow the official instructions on the project's website.
```bash
brew update
brew install postgres
brew install homebrew/versions/postgresql94
```
2. Launch PostgreSQL
@ -282,42 +321,73 @@ Otherwise, please follow the official instructions on the project's website.
Before running `rake db:setup`, you have to make sure that the user configured in [config/database.yml](config/database.yml.default) for the `development` environment exists.
To create it, please follow these instructions:
1. Login as the postgres user
1. Run the PostgreSQL administration command line interface, logged as the postgres user
- For Ubuntu/Debian:
```bash
sudo -i -u postgres
```
2. Run the PostgreSQL administration command line interface
```bash
psql
```
- For MacOS X:
3. Create a new user in PostgreSQL (in this example, the user will be named `sleede`)
```bash
sudo psql -U $(whoami) postgres
```
If you get an error running this command, please check your [pg_hba.conf](https://www.postgresql.org/docs/current/static/auth-pg-hba-conf.html) file.
2. 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
3. Grant him the right to create databases
```sql
ALTER ROLE sleede WITH CREATEDB;
```
5. Then, create the fablab_development and fablab_test databases
4. Then, create the fabmanager_development and fabmanager_test databases
```sql
CREATE DATABASE fablab_development OWNER sleede;
CREATE DATABASE fablab_test OWNER sleede;
CREATE DATABASE fabmanager_development OWNER sleede;
CREATE DATABASE fabmanager_test OWNER sleede;
```
6. To finish, attribute a password to this user
5. To finish, attribute a password to this user
```sql
ALTER USER sleede WITH ENCRYPTED PASSWORD 'sleede';
```
6. Finally, have a look at the [PostgreSQL Limitations](#postgresql-limitations) section or some errors will occurs preventing you from finishing the installation procedure.
<a name="postgresql-limitations"></a>
### PostgreSQL Limitations
- While setting up the database, we'll need to activate two PostgreSQL extensions: [unaccent](https://www.postgresql.org/docs/current/static/unaccent.html) and [trigram](https://www.postgresql.org/docs/current/static/pgtrgm.html).
This can only be achieved if the user, configured in `config/database.yml`, was granted the _SUPERUSER_ role **OR** if these extensions were white-listed.
So here's your choices, mainly depending on your security requirements:
- Use the default PostgreSQL super-user (postgres) as the database user of fab-manager.
- Set your user as _SUPERUSER_; run the following command in `psql` (after replacing `sleede` with you user name):
```sql
ALTER USER sleede WITH SUPERUSER;
```
- Install and configure the PostgreSQL extension [pgextwlist](https://github.com/dimitri/pgextwlist).
Please follow the instructions detailed on the extension website to whitelist `unaccent` and `trigram` for the user configured in `config/database.yml`.
- Some users may want to use another DBMS than PostgreSQL.
This is currently not supported, because of some PostgreSQL specific instructions that cannot be efficiently handled with the ActiveRecord ORM:
- `app/controllers/api/members_controllers.rb@list` is using `ILIKE`
- `app/controllers/api/invoices_controllers.rb@list` is using `ILIKE` and `date_trunc()`
- `db/migrate/20160613093842_create_unaccent_function.rb` is using [unaccent](https://www.postgresql.org/docs/current/static/unaccent.html) and [trigram](https://www.postgresql.org/docs/current/static/pgtrgm.html) modules and defines a PL/pgSQL function (`f_unaccent()`)
- `app/controllers/api/members_controllers.rb@search` is using `f_unaccent()` (see above) and `regexp_replace()`
- `db/migrate/20150604131525_add_meta_data_to_notifications.rb` is using [jsonb](https://www.postgresql.org/docs/9.4/static/datatype-json.html), a PostgreSQL 9.4+ datatype.
- `db/migrate/20160915105234_add_transformation_to_o_auth2_mapping.rb` is using [jsonb](https://www.postgresql.org/docs/9.4/static/datatype-json.html), a PostgreSQL 9.4+ datatype.
- If you intend to contribute to the project code, you will need to run the test suite with `rake test`.
This also requires your user to have the _SUPERUSER_ role.
Please see the [known issues](#known-issues) section for more informations about this.
<a name="elasticsearch"></a>
## ElasticSearch
@ -354,7 +424,7 @@ For a more detailed guide concerning the ElasticSearch installation, please chec
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:
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/Ubuntu 16.04), you will need to run:
```bash
# System V
@ -364,6 +434,12 @@ For a more detailed guide concerning the ElasticSearch installation, please chec
sudo /bin/systemctl enable elasticsearch.service
```
5. Restart the host operating system to complete the installation
```bash
sudo reboot
```
<a name="elasticsearch-on-macosx"></a>
### Install ElasticSearch on MacOS X
@ -387,17 +463,11 @@ brew install homebrew/versions/elasticsearch17
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.
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 command.
```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
# Here for the 50 last days
rake fablab:generate_stats[50]
```
<a name="backup-and-restore-elasticsearch"></a>
@ -531,6 +601,11 @@ See https://angular-ui.github.io/bootstrap/#uibdateparser-s-format-codes for a l
**BEWARE**: years format with less than 4 digits will result in problems because the system won't be able to distinct dates with the same less significant digits, eg. 50 could mean 1950 or 2050.
EXCEL_DATE_FORMAT
Date format for dates shown in exported Excel files (eg. statistics)
See https://support.microsoft.com/en-us/kb/264372 for a list a available formats.
<a name="i18n-apply"></a>
#### Applying changes
@ -570,12 +645,21 @@ To install a plugin, you just have to copy the plugin folder which contains its
You can see an example on the [repo of navinum gamification plugin](https://github.com/LaCasemate/navinum-gamification)
<a name="sso"></a>
## Single Sign-On
Fab-manager can be connected to a [Single Sign-On](https://en.wikipedia.org/wiki/Single_sign-on) server which will provide its own authentication for the platform's users.
Currently OAuth 2 is the only supported protocol for SSO authentication.
For an example of how to use configure a SSO in Fab-manager, please read [sso_with_github.md](doc/sso_with_github.md).
Developers may find informations on how to implement their own authentication protocol in [sso_authentication.md](doc/sso_authentication.md).
<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 happen 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.
This may happen 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:
@ -583,9 +667,9 @@ You can see an example on the [repo of navinum gamification plugin](https://gith
web.1 | Exiting
worker.1 | ...lib/redis/client.rb...:in `_parse_options'
This may happen 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.
This may happen 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.
- Due to a stripe limitation, you won't be ble to create plans longer than one year.
@ -599,20 +683,22 @@ You can see an example on the [repo of navinum gamification plugin](https://gith
test_after_commit (1.0.0) lib/test_after_commit/database_statements.rb:11:in `block in transaction'
test_after_commit (1.0.0) lib/test_after_commit/database_statements.rb:5:in `transaction'
This is due to an ActiveRecord behavior witch disable referential integrity in PostgreSQL to load the fixtures.
PostgreSQL will prevent any users to disable referential integrity on the fly if they doesn't have the `SUPERUSER` role.
To fix that, logon as the `postgres` user and run the PostgreSQL shell (see [Setup the FabManager database in PostgreSQL](#setup-fabmanager-in-postgresql) for an example).
Then, run the following command (replace `sleede` with your test database user, as specified in your database.yml):
This is due to an ActiveRecord behavior witch disable referential integrity in PostgreSQL to load the fixtures.
PostgreSQL will prevent any users to disable referential integrity on the fly if they doesn't have the `SUPERUSER` role.
To fix that, logon as the `postgres` user and run the PostgreSQL shell (see [Setup the FabManager database in PostgreSQL](#setup-fabmanager-in-postgresql) for an example).
Then, run the following command (replace `sleede` with your test database user, as specified in your database.yml):
ALTER ROLE sleede WITH SUPERUSER;
DO NOT do this in a production environment, as this would lead to a serious security issue.
DO NOT do this in a production environment, unless you know what you're doing: this could lead to a serious security issue.
- Using another DBMS than PostgreSQL is not supported, because of some PostgreSQL specific instructions:
- `app/controllers/api/members_controllers.rb@list` is using `ILIKE`
- `app/controllers/api/invoices_controllers.rb@list` is using `ILIKE` and `date_trunc()`
- `db/migrate/20160613093842_create_unaccent_function.rb` is using [unaccent](https://www.postgresql.org/docs/current/static/unaccent.html) and [trigram](https://www.postgresql.org/docs/current/static/pgtrgm.html) modules and defines a PL/pgSQL function (`f_unaccent()`)
- `app/controllers/api/members_controllers.rb@search` is using `f_unaccent()` (see above) and `regexp_replace()`
- With Ubuntu 16.04, ElasticSearch may refuse to start even after having configured the service with systemd.
To solve this issue, you may have to set `START_DAEMON` to `true` in `/etc/default/elasticsearch`.
Then reload ElasticSearch with:
```bash
sudo systemctl restart elasticsearch.service
```
<a name="related-documentation"></a>
## Related Documentation

View File

@ -20,7 +20,7 @@ angular.module('application', ['ngCookies', 'ngResource', 'ngSanitize', 'ngCooki
'ui.select', 'ui.calendar', 'angularMoment', 'Devise', 'DeviseModal', 'angular-growl', 'xeditable',
'checklist-model', 'unsavedChanges', 'angular-loading-bar', 'ngTouch', 'angular-google-analytics',
'angularUtils.directives.dirDisqus', 'summernote', 'elasticsearch', 'angular-medium-editor', 'naif.base64',
'minicolors', 'pascalprecht.translate', 'ngFitText']).
'minicolors', 'pascalprecht.translate', 'ngFitText', 'ngAside']).
config(['$httpProvider', 'AuthProvider', "growlProvider", "unsavedWarningsConfigProvider", "AnalyticsProvider", "uibDatepickerPopupConfig", "$provide", "$translateProvider",
function($httpProvider, AuthProvider, growlProvider, unsavedWarningsConfigProvider, AnalyticsProvider, uibDatepickerPopupConfig, $provide, $translateProvider) {
@ -125,6 +125,24 @@ config(['$httpProvider', 'AuthProvider', "growlProvider", "unsavedWarningsConfig
// see https://github.com/revolunet/angular-google-analytics#automatic-page-view-tracking
Analytics.pageView();
/**
* This helper method builds and return an array contaning every integers between
* the provided start and end.
* @param start {number}
* @param end {number}
* @return {Array} [start .. end]
*/
$rootScope.intArray = function(start, end) {
var arr = [];
for (var i = start; i < end; i++) { arr.push(i); }
return arr;
};
}]).constant('angularMomentConfig', {
timezone: Fablab.timezone
});
angular.isUndefinedOrNull = function(val) {
return angular.isUndefined(val) || val === null
};

View File

@ -67,6 +67,7 @@
//= require messageformat/messageformat
//= require angular-translate-interpolation-messageformat/angular-translate-interpolation-messageformat
//= require ngFitText/dist/ng-FitText.min
//= require angular-aside/dist/js/angular-aside
//= require_tree ./controllers
//= require_tree ./services
//= require_tree ./directives

View File

@ -34,6 +34,102 @@ check_oauth2_id_is_mapped = (mappings) ->
return false
##
# Provides a set of common callback methods and data to the $scope parameter. These methods are used
# in the various authentication providers' controllers.
#
# Provides :
# - $scope.authMethods
# - $scope.mappingFields
# - $scope.cancel()
# - $scope.defineDataMapping(mapping)
#
# Requires :
# - mappingFieldsPromise: retrieved by AuthProvider.mapping_fields()
# - $state (Ui-Router) [ 'app.admin.members' ]
##
class AuthenticationController
constructor: ($scope, $state, $uibModal, mappingFieldsPromise)->
## list of supported authentication methods
$scope.authMethods = METHODS
## list of fields that can be mapped through the SSO
$scope.mappingFields = mappingFieldsPromise
##
# Changes the admin's view to the members list page
##
$scope.cancel = ->
$state.go('app.admin.members')
##
# Open a modal allowing to specify the data mapping for the given field
##
$scope.defineDataMapping = (mapping) ->
$uibModal.open
templateUrl: '<%= asset_path "admin/authentications/_data_mapping.html" %>'
size: 'md'
resolve:
field: -> mapping
datatype: ->
for field in $scope.mappingFields[mapping.local_model]
if field[0] == mapping.local_field
return field[1]
controller: ['$scope', '$uibModalInstance', 'field', 'datatype', ($scope, $uibModalInstance, field, datatype) ->
## parent field
$scope.field = field
## expected data type
$scope.datatype = datatype
## data transformation rules
$scope.transformation =
rules: field.transformation || {type: datatype}
## available transformation formats
$scope.formats =
date: [
{
label: 'ISO 8601',
value: 'iso8601'
},
{
label: 'RFC 2822',
value: 'rfc2822'
},
{
label: 'RFC 3339',
value: 'rfc3339'
},
{
label: 'Timestamp (s)'
value: 'timestamp-s'
},
{
label: 'Timestamp (ms)',
value: 'timestamp-ms'
}
]
## Create a new mapping between anything and an expected integer
$scope.addIntegerMapping = ->
unless angular.isArray $scope.transformation.rules.mapping
$scope.transformation.rules.mapping = []
$scope.transformation.rules.mapping.push({from:'', to:0})
## close and save the modifications
$scope.ok = ->
$uibModalInstance.close($scope.transformation.rules)
## do not save the modifications
$scope.cancel = ->
$uibModalInstance.dismiss()
]
.result['finally'](null).then (transfo_rules) ->
mapping.transformation = transfo_rules
##
# Page listing all authentication providers
@ -103,15 +199,12 @@ Application.Controllers.controller "AuthentificationController", ["$scope", "$st
##
# 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
Application.Controllers.controller "NewAuthenticationController", ["$scope", "$state", "$rootScope", "$uibModal", "dialogs", "growl", "mappingFieldsPromise", "authProvidersPromise", "AuthProvider", '_t'
, ($scope, $state, $rootScope, $uibModal, dialogs, growl, mappingFieldsPromise, authProvidersPromise, AuthProvider, _t) ->
$scope.mode = 'creation'
## default parameters for the new authentication provider
$scope.provider = {
name: '',
providable_type: '',
@ -172,12 +265,8 @@ Application.Controllers.controller "NewAuthenticationController", ["$scope", "$s
##
# Changes the admin's view to the members list page
##
$scope.cancel = ->
$state.go('app.admin.members')
## Using the AuthenticationController
new AuthenticationController($scope, $state, $uibModal, mappingFieldsPromise)
]
@ -185,17 +274,14 @@ Application.Controllers.controller "NewAuthenticationController", ["$scope", "$s
##
# 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) ->
Application.Controllers.controller "EditAuthenticationController", ["$scope", "$state", "$stateParams", "$rootScope", "$uibModal", "dialogs", "growl", 'providerPromise', 'mappingFieldsPromise', 'AuthProvider', '_t'
, ($scope, $state, $stateParams, $rootScope, $uibModal, dialogs, growl, providerPromise, mappingFieldsPromise, AuthProvider, _t) ->
## parameters of the currently edited authentication provider
$scope.provider = providerPromise
$scope.authMethods = METHODS
$scope.mode = 'edition'
$scope.mappingFields = mappingFieldsPromise
##
# Update the current provider with the new inputs
##
@ -210,10 +296,8 @@ Application.Controllers.controller "EditAuthenticationController", ["$scope", "$
, ->
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')
## Using the AuthenticationController
new AuthenticationController($scope, $state, $uibModal, mappingFieldsPromise)
]

View File

@ -4,8 +4,8 @@
# 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) ->
Application.Controllers.controller "AdminCalendarController", ["$scope", "$state", "$uibModal", "moment", "Availability", 'Slot', 'Setting', 'growl', 'dialogs', 'bookingWindowStart', 'bookingWindowEnd', 'machinesPromise', '_t', 'uiCalendarConfig', 'CalendarConfig'
($scope, $state, $uibModal, moment, Availability, Slot, Setting, growl, dialogs, bookingWindowStart, bookingWindowEnd, machinesPromise, _t, uiCalendarConfig, CalendarConfig) ->
@ -17,9 +17,6 @@ Application.Controllers.controller "AdminCalendarController", ["$scope", "$state
# 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
@ -36,40 +33,17 @@ Application.Controllers.controller "AdminCalendarController", ["$scope", "$state
## bind the availabilities slots with full-Calendar events
$scope.eventSources = []
$scope.eventSources.push
events: availabilitiesPromise
url: '/api/availabilities'
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
$scope.calendarConfig = CalendarConfig
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
minTime: moment.duration(moment(bookingWindowStart.setting.value).format('HH:mm:ss'))
maxTime: moment.duration(moment(bookingWindowEnd.setting.value).format('HH:mm:ss'))
select: (start, end, jsEvent, view) ->
calendarSelectCb(start, end, jsEvent, view)
eventClick: (event, jsEvent, view)->
@ -77,10 +51,6 @@ Application.Controllers.controller "AdminCalendarController", ["$scope", "$state
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'))
##
@ -141,7 +111,7 @@ Application.Controllers.controller "AdminCalendarController", ["$scope", "$state
# update the machine_ids attribute
$scope.availability.machine_ids = data.machine_ids
$scope.availability.title = data.title
$scope.calendar.fullCalendar 'rerenderEvents'
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
# notify the admin
growl.success(_t('the_machine_was_successfully_removed_from_the_slot'))
, (data, status) -> # failed
@ -180,7 +150,7 @@ Application.Controllers.controller "AdminCalendarController", ["$scope", "$state
end: -> end
# when the modal is closed, we send the slot to the server for saving
modalInstance.result.then (availability) ->
$scope.calendar.fullCalendar 'renderEvent',
uiCalendarConfig.calendars.calendar.fullCalendar 'renderEvent',
id: availability.id
title: availability.title,
start: availability.start_at
@ -189,12 +159,13 @@ Application.Controllers.controller "AdminCalendarController", ["$scope", "$state
backgroundColor: availability.backgroundColor
borderColor: availability.borderColor
tag_ids: availability.tag_ids
tags: availability.tags
machine_ids: availability.machine_ids
, true
, ->
$scope.calendar.fullCalendar('unselect')
uiCalendarConfig.calendars.calendar.fullCalendar('unselect')
$scope.calendar.fullCalendar('unselect')
uiCalendarConfig.calendars.calendar.fullCalendar('unselect')
@ -209,7 +180,7 @@ Application.Controllers.controller "AdminCalendarController", ["$scope", "$state
# 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
uiCalendarConfig.calendars.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)
@ -231,12 +202,12 @@ Application.Controllers.controller "AdminCalendarController", ["$scope", "$state
# @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)
if event.tags.length > 0
html = ''
for tag in event.tags
html += "<span class='label label-success text-white'>#{tag.name}</span> "
element.find('.fc-title').append("<br/>"+html)
return
]

View File

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

View File

@ -1,296 +0,0 @@
'use strict'
### COMMON CODE ###
##
# Provides a set of common properties and methods to the $scope parameter. They are used
# in the various events' admin controllers.
#
# Provides :
# - $scope.categories = [{Category}]
# - $scope.datePicker = {}
# - $scope.submited(content)
# - $scope.cancel()
# - $scope.addFile()
# - $scope.deleteFile(file)
# - $scope.fileinputClass(v)
# - $scope.toggleStartDatePicker($event)
# - $scope.toggleEndDatePicker($event)
# - $scope.toggleRecurrenceEnd(e)
#
# Requires :
# - $scope.event.event_files_attributes = []
# - $state (Ui-Router) [ 'app.public.events_list' ]
##
class EventsController
constructor: ($scope, $state, Event, Category) ->
## Retrieve the list of categories from the server (stage, atelier, ...)
Category.query().$promise.then (data)->
$scope.categories = data.map (d) ->
id: d.id
name: d.name
## default parameters for AngularUI-Bootstrap datepicker
$scope.datePicker =
format: Fablab.uibDateFormat
startOpened: false # default: datePicker is not shown
endOpened: false
recurrenceEndOpened: false
options:
startingDay: Fablab.weekStartingDay
##
# For use with ngUpload (https://github.com/twilson63/ngUpload).
# Intended to be the callback when an upload is done: any raised error will be stacked in the
# $scope.alerts array. If everything goes fine, the user is redirected to the project page.
# @param content {Object} JSON - The upload's result
##
$scope.submited = (content) ->
if !content.id?
$scope.alerts = []
angular.forEach content, (v, k)->
angular.forEach v, (err)->
$scope.alerts.push({msg: k+': '+err, type: 'danger'})
else
$state.go('app.public.events_list')
##
# Changes the user's view to the events list page
##
$scope.cancel = ->
$state.go('app.public.events_list')
##
# For use with 'ng-class', returns the CSS class name for the uploads previews.
# The preview may show a placeholder or the content of the file depending on the upload state.
# @param v {*} any attribute, will be tested for truthiness (see JS evaluation rules)
##
$scope.fileinputClass = (v)->
if v
'fileinput-exists'
else
'fileinput-new'
##
# This will create a single new empty entry into the event's attachements list.
##
$scope.addFile = ->
$scope.event.event_files_attributes.push {}
##
# This will remove the given file from the event's attachements list. If the file was previously uploaded
# to the server, it will be marked for deletion on the server. Otherwise, it will be simply truncated from
# the attachements array.
# @param file {Object} the file to delete
##
$scope.deleteFile = (file) ->
index = $scope.event.event_files_attributes.indexOf(file)
if file.id?
file._destroy = true
else
$scope.event.event_files_attributes.splice(index, 1)
##
# Show/Hide the "start" datepicker (open the drop down/close it)
##
$scope.toggleStartDatePicker = ($event) ->
$event.preventDefault()
$event.stopPropagation()
$scope.datePicker.startOpened = !$scope.datePicker.startOpened
##
# Show/Hide the "end" datepicker (open the drop down/close it)
##
$scope.toggleEndDatePicker = ($event) ->
$event.preventDefault()
$event.stopPropagation()
$scope.datePicker.endOpened = !$scope.datePicker.endOpened
##
# Masks/displays the recurrence pane allowing the admin to set the current event as recursive
##
$scope.toggleRecurrenceEnd = (e)->
e.preventDefault()
e.stopPropagation()
$scope.datePicker.recurrenceEndOpened = !$scope.datePicker.recurrenceEndOpened
##
# Controller used in the events listing page (admin view)
##
Application.Controllers.controller "AdminEventsController", ["$scope", "$state", 'Event', 'eventsPromise', ($scope, $state, Event, eventsPromise) ->
### PUBLIC SCOPE ###
## By default, the pagination mode is activated to limit the page size
$scope.paginateActive = true
## The events displayed on the page
$scope.events = eventsPromise
## Current virtual page
$scope.page = 2
##
# Adds a bucket of events to the bottom of the page, grouped by month
##
$scope.loadMoreEvents = ->
Event.query {page: $scope.page}, (data)->
$scope.events = $scope.events.concat data
paginationCheck(data, $scope.events)
$scope.page += 1
### PRIVATE SCOPE ###
##
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
paginationCheck(eventsPromise, $scope.events)
##
# Check if all events are already displayed OR if the button 'load more events'
# is required
# @param lastEvents {Array} last events loaded onto the diplay (ie. last "page")
# @param events {Array} full list of events displayed on the page (not only the last retrieved)
##
paginationCheck = (lastEvents, events)->
if lastEvents.length > 0
$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
]
##
# Controller used in the event creation page
##
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
$scope.actionUrl = "/api/events/"
## Form action on the above URL
$scope.method = 'post'
## Default event parameters
$scope.event =
event_files_attributes: []
start_date: new Date()
end_date: new Date()
start_time: new Date()
end_time: new Date()
all_day: 'false'
recurrence: 'none'
category_ids: []
## Possible types of recurrences for an event
$scope.recurrenceTypes = [
{label: _t('none'), value: 'none'},
{label: _t('every_days'), value: 'day'},
{label: _t('every_week'), value: 'week'},
{label: _t('every_month'), value: 'month'},
{label: _t('every_year'), value: 'year'}
]
## 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)
]
##
# Controller used in the events edition page
##
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
## Form action on the above URL
$scope.method = 'put'
## Retrieve the event details, in case of error the user is redirected to the events listing
$scope.event = eventPromise
## 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, Event, Category)
## !!! MUST BE CALLED AT THE END of the controller
initialize()
]

View File

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

View File

@ -334,7 +334,7 @@ Application.Controllers.controller "GraphsController", ["$scope", "$state", "$ro
callback(results)
recursiveCb()
else # palmares (ranking)
queryElasticRanking index.es_type_key, $scope.ranking.groupCriterion, $scope.ranking.sortCriterion, index.graph.limit, (results, error) ->
queryElasticRanking index.es_type_key, $scope.ranking.groupCriterion, $scope.ranking.sortCriterion, (results, error) ->
if (error)
callback([], error)
else
@ -373,17 +373,18 @@ Application.Controllers.controller "GraphsController", ["$scope", "$state", "$ro
##
# 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 esType {string} elasticSearch document type (subscription|machine|training|...)
# @param groupKey {string} statistics subtype or custom field
# @param sortKey {string} statistics type or 'ca'
# @param callback {function} function be to run after results were retrieved,
# it will receive two parameters : results {Array}, error {String} (if any)
##
queryElasticRanking = (esType, groupKey, sortKey, limit, callback) ->
queryElasticRanking = (esType, groupKey, sortKey, 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'
if !esType or !groupKey or !sortKey
callback([], '[graphsController::queryElasticRanking] Error: invalid parameters provided')
# run query

View File

@ -133,6 +133,8 @@ Application.Controllers.controller "InvoicesController", ["$scope", "$state", 'I
sample = sample.replace(/X\[([^\]]+)\]/g, (match, p1, offset, string) ->
p1
)
# information about wallet (W[text]) - does not apply here
sample = sample.replace(/W\[([^\]]+)\]/g, "")
# information about refunds (R[text]) - does not apply here
sample = sample.replace(/R\[([^\]]+)\]/g, "")
sample
@ -494,6 +496,7 @@ Application.Controllers.controller 'AvoirModalController', ["$scope", "$uibModal
{name: _t('by_cash'), value: 'cash'}
{name: _t('by_cheque'), value: 'cheque'}
{name: _t('by_transfer'), value: 'transfer'}
{name: _t('by_wallet'), value: 'wallet'}
]
## If a subscription was took with the current invoice, should it be canceled or not

View File

@ -105,8 +105,8 @@ class MembersController
##
# Controller used in the members/groups management page
##
Application.Controllers.controller "AdminMembersController", ["$scope", 'membersPromise', 'adminsPromise', 'growl', 'Admin', 'dialogs', '_t', 'Member'
, ($scope, membersPromise, adminsPromise, growl, Admin, dialogs, _t, Member) ->
Application.Controllers.controller "AdminMembersController", ["$scope", 'membersPromise', 'adminsPromise', 'growl', 'Admin', 'dialogs', '_t', 'Member', 'Export'
, ($scope, membersPromise, adminsPromise, growl, Admin, dialogs, _t, Member, Export) ->
@ -204,6 +204,16 @@ Application.Controllers.controller "AdminMembersController", ["$scope", 'members
resetSearchMember()
memberSearch()
##
# Callback to alert the admin that the export request was acknowledged and is
# processing right now.
##
$scope.alertExport = (type) ->
Export.status({category: 'users', type: type}).then (res) ->
unless (res.data.exists)
growl.success _t('export_is_running_you_ll_be_notified_when_its_ready')
### PRIVATE SCOPE ###
@ -260,8 +270,8 @@ Application.Controllers.controller "AdminMembersController", ["$scope", 'members
##
# Controller used in the member edition page
##
Application.Controllers.controller "EditMemberController", ["$scope", "$state", "$stateParams", "Member", 'Training', 'dialogs', 'growl', 'Group', 'Subscription', 'CSRF', 'memberPromise', 'tagsPromise', '$uibModal', 'Plan', '$filter', '_t'
, ($scope, $state, $stateParams, Member, Training, dialogs, growl, Group, Subscription, CSRF, memberPromise, tagsPromise, $uibModal, Plan, $filter, _t) ->
Application.Controllers.controller "EditMemberController", ["$scope", "$state", "$stateParams", "Member", 'Training', 'dialogs', 'growl', 'Group', 'Subscription', 'CSRF', 'memberPromise', 'tagsPromise', '$uibModal', 'Plan', '$filter', '_t', 'walletPromise', 'transactionsPromise', 'activeProviderPromise', 'Wallet'
, ($scope, $state, $stateParams, Member, Training, dialogs, growl, Group, Subscription, CSRF, memberPromise, tagsPromise, $uibModal, Plan, $filter, _t, walletPromise, transactionsPromise, activeProviderPromise, Wallet) ->
@ -300,6 +310,17 @@ Application.Controllers.controller "EditMemberController", ["$scope", "$state",
## Profiles types (student/standard/...)
$scope.groups = []
## the user wallet
$scope.wallet = walletPromise
## user wallet transactions
$scope.transactions = transactionsPromise
## used in wallet partial template to identify parent view
$scope.view = 'member_edit'
# current active authentication provider
$scope.activeProvider = activeProviderPromise
##
@ -396,6 +417,39 @@ Application.Controllers.controller "EditMemberController", ["$scope", "$state",
$scope.subscription = subscription
$scope.createWalletCreditModal = (user, wallet)->
modalInstance = $uibModal.open
animation: true,
templateUrl: '<%= asset_path "wallet/credit_modal.html" %>'
controller: ['$scope', '$uibModalInstance', 'Wallet', '$locale', ($scope, $uibModalInstance, Wallet, $locale) ->
## currency symbol for the current locale (cf. angular-i18n)
$scope.currencySymbol = $locale.NUMBER_FORMATS.CURRENCY_SYM
##
# Modal dialog validation callback
##
$scope.ok = ->
Wallet.credit { id: wallet.id }, { amount: $scope.amount }, (_wallet)->
growl.success(_t('wallet_credit_successfully'))
$uibModalInstance.close(_wallet)
, (error)->
growl.error(_t('a_problem_occurred_for_wallet_credit'))
##
# Modal dialog cancellation callback
##
$scope.cancel = ->
$uibModalInstance.dismiss('cancel')
]
# once the form was validated succesfully ...
modalInstance.result.then (wallet) ->
$scope.wallet = wallet
Wallet.transactions {id: wallet.id}, (transactions) ->
$scope.transactions = transactions
### PRIVATE SCOPE ###

View File

@ -60,6 +60,18 @@ class PlanController
##
# Retrieve the name of a machine from its ID
# @param machine_id {number} machine identifier
# @returns {string} Machine's name
##
$scope.getMachineName = (machine_id) ->
for machine in $scope.machines
if machine.id == machine_id
return machine.name

View File

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

View File

@ -3,8 +3,8 @@
##
# Controller used in the prices edition page
##
Application.Controllers.controller "EditPricingController", ["$scope", "$state", '$uibModal', 'TrainingsPricing', '$filter', 'Credit', 'Pricing', 'Plan', 'plans', 'groups', 'growl', 'machinesPricesPromise', 'Price', 'dialogs', 'trainingsPricingsPromise', 'trainingsPromise', 'machineCreditsPromise', 'machinesPromise', 'trainingCreditsPromise', '_t'
, ($scope, $state, $uibModal, TrainingsPricing, $filter, Credit, Pricing, Plan, plans, groups, growl, machinesPricesPromise, Price, dialogs, trainingsPricingsPromise, trainingsPromise, machineCreditsPromise, machinesPromise, trainingCreditsPromise, _t) ->
Application.Controllers.controller "EditPricingController", ["$scope", "$state", '$uibModal', 'TrainingsPricing', '$filter', 'Credit', 'Pricing', 'Plan', 'Coupon', 'plans', 'groups', 'growl', 'machinesPricesPromise', 'Price', 'dialogs', 'trainingsPricingsPromise', 'trainingsPromise', 'machineCreditsPromise', 'machinesPromise', 'trainingCreditsPromise', 'couponsPromise', '_t'
, ($scope, $state, $uibModal, TrainingsPricing, $filter, Credit, Pricing, Plan, Coupon, plans, groups, growl, machinesPricesPromise, Price, dialogs, trainingsPricingsPromise, trainingsPromise, machineCreditsPromise, machinesPromise, trainingCreditsPromise, couponsPromise, _t) ->
### PUBLIC SCOPE ###
## List of machines prices (not considering any plan)
@ -34,6 +34,9 @@ Application.Controllers.controller "EditPricingController", ["$scope", "$state",
## List of machines
$scope.machines = machinesPromise
## List of coupons
$scope.coupons = couponsPromise
## The plans list ordering. Default: by group
$scope.orderPlans = 'group_id'
@ -275,7 +278,7 @@ Application.Controllers.controller "EditPricingController", ["$scope", "$state",
##
$scope.deletePlan = (plans, id) ->
if typeof id != 'number'
console.error('[editPricingController::deletePlan] Error: invalid id parameter')
console.error('[EditPricingController::deletePlan] Error: invalid id parameter')
else
# open a confirmation dialog
dialogs.confirm
@ -287,10 +290,10 @@ Application.Controllers.controller "EditPricingController", ["$scope", "$state",
# 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)
$scope.plans.splice(findItemIdxById(plans, id), 1)
, (error) ->
console.error('[editPricingController::deletePlan] Error: '+error.statusText) if error.statusText
console.error('[EditPricingController::deletePlan] Error: '+error.statusText) if error.statusText
growl.error(_t('unable_to_delete_the_specified_subscription_an_error_occurred'))
@ -308,12 +311,70 @@ Application.Controllers.controller "EditPricingController", ["$scope", "$state",
### PRIVATE SCOPE ###
##
# Delete a coupon from the server's database and, in case of success, from the list in memory
# @param coupons {Array<Object>} should be called with $scope.coupons
# @param id {number} ID of the coupon to delete
##
$scope.deleteCoupon = (coupons, id) ->
if typeof id != 'number'
console.error('[EditPricingController::deleteCoupon] Error: invalid id parameter')
else
# open a confirmation dialog
dialogs.confirm
resolve:
object: ->
title: _t('confirmation_required')
msg: _t('do_you_really_want_to_delete_this_coupon')
, ->
# the admin has confirmed, delete the coupon
Coupon.delete {id: id}, (res) ->
growl.success(_t('coupon_was_successfully_deleted'))
$scope.coupons.splice(findItemIdxById(coupons, id), 1)
findPlanIdxById = (plans, id)->
(plans.map (plan)->
plan.id
).indexOf(id)
, (error) ->
console.error('[EditPricingController::deleteCoupon] Error: '+error.statusText) if error.statusText
if error.status == 422
growl.error(_t('unable_to_delete_the_specified_coupon_already_in_use'))
else
growl.error(_t('unable_to_delete_the_specified_coupon_an_unexpected_error_occurred'))
##
# Open a modal allowing to select an user and send him the details of the provided coupon
# @param coupon {Object} The coupon to send
##
$scope.sendCouponToUser = (coupon) ->
$uibModal.open
templateUrl: '<%= asset_path "admin/pricing/sendCoupon.html" %>'
resolve:
coupon: -> coupon
size: 'md'
controller: ['$scope', '$uibModalInstance', 'Coupon', 'coupon', '_t', ($scope, $uibModalInstance, Coupon, coupon, _t) ->
## Member, receiver of the coupon
$scope.ctrl =
member: null
## Details of the coupon to send
$scope.coupon = coupon
## Callback to validate sending of the coupon
$scope.ok = ->
Coupon.send {coupon_code: coupon.code, user_id: $scope.ctrl.member.id}, (res) ->
growl.success(_t('coupon_successfully_sent_to_USER', {USER: $scope.ctrl.member.name}))
$uibModalInstance.close({user_id: $scope.ctrl.member.id})
, (err) ->
growl.error(_t('an_error_occurred_unable_to_send_the_coupon'))
## Callback to close the modal and cancel the sending process
$scope.cancel = ->
$uibModalInstance.dismiss('cancel')
]
### PRIVATE SCOPE ###
##
# Kind of constructor: these actions will be realized first when the controller is loaded
@ -329,6 +390,19 @@ Application.Controllers.controller "EditPricingController", ["$scope", "$state",
##
# Retrieve an item index by its ID from the given array of objects
# @param items {Array<{id:number}>}
# @param id {number}
# @returns {number} item index in the provided array
##
findItemIdxById = (items, id)->
(items.map (item)->
item.id
).indexOf(id)
##
# Group the given credits array into a map associating the plan ID with its associated trainings/machines
# @return {Object} the association map

View File

@ -45,7 +45,6 @@ Application.Controllers.controller "SettingsController", ["$scope", 'Setting', '
$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 }
@ -74,6 +73,14 @@ Application.Controllers.controller "SettingsController", ["$scope", 'Setting', '
$scope.cancelDelay =
name: 'booking_cancel_delay'
value: parseInt(settingsPromise.booking_cancel_delay)
$scope.enableReminder =
name: 'reminder_enable'
value: (settingsPromise.reminder_enable == 'true')
$scope.reminderDelay =
name: 'reminder_delay'
value: parseInt(settingsPromise.reminder_delay)
@ -108,7 +115,7 @@ Application.Controllers.controller "SettingsController", ["$scope", 'Setting', '
value = setting.value
Setting.update { name: setting.name }, { value: value }, (data)->
growl.success(_t('customization_of_SETTING_successfully_saved', {SETTING:setting.name}))
growl.success(_t('customization_of_SETTING_successfully_saved', {SETTING:_t(setting.name)}))
, (error)->
console.log(error)

View File

@ -1,7 +1,9 @@
'use strict'
Application.Controllers.controller "StatisticsController", ["$scope", "$state", "$rootScope", "Statistics", "es", "Member", '_t', 'membersPromise', 'statisticsPromise'
, ($scope, $state, $rootScope, Statistics, es, Member, _t, membersPromise, statisticsPromise) ->
Application.Controllers.controller "StatisticsController", ["$scope", "$state", "$rootScope", '$uibModal', "es", "Member", '_t', 'membersPromise', 'statisticsPromise'
, ($scope, $state, $rootScope, $uibModal, es, Member, _t, membersPromise, statisticsPromise) ->
@ -53,9 +55,13 @@ Application.Controllers.controller "StatisticsController", ["$scope", "$state",
## total of the stat field for non simple types
$scope.sumStat = 0
## Results of custom aggregations for the current type
$scope.customAggs = {}
## default: results are not sorted
$scope.sorting =
ca: 'none'
date: 'desc'
## active tab will be set here
$scope.selectedIndex = null
@ -148,6 +154,7 @@ Application.Controllers.controller "StatisticsController", ["$scope", "$state",
$scope.customFilter.value = null
$scope.customFilter.exclude = false
$scope.sorting.ca = 'none'
$scope.sorting.date = 'desc'
buildCustomFiltersList()
refreshStats()
@ -271,6 +278,33 @@ Application.Controllers.controller "StatisticsController", ["$scope", "$state",
##
# Open a modal dialog asking the user for details about exporting the statistics tables to an excel file
##
$scope.exportToExcel = ->
options =
templateUrl: '<%= asset_path "admin/statistics/export.html" %>'
size: 'sm'
controller: 'ExportStatisticsController'
resolve:
dates: ->
start: $scope.datePickerStart.selected
end: $scope.datePickerEnd.selected
query: ->
custom = buildCustomFilterQuery()
buildElasticDataQuery($scope.type.active.key, custom, $scope.agePicker.start, $scope.agePicker.end, moment($scope.datePickerStart.selected), moment($scope.datePickerEnd.selected), $scope.sorting)
index: ->
key: $scope.selectedIndex.es_type_key
type: ->
key: $scope.type.active.key
$uibModal.open options
.result['finally'](null).then (info)->
console.log(info)
### PRIVATE SCOPE ###
##
@ -306,14 +340,10 @@ Application.Controllers.controller "StatisticsController", ["$scope", "$state",
$scope.sumCA = 0
$scope.averageAge = 0
$scope.sumStat = 0
$scope.customAggs = {}
$scope.totalHits = null
$scope.searchDate = new Date()
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
custom = buildCustomFilterQuery()
queryElasticStats $scope.selectedIndex.es_type_key, $scope.type.active.key, custom, (res, err)->
if (err)
console.error("[statisticsController::refreshStats] Unable to refresh due to "+err)
@ -324,6 +354,8 @@ Application.Controllers.controller "StatisticsController", ["$scope", "$state",
$scope.averageAge = Math.round(res.aggregations.average_age.value * 100) / 100
$scope.sumStat = res.aggregations.total_stat.value
$scope.scrollId = res._scroll_id
for custom in $scope.type.active.custom_aggregations
$scope.customAggs[custom.field] = res.aggregations[custom.field].value
@ -347,6 +379,9 @@ Application.Controllers.controller "StatisticsController", ["$scope", "$state",
"type": index
"size": RESULTS_PER_PAGE
"scroll": ES_SCROLL_TIME+'m'
"stat-type": type
"start-date": moment($scope.datePickerStart.selected).format()
"end-date": moment($scope.datePickerEnd.selected).format()
"body": buildElasticDataQuery(type, custom, $scope.agePicker.start, $scope.agePicker.end, moment($scope.datePickerStart.selected), moment($scope.datePickerEnd.selected), $scope.sorting)
, (error, response) ->
if (error)
@ -429,7 +464,7 @@ Application.Controllers.controller "StatisticsController", ["$scope", "$state",
"field": "age"
"total_stat":
"sum":
"field": "sta"
"field": "stat"
}
q
@ -485,8 +520,141 @@ Application.Controllers.controller "StatisticsController", ["$scope", "$state",
##
# Build and return an object according to the custom filter set by the user, used to request elasticsearch
# @return {Object|null}
##
buildCustomFilterQuery = ->
custom = null
if !angular.isUndefinedOrNull($scope.customFilter.criterion) and
!angular.isUndefinedOrNull($scope.customFilter.criterion.key) and
!angular.isUndefinedOrNull($scope.customFilter.value)
custom = {}
custom.key = $scope.customFilter.criterion.key
custom.value = $scope.customFilter.value
custom.exclude = $scope.customFilter.exclude
custom
# init the controller (call at the end !)
initialize()
]
Application.Controllers.controller 'ExportStatisticsController', [ '$scope', '$uibModalInstance', 'Export','dates', 'query', 'index', 'type', 'CSRF', 'growl', '_t'
, ($scope, $uibModalInstance, Export, dates, query, index, type, CSRF, growl, _t) ->
## Retrieve Anti-CSRF tokens from cookies
CSRF.setMetaTags()
## Bindings for date range
$scope.dates = dates
## Body of the query to export
$scope.query = JSON.stringify(query)
## API URL where the form will be posted
$scope.actionUrl = '/stats/'+index.key+'/export'
## Key of the current search' statistic type
$scope.typeKey = type.key
## Form action on the above URL
$scope.method = "post"
## Anti-CSRF token to inject into the download form
$scope.csrfToken = angular.element('meta[name="csrf-token"]')[0].content
## Binding of the export type (global / current)
$scope.export =
type: 'current'
## datePicker parameters for interval beginning
$scope.exportStart =
format: Fablab.uibDateFormat
opened: false # default: datePicker is not shown
minDate: null
maxDate: moment().subtract(1, 'day').toDate()
options:
startingDay: Fablab.weekStartingDay
## datePicker parameters for interval ending
$scope.exportEnd =
format: Fablab.uibDateFormat
opened: false # default: datePicker is not shown
minDate: null
maxDate: moment().subtract(1, 'day').toDate()
options:
startingDay: Fablab.weekStartingDay
##
# Callback to open the datepicker (interval start)
# @param $event {Object} jQuery event object
##
$scope.toggleStartDatePicker = ($event) ->
$scope.exportStart.opened = !$scope.exportStart.opened
##
# Callback to open the datepicker (interval end)
# @param $event {Object} jQuery event object
##
$scope.toggleEndDatePicker = ($event) ->
$scope.exportEnd.opened = !$scope.exportEnd.opened
##
# Callback when exchanging the export type between 'global' and 'current view'
# Adjust the query and the requesting url according to this type.
##
$scope.setRequest = ->
if $scope.export.type == 'global'
$scope.actionUrl = '/stats/global/export'
$scope.query = JSON.stringify(
"query":
"bool":
"must": [
{
"range":
"date":
"gte": moment($scope.dates.start).format()
"lte": moment($scope.dates.end).format()
}
]
)
else
$scope.actionUrl = '/stats/'+index.key+'/export'
$scope.query = JSON.stringify(query)
##
# Callback to close the modal, telling the caller what is exported
##
$scope.exportData = ->
statusQry = {category: 'statistics', type: $scope.export.type, query: $scope.query}
unless $scope.export.type == 'global'
statusQry['type'] = index.key
statusQry['key'] = type.key
Export.status(statusQry).then (res) ->
unless (res.data.exists)
growl.success _t('export_is_running_you_ll_be_notified_when_its_ready')
$uibModalInstance.close(statusQry)
##
# Callback to cancel the export and close the modal
##
$scope.cancel = ->
$uibModalInstance.dismiss('cancel')
]

View File

@ -1,7 +1,148 @@
'use strict'
Application.Controllers.controller "TrainingsController", ["$scope", "$state", "$uibModal", 'Training', 'trainingsPromise', 'machinesPromise', '_t', 'growl'
, ($scope, $state, $uibModal, Training, trainingsPromise, machinesPromise, _t, growl) ->
### COMMON CODE ###
##
# Provides a set of common callback methods to the $scope parameter. These methods are used
# in the various trainings' admin controllers.
#
# Provides :
# - $scope.submited(content)
# - $scope.fileinputClass(v)
#
# Requires :
# - $state (Ui-Router) [ 'app.admin.trainings' ]
##
class TrainingsController
constructor: ($scope, $state) ->
##
# For use with ngUpload (https://github.com/twilson63/ngUpload).
# Intended to be the callback when the upload is done: any raised error will be stacked in the
# $scope.alerts array. If everything goes fine, the user is redirected to the trainings list.
# @param content {Object} JSON - The upload's result
##
$scope.submited = (content) ->
if !content.id?
$scope.alerts = []
angular.forEach content, (v, k)->
angular.forEach v, (err)->
$scope.alerts.push
msg: k+': '+err
type: 'danger'
else
$state.go('app.admin.trainings')
##
# Changes the current user's view, redirecting him to the machines list
##
$scope.cancel = ->
$state.go('app.admin.trainings')
##
# For use with 'ng-class', returns the CSS class name for the uploads previews.
# The preview may show a placeholder or the content of the file depending on the upload state.
# @param v {*} any attribute, will be tested for truthiness (see JS evaluation rules)
##
$scope.fileinputClass = (v)->
if v
'fileinput-exists'
else
'fileinput-new'
##
# Controller used in the training creation page (admin)
##
Application.Controllers.controller "NewTrainingController", [ '$scope', '$state', 'machinesPromise', 'CSRF'
, ($scope, $state, machinesPromise, CSRF) ->
### PUBLIC SCOPE ###
## Form action on the following URL
$scope.method = 'post'
## API URL where the form will be posted
$scope.actionUrl = '/api/trainings/'
## list of machines
$scope.machines = machinesPromise
### PRIVATE SCOPE ###
##
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
CSRF.setMetaTags()
## Using the TrainingsController
new TrainingsController($scope, $state)
## !!! MUST BE CALLED AT THE END of the controller
initialize()
]
##
# Controller used in the training edition page (admin)
##
Application.Controllers.controller "EditTrainingController", [ '$scope', '$state', '$stateParams', 'trainingPromise', 'machinesPromise', 'CSRF'
, ($scope, $state, $stateParams, trainingPromise, machinesPromise, CSRF) ->
### PUBLIC SCOPE ###
## Form action on the following URL
$scope.method = 'patch'
## API URL where the form will be posted
$scope.actionUrl = '/api/trainings/' + $stateParams.id
## Details of the training to edit (id in URL)
$scope.training = trainingPromise
## list of machines
$scope.machines = machinesPromise
### PRIVATE SCOPE ###
##
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
CSRF.setMetaTags()
## Using the TrainingsController
new TrainingsController($scope, $state)
## !!! MUST BE CALLED AT THE END of the controller
initialize()
]
##
# Controller used in the trainings management page, allowing admins users to see and manage the list of trainings and reservations.
##
Application.Controllers.controller "TrainingsAdminController", ["$scope", "$state", "$uibModal", 'Training', 'trainingsPromise', 'machinesPromise', '_t', 'growl', 'dialogs'
, ($scope, $state, $uibModal, Training, trainingsPromise, machinesPromise, _t, growl, dialogs) ->
@ -40,35 +181,6 @@ Application.Controllers.controller "TrainingsController", ["$scope", "$state", "
##
# 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/
@ -138,30 +250,17 @@ Application.Controllers.controller "TrainingsController", ["$scope", "$state", "
# @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
]
)
dialogs.confirm
resolve:
object: ->
title: _t('confirmation_required')
msg: _t('do_you_really_want_to_delete_this_training')
, -> # deletion confirmed
training.$delete ->
$scope.trainings.splice(index, 1)
growl.info(_t('training_successfully_deleted'))
, (error)->
growl.warning(_t('unable_to_delete_the_training_because_some_users_alredy_booked_it'))
@ -197,8 +296,15 @@ Application.Controllers.controller "TrainingsController", ["$scope", "$state", "
# The selected training details will be loaded from the API and rendered into the accordions.
##
$scope.selectTrainingToMonitor = ->
Training.get {id: $scope.monitoring.training.id}, (training) ->
Training.availabilities {id: $scope.monitoring.training.id}, (training) ->
$scope.groupedAvailabilities = groupAvailabilities([training])
# we open current year/month by default
now = moment()
$scope.accordions[training.name] = {}
$scope.accordions[training.name][now.year()] =
isOpenFirst: true
$scope.accordions[training.name][now.year()][now.month()] =
isOpenFirst: true

View File

@ -1,7 +1,7 @@
'use strict'
Application.Controllers.controller 'ApplicationController', ["$rootScope", "$scope", "$window", "Session", "AuthService", "Auth", "$uibModal", "$state", 'growl', 'Notification', '$interval', "Setting", '_t'
, ($rootScope, $scope, $window, Session, AuthService, Auth, $uibModal, $state, growl, Notification, $interval, Setting, _t) ->
Application.Controllers.controller 'ApplicationController', ["$rootScope", "$scope", "$window", "Session", "AuthService", "Auth", "$uibModal", "$state", 'growl', 'Notification', '$interval', "Setting", '_t', 'Version'
, ($rootScope, $scope, $window, Session, AuthService, Auth, $uibModal, $state, growl, Notification, $interval, Setting, _t, Version) ->
@ -14,14 +14,24 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
### PUBLIC SCOPE ###
## Fab-manager's version
$scope.version =
version: ''
##
# Set the current user to the provided value and initialize the session
# @param user {Object} Rails/Devise user
##
$scope.setCurrentUser = (user) ->
$rootScope.currentUser = user
Session.create(user);
getNotifications()
unless angular.isUndefinedOrNull(user)
$rootScope.currentUser = user
Session.create(user);
getNotifications()
# fab-manager's app-version
if user.role == 'admin'
$scope.version = Version.get()
else
$scope.version = {version: ''}
##
@ -88,6 +98,7 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
# default user's parameters
$scope.user =
is_allow_contact: true
is_allow_newsletter: false
# Errors display
$scope.alerts = []
@ -98,11 +109,18 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
$scope.ok = ->
# try to create the account
$scope.alerts = []
# remove 'organization' attribute
orga = $scope.user.organization
delete $scope.user.organization
# register on server
Auth.register($scope.user).then (user) ->
# creation successful
$uibModalInstance.close(user)
, (error) ->
# creation failed...
# restore organization param
$scope.user.organization = orga
# display errors
angular.forEach error.data.errors, (v, k) ->
angular.forEach v, (err) ->
$scope.alerts.push
@ -197,6 +215,7 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
# try to retrieve any currently logged user
Auth.login().then (user) ->
$scope.setCurrentUser(user)
# force users to complete their profile if they are not
if user.need_completion
$state.transitionTo('app.logged.profileCompletion')
, (error) ->

View File

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

View File

@ -1,13 +1,7 @@
'use strict'
Application.Controllers.controller "EventsController", ["$scope", "$state", 'Event', ($scope, $state, Event) ->
### PRIVATE STATIC CONSTANTS ###
# Number of events added to the page when the user clicks on 'load next events'
EVENTS_PER_PAGE = 12
Application.Controllers.controller "EventsController", ["$scope", "$state", 'Event', 'categoriesPromise', 'themesPromise', 'ageRangesPromise'
, ($scope, $state, Event, categoriesPromise, themesPromise, ageRangesPromise) ->
@ -16,33 +10,40 @@ Application.Controllers.controller "EventsController", ["$scope", "$state", 'Eve
## 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
## List of categories for the events
$scope.categories = categoriesPromise
## List of events themes
$scope.themes = themesPromise
## List of age ranges
$scope.ageRanges = ageRangesPromise
## Hide or show the 'load more' button
$scope.noMoreResults = false
## Active filters for the events list
$scope.filters =
category_id: null
theme_id: null
age_range_id: null
##
# Adds EVENTS_PER_PAGE events to the bottom of the page, grouped by month
# Adds a resultset of events to the bottom of the page, grouped by month
##
$scope.loadMoreEvents = ->
Event.query {page: $scope.page}, (data) ->
Event.query Object.assign({page: $scope.page}, $scope.filters), (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
groupEvents($scope.events)
$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
if (!data[0] || data[0].nb_total_events <= $scope.events.length)
$scope.noMoreResults = true
@ -55,13 +56,69 @@ Application.Controllers.controller "EventsController", ["$scope", "$state", 'Eve
##
# Callback to refresh the events list according to the filters set
##
$scope.filterEvents = ->
# reinitialize results datasets
$scope.page = 1
$scope.eventsGroupByMonth = {}
$scope.events = []
$scope.monthOrder = []
$scope.noMoreResults = false
# run a search query
Event.query Object.assign({page: $scope.page}, $scope.filters), (data) ->
$scope.events = data
groupEvents(data)
$scope.page += 1
if (!data[0] || data[0].nb_total_events <= $scope.events.length)
$scope.noMoreResults = true
##
# Test if the provided event occurs on a single day or on many days
# @param event {{start_date:Date, end_date:Date}} Event object as retreived from the API
# @return {boolean} false if the event occurs on many days
##
$scope.onSingleDay = (event) ->
moment(event.start_date).isSame(event.end_date, 'day')
### PRIVATE SCOPE ###
##
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
$scope.loadMoreEvents()
$scope.filterEvents()
##
# Group the provided events by month/year and concat them with existing results
# Then compute the ordered list of months for the complete resultset.
# Affect the resulting events groups in $scope.eventsGroupByMonth and the ordered month keys in $scope.monthOrder.
# @param {Array} Events retrived from the API
##
groupEvents = (events) ->
if events.length > 0
eventsGroupedByMonth = _.groupBy(events, (obj) ->
_.map ['month', 'year'], (key, value) -> obj[key]
)
$scope.eventsGroupByMonth = Object.assign($scope.eventsGroupByMonth, eventsGroupedByMonth)
monthsOrder = _.sortBy _.keys($scope.eventsGroupByMonth), (k)->
monthYearArray = k.split(',')
date = new Date()
date.setMonth(monthYearArray[0])
date.setYear(monthYearArray[1])
return -date.getTime()
$scope.monthOrder = monthsOrder
@ -75,13 +132,12 @@ Application.Controllers.controller "EventsController", ["$scope", "$state", 'Eve
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) ->
Application.Controllers.controller "ShowEventController", ["$scope", "$state", "$stateParams", "Event", '$uibModal', 'Member', 'Reservation', 'Price', 'CustomAsset', 'eventPromise', 'growl', '_t', 'Wallet', 'helpers', 'priceCategoriesPromise', 'settingsPromise',
($scope, $state, $stateParams, Event, $uibModal, Member, Reservation, Price, CustomAsset, eventPromise, growl, _t, Wallet, helpers, priceCategoriesPromise, settingsPromise) ->
### PUBLIC SCOPE ###
$scope.reducedAmountAlert = reducedAmountAlert.setting.value
## reservations for the currently shown event
$scope.reservations = []
@ -92,18 +148,30 @@ Application.Controllers.controller "ShowEventController", ["$scope", "$state", "
## parameters for a new reservation
$scope.reserve =
nbPlaces: []
nbReducedPlaces: []
nbPlaces:
normal: []
nbReservePlaces: 0
nbReserveReducedPlaces: 0
tickets: {}
toReserve: false
amountTotal : 0
totalSeats: 0
## Discount coupon to apply to the basket, if any
$scope.coupon =
applied: null
# get the details for the current event (event's id is recovered from the current URL)
## Get the details for the current event (event's id is recovered from the current URL)
$scope.event = eventPromise
## List of price categories for the events
$scope.priceCategories = priceCategoriesPromise
## Global config: is the user authorized to change his bookings slots?
$scope.enableBookingMove = (settingsPromise.booking_move_enable == "true")
## Global config: delay in hours before a booking while changing the booking slot is forbidden
$scope.moveBookingDelay = parseInt(settingsPromise.booking_move_delay)
##
@ -117,21 +185,27 @@ Application.Controllers.controller "ShowEventController", ["$scope", "$state", "
##
# Callback to call when the number of places change in the current booking
# Callback to call when the number of tickets to book changes in the current booking
##
$scope.changeNbPlaces = ->
reste = $scope.event.nb_free_places - $scope.reserve.nbReservePlaces
$scope.reserve.nbReducedPlaces = [0..reste]
$scope.computeEventAmount()
# compute the total remaing places
remain = $scope.event.nb_free_places - $scope.reserve.nbReservePlaces
for ticket of $scope.reserve.tickets
remain -= $scope.reserve.tickets[ticket]
# we store the total number of seats booked, this is used to know if the 'pay' button must be shown
$scope.reserve.totalSeats = $scope.event.nb_free_places - remain
# update the availables seats for full price tickets
fullPriceRemains = $scope.reserve.nbReservePlaces + remain
$scope.reserve.nbPlaces.normal = [0..fullPriceRemains]
# update the available seats for other prices tickets
for key of $scope.reserve.nbPlaces
if key != 'normal'
priceRemain = $scope.reserve.tickets[key] + remain
$scope.reserve.nbPlaces[key] = [0..priceRemain]
##
# 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]
# recompute the total price
$scope.computeEventAmount()
@ -185,11 +259,13 @@ Application.Controllers.controller "ShowEventController", ["$scope", "$state", "
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)
Wallet.getWalletByUser {user_id: $scope.ctrl.member.id}, (wallet) ->
amountToPay = helpers.getAmountToPay($scope.reserve.amountTotal, wallet.amount)
if $scope.currentUser.role isnt 'admin' and amountToPay > 0
payByStripe(reservation)
else
if $scope.currentUser.role is 'admin' or amountToPay is 0
payOnSite(reservation)
else
# otherwise we alert, this error musn't occur when the current user is not admin
growl.error(_t('please_select_a_member_first'))
@ -206,20 +282,31 @@ Application.Controllers.controller "ShowEventController", ["$scope", "$state", "
reservable_type: 'Event'
slots_attributes: []
nb_reserve_places: $scope.reserve.nbReservePlaces
nb_reserve_reduced_places: $scope.reserve.nbReserveReducedPlaces
tickets_attributes: []
# a single slot is used for events
reservation.slots_attributes.push
start_at: $scope.event.start_date
end_at: $scope.event.end_date
availability_id: $scope.event.availability.id
# iterate over reservations per prices
for price_id, seats of $scope.reserve.tickets
reservation.tickets_attributes.push
event_price_category_id: price_id
booked: seats
# set the attempting marker
$scope.attempting = true
# save the reservation to the API
Reservation.save reservation: reservation, (reservation) ->
# reservation successfull
afterPayment(reservation)
$scope.attempting = false
, (response)->
# reservation failed
$scope.alerts = []
$scope.alerts.push
msg: response.data.card[0]
type: 'danger'
# unset the attempting marker
$scope.attempting = false
@ -227,7 +314,7 @@ Application.Controllers.controller "ShowEventController", ["$scope", "$state", "
##
# 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 reservation {{id:number, reservable_id:number, nb_reserve_places:number}}
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
##
$scope.modifyReservation = (reservation, e)->
@ -247,7 +334,7 @@ Application.Controllers.controller "ShowEventController", ["$scope", "$state", "
# 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)
if e.nb_free_places > reservation.total_booked_seats
$scope.reservation.reservable_id = e.id
break
@ -277,23 +364,29 @@ Application.Controllers.controller "ShowEventController", ["$scope", "$state", "
$uibModalInstance.dismiss('cancel')
]
.result['finally'](null).then (reservation)->
# remove the reservation from the user's reservations list for this event (occurrence)
$scope.reservations.splice(index, 1)
$scope.event.nb_free_places = $scope.event.nb_free_places + reservation.nb_reserve_places + reservation.nb_reserve_reduced_places
# add the number of places transfered (to the new date) to the total of free places for this event
$scope.event.nb_free_places = $scope.event.nb_free_places + reservation.total_booked_seats
# remove the number of places transfered from the total of free places of the receiving occurrance
angular.forEach $scope.event.recurrence_events, (e)->
if e.id is parseInt(reservation.reservable_id, 10)
e.nb_free_places = e.nb_free_places - reservation.nb_reserve_places - reservation.nb_reserve_reduced_places
if e.id is parseInt(reservation.reservable.id, 10)
e.nb_free_places = e.nb_free_places - reservation.total_booked_seats
##
# Checks if the provided reservation is able to be modified
# @param reservation {{nb_reserve_places:number, nb_reserve_reduced_places:number}}
# Checks if the provided reservation is able to be moved (date change)
# @param reservation {{total_booked_seats:number}}
##
$scope.reservationCanModify = (reservation)->
slotStart = moment(reservation.slots[0].start_at)
now = moment()
isAble = false
angular.forEach $scope.event.recurrence_events, (e)->
isAble = true if e.nb_free_places > (reservation.nb_reserve_places + reservation.nb_reserve_reduced_places)
isAble
isAble = true if e.nb_free_places >= reservation.total_booked_seats
return (isAble and $scope.enableBookingMove and slotStart.diff(now, "hours") >= $scope.moveBookingDelay)
@ -305,36 +398,62 @@ Application.Controllers.controller "ShowEventController", ["$scope", "$state", "
# 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) ->
Price.compute mkRequestParams(r, $scope.coupon.applied), (res) ->
$scope.reserve.amountTotal = res.price
else
$scope.reserve.amountTotal = null
##
# Return the URL allowing to share the current project on the Facebook social network
##
$scope.shareOnFacebook = ->
'https://www.facebook.com/share.php?u='+$state.href('app.public.events_show', {id: $scope.event.id}, {absolute: true}).replace('#', '%23')
##
# Return the URL allowing to share the current project on the Twitter social network
##
$scope.shareOnTwitter = ->
'https://twitter.com/intent/tweet?url='+encodeURIComponent($state.href('app.public.events_show', {id: $scope.event.id}, {absolute: true}))+'&text='+encodeURIComponent($scope.event.title)
##
# Return the textual description of the conditions applyable to the given price's category
# @param category_id {number} ID of the price's category
##
$scope.getPriceCategoryConditions = (category_id) ->
for cat in $scope.priceCategories
if cat.id == category_id
return cat.conditions
### PRIVATE SCOPE ###
##
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
# gather the current user or the list of users if the current user is an admin
# set the controlled user as the current user if the current user is not an admin
if $scope.currentUser
if $scope.currentUser.role isnt 'admin'
$scope.ctrl.member = $scope.currentUser
# 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]
resetEventReserve()
# if non-admin, get the current user's reservations into $scope.reservations
if $scope.currentUser
getReservations($scope.event.id, 'Event', $scope.currentUser.id)
# watch when a coupon is applied to re-compute the total price
$scope.$watch 'coupon.applied', (newValue, oldValue) ->
unless newValue == null and oldValue == null
$scope.computeEventAmount()
##
@ -353,8 +472,8 @@ Application.Controllers.controller "ShowEventController", ["$scope", "$state", "
# 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}}
# @param event {Object} Current event
# @return {{user_id:number, reservable_id:number, reservable_type:string, slots_attributes:Array<Object>, nb_reserve_places:number}}
##
mkReservation = (member, reserve, event) ->
reservation =
@ -363,7 +482,7 @@ Application.Controllers.controller "ShowEventController", ["$scope", "$state", "
reservable_type: 'Event'
slots_attributes: []
nb_reserve_places: reserve.nbReservePlaces
nb_reserve_reduced_places: reserve.nbReserveReducedPlaces
tickets_attributes: []
reservation.slots_attributes.push
start_at: event.start_date
@ -371,22 +490,49 @@ Application.Controllers.controller "ShowEventController", ["$scope", "$state", "
availability_id: event.availability.id
offered: event.offered || false
for evt_px_cat in event.prices
booked = reserve.tickets[evt_px_cat.id]
if booked > 0
reservation.tickets_attributes.push
event_price_category_id: evt_px_cat.id
booked: booked
reservation
##
# Format the parameters expected by /api/prices/compute or /api/reservations and return the resulting object
# @param reservation {Object} as returned by mkReservation()
# @param coupon {Object} Coupon as returned from the API
# @return {{reservation:Object, coupon_code:string}}
##
mkRequestParams = (reservation, coupon) ->
params =
reservation: reservation
coupon_code: (coupon.code if coupon)
params
##
# Set the current reservation to the default values. This implies to reservation form to be hidden.
##
resetEventReserve = ->
if $scope.event
$scope.reserve =
nbPlaces: [0..$scope.event.nb_free_places]
nbReducedPlaces: [0..$scope.event.nb_free_places]
nbPlaces:
normal: [0..$scope.event.nb_free_places]
nbReservePlaces: 0
nbReserveReducedPlaces: 0
tickets: {}
toReserve: false
amountTotal : 0
totalSeats: 0
for evt_px_cat in $scope.event.prices
$scope.reserve.nbPlaces[evt_px_cat.id] = [0..$scope.event.nb_free_places]
$scope.reserve.tickets[evt_px_cat.id] = 0
$scope.event.offered = false
@ -403,16 +549,24 @@ Application.Controllers.controller "ShowEventController", ["$scope", "$state", "
reservation: ->
reservation
price: ->
Price.compute({reservation: reservation}).$promise
Price.compute(mkRequestParams(reservation, $scope.coupon.applied)).$promise
wallet: ->
Wallet.getWalletByUser({user_id: reservation.user_id}).$promise
cgv: ->
CustomAsset.get({name: 'cgv-file'}).$promise
objectToPay: ->
eventToReserve: $scope.event
reserve: $scope.reserve
member: $scope.ctrl.member
controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'cgv', 'Auth', 'Reservation', 'growl', ($scope, $uibModalInstance, $state, reservation, price, cgv, Auth, Reservation, growl) ->
coupon: ->
$scope.coupon.applied
controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'cgv', 'Auth', 'Reservation', 'growl', 'wallet', 'helpers', '$locale', '$filter', 'coupon',
($scope, $uibModalInstance, $state, reservation, price, cgv, Auth, Reservation, growl, wallet, helpers, $locale, $filter, coupon) ->
# user wallet amount
$scope.walletAmount = wallet.amount
# Price
$scope.amount = price.price
$scope.amount = helpers.getAmountToPay(price.price, wallet.amount)
# CGV
$scope.cgv = cgv.custom_asset
@ -420,6 +574,10 @@ Application.Controllers.controller "ShowEventController", ["$scope", "$state", "
# Reservation
$scope.reservation = reservation
$scope.currencySymbol = $locale.NUMBER_FORMATS.CURRENCY_SYM
$scope.numberFilter = $filter('number')
# Callback for the stripe payment authorization
$scope.payment = (status, response) ->
if response.error
@ -427,7 +585,7 @@ Application.Controllers.controller "ShowEventController", ["$scope", "$state", "
else
$scope.attempting = true
$scope.reservation.card_token = response.id
Reservation.save reservation: $scope.reservation, (reservation) ->
Reservation.save mkRequestParams($scope.reservation, coupon), (reservation) ->
$uibModalInstance.close(reservation)
, (response)->
$scope.alerts = []
@ -453,24 +611,42 @@ Application.Controllers.controller "ShowEventController", ["$scope", "$state", "
reservation: ->
reservation
price: ->
Price.compute({reservation: reservation}).$promise
controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'Auth', 'Reservation', ($scope, $uibModalInstance, $state, reservation, price, Auth, Reservation) ->
Price.compute(mkRequestParams(reservation, $scope.coupon.applied)).$promise
wallet: ->
Wallet.getWalletByUser({user_id: reservation.user_id}).$promise
coupon: ->
$scope.coupon.applied
controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'Auth', 'Reservation', 'wallet', '$locale', 'helpers', '$filter', 'coupon',
($scope, $uibModalInstance, $state, reservation, price, Auth, Reservation, wallet, $locale, helpers, $filter, coupon) ->
# user wallet amount
$scope.walletAmount = wallet.amount
# Price
$scope.amount = price.price
$scope.price = price.price
# price to pay
$scope.amount = helpers.getAmountToPay(price.price, wallet.amount)
# Reservation
$scope.reservation = reservation
$scope.currencySymbol = $locale.NUMBER_FORMATS.CURRENCY_SYM
$scope.numberFilter = $filter('number')
# Button label
if $scope.amount > 0
$scope.validButtonName = _t('confirm_(payment_on_site)')
$scope.validButtonName = _t('confirm_payment_of_html', {ROLE:$scope.currentUser.role, AMOUNT:$filter('currency')($scope.amount)}, "messageformat")
else
$scope.validButtonName = _t('confirm')
if price.price > 0 and $scope.walletAmount == 0
$scope.validButtonName = _t('confirm_payment_of_html', {ROLE:$scope.currentUser.role, AMOUNT:$filter('currency')(price.price)}, "messageformat")
else
$scope.validButtonName = _t('confirm')
# Callback to validate the payment
$scope.ok = ->
$scope.attempting = true
Reservation.save reservation: $scope.reservation, (reservation) ->
Reservation.save mkRequestParams($scope.reservation, coupon), (reservation) ->
$uibModalInstance.close(reservation)
$scope.attempting = true
, (response)->
@ -496,7 +672,7 @@ Application.Controllers.controller "ShowEventController", ["$scope", "$state", "
# @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
$scope.event.nb_free_places = $scope.event.nb_free_places - reservation.total_booked_seats
resetEventReserve()
$scope.reserveSuccess = true
$scope.reservations.push reservation

View File

@ -126,7 +126,7 @@ _reserveMachine = (machine, e) ->
# modal is close with validation
$scope.ok = ->
$state.go('app.logged.trainings_reserve')
$state.go('app.logged.trainings_reserve', {id: $scope.machine.trainings[0].id})
$uibModalInstance.close(machine)
# modal is closed with escaping
@ -231,8 +231,8 @@ Application.Controllers.controller "EditMachineController", ["$scope", '$state',
##
# Controller used in the machine details page (public)
##
Application.Controllers.controller "ShowMachineController", ['$scope', '$state', '$uibModal', '$stateParams', '_t', 'Machine', 'growl', 'machinePromise'
, ($scope, $state, $uibModal, $stateParams, _t, Machine, growl, machinePromise) ->
Application.Controllers.controller "ShowMachineController", ['$scope', '$state', '$uibModal', '$stateParams', '_t', 'Machine', 'growl', 'machinePromise', 'dialogs'
, ($scope, $state, $uibModal, $stateParams, _t, Machine, growl, machinePromise, dialogs) ->
## Retrieve the details for the machine id in the URL, if an error occurs redirect the user to the machines list
$scope.machine = machinePromise
@ -245,11 +245,17 @@ Application.Controllers.controller "ShowMachineController", ['$scope', '$state',
if $scope.currentUser.role isnt 'admin'
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'))
dialogs.confirm
resolve:
object: ->
title: _t('confirmation_required')
msg: _t('do_you_really_want_to_delete_this_machine')
, -> # deletion confirmed
# delete the machine then redirect to the machines listing
machine.$delete ->
$state.go('app.public.machines_list')
, (error)->
growl.warning(_t('the_machine_cant_be_deleted_because_it_is_already_reserved_by_some_users'))
##
# Callback to book a reservation for the current machine
##
@ -268,38 +274,26 @@ Application.Controllers.controller "ShowMachineController", ['$scope', '$state',
# 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) ->
Application.Controllers.controller "ReserveMachineController", ["$scope", "$state", '$stateParams', "$uibModal", '_t', "moment", 'Machine', 'Auth', 'dialogs', '$timeout', 'Price', 'Member', 'Availability', 'Slot', 'Setting', 'CustomAsset', 'plansPromise', 'groupsPromise', 'growl', 'machinePromise', 'settingsPromise', 'Wallet', 'helpers', 'uiCalendarConfig', 'CalendarConfig',
($scope, $state, $stateParams, $uibModal, _t, moment, Machine, Auth, dialogs, $timeout, Price, Member, Availability, Slot, Setting, CustomAsset, plansPromise, groupsPromise, growl, machinePromise, settingsPromise, Wallet, helpers, uiCalendarConfig, CalendarConfig) ->
### 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'
FREE_SLOT_BORDER_COLOR = '<%= AvailabilityHelper::MACHINE_COLOR %>'
# Slot already booked by another user
UNAVAILABLE_SLOT_BORDER_COLOR = '#1d98ec'
UNAVAILABLE_SLOT_BORDER_COLOR = '<%= AvailabilityHelper::MACHINE_IS_RESERVED_BY_USER %>'
# Slot free to be booked
BOOKED_SLOT_BORDER_COLOR = '#b2e774'
BOOKED_SLOT_BORDER_COLOR = '<%= AvailabilityHelper::IS_RESERVED_BY_CURRENT_USER %>'
### 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 = []
@ -321,12 +315,13 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat
## total amount of the bill to pay
$scope.amountTotal = 0
## Discount coupon to apply to the basket, if any
$scope.coupon =
applied: null
## 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
@ -340,34 +335,12 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat
member: {}
## current machine to reserve
$scope.machine = {}
$scope.machine = machinePromise
## 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
$scope.calendarConfig = CalendarConfig
minTime: moment.duration(moment(settingsPromise.booking_window_start).format('HH:mm:ss'))
maxTime: moment.duration(moment(settingsPromise.booking_window_end).format('HH:mm:ss'))
eventClick: (event, jsEvent, view) ->
calendarEventClickCb(event, jsEvent, view)
eventRender: (event, element, view) ->
@ -391,12 +364,6 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat
## 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'))
##
@ -412,7 +379,7 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat
$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'
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
@ -425,7 +392,7 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat
$scope.slotToPlace.backgroundColor = 'white'
$scope.slotToPlace.title = ''
$scope.slotToPlace = null
$scope.calendar.fullCalendar 'rerenderEvents'
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
@ -458,7 +425,7 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat
$scope.slotToModify.can_modify = false
$scope.slotToModify = null
$scope.calendar.fullCalendar 'rerenderEvents'
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
, (err) -> # failure
growl.error(_t('unable_to_change_the_reservation'))
console.error(err)
@ -476,7 +443,7 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat
$scope.slotToModify.backgroundColor = 'white'
$scope.slotToModify = null
$scope.calendar.fullCalendar 'rerenderEvents'
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
@ -532,8 +499,8 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat
$scope.plansAreShown = false
updateCartPrice()
$timeout ->
$scope.calendar.fullCalendar 'refetchEvents'
$scope.calendar.fullCalendar 'rerenderEvents'
uiCalendarConfig.calendars.calendar.fullCalendar 'refetchEvents'
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
@ -569,11 +536,13 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat
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)
Wallet.getWalletByUser {user_id: $scope.ctrl.member.id}, (wallet) ->
amountToPay = helpers.getAmountToPay($scope.amountTotal, wallet.amount)
if $scope.currentUser.role isnt 'admin' and amountToPay > 0
payByStripe(reservation)
else
if $scope.currentUser.role is 'admin' or amountToPay is 0
payOnSite(reservation)
else
# otherwise we alert, this error musn't occur when the current user is not admin
growl.error(_t('please_select_a_member_first'))
@ -637,11 +606,10 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat
if $scope.currentUser.role isnt 'admin'
$scope.ctrl.member = $scope.currentUser
$scope.machine = Machine.get {id: $stateParams.id}
, ->
return
, ->
$state.go('app.public.machines_list')
# watch when a coupon is applied to re-compute the total price
$scope.$watch 'coupon.applied', (newValue, oldValue) ->
unless newValue == null and oldValue == null
updateCartPrice()
@ -670,13 +638,28 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat
##
# Format the parameters expected by /api/prices/compute or /api/reservations and return the resulting object
# @param reservation {Object} as returned by mkReservation()
# @param coupon {Object} Coupon as returned from the API
# @return {{reservation:Object, coupon_code:string}}
##
mkRequestParams = (reservation, coupon) ->
params =
reservation: reservation
coupon_code: (coupon.code if coupon)
params
##
# 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) ->
Price.compute mkRequestParams(r, $scope.coupon.applied), (res) ->
$scope.amountTotal = res.price
setSlotsDetails(res.details)
else
@ -732,7 +715,7 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat
$scope.slotToModify = event
event.backgroundColor = '#eee'
event.title = _t('i_change')
$scope.calendar.fullCalendar 'rerenderEvents'
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
else if type == 'cancel'
dialogs.confirm
resolve:
@ -750,7 +733,7 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat
$scope.canceledSlot.is_reserved = false
$scope.canceledSlot.can_modify = false
$scope.canceledSlot = null
$scope.calendar.fullCalendar 'rerenderEvents'
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
, -> # error while canceling
growl.error _t('cancellation_failed')
, ->
@ -758,14 +741,14 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat
$scope.selectedPlan = null
$scope.modifiedSlots = null
$scope.calendar.fullCalendar 'rerenderEvents'
uiCalendarConfig.calendars.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/
@ -776,6 +759,7 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat
for tag in event.tags
html += "<span class='label label-success text-white' title='#{tag.name}'>#{tag.name}</span>"
element.find('.fc-time').append(html)
return
@ -791,12 +775,20 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat
reservation: ->
reservation
price: ->
Price.compute({reservation: reservation}).$promise
Price.compute(mkRequestParams(reservation, $scope.coupon.applied)).$promise
wallet: ->
Wallet.getWalletByUser({user_id: reservation.user_id}).$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) ->
coupon: ->
$scope.coupon.applied
controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'cgv', 'Auth', 'Reservation', 'wallet', 'helpers', '$locale', '$filter', 'coupon',
($scope, $uibModalInstance, $state, reservation, price, cgv, Auth, Reservation, wallet, helpers, $locale, $filter, coupon) ->
# user wallet amount
$scope.walletAmount = wallet.amount
# Price
$scope.amount = price.price
$scope.amount = helpers.getAmountToPay(price.price, wallet.amount)
# CGV
$scope.cgv = cgv.custom_asset
@ -804,6 +796,12 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat
# Reservation
$scope.reservation = reservation
# Currency symbol or abreviation for the current locale
$scope.currencySymbol = $locale.NUMBER_FORMATS.CURRENCY_SYM
# Used in wallet info template to interpolate some translations
$scope.numberFilter = $filter('number')
##
# Callback to process the payment with Stripe, triggered on button click
##
@ -813,7 +811,7 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat
else
$scope.attempting = true
$scope.reservation.card_token = response.id
Reservation.save reservation: $scope.reservation, (reservation) ->
Reservation.save mkRequestParams($scope.reservation, coupon), (reservation) ->
$uibModalInstance.close(reservation)
, (response)->
$scope.alerts = []
@ -839,27 +837,47 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat
reservation: ->
reservation
price: ->
Price.compute({reservation: reservation}).$promise
controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'Auth', 'Reservation', ($scope, $uibModalInstance, $state, reservation, price, Auth, Reservation) ->
Price.compute(mkRequestParams(reservation, $scope.coupon.applied)).$promise
wallet: ->
Wallet.getWalletByUser({user_id: reservation.user_id}).$promise
coupon: ->
$scope.coupon.applied
controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'Auth', 'Reservation', 'wallet', 'helpers', '$filter', '$locale', 'coupon',
($scope, $uibModalInstance, $state, reservation, price, Auth, Reservation, wallet, helpers, $filter, $locale, coupon) ->
# Price
$scope.amount = price.price
# user wallet amount
$scope.walletAmount = wallet.amount
# Global price (total of all items)
$scope.price = price.price
# Price to pay (wallet deducted)
$scope.amount = helpers.getAmountToPay(price.price, wallet.amount)
# Reservation
$scope.reservation = reservation
# Currency symbol or abreviation for the current locale
$scope.currencySymbol = $locale.NUMBER_FORMATS.CURRENCY_SYM
# Used in wallet info template to interpolate some translations
$scope.numberFilter = $filter('number')
# Button label
if $scope.amount > 0
$scope.validButtonName = _t('confirm_(payment_on_site)')
$scope.validButtonName = _t('confirm_payment_of_html', {ROLE:$scope.currentUser.role, AMOUNT:$filter('currency')($scope.amount)}, "messageformat")
else
$scope.validButtonName = _t('confirm')
if price.price > 0 and $scope.walletAmount == 0
$scope.validButtonName = _t('confirm_payment_of_html', {ROLE:$scope.currentUser.role, AMOUNT:$filter('currency')(price.price)}, "messageformat")
else
$scope.validButtonName = _t('confirm')
##
# Callback to process the local payment, triggered on button click
##
$scope.ok = ->
$scope.attempting = true
Reservation.save reservation: $scope.reservation, (reservation) ->
Reservation.save mkRequestParams($scope.reservation, coupon), (reservation) ->
$uibModalInstance.close(reservation)
$scope.attempting = true
, (response)->
@ -930,8 +948,8 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat
Auth._currentUser.subscribed_plan = angular.copy($scope.selectedPlan)
$scope.plansAreShown = false
$scope.calendar.fullCalendar 'refetchEvents'
$scope.calendar.fullCalendar 'rerenderEvents'
uiCalendarConfig.calendars.calendar.fullCalendar 'refetchEvents'
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'

View File

@ -16,18 +16,23 @@ Application.Controllers.controller "MainNavController", ["$scope", "$location",
{
state: 'app.public.machines_list'
linkText: 'reserve_a_machine'
linkIcon: 'calendar'
linkIcon: 'cogs'
}
{
state: 'app.logged.trainings_reserve'
state: 'app.public.trainings_list'
linkText: 'trainings_registrations'
linkIcon: 'graduation-cap'
}
{
state: 'app.public.events_list'
linkText: 'courses_and_workshops_registrations'
linkText: 'events_registrations'
linkIcon: 'tags'
}
{
state: 'app.public.calendar'
linkText: 'public_calendar'
linkIcon: 'calendar'
}
{
state: 'app.public.projects_list'
linkText: 'projects_gallery'
@ -73,7 +78,7 @@ Application.Controllers.controller "MainNavController", ["$scope", "$location",
}
{
state: 'app.admin.events'
linkText: 'courses_and_workshops_monitoring'
linkText: 'manage_the_events'
linkIcon: 'tags'
}
{

View File

@ -1,7 +1,7 @@
'use strict'
Application.Controllers.controller "PlansIndexController", ["$scope", "$rootScope", "$state", '$uibModal', 'Auth', 'dialogs', 'growl', 'plansPromise', 'groupsPromise', 'Subscription', 'Member', 'subscriptionExplicationsPromise', '_t'
, ($scope, $rootScope, $state, $uibModal, Auth, dialogs, growl, plansPromise, groupsPromise, Subscription, Member, subscriptionExplicationsPromise, _t) ->
Application.Controllers.controller "PlansIndexController", ["$scope", "$rootScope", "$state", '$uibModal', 'Auth', 'dialogs', 'growl', 'plansPromise', 'groupsPromise', 'Subscription', 'Member', 'subscriptionExplicationsPromise', '_t', 'Wallet', 'helpers'
, ($scope, $rootScope, $state, $uibModal, Auth, dialogs, growl, plansPromise, groupsPromise, Subscription, Member, subscriptionExplicationsPromise, _t, Wallet, helpers) ->
@ -30,11 +30,20 @@ Application.Controllers.controller "PlansIndexController", ["$scope", "$rootScop
member_id: null
## already subscribed plan of the current user
$scope.paidPlan = null
$scope.paid =
plan: null
## plan to subscribe (shopping cart)
$scope.selectedPlan = null
## Discount coupon to apply to the basket, if any
$scope.coupon =
applied: null
## Storage for the total price (plan price + coupon, if any)
$scope.cart =
total: null
## text that appears in the bottom-right box of the page (subscriptions rules details)
$scope.subscriptionExplicationsAlert = subscriptionExplicationsPromise.setting.value
@ -44,7 +53,7 @@ Application.Controllers.controller "PlansIndexController", ["$scope", "$rootScop
##
$scope.updateMember = ->
$scope.selectedPlan = null
$scope.paidPlan = null
$scope.paid.plan = null
$scope.group.change = false
Member.get {id: $scope.ctrl.member.id}, (member) ->
$scope.ctrl.member = member
@ -61,6 +70,7 @@ Application.Controllers.controller "PlansIndexController", ["$scope", "$rootScop
if $scope.isAuthenticated()
if $scope.selectedPlan != plan
$scope.selectedPlan = plan
updateCartPrice()
else
$scope.selectedPlan = null
else
@ -72,10 +82,13 @@ Application.Controllers.controller "PlansIndexController", ["$scope", "$rootScop
# Callback to trigger the payment process of the subscription
##
$scope.openSubscribePlanModal = ->
if $scope.currentUser.role isnt 'admin'
payByStripe()
else
payOnSite()
Wallet.getWalletByUser {user_id: $scope.ctrl.member.id}, (wallet) ->
amountToPay = helpers.getAmountToPay($scope.cart.total, wallet.amount)
if $scope.currentUser.role isnt 'admin' and amountToPay > 0
payByStripe()
else
if $scope.currentUser.role is 'admin' or amountToPay is 0
payOnSite()
@ -144,12 +157,33 @@ Application.Controllers.controller "PlansIndexController", ["$scope", "$rootScop
if $scope.currentUser
if $scope.currentUser.role isnt 'admin'
$scope.ctrl.member = $scope.currentUser
$scope.paidPlan = $scope.currentUser.subscribed_plan
$scope.paid.plan = $scope.currentUser.subscribed_plan
$scope.group.id = $scope.currentUser.group_id
$scope.$on 'devise:new-session', (event, user)->
$scope.ctrl.member = user
# watch when a coupon is applied to re-compute the total price
$scope.$watch 'coupon.applied', (newValue, oldValue) ->
unless newValue == null and oldValue == null
updateCartPrice()
##
# Compute the total amount for the current reservation according to the previously set parameters
# and assign the result in $scope.reserve.amountTotal
##
updateCartPrice = ->
# first we check that a user was selected
if Object.keys($scope.ctrl.member).length > 0
$scope.cart.total = $scope.selectedPlan.amount
# apply the coupon if any
if $scope.coupon.applied
discount = $scope.cart.total * $scope.coupon.applied.percent_off / 100
$scope.cart.total -= discount
else
$scope.reserve.amountTotal = null
##
@ -162,18 +196,43 @@ Application.Controllers.controller "PlansIndexController", ["$scope", "$rootScop
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
price: -> $scope.cart.total
wallet: ->
Wallet.getWalletByUser({user_id: $scope.ctrl.member.id}).$promise
coupon: -> $scope.coupon.applied
controller: ['$scope', '$uibModalInstance', '$state', 'selectedPlan', 'member', 'price', 'Subscription', 'CustomAsset', 'wallet', 'helpers', '$locale', '$filter', 'coupon',
($scope, $uibModalInstance, $state, selectedPlan, member, price, Subscription, CustomAsset, wallet, helpers, $locale, $filter, coupon) ->
# user wallet amount
$scope.walletAmount = wallet.amount
# Final price to pay by the user
$scope.amount = helpers.getAmountToPay(price, wallet.amount)
# The plan that the user is about to subscribe
$scope.selectedPlan = selectedPlan
# Currency symbol or abreviation for the current locale
$scope.currencySymbol = $locale.NUMBER_FORMATS.CURRENCY_SYM
# Used in wallet info template to interpolate some translations
$scope.numberFilter = $filter('number')
# retrieve the CGV
CustomAsset.get {name: 'cgv-file'}, (cgv) ->
$scope.cgv = cgv.custom_asset
##
# Callback for click on the 'proceed' button.
# Handle the stripe's card tokenization process response and save the subscription to the API with the
# card token just created.
##
$scope.payment = (status, response) ->
if response.error
growl.error(response.error.message)
else
$scope.attempting = true
Subscription.save
coupon_code: (coupon.code if coupon)
subscription:
plan_id: selectedPlan.id
user_id: member.id
@ -188,7 +247,7 @@ Application.Controllers.controller "PlansIndexController", ["$scope", "$rootScop
.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.paid.plan = angular.copy($scope.selectedPlan)
$scope.selectedPlan = null
@ -203,12 +262,50 @@ Application.Controllers.controller "PlansIndexController", ["$scope", "$rootScop
resolve:
selectedPlan: -> $scope.selectedPlan
member: -> $scope.ctrl.member
controller: ['$scope', '$uibModalInstance', '$state', 'selectedPlan', 'member', 'Subscription', ($scope, $uibModalInstance, $state, selectedPlan, member, Subscription) ->
price: -> $scope.cart.total
wallet: ->
Wallet.getWalletByUser({user_id: $scope.ctrl.member.id}).$promise
coupon: -> $scope.coupon.applied
controller: ['$scope', '$uibModalInstance', '$state', 'selectedPlan', 'member', 'price', 'Subscription', 'wallet', 'helpers', '$locale', '$filter', 'coupon',
($scope, $uibModalInstance, $state, selectedPlan, member, price, Subscription, wallet, helpers, $locale, $filter, coupon) ->
# user wallet amount
$scope.walletAmount = wallet.amount
# subcription price, coupon subtracted if any
$scope.price = price
# price to pay
$scope.amount = helpers.getAmountToPay($scope.price, wallet.amount)
# Currency symbol or abreviation for the current locale
$scope.currencySymbol = $locale.NUMBER_FORMATS.CURRENCY_SYM
# Used in wallet info template to interpolate some translations
$scope.numberFilter = $filter('number')
# The plan that the user is about to subscribe
$scope.plan = selectedPlan
# The member who is subscribing a plan
$scope.member = member
# Button label
if $scope.amount > 0
$scope.validButtonName = _t('confirm_payment_of_html', {ROLE:$scope.currentUser.role, AMOUNT:$filter('currency')($scope.amount)}, "messageformat")
else
if price.price > 0 and $scope.walletAmount == 0
$scope.validButtonName = _t('confirm_payment_of_html', {ROLE:$scope.currentUser.role, AMOUNT:$filter('currency')(price.price)}, "messageformat")
else
$scope.validButtonName = _t('confirm')
##
# Callback for the 'proceed' button.
# Save the subscription to the API
##
$scope.ok = ->
$scope.attempting = true
Subscription.save
coupon_code: (coupon.code if coupon)
subscription:
plan_id: selectedPlan.id
user_id: member.id
@ -219,16 +316,18 @@ Application.Controllers.controller "PlansIndexController", ["$scope", "$rootScop
$scope.alerts.push({msg: _t('an_error_occured_during_the_payment_process_please_try_again_later'), type: 'danger' })
$scope.attempting = false
##
# Callback for the 'cancel' button.
# Close the modal box.
##
$scope.cancel = ->
$uibModalInstance.dismiss('cancel')
]
.result['finally'](null).then (reservation)->
$scope.ctrl.member.subscribed_plan = angular.copy($scope.selectedPlan)
Auth._currentUser.subscribed_plan = angular.copy($scope.selectedPlan)
index = $scope.members.indexOf($scope.ctrl.member)
$scope.members.splice(index, 1)
$scope.ctrl.member = null
$scope.paidPlan = angular.copy($scope.selectedPlan)
$scope.paid.plan = angular.copy($scope.selectedPlan)
$scope.selectedPlan = null

View File

@ -7,16 +7,19 @@
# in the various projects' admin controllers.
#
# Provides :
# - $scope.totalSteps
# - $scope.machines = [{Machine}]
# - $scope.components = [{Component}]
# - $scope.themes = [{Theme}]
# - $scope.licences = [{Licence}]
# - $scope.allowedExtensions = [{String}]
# - $scope.submited(content)
# - $scope.cancel()
# - $scope.addFile()
# - $scope.deleteFile(file)
# - $scope.addStep()
# - $scope.deleteStep(step)
# - $scope.changeStepIndex(step, newIdx)
#
# Requires :
# - $scope.project.project_caos_attributes = []
@ -24,7 +27,7 @@
# - $state (Ui-Router) [ 'app.public.projects_show', 'app.public.projects_list' ]
##
class ProjectsController
constructor: ($scope, $state, Project, Machine, Member, Component, Theme, Licence, $document, Diacritics)->
constructor: ($scope, $state, Project, Machine, Member, Component, Theme, Licence, $document, Diacritics, dialogs, allowedExtensions, _t)->
## Retrieve the list of machines from the server
Machine.query().$promise.then (data)->
@ -50,6 +53,12 @@ class ProjectsController
id: d.id
name: d.name
## Total number of documentation steps for the current project
$scope.totalSteps = $scope.project.project_steps_attributes.length
## List of extensions allowed for CAD attachements upload
$scope.allowedExtensions = allowedExtensions
##
@ -74,14 +83,6 @@ class ProjectsController
##
# Changes the user's view to the projects list page
##
$scope.cancel = ->
$state.go('app.public.projects_list')
##
# For use with 'ng-class', returns the CSS class name for the uploads previews.
# The preview may show a placeholder or the content of the file depending on the upload state.
@ -122,22 +123,55 @@ class ProjectsController
# This will create a single new empty entry into the project's steps list.
##
$scope.addStep = ->
$scope.project.project_steps_attributes.push {}
$scope.totalSteps += 1
$scope.project.project_steps_attributes.push { step_nb: $scope.totalSteps }
##
# This will remove the given stip from the project's steps list. If the step was previously saved
# This will remove the given step from the project's steps list. If the step was previously saved
# on the server, it will be marked for deletion for the next saving. Otherwise, it will be simply truncated from
# the steps array.
# @param file {Object} the file to delete
##
$scope.deleteStep = (step) ->
index = $scope.project.project_steps_attributes.indexOf(step)
if step.id?
step._destroy = true
else
$scope.project.project_steps_attributes.splice(index, 1)
dialogs.confirm
resolve:
object: ->
title: _t('confirmation_required')
msg: _t('do_you_really_want_to_delete_this_step')
, -> # deletion confirmed
index = $scope.project.project_steps_attributes.indexOf(step)
if step.id?
step._destroy = true
else
$scope.project.project_steps_attributes.splice(index, 1)
# update the new total number of steps
$scope.totalSteps -= 1
# reindex the remaning steps
for s in $scope.project.project_steps_attributes
if s.step_nb > step.step_nb
s.step_nb -= 1
##
# Change the step_nb property of the given step to the new value provided. The step that was previously at this
# index will be assigned to the old position of the provided step.
# @param event {Object} see https://docs.angularjs.org/guide/expression#-event-
# @param step {Object} the project's step to reindex
# @param newIdx {number} the new index to assign to the step
##
$scope.changeStepIndex = (event, step, newIdx) ->
event.preventDefault() if event
for s in $scope.project.project_steps_attributes
if s.step_nb == newIdx
s.step_nb = step.step_nb
step.step_nb = newIdx
break
false
$scope.autoCompleteName = (nameLookup) ->
@ -286,8 +320,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', 'Diacritics'
, ($scope, $state, Project, Machine, Member, Component, Theme, Licence, $document, CSRF, Diacritics) ->
Application.Controllers.controller "NewProjectController", ["$scope", "$state", 'Project', 'Machine', 'Member', 'Component', 'Theme', 'Licence', '$document', 'CSRF', 'Diacritics', 'dialogs', 'allowedExtensions', '_t'
, ($scope, $state, Project, Machine, Member, Component, Theme, Licence, $document, CSRF, Diacritics, dialogs, allowedExtensions, _t) ->
CSRF.setMetaTags()
## API URL where the form will be posted
@ -304,7 +338,7 @@ Application.Controllers.controller "NewProjectController", ["$scope", "$state",
$scope.matchingMembers = []
## Using the ProjectsController
new ProjectsController($scope, $state, Project, Machine, Member, Component, Theme, Licence, $document, Diacritics)
new ProjectsController($scope, $state, Project, Machine, Member, Component, Theme, Licence, $document, Diacritics, dialogs, allowedExtensions, _t)
]
@ -312,8 +346,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', 'projectPromise', 'Diacritics'
, ($scope, $state, $stateParams, Project, Machine, Member, Component, Theme, Licence, $document, CSRF, projectPromise, Diacritics) ->
Application.Controllers.controller "EditProjectController", ["$scope", "$state", '$stateParams', 'Project', 'Machine', 'Member', 'Component', 'Theme', 'Licence', '$document', 'CSRF', 'projectPromise', 'Diacritics', 'dialogs', 'allowedExtensions', '_t'
, ($scope, $state, $stateParams, Project, Machine, Member, Component, Theme, Licence, $document, CSRF, projectPromise, Diacritics, dialogs, allowedExtensions, _t) ->
CSRF.setMetaTags()
## API URL where the form will be posted
@ -330,7 +364,7 @@ Application.Controllers.controller "EditProjectController", ["$scope", "$state",
name: u.full_name
## Using the ProjectsController
new ProjectsController($scope, $state, Project, Machine, Member, Component, Theme, Licence, $document, Diacritics)
new ProjectsController($scope, $state, Project, Machine, Member, Component, Theme, Licence, $document, Diacritics, dialogs, allowedExtensions, _t)
]
@ -394,6 +428,8 @@ Application.Controllers.controller "ShowProjectController", ["$scope", "$state",
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
@ -429,4 +465,19 @@ Application.Controllers.controller "ShowProjectController", ["$scope", "$state",
growl.error(_t('an_error_occured_while_sending_your_report'))
]
##
# Return the URL allowing to share the current project on the Facebook social network
##
$scope.shareOnFacebook = ->
'https://www.facebook.com/share.php?u='+$state.href('app.public.projects_show', {id: $scope.project.slug}, {absolute: true}).replace('#', '%23')
##
# Return the URL allowing to share the current project on the Twitter social network
##
$scope.shareOnTwitter = ->
'https://twitter.com/intent/tweet?url='+encodeURIComponent($state.href('app.public.projects_show', {id: $scope.project.slug}, {absolute: true}))+'&text='+encodeURIComponent($scope.project.name)
]

View File

@ -1,40 +1,99 @@
'use strict'
##
# Public listing of the trainings
##
Application.Controllers.controller "TrainingsController", ['$scope', '$state', 'trainingsPromise', ($scope, $state, trainingsPromise) ->
## List of trainings
$scope.trainings = trainingsPromise
##
# Callback for the 'reserve' button
##
$scope.reserveTraining = (training, event) ->
$state.go('app.logged.trainings_reserve', {id: training.slug})
##
# Callback for the 'show' button
##
$scope.showTraining = (training) ->
$state.go('app.public.training_show', {id: training.slug})
]
##
# Public view of a specific training
##
Application.Controllers.controller "ShowTrainingController", ['$scope', '$state', 'trainingPromise', 'growl', '_t', 'dialogs', ($scope, $state, trainingPromise, growl, _t, dialogs) ->
## Current training
$scope.training = trainingPromise
##
# Callback to delete the current training (admins only)
##
$scope.delete = (training) ->
# check the permissions
if $scope.currentUser.role isnt 'admin'
console.error _t('unauthorized_operation')
else
dialogs.confirm
resolve:
object: ->
title: _t('confirmation_required')
msg: _t('do_you_really_want_to_delete_this_training')
, -> # deletion confirmed
# delete the training then redirect to the trainings listing
training.$delete ->
$state.go('app.public.trainings_list')
, (error)->
growl.warning(_t('the_training_cant_be_deleted_because_it_is_already_reserved_by_some_users'))
##
# Callback for the 'reserve' button
##
$scope.reserveTraining = (training, event) ->
$state.go('app.logged.trainings_reserve', {id: training.id})
##
# Revert view to the full list of trainings ("<-" button)
##
$scope.cancel = (event) ->
$state.go('app.public.trainings_list')
]
##
# Controller used in the training reservation agenda page.
# This controller is very similar to the machine reservation controller with one major difference: here, ONLY ONE
# training can be reserved during the reservation process (the shopping cart may contains only one training and a subscription).
##
Application.Controllers.controller "ReserveTrainingController", ["$scope", "$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) ->
Application.Controllers.controller "ReserveTrainingController", ["$scope", "$state", '$stateParams', '$filter', '$compile', "$uibModal", 'Auth', 'dialogs', '$timeout', 'Price', 'Availability', 'Slot', 'Member', 'Setting', 'CustomAsset', 'availabilityTrainingsPromise', 'plansPromise', 'groupsPromise', 'growl', 'settingsPromise', 'trainingPromise', '_t', 'Wallet', 'helpers', 'uiCalendarConfig', 'CalendarConfig'
($scope, $state, $stateParams, $filter, $compile, $uibModal, Auth, dialogs, $timeout, Price, Availability, Slot, Member, Setting, CustomAsset, availabilityTrainingsPromise, plansPromise, groupsPromise, growl, settingsPromise, trainingPromise, _t, Wallet, helpers, uiCalendarConfig, CalendarConfig) ->
### 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'
FREE_SLOT_BORDER_COLOR = '<%= AvailabilityHelper::TRAINING_COLOR %>'
### 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' } ]
@ -71,36 +130,21 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
## 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'
## Selected training unless 'all' trainings are displayed
$scope.training = trainingPromise
allDaySlot: false
defaultView: 'agendaWeek'
editable: false
## Discount coupon to apply to the basket, if any
$scope.coupon =
applied: null
## fullCalendar (v2) configuration
$scope.calendarConfig = CalendarConfig
minTime: moment.duration(moment(settingsPromise.booking_window_start).format('HH:mm:ss'))
maxTime: moment.duration(moment(settingsPromise.booking_window_end).format('HH:mm:ss'))
eventClick: (event, jsEvent, view) ->
calendarEventClickCb(event, jsEvent, view)
eventAfterAllRender: (view)->
$scope.events = $scope.calendar.fullCalendar 'clientEvents'
$scope.events = uiCalendarConfig.calendars.calendar.fullCalendar 'clientEvents'
eventRender: (event, element, view) ->
eventRenderCb(event, element, view)
@ -112,8 +156,6 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
$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'))
@ -125,8 +167,8 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
if $scope.ctrl.member
Member.get {id: $scope.ctrl.member.id}, (member) ->
$scope.ctrl.member = member
Availability.trainings {member_id: $scope.ctrl.member.id}, (trainings) ->
$scope.calendar.fullCalendar 'removeEvents'
Availability.trainings {trainingId: $stateParams.id, member_id: $scope.ctrl.member.id}, (trainings) ->
uiCalendarConfig.calendars.calendar.fullCalendar 'removeEvents'
$scope.eventSources.push
events: trainings
textColor: 'black'
@ -162,8 +204,8 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
$scope.selectedPlan = null
$scope.trainingIsValid = false
$timeout ->
$scope.calendar.fullCalendar 'refetchEvents'
$scope.calendar.fullCalendar 'rerenderEvents'
uiCalendarConfig.calendars.fullCalendar 'refetchEvents'
uiCalendarConfig.calendars.fullCalendar 'rerenderEvents'
@ -176,11 +218,13 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
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)
Wallet.getWalletByUser {user_id: $scope.ctrl.member.id}, (wallet) ->
amountToPay = helpers.getAmountToPay($scope.amountTotal, wallet.amount)
if $scope.currentUser.role isnt 'admin' and amountToPay > 0
payByStripe(reservation)
else
if $scope.currentUser.role is 'admin' or amountToPay is 0
payOnSite(reservation)
else
# otherwise we alert, this error musn't occur when the current user is not admin
growl.error(_t('please_select_a_member_first'))
@ -235,7 +279,7 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
$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'
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
@ -248,7 +292,7 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
$scope.slotToPlace.backgroundColor = 'white'
$scope.slotToPlace.title = $scope.slotToPlace.training.name
$scope.slotToPlace = null
$scope.calendar.fullCalendar 'rerenderEvents'
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
@ -281,7 +325,7 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
$scope.slotToModify.can_modify = false
$scope.slotToModify.is_completed = false if $scope.slotToModify.is_completed
$scope.slotToModify = null
$scope.calendar.fullCalendar 'rerenderEvents'
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
, -> # failure
growl.error('an_error_occured_preventing_the_booked_slot_from_being_modified')
@ -297,7 +341,7 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
$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'
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
@ -307,7 +351,7 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
$scope.updatePrices = ->
if Object.keys($scope.ctrl.member).length > 0
r = mkReservation($scope.ctrl.member, $scope.selectedTraining, $scope.selectedPlan)
Price.compute {reservation: r}, (res) ->
Price.compute mkRequestParams(r, $scope.coupon.applied), (res) ->
$scope.amountTotal = res.price
else
$scope.amountTotal = null
@ -324,6 +368,11 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
Member.get id: $scope.currentUser.id, (member) ->
$scope.ctrl.member = member
# watch when a coupon is applied to re-compute the total price
$scope.$watch 'coupon.applied', (newValue, oldValue) ->
unless newValue == null and oldValue == null
$scope.updatePrices()
##
@ -351,6 +400,21 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
##
# Format the parameters expected by /api/prices/compute or /api/reservations and return the resulting object
# @param reservation {Object} as returned by mkReservation()
# @param coupon {Object} Coupon as returned from the API
# @return {{reservation:Object, coupon_code:string}}
##
mkRequestParams = (reservation, coupon) ->
params =
reservation: reservation
coupon_code: (coupon.code if coupon)
params
##
# 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 ...),
@ -362,6 +426,7 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
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
$scope.coupon.applied = null
if event != $scope.selectedTraining
$scope.selectedTraining = event
$scope.selectedTraining.offered = false
@ -378,7 +443,7 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
angular.forEach $scope.events, (e)->
if event.id != e.id
e.backgroundColor = 'white'
$scope.calendar.fullCalendar 'rerenderEvents'
uiCalendarConfig.calendars.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)
@ -388,7 +453,7 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
$scope.slotToPlace = event
event.backgroundColor = '#bbb'
event.title = event.training.name + ' - ' + _t('i_shift')
$scope.calendar.fullCalendar 'rerenderEvents'
uiCalendarConfig.calendars.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)
@ -406,7 +471,7 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
$scope.slotToModify = event
event.backgroundColor = '#eee'
event.title = event.training.name + ' - ' + _t('i_change')
$scope.calendar.fullCalendar 'rerenderEvents'
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
else if type == 'cancel'
dialogs.confirm
resolve:
@ -425,7 +490,7 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
$scope.canceledSlot.can_modify = false
$scope.canceledSlot.is_completed = false if event.is_completed
$scope.canceledSlot = null
$scope.calendar.fullCalendar 'rerenderEvents'
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
, -> # error while canceling
growl.error _t('cancellation_failed')
, -> # canceled
@ -440,11 +505,12 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
# @see http://fullcalendar.io/docs/event_rendering/eventRender/
##
eventRenderCb = (event, element, view)->
element.attr(
'uib-popover': event.training.description
'popover-trigger': 'mouseenter'
)
$compile(element)($scope)
# Comment these codes for show a popup of description, because we add feature page of training
#element.attr(
# 'uib-popover': $filter('humanize')($filter('simpleText')(event.training.description), 70)
# 'popover-trigger': 'mouseenter'
#)
#$compile(element)($scope)
@ -460,12 +526,20 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
reservation: ->
reservation
price: ->
Price.compute({reservation: reservation}).$promise
Price.compute(mkRequestParams(reservation, $scope.coupon.applied)).$promise
wallet: ->
Wallet.getWalletByUser({user_id: reservation.user_id}).$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) ->
coupon: ->
$scope.coupon.applied
controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'wallet', 'cgv', 'Auth', 'Reservation', '$locale', 'helpers', '$filter', 'coupon'
($scope, $uibModalInstance, $state, reservation, price, wallet, cgv, Auth, Reservation, $locale, helpers, $filter, coupon) ->
# user wallet amount
$scope.walletAmount = wallet.amount
# Price
$scope.amount = price.price
$scope.amount = helpers.getAmountToPay(price.price, wallet.amount)
# CGV
$scope.cgv = cgv.custom_asset
@ -473,6 +547,10 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
# Reservation
$scope.reservation = reservation
$scope.currencySymbol = $locale.NUMBER_FORMATS.CURRENCY_SYM
$scope.numberFilter = $filter('number')
##
# Callback to process the payment with Stripe, triggered on button click
##
@ -482,7 +560,7 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
else
$scope.attempting = true
$scope.reservation.card_token = response.id
Reservation.save reservation: $scope.reservation, (reservation) ->
Reservation.save mkRequestParams($scope.reservation, coupon), (reservation) ->
$uibModalInstance.close(reservation)
, (response)->
$scope.alerts = []
@ -511,26 +589,44 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
reservation: ->
reservation
price: ->
Price.compute({reservation: reservation}).$promise
controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'Auth', 'Reservation', ($scope, $uibModalInstance, $state, reservation, price, Auth, Reservation) ->
Price.compute(mkRequestParams(reservation, $scope.coupon.applied)).$promise
wallet: ->
Wallet.getWalletByUser({user_id: reservation.user_id}).$promise
coupon: ->
$scope.coupon.applied
controller: ['$scope', '$uibModalInstance', '$state', '$filter', 'reservation', 'price', 'wallet', 'Auth', 'Reservation', '$locale', 'helpers', 'coupon'
($scope, $uibModalInstance, $state, $filter, reservation, price, wallet, Auth, Reservation, $locale, helpers, coupon) ->
# user wallet amount
$scope.walletAmount = wallet.amount
# Price
$scope.amount = price.price
$scope.price = price.price
# price to pay
$scope.amount = helpers.getAmountToPay(price.price, wallet.amount)
# Reservation
$scope.reservation = reservation
$scope.currencySymbol = $locale.NUMBER_FORMATS.CURRENCY_SYM
$scope.numberFilter = $filter('number')
# Button label
if $scope.amount > 0
$scope.validButtonName = _t('confirm_(payment_on_site)')
$scope.validButtonName = _t('confirm_payment_of_html', {ROLE:$scope.currentUser.role, AMOUNT:$filter('currency')($scope.amount)}, "messageformat")
else
$scope.validButtonName = _t('confirm')
if price.price > 0 and $scope.walletAmount == 0
$scope.validButtonName = _t('confirm_payment_of_html', {ROLE:$scope.currentUser.role, AMOUNT:$filter('currency')(price.price)}, "messageformat")
else
$scope.validButtonName = _t('confirm')
##
# Callback to process the local payment, triggered on button click
##
$scope.ok = ->
$scope.attempting = true
Reservation.save reservation: $scope.reservation, (reservation) ->
Reservation.save mkRequestParams($scope.reservation, coupon), (reservation) ->
$uibModalInstance.close(reservation)
$scope.attempting = true
, (response)->
@ -553,7 +649,7 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
# 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) ->
Price.compute mkRequestParams(r, $scope.coupon.applied), (res) ->
$scope.selectedTrainingAmount = res.price
else
$scope.selectedTrainingAmount = null
@ -587,8 +683,8 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
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'
uiCalendarConfig.calendars.calendar.fullCalendar 'refetchEvents'
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'

View File

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

View File

@ -0,0 +1,48 @@
Application.Directives.directive 'coupon', [ 'Coupon', 'growl', '_t', (Coupon, growl, _t) ->
{
restrict: 'E'
scope:
show: '='
coupon: '='
userId: '@'
hasSelectSlot: '='
templateUrl: '<%= asset_path "shared/_coupon.html" %>'
link: ($scope, element, attributes) ->
# Whether code input is shown or not (ie. the link 'I have a coupon' is shown)
$scope.code =
input: false
# Available status are: 'pending', 'valid', 'invalid'
$scope.status = 'pending'
# Binding for the code inputed
$scope.couponCode = null
$scope.$watch 'hasSelectSlot', (newValue) ->
unless newValue
$scope.coupon = null
$scope.couponCode = null
$scope.code.input = false
##
# Callback to validate the code
##
$scope.validateCode = ->
if $scope.couponCode == ''
$scope.status = 'pending'
$scope.coupon = null
else
Coupon.validate {code: $scope.couponCode, user_id: $scope.userId}, (res) ->
$scope.status = 'valid'
$scope.coupon = res
growl.success(_t('the_coupon_has_been_applied_you_get_PERCENT_discount', {PERCENT: res.percent_off}))
, (err) ->
$scope.status = 'invalid'
$scope.coupon = null
growl.error(_t('unable_to_apply_the_coupon_because_'+err.data.status))
}
]

View File

@ -18,7 +18,7 @@ Application.Directives.directive 'url', [ ->
Application.Directives.directive 'endpoint', [ ->
ENDPOINT_REGEXP = /^\/([-._~:?#\[\]@!$&'()*+,;=%\w]+\/?)*$/
ENDPOINT_REGEXP = /^\/?([-._~:?#\[\]@!$&'()*+,;=%\w]+\/?)*$/
{
require: 'ngModel'
link: (scope, element, attributes, ctrl) ->

View File

@ -98,10 +98,25 @@ Application.Filters.filter "humanize", [ ->
Humanize.truncate(element, param, null)
]
##
# This filter will convert ASCII carriage-return character to the HTML break-line tag
##
Application.Filters.filter "breakFilter", [ ->
(text) ->
if text != undefined
text.replace(/\n/g, '<br />')
if text?
text.replace(/\n+/g, '<br />')
]
##
# This filter will take a HTML text as input and will return it without the html tags
##
Application.Filters.filter "simpleText", [ ->
(text) ->
if text?
text = text.replace(/<br\s*\/?>/g, '\n')
text.replace(/<\/?\w+[^>]*>/g, '')
else
""
]
Application.Filters.filter "toTrusted", [ "$sce", ($sce) ->
@ -217,4 +232,29 @@ Application.Filters.filter 'toIsoDate', [ ->
return date unless (date instanceof Date || moment.isMoment(date))
moment(date).format('YYYY-MM-DD')
]
]
Application.Filters.filter 'booleanFormat', [ '_t', (_t) ->
(boolean) ->
if boolean or boolean == 'true'
_t('yes')
else
_t('no')
]
Application.Filters.filter 'booleanFormat', [ '_t', (_t) ->
(boolean) ->
if (typeof boolean == 'boolean' and boolean) or (typeof boolean == 'string' and boolean == 'true')
_t('yes')
else
_t('no')
]
Application.Filters.filter 'maxCount', [ '_t', (_t) ->
(max) ->
if typeof max == 'undefined' or max == null or (typeof max == 'number' and max == 0)
_t('unlimited')
else
max
]

View File

@ -197,6 +197,22 @@ angular.module('application.router', ['ui.router']).
translations: [ 'Translations', (Translations) ->
Translations.query('app.logged.dashboard.invoices').$promise
]
.state 'app.logged.dashboard.wallet',
url: '/wallet'
views:
'main@':
templateUrl: '<%= asset_path "dashboard/wallet.html" %>'
controller: 'WalletController'
resolve:
walletPromise: ['Wallet', 'currentUser', (Wallet, currentUser)->
Wallet.getWalletByUser(user_id: currentUser.id).$promise
]
transactionsPromise: ['Wallet', 'walletPromise', (Wallet, walletPromise)->
Wallet.transactions(id: walletPromise.id).$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query(['app.shared.wallet']).$promise
]
# members
@ -232,7 +248,7 @@ angular.module('application.router', ['ui.router']).
url: '/projects?q&page&theme_id&component_id&machine_id&from&whole_network'
views:
'main@':
templateUrl: '<%= asset_path "projects/index.html" %>'
templateUrl: '<%= asset_path "projects/index.html.erb" %>'
controller: 'ProjectsController'
resolve:
themesPromise: ['Theme', (Theme)->
@ -254,6 +270,9 @@ angular.module('application.router', ['ui.router']).
templateUrl: '<%= asset_path "projects/new.html" %>'
controller: 'NewProjectController'
resolve:
allowedExtensions: ['Project', (Project)->
Project.allowedExtensions().$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query(['app.logged.projects_new', 'app.shared.project']).$promise
]
@ -280,6 +299,9 @@ angular.module('application.router', ['ui.router']).
projectPromise: ['$stateParams', 'Project', ($stateParams, Project)->
Project.get(id: $stateParams.id).$promise
]
allowedExtensions: ['Project', (Project)->
Project.allowedExtensions().$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query(['app.logged.projects_edit', 'app.shared.project']).$promise
]
@ -290,7 +312,7 @@ angular.module('application.router', ['ui.router']).
url: '/machines'
views:
'main@':
templateUrl: '<%= asset_path "machines/index.html" %>'
templateUrl: '<%= asset_path "machines/index.html.erb" %>'
controller: 'MachinesController'
resolve:
machinesPromise: ['Machine', (Machine)->
@ -335,6 +357,9 @@ angular.module('application.router', ['ui.router']).
groupsPromise: ['Group', (Group)->
Group.query().$promise
]
machinePromise: ['Machine', '$stateParams', (Machine, $stateParams)->
Machine.get(id: $stateParams.id).$promise
]
settingsPromise: ['Setting', (Setting)->
Setting.query(names: "['machine_explications_alert',
'booking_window_start',
@ -347,7 +372,8 @@ angular.module('application.router', ['ui.router']).
]
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
'app.shared.stripe', 'app.shared.valid_reservation_modal', 'app.shared.confirm_modify_slot_modal',
'app.shared.wallet', 'app.shared.coupon_input']).$promise
]
.state 'app.admin.machines_edit',
url: '/machines/:id/edit'
@ -363,8 +389,34 @@ angular.module('application.router', ['ui.router']).
Translations.query(['app.admin.machines_edit', 'app.shared.machine']).$promise
]
# trainings
.state 'app.public.trainings_list',
url: '/trainings'
views:
'main@':
templateUrl: '<%= asset_path "trainings/index.html.erb" %>'
controller: 'TrainingsController'
resolve:
trainingsPromise: ['Training', (Training)->
Training.query({ public_page: true }).$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query(['app.public.trainings_list']).$promise
]
.state 'app.public.training_show',
url: '/trainings/:id'
views:
'main@':
templateUrl: '<%= asset_path "trainings/show.html" %>'
controller: 'ShowTrainingController'
resolve:
trainingPromise: ['Training', '$stateParams', (Training, $stateParams)->
Training.get({id: $stateParams.id}).$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query(['app.public.training_show']).$promise
]
.state 'app.logged.trainings_reserve',
url: '/trainings/reserve'
url: '/trainings/:id/reserve'
views:
'main@':
templateUrl: '<%= asset_path "trainings/reserve.html" %>'
@ -379,8 +431,11 @@ angular.module('application.router', ['ui.router']).
groupsPromise: ['Group', (Group)->
Group.query().$promise
]
availabilityTrainingsPromise: ['Availability', (Availability)->
Availability.trainings().$promise
availabilityTrainingsPromise: ['Availability', '$stateParams', (Availability, $stateParams)->
Availability.trainings({trainingId: $stateParams.id}).$promise
]
trainingPromise: ['Training', '$stateParams', (Training, $stateParams)->
Training.get({id: $stateParams.id}).$promise unless $stateParams.id == 'all'
]
settingsPromise: ['Setting', (Setting)->
Setting.query(names: "['booking_window_start',
@ -395,14 +450,15 @@ angular.module('application.router', ['ui.router']).
]
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
'app.shared.stripe', 'app.shared.valid_reservation_modal', 'app.shared.confirm_modify_slot_modal',
'app.shared.wallet', 'app.shared.coupon_input']).$promise
]
# notifications
.state 'app.logged.notifications',
url: '/notifications'
views:
'main@':
templateUrl: '<%= asset_path "notifications/index.html" %>'
templateUrl: '<%= asset_path "notifications/index.html.erb" %>'
controller: 'NotificationsController'
resolve:
translations: [ 'Translations', (Translations) ->
@ -415,7 +471,7 @@ angular.module('application.router', ['ui.router']).
abstract: Fablab.withoutPlans
views:
'main@':
templateUrl: '<%= asset_path "plans/index.html" %>'
templateUrl: '<%= asset_path "plans/index.html.erb" %>'
controller: 'PlansIndexController'
resolve:
subscriptionExplicationsPromise: ['Setting', (Setting)->
@ -428,7 +484,8 @@ angular.module('application.router', ['ui.router']).
Group.query().$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query(['app.public.plans', 'app.shared.member_select', 'app.shared.stripe']).$promise
Translations.query(['app.public.plans', 'app.shared.member_select', 'app.shared.stripe', 'app.shared.wallet',
'app.shared.coupon_input']).$promise
]
# events
@ -436,9 +493,18 @@ angular.module('application.router', ['ui.router']).
url: '/events'
views:
'main@':
templateUrl: '<%= asset_path "events/index.html" %>'
templateUrl: '<%= asset_path "events/index.html.erb" %>'
controller: 'EventsController'
resolve:
categoriesPromise: ['Category', (Category) ->
Category.query().$promise
]
themesPromise: ['EventTheme', (EventTheme) ->
EventTheme.query().$promise
]
ageRangesPromise: ['AgeRange', (AgeRange) ->
AgeRange.query().$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query('app.public.events_list').$promise
]
@ -452,11 +518,39 @@ angular.module('application.router', ['ui.router']).
eventPromise: ['Event', '$stateParams', (Event, $stateParams)->
Event.get(id: $stateParams.id).$promise
]
reducedAmountAlert: ['Setting', (Setting)->
Setting.get(name: 'event_reduced_amount_alert').$promise
priceCategoriesPromise: ['PriceCategory', (PriceCategory) ->
PriceCategory.query().$promise
]
settingsPromise: ['Setting', (Setting)->
Setting.query(names: "['booking_move_enable', 'booking_move_delay']").$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query(['app.public.events_show', 'app.shared.member_select', 'app.shared.stripe', 'app.shared.valid_reservation_modal']).$promise
Translations.query(['app.public.events_show', 'app.shared.member_select', 'app.shared.stripe',
'app.shared.valid_reservation_modal', 'app.shared.wallet', 'app.shared.coupon_input']).$promise
]
# global calendar (trainings, machines and events)
.state 'app.public.calendar',
url: '/calendar'
views:
'main@':
templateUrl: '<%= asset_path "calendar/calendar.html" %>'
controller: 'CalendarController'
resolve:
bookingWindowStart: ['Setting', (Setting)->
Setting.get(name: 'booking_window_start').$promise
]
bookingWindowEnd: ['Setting', (Setting)->
Setting.get(name: 'booking_window_end').$promise
]
trainingsPromise: ['Training', (Training)->
Training.query().$promise
]
machinesPromise: ['Machine', (Machine)->
Machine.query().$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query(['app.public.calendar']).$promise
]
# --- namespace /admin/... ---
@ -468,9 +562,6 @@ angular.module('application.router', ['ui.router']).
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
]
@ -489,7 +580,7 @@ angular.module('application.router', ['ui.router']).
url: '/admin/project_elements'
views:
'main@':
templateUrl: '<%= asset_path "admin/project_elements/index.html" %>'
templateUrl: '<%= asset_path "admin/project_elements/index.html.erb" %>'
controller: 'ProjectElementsController'
resolve:
componentsPromise: ['Component', (Component)->
@ -510,8 +601,8 @@ angular.module('application.router', ['ui.router']).
url: '/admin/trainings'
views:
'main@':
templateUrl: '<%= asset_path "admin/trainings/index.html" %>'
controller: 'TrainingsController'
templateUrl: '<%= asset_path "admin/trainings/index.html.erb" %>'
controller: 'TrainingsAdminController'
resolve:
trainingsPromise: ['Training', (Training)->
Training.query().$promise
@ -520,20 +611,60 @@ angular.module('application.router', ['ui.router']).
Machine.query().$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query('app.admin.trainings').$promise
Translations.query(['app.admin.trainings', 'app.shared.trainings']).$promise
]
.state 'app.admin.trainings_new',
url: '/admin/trainings/new'
views:
'main@':
templateUrl: '<%= asset_path "admin/trainings/new.html" %>'
controller: 'NewTrainingController'
resolve:
machinesPromise: ['Machine', (Machine)->
Machine.query().$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query(['app.admin.trainings_new', 'app.shared.trainings']).$promise
]
.state 'app.admin.trainings_edit',
url: '/admin/trainings/:id/edit'
views:
'main@':
templateUrl: '<%= asset_path "admin/trainings/edit.html" %>'
controller: 'EditTrainingController'
resolve:
trainingPromise: ['Training', '$stateParams', (Training, $stateParams)->
Training.get(id: $stateParams.id).$promise
]
machinesPromise: ['Machine', (Machine)->
Machine.query().$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query('app.shared.trainings').$promise
]
# events
.state 'app.admin.events',
url: '/admin/events'
views:
'main@':
templateUrl: '<%= asset_path "admin/events/index.html" %>'
templateUrl: '<%= asset_path "admin/events/index.html.erb" %>'
controller: 'AdminEventsController'
resolve:
eventsPromise: ['Event', (Event)->
Event.query(page: 1).$promise
]
categoriesPromise: ['Category', (Category) ->
Category.query().$promise
]
themesPromise: ['EventTheme', (EventTheme) ->
EventTheme.query().$promise
]
ageRangesPromise: ['AgeRange', (AgeRange) ->
AgeRange.query().$promise
]
priceCategoriesPromise: ['PriceCategory', (PriceCategory) ->
PriceCategory.query().$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query('app.admin.events').$promise
]
@ -544,6 +675,18 @@ angular.module('application.router', ['ui.router']).
templateUrl: '<%= asset_path "events/new.html" %>'
controller: 'NewEventController'
resolve:
categoriesPromise: ['Category', (Category) ->
Category.query().$promise
]
themesPromise: ['EventTheme', (EventTheme) ->
EventTheme.query().$promise
]
ageRangesPromise: ['AgeRange', (AgeRange) ->
AgeRange.query().$promise
]
priceCategoriesPromise: ['PriceCategory', (PriceCategory) ->
PriceCategory.query().$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query(['app.admin.events_new', 'app.shared.event']).$promise
]
@ -557,6 +700,18 @@ angular.module('application.router', ['ui.router']).
eventPromise: ['Event', '$stateParams', (Event, $stateParams)->
Event.get(id: $stateParams.id).$promise
]
categoriesPromise: ['Category', (Category) ->
Category.query().$promise
]
themesPromise: ['EventTheme', (EventTheme) ->
EventTheme.query().$promise
]
ageRangesPromise: ['AgeRange', (AgeRange) ->
AgeRange.query().$promise
]
priceCategoriesPromise: ['PriceCategory', (PriceCategory) ->
PriceCategory.query().$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query(['app.admin.events_edit', 'app.shared.event']).$promise
]
@ -582,7 +737,7 @@ angular.module('application.router', ['ui.router']).
url: '/admin/pricing'
views:
'main@':
templateUrl: '<%= asset_path "admin/pricing/index.html" %>'
templateUrl: '<%= asset_path "admin/pricing/index.html.erb" %>'
controller: 'EditPricingController'
resolve:
plans: ['Plan', (Plan) ->
@ -598,7 +753,7 @@ angular.module('application.router', ['ui.router']).
TrainingsPricing.query().$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query('app.admin.pricing').$promise
Translations.query(['app.admin.pricing', 'app.shared.member_select', 'app.shared.coupon']).$promise
]
trainingsPromise: ['Training', (Training) ->
Training.query().$promise
@ -612,6 +767,9 @@ angular.module('application.router', ['ui.router']).
trainingCreditsPromise: ['Credit', (Credit) ->
Credit.query({creditable_type: 'Training'}).$promise
]
couponsPromise: ['Coupon', (Coupon) ->
Coupon.query().$promise
]
# plans
.state 'app.admin.plans',
@ -656,6 +814,30 @@ angular.module('application.router', ['ui.router']).
Translations.query(['app.admin.plans.edit', 'app.shared.plan']).$promise
]
# coupons
.state 'app.admin.coupons_new',
url: '/admin/coupons/new'
views:
'main@':
templateUrl: '<%= asset_path "admin/coupons/new.html" %>'
controller: 'NewCouponController'
resolve:
translations: [ 'Translations', (Translations) ->
Translations.query(['app.admin.coupons_new', 'app.shared.coupon']).$promise
]
.state 'app.admin.coupons_edit',
url: '/admin/coupons/:id/edit'
views:
'main@':
templateUrl: '<%= asset_path "admin/coupons/edit.html" %>'
controller: 'EditCouponController'
resolve:
couponPromise: ['Coupon', '$stateParams', (Coupon, $stateParams) ->
Coupon.get({id: $stateParams.id}).$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query(['app.admin.coupons_edit', 'app.shared.coupon']).$promise
]
@ -664,7 +846,7 @@ angular.module('application.router', ['ui.router']).
url: '/admin/invoices'
views:
'main@':
templateUrl: '<%= asset_path "admin/invoices/index.html" %>'
templateUrl: '<%= asset_path "admin/invoices/index.html.erb" %>'
controller: 'InvoicesController'
resolve:
settings: ['Setting', (Setting)->
@ -695,16 +877,16 @@ angular.module('application.router', ['ui.router']).
url: '/admin/members'
views:
'main@':
templateUrl: '<%= asset_path "admin/members/index.html" %>'
templateUrl: '<%= asset_path "admin/members/index.html.erb" %>'
controller: 'AdminMembersController'
'groups@app.admin.members':
templateUrl: '<%= asset_path "admin/groups/index.html" %>'
templateUrl: '<%= asset_path "admin/groups/index.html.erb" %>'
controller: 'GroupsController'
'tags@app.admin.members':
templateUrl: '<%= asset_path "admin/tags/index.html" %>'
templateUrl: '<%= asset_path "admin/tags/index.html.erb" %>'
controller: 'TagsController'
'authentification@app.admin.members':
templateUrl: '<%= asset_path "admin/authentications/index.html" %>'
templateUrl: '<%= asset_path "admin/authentications/index.html.erb" %>'
controller: 'AuthentificationController'
resolve:
membersPromise: ['Member', (Member)->
@ -745,11 +927,20 @@ angular.module('application.router', ['ui.router']).
memberPromise: ['Member', '$stateParams', (Member, $stateParams)->
Member.get(id: $stateParams.id).$promise
]
activeProviderPromise: ['AuthProvider', (AuthProvider) ->
AuthProvider.active().$promise
]
walletPromise: ['Wallet', '$stateParams', (Wallet, $stateParams)->
Wallet.getWalletByUser(user_id: $stateParams.id).$promise
]
transactionsPromise: ['Wallet', 'walletPromise', (Wallet, walletPromise)->
Wallet.transactions(id: walletPromise.id).$promise
]
tagsPromise: ['Tag', (Tag)->
Tag.query().$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query(['app.admin.members_edit', 'app.shared.user', 'app.shared.user_admin']).$promise
Translations.query(['app.admin.members_edit', 'app.shared.user', 'app.shared.user_admin', 'app.shared.wallet']).$promise
]
.state 'app.admin.admins_new',
url: '/admin/admins/new'
@ -804,7 +995,7 @@ angular.module('application.router', ['ui.router']).
url: '/admin/statistics'
views:
'main@':
templateUrl: '<%= asset_path "admin/statistics/index.html" %>'
templateUrl: '<%= asset_path "admin/statistics/index.html.erb" %>'
controller: 'StatisticsController'
resolve:
membersPromise: ['Member', (Member) ->
@ -832,7 +1023,7 @@ angular.module('application.router', ['ui.router']).
url: '/admin/settings'
views:
'main@':
templateUrl: '<%= asset_path "admin/settings/index.html" %>'
templateUrl: '<%= asset_path "admin/settings/index.html.erb" %>'
controller: 'SettingsController'
resolve:
settingsPromise: ['Setting', (Setting)->
@ -846,7 +1037,6 @@ angular.module('application.router', ['ui.router']).
'training_explications_alert',
'training_information_message',
'subscription_explications_alert',
'event_reduced_amount_alert',
'booking_window_start',
'booking_window_end',
'booking_move_enable',
@ -856,7 +1046,9 @@ angular.module('application.router', ['ui.router']).
'main_color',
'secondary_color',
'fablab_name',
'name_genre'
'name_genre',
'reminder_enable',
'reminder_delay'
]").$promise
]
cguFile: ['CustomAsset', (CustomAsset) ->
@ -880,7 +1072,7 @@ angular.module('application.router', ['ui.router']).
url: '/open_api_clients'
views:
'main@':
templateUrl: '<%= asset_path "admin/open_api_clients/index.html" %>'
templateUrl: '<%= asset_path "admin/open_api_clients/index.html.erb" %>'
controller: 'OpenAPIClientsController'
resolve:
clientsPromise: ['OpenAPIClient', (OpenAPIClient)->

View File

@ -0,0 +1,8 @@
'use strict'
Application.Services.factory 'AgeRange', ["$resource", ($resource)->
$resource "/api/age_ranges/:id",
{id: "@id"},
update:
method: 'PUT'
]

View File

@ -14,7 +14,8 @@ Application.Services.factory 'Availability', ["$resource", ($resource)->
isArray: true
trainings:
method: 'GET'
url: '/api/availabilities/trainings'
url: '/api/availabilities/trainings/:trainingId'
params: {trainingId: "@trainingId"}
isArray: true
update:
method: 'PUT'

View File

@ -0,0 +1,38 @@
'use strict'
Application.Services.factory 'CalendarConfig', [->
(options = {}) ->
# The calendar is divided in slots of 1 hour
BASE_SLOT = '01:00:00'
# The calendar will be initialized positioned under 9:00 AM
DEFAULT_CALENDAR_POSITION = '09:00:00'
defaultOptions =
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
Object.assign({}, defaultOptions, options)
]

View File

@ -0,0 +1,14 @@
'use strict'
Application.Services.factory 'Coupon', ["$resource", ($resource)->
$resource "/api/coupons/:id",
{id: "@id"},
update:
method: 'PUT'
validate:
method: 'POST'
url: '/api/coupons/validate'
send:
method: 'POST'
url: '/api/coupons/send'
]

View File

@ -0,0 +1,8 @@
'use strict'
Application.Services.factory 'EventTheme', ["$resource", ($resource)->
$resource "/api/event_themes/:id",
{id: "@id"},
update:
method: 'PUT'
]

View File

@ -0,0 +1,6 @@
'use strict'
Application.Services.factory 'Export', ["$http", ($http)->
status: (query) ->
$http.post('/api/exports/status', query)
]

View File

@ -0,0 +1,6 @@
'use strict'
Application.Services.factory 'helpers', [()->
getAmountToPay: (price, walletAmount)->
if walletAmount > price then 0 else price - walletAmount
]

View File

@ -0,0 +1,8 @@
'use strict'
Application.Services.factory 'PriceCategory', ["$resource", ($resource)->
$resource "/api/price_categories/:id",
{id: "@id"},
update:
method: 'PUT'
]

View File

@ -11,4 +11,8 @@ Application.Services.factory 'Project', ["$resource", ($resource)->
method: 'GET'
url: '/api/projects/search'
isArray: false
allowedExtensions:
method: 'GET'
url: '/api/projects/allowed_extensions'
isArray: true
]

View File

@ -5,4 +5,7 @@ Application.Services.factory 'Training', ["$resource", ($resource)->
{id: "@id"},
update:
method: 'PUT'
availabilities:
method: 'GET'
url: "/api/trainings/:id/availabilities"
]

View File

@ -0,0 +1,5 @@
'use strict'
Application.Services.factory 'Version', ["$resource", ($resource)->
$resource "/api/version"
]

View File

@ -0,0 +1,18 @@
'use strict'
Application.Services.factory 'Wallet', ["$resource", ($resource)->
$resource "/api/wallet",
{},
getWalletByUser:
method: 'GET'
url: '/api/wallet/by_user/:user_id'
isArray: false
transactions:
method: 'GET'
url: '/api/wallet/:id/transactions'
isArray: true
credit:
method: 'PUT'
url: '/api/wallet/:id/credit'
isArray: false
]

View File

@ -62,11 +62,9 @@ p {
line-height: rem-calc(24);
&.intro, .intro {
font-family: $font-proxima-condensed;
font-size: rem-calc(16);
line-height: rem-calc(24);
margin: 1.038em 0 30px 0;
font-weight: 600;
}
}

View File

@ -2,7 +2,7 @@
.btn-default:hover, .btn-default:focus, .btn-default:active, .btn-default.active, .open > .btn-default.dropdown-toggle {
background-color: #f2f2f2;
}
.btn{
@ -24,6 +24,30 @@
}
}
.btn-facebook {
border: 1px solid #8b9dc3;
background-color: #3b5998;
color: white;
&:hover {
border: 1px solid #7d8fb4;
background-color: #394c89;
color: white;
}
}
.btn-twitter {
border: 1px solid #ccd6dd;
background-color: #55acee;
color: white;
&:hover {
border: 1px solid #bdc7ce;
background-color: #539fdf;
color: white;
}
}
.btn-block {
padding-left: 12px;

View File

@ -6,6 +6,7 @@
.bg-token { background-color: rgba(230, 208, 137, 0.49); }
.bg-machine { background-color: $beige; }
.bg-formation { background-color: $violet; }
.bg-event { background-color: $japonica; }
.bg-atelier { background-color: $blue; }
.bg-stage { background-color: $violet; }
.bg-success { background-color: $brand-success; }
@ -35,3 +36,7 @@
.text-blue { color: $blue; }
.text-muted { color: $text-muted; }
.text-danger, .red { color: $red !important; }
.text-purple { color: $violet !important; }
.text-japonica { color: $japonica !important; }
.text-beige { color: $beige !important; }
.text-green, .green { color: #79C84A !important; }

View File

@ -154,8 +154,11 @@
}
.article-thumbnail {
// max-height: 400px;
overflow: hidden;
img {
height: 400px;
}
}
}
@ -417,22 +420,23 @@
.event {
transition: all 0.07s linear;
overflow: hidden;
}
.event:hover {
//background-color: #cb1117;
color: white;
// background-color: #cb1117;
color: white;
}
.event:hover * {
color: #eee;
border-color: #eee;
color: #eee !important;
border-color: #eee;
}
.box-h-m {
height: 150px;
max-height: 150px;
height: 175px;
max-height: 175px;
}
.half-w {
@ -446,25 +450,31 @@ border-color: #d0d0d0;
padding: 10px;
}
.crop-130 {
height: 130px;
width: 130px;
max-width: 130px;
max-height: 130px;
.crop-155 {
height: 155px;
width: 155px;
max-width: 155px;
max-height: 155px;
overflow: hidden;
vertical-align: bottom;
}
.crop-130 img {
height: 130px;
.crop-155 img {
height: 155px;
width: auto;
}
@media only screen and (max-width: 1280px) and (min-width: 770px) {
.crop-130 {
@media only screen and (max-width: 1375px) and (min-width: 770px) {
.crop-155 {
height: 90px;
width: 90px;
margin-top: 25px;
margin-top: 35px;
}
}
@media only screen and (max-width: 1375px) and (min-width: 1125px) {
.half-w {
width: 60%;
}
}
@ -489,6 +499,11 @@ padding: 10px;
@media only screen and (max-width: 768px) {
display: none;
}
.app-version {
margin-right: 10px;
color: #999;
}
}
.disabling-overlay {
@ -503,4 +518,78 @@ padding: 10px;
z-index:10;
border-radius: 3px;
}
}
.wallet-amount-container {
padding: 20px 0;
border-top: 2px dotted $border-color;
border-bottom: 2px dotted $border-color;
margin-bottom: 20px;
text-align: center;
.wallet-amount {
font-size: rem-calc(40);
font-weight: 700;
font-style: italic;
color: #616161;
span {
font-weight: 500;
font-size: .7em;
}
&.cr-green {
color: $green;
}
}
}
.amountGroup {
input {
display: inline-block;
width: 100px;
margin-left: 5px;
padding-right: 6px;
font-weight: bold;
color: $green;
font-size: 1.2em;
line-height: 0;
}
.afterAmount {
margin-left: -35px;
font-weight: bold;
color: $green;
font-size: 1.2em;
line-height: 0;
}
}
.checkbox-group {
display: flex;
justify-content: flex-start;
input[type=checkbox] {
font-size: 16px;
width: 2em;
}
}
.link-icon {
color: #1c94c4;
i { margin: 0 5px 0 10px; }
span {
border-bottom: 1px dashed #00b3ee;
text-decoration: none;
cursor: pointer;
}
}
.description-hover {
span {
display: inline-block;
border-bottom: 1px dashed #00b3ee;
cursor: help;
}
}

View File

@ -88,7 +88,7 @@
color: black;
}
.heading-title {
overflow: hidden;
//overflow: hidden;
height: 94px;
h1 {
margin: 0 0 0 15px;
@ -604,4 +604,14 @@ body.container{
display: inherit;
text-align: center;
height: 50px;
}
}
.calendar-filter {
h3 {
line-height: 2.1rem !important;
}
}
.calendar-filter-aside {
padding: 20px;
}

View File

@ -91,7 +91,7 @@
cursor: pointer;
z-index: 9999;
text-align: right;
.training-reserve &, .machine-reserve & { display: none; }
.training-reserve &, .machine-reserve &, .public-calendar & { display: none; }
}
.fc-v-event.fc-end {
@ -102,6 +102,15 @@
display: none !important;
}
.calendar-filter {
.badge {
cursor: pointer;
&.inactive {
opacity: 0.2;
}
}
}

View File

@ -102,6 +102,7 @@ p, .widget p {
.text-italic { font-style: italic; }
.text-center { text-align: center; }
.text-right { text-align: right; }
.text-active, .active > .text, .active > .auto .text{display: none !important;}
.active > .text-active, .active > .auto .text-active{display: inline-block !important;}
@ -125,7 +126,9 @@ p, .widget p {
.pull-in{margin-left: -15px;margin-right: -15px;}
.pull-out{margin:-10px -15px;}
.width-35 { width: 35% !important; }
.width-70 { width: 70%; }
.width-90 { width: 90%; }
.b{border: 1px solid rgba(0, 0, 0, 0.05)}
.b-a{border: 1px solid $border-color}
@ -171,6 +174,7 @@ p, .widget p {
}
.r-n { border-radius: 0 0 0 0; }
.p-xs { padding: 5px;}
.p-lg { padding: 30px; }
.p-l { padding: 16px; }
@ -342,6 +346,7 @@ p, .widget p {
@media screen and (min-width: $screen-lg-min) {
.b-r-lg {border-right: 1px solid $border-color; }
.hide-b-r-lg { border: none !important; }
}

View File

@ -13,6 +13,7 @@
*= require bootstrap-switch/dist/css/bootstrap3/bootstrap-switch.min
*= require summernote/dist/summernote
*= require jquery-minicolors/jquery.minicolors.css
*= require angular-aside/dist/css/angular-aside
*/
@import "app.functions";

View File

@ -43,6 +43,7 @@ $blue: $brand-info;
$green: $brand-success;
$beige: #e4cd78;
$violet: #bd7ae9;
$japonica: #dd7e6b;
$border-color: #dddddd;
$header-bg: $bg-gray;

View File

@ -0,0 +1,65 @@
<div class="modal-header">
<h3 class="modal-title"><span translate>{{ 'data_mapping' }}</span> : {{field.local_field}}</h3>
</div>
<div class="modal-body m-lg">
<div>
<span translate>{{ 'expected_data_type' }}</span> : {{datatype}}
</div>
<form name="mappingForm" class="m-t-md">
<ng-switch on="datatype">
<!-- BOOLEAN -->
<div ng-switch-when="boolean">
<label for="add_mapping" translate>{{ 'mappings' }}</label>
<ul class="list-unstyled">
<li class="m-t-sm m-l">
<input type="text"
name="true_value"
id="true_value"
class="form-control inline width-35 m-r "
ng-model="transformation.rules.false_value">
<i class="fa fa-arrows-h"></i>
<label for="true_value" class="m-l">true</label>
</li>
<li class="m-t-sm m-l">
<input type="text"
name="false_value"
id="false_value"
class="form-control inline width-35 m-r "
ng-model="transformation.rules.true_value">
<i class="fa fa-arrows-h"></i>
<label for="false_value" class="m-l">false</label>
</li>
</ul>
</div>
<!-- DATE -->
<div ng-switch-when="date">
<label for="date_format" translate>{{ 'input_format' }}</label>
<select name="date_format"
id="date_format"
class="form-control"
ng-model="transformation.rules.format"
ng-options="format.value as format.label for format in formats.date">
</select>
</div>
<!-- INTEGER -->
<div ng-switch-when="integer">
<label for="add_mapping" translate>{{ 'mappings' }}</label>
<button class="btn btn-default pull-right" ng-click="addIntegerMapping()"><i class="fa fa-plus"></i></button>
<ul class="list-unstyled">
<li ng-repeat="map in transformation.rules.mapping" class="m-t-sm m-l">
<input type="text" class="form-control inline width-35 m-r " ng-model="map.from">
<i class="fa fa-arrows-h"></i>
<input type="number" class="form-control inline width-35 m-l" ng-model="map.to">
</li>
</ul>
</div>
</ng-switch>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-primary" ng-click="ok()" ng-disabled="!mappingForm.$valid" ng-if="datatype != 'string' && datatype != 'text'" translate>{{ 'confirm' }}</button>
<button class="btn btn-warning" ng-click="cancel()" translate>{{ 'cancel' }}</button>
</div>

View File

@ -21,6 +21,9 @@
<td>{{m.api_data_type}}</td>
<td>{{m.api_field}}</td>
<td>
<button class="btn btn-info" ng-click="defineDataMapping(m)">
<i class="fa fa-info-circle"></i>
</button>
<button class="btn btn-danger" ng-click="m._destroy = true">
<i class="fa fa-trash-o"></i>
</button>
@ -38,7 +41,7 @@
<td ng-class="{'has-error': mappingForm['auth_mapping[local_field]'].$dirty && mappingForm['auth_mapping[local_field]'].$invalid}">
<select class="form-control"
name="auth_mapping[local_field]"
ng-options="field for field in mappingFields[newMapping.local_model]"
ng-options="field[0] as field[0] for field in mappingFields[newMapping.local_model]"
ng-model="newMapping.local_field"
required>
</select>

View File

@ -14,6 +14,8 @@
<tr>
<th style="width:15%" translate>{{ 'name' }}</th>
<th style="width:15%" translate>{{ 'strategy_name' }}</th>
<th style="width:15%" translate>{{ 'type' }}</th>
<th style="width:15%" translate>{{ 'state' }}</th>
@ -24,6 +26,7 @@
<tbody>
<tr ng-repeat="provider in providers | filter:searchFilter">
<td>{{ provider.name }}</td>
<td>{{ provider.strategy_name }}</td>
<td>{{ getType(provider.providable_type) }}</td>
<td>{{ getState(provider.status) }}</td>
<td>

View File

@ -0,0 +1,103 @@
<div class="form-group" ng-class="{'has-error': couponForm['coupon[name]'].$dirty && couponForm['coupon[name]'].$invalid}">
<label for="coupon[name]">{{ 'name' | translate }} *</label>
<input type="text" id="coupon[name]"
name="coupon[name]"
class="form-control"
ng-model="coupon.name"
required="required"/>
<span class="help-block error" ng-show="couponForm['coupon[name]'].$dirty && couponForm['coupon[name]'].$error.required" translate>{{ 'name_is_required' }}</span>
</div>
<div class="form-group" ng-class="{'has-error': couponForm['coupon[code]'].$dirty && couponForm['coupon[code]'].$invalid}">
<label for="coupon[code]">{{ 'code' | translate }} *</label>
<input type="text" id="coupon[code]"
name="coupon[code]"
class="form-control"
ng-model="coupon.code"
ng-pattern="/^[A-Z0-9\-]+$/"
ng-disabled="mode == 'EDIT'"
required="required"/>
<span class="help-block error" ng-show="couponForm['coupon[code]'].$dirty && couponForm['coupon[code]'].$error.required" translate>{{ 'code_is_required' }}</span>
<span class="help-block error" ng-show="couponForm['coupon[code]'].$dirty && couponForm['coupon[code]'].$error.pattern" translate>{{ 'code_must_be_composed_of_capital_letters_digits_and_or_dashes' }}</span>
</div>
<div class="form-group" ng-class="{'has-error': couponForm['coupon[percent_off]'].$dirty && couponForm['coupon[percent_off]'].$invalid}">
<label for="coupon[percent_off]">{{ 'percent_off' | translate }} *</label>
<div class="input-group">
<input type="number" id="coupon[percent_off]"
name="coupon[percent_off]"
class="form-control"
ng-model="coupon.percent_off"
min="0"
max="100"
ng-disabled="mode == 'EDIT'"
required="required"/>
<span class="input-group-addon"><i class="fa fa-percent"></i></span>
</div>
<span class="help-block error" ng-show="couponForm['coupon[percent_off]'].$dirty && couponForm['coupon[percent_off]'].$error.required" translate>{{ 'percent_off_is_required' }}</span>
<span class="help-block error" ng-show="couponForm['coupon[percent_off]'].$dirty && (couponForm['coupon[percent_off]'].$error.min || couponForm['coupon[percent_off]'].$error.max)" translate>{{ 'percentage_must_be_between_0_and_100' }}</span>
</div>
<div class="form-group" ng-class="{'has-error': couponForm['coupon[validity_per_user]'].$dirty && couponForm['coupon[validity_per_user]'].$invalid}">
<label for="coupon[validity_per_user]">{{ 'validity_per_user' | translate }} *</label>
<select id="coupon[validity_per_user]"
name="coupon[validity_per_user]"
class="form-control"
ng-model="coupon.validity_per_user"
required="required"
ng-disabled="mode == 'EDIT'"
ng-options="( validity | translate ) for validity in validities">
</select>
<span class="help-block error" ng-show="couponForm['coupon[validity_per_user]'].$dirty && couponForm['coupon[validity_per_user]'].$error.required" translate>{{ 'validity_per_user_is_required' }}</span>
</div>
<div class="form-group">
<label for="coupon[valid_until]" translate>{{ 'valid_until' }}</label>
<div class="input-group">
<input type="text" id="coupon[valid_until]"
name="coupon[valid_until]"
class="form-control"
ng-model="coupon.valid_until"
uib-datepicker-popup="{{datePicker.format}}"
datepicker-options="datePicker.options"
is-open="datePicker.opened"
min-date="datePicker.minDate"
ng-disabled="mode == 'EDIT'"
ng-click="toggleDatePicker($event)"/>
<span class="input-group-btn">
<button type="button" class="btn btn-default" ng-click="toggleDatePicker($event)" ng-disabled="mode == 'EDIT'"><i class="fa fa-calendar"></i></button>
</span>
</div>
<span class="help-block text-info text-xs">
<i class="fa fa-lightbulb-o"></i> {{ 'leave_empty_for_no_limit' | translate }}
</span>
</div>
<div class="form-group" ng-class="{'has-error': couponForm['coupon[max_usages]'].$dirty && couponForm['coupon[max_usages]'].$invalid}">
<label for="coupon[max_usages]">{{ 'max_usages' | translate }}</label>
<input type="number" id="coupon[max_usages]"
name="coupon[max_usages]"
class="form-control"
ng-model="coupon.max_usages"
ng-disabled="mode == 'EDIT'"
min="0"/>
<span class="help-block error" ng-show="couponForm['coupon[max_usages]'].$dirty && couponForm['coupon[max_usages]'].$error.min" translate>{{ 'max_usages_must_be_equal_or_greater_than_0' }}</span>
<span class="help-block text-info text-xs">
<i class="fa fa-lightbulb-o"></i> {{ 'leave_empty_for_no_limit' | translate }}
</span>
</div>
<div class="form-group">
<label for="coupon[active]" translate>{{ 'enabled' }}</label>
<input bs-switch
ng-model="coupon.active"
id="coupon[active]"
type="checkbox"
class="form-control"
switch-on-text="{{ 'yes' | translate }}"
switch-off-text="{{ 'no' | translate }}"
switch-animate="true" />
<input type="hidden" name="coupon[active]" value="{{coupon.active}}"/>
</div>

View File

@ -0,0 +1,40 @@
<section class="heading b-b">
<div class="row no-gutter">
<div class="col-xs-2 col-sm-2 col-md-1">
<section class="heading-btn">
<a href="#" ng-click="backPrevLocation($event)"><i class="fa fa-long-arrow-left "></i></a>
</section>
</div>
<div class="col-xs-7 col-sm-7 col-md-8 b-l">
<section class="heading-title">
<h1>{{ 'coupon' | translate }} : {{ coupon.name }}</h1>
</section>
</div>
<div class="col-xs-3 col-md-3">
<section class="heading-actions wrapper">
<a class="btn btn-lg btn-block btn-default m-t-xs" ui-sref="app.admin.pricing" translate>{{ 'cancel' }}</a>
</section>
</div>
</div>
</section>
<div class="row no-gutter">
<div class=" col-sm-12 col-md-9 b-r nopadding">
<div id="couponForm">
<form name="couponForm" novalidate="novalidate" class="col-lg-7 col-lg-offset-2 m-t-lg form-group">
<ng-include src="'<%= asset_path 'admin/coupons/_form.html' %>'"></ng-include>
<div class="panel-footer no-padder">
<input type="button" value="{{ 'confirm_changes' | translate }}" class="r-b btn-valid btn btn-warning btn-block p-lg btn-lg text-u-c" ng-disabled="couponForm.$invalid" ng-click="updateCoupon()"/>
</div>
</form>
</div>
</div>
</div>

View File

@ -0,0 +1,33 @@
<section class="heading b-b">
<div class="row no-gutter">
<div class="col-xs-2 col-sm-2 col-md-1">
<section class="heading-btn">
<a href="#" ng-click="backPrevLocation($event)"><i class="fa fa-long-arrow-left "></i></a>
</section>
</div>
<div class="col-xs-10 col-sm-10 col-md-8 b-l">
<section class="heading-title">
<h1 translate>{{ 'add_a_coupon' }}</h1>
</section>
</div>
</div>
</section>
<div class="row no-gutter">
<div class=" col-sm-12 col-md-9 b-r nopadding">
<div id="couponForm">
<form name="couponForm" novalidate="novalidate" class="col-lg-10 col-lg-offset-2 m-t-lg form-group">
<ng-include src="'<%= asset_path 'admin/coupons/_form.html' %>'"></ng-include>
<div class="panel-footer no-padder">
<input type="button" value="{{ 'save' | translate }}" class="r-b btn-valid btn btn-warning btn-block p-lg btn-lg text-u-c" ng-disabled="couponForm.$invalid" ng-click="saveCoupon()"/>
</div>
</form>
</div>
</div>
</div>

View File

@ -0,0 +1,120 @@
<div class="m-t">
<h3 translate>{{ 'categories' }}</h3>
<p translate>{{ 'at_least_one_category_is_required' }}</p>
<button type="button" class="btn btn-warning m-b m-t" ng-click="addElement('category')" translate>{{ 'add_a_category' }}</button>
<table class="table">
<thead>
<tr>
<th style="width:80%" translate>{{ 'name' }}</th>
<th style="width:20%"></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="category in categories">
<td>
<span editable-text="category.name" e-cols="100" e-name="name" e-form="rowform" e-required>
{{ category.name }}
</span>
</td>
<td>
<!-- form -->
<form editable-form name="rowform" onbeforesave="saveElement('category', $data, category.id)" ng-show="rowform.$visible" class="form-buttons form-inline" shown="inserted.category == category">
<button type="submit" ng-disabled="rowform.$waiting" class="btn btn-warning">
<i class="fa fa-check"></i>
</button>
<button type="button" ng-disabled="rowform.$waiting" ng-click="cancelElement('category', rowform, $index)" class="btn btn-default">
<i class="fa fa-times"></i>
</button>
</form>
<div class="buttons" ng-show="!rowform.$visible">
<button class="btn btn-default" ng-click="rowform.$show()">
<i class="fa fa-edit"></i> <span class="hidden-xs hidden-sm" translate>{{ 'edit' }}</span>
</button>
<button class="btn btn-danger" ng-click="removeElement('category', $index)">
<i class="fa fa-trash-o"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
<h3 translate>{{ 'themes' }}</h3>
<button type="button" class="btn btn-warning m-b m-t" ng-click="addElement('theme')" translate>{{ 'add_a_theme' }}</button>
<table class="table">
<thead>
<tr>
<th style="width:80%" translate>{{ 'name' }}</th>
<th style="width:20%"></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="theme in themes">
<td>
<span editable-text="theme.name" e-cols="100" e-name="name" e-form="rowform" e-required>
{{ theme.name }}
</span>
</td>
<td>
<!-- form -->
<form editable-form name="rowform" onbeforesave="saveElement('theme', $data, theme.id)" ng-show="rowform.$visible" class="form-buttons form-inline" shown="inserted.theme == theme">
<button type="submit" ng-disabled="rowform.$waiting" class="btn btn-warning">
<i class="fa fa-check"></i>
</button>
<button type="button" ng-disabled="rowform.$waiting" ng-click="cancelElement('theme', rowform, $index)" class="btn btn-default">
<i class="fa fa-times"></i>
</button>
</form>
<div class="buttons" ng-show="!rowform.$visible">
<button class="btn btn-default" ng-click="rowform.$show()">
<i class="fa fa-edit"></i> <span class="hidden-xs hidden-sm" translate>{{ 'edit' }}</span>
</button>
<button class="btn btn-danger" ng-click="removeElement('theme', $index)">
<i class="fa fa-trash-o"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
<h3 translate>{{ 'age_ranges' }}</h3>
<button type="button" class="btn btn-warning m-b m-t" ng-click="addElement('age_range')" translate>{{ 'add_a_range' }}</button>
<table class="table">
<thead>
<tr>
<th style="width:80%" translate>{{ 'name' }}</th>
<th style="width:20%"></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="range in ageRanges">
<td>
<span editable-text="range.name" e-cols="100" e-name="name" e-form="rowform" e-required>
{{ range.name }}
</span>
</td>
<td>
<!-- form -->
<form editable-form name="rowform" onbeforesave="saveElement('age_range', $data, range.id)" ng-show="rowform.$visible" class="form-buttons form-inline" shown="inserted.age_range == range">
<button type="submit" ng-disabled="rowform.$waiting" class="btn btn-warning">
<i class="fa fa-check"></i>
</button>
<button type="button" ng-disabled="rowform.$waiting" ng-click="cancelElement('age_range', rowform, $index)" class="btn btn-default">
<i class="fa fa-times"></i>
</button>
</form>
<div class="buttons" ng-show="!rowform.$visible">
<button class="btn btn-default" ng-click="rowform.$show()">
<i class="fa fa-edit"></i> <span class="hidden-xs hidden-sm" translate>{{ 'edit' }}</span>
</button>
<button class="btn btn-danger" ng-click="removeElement('age_range', $index)">
<i class="fa fa-trash-o"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>

View File

@ -7,7 +7,7 @@
</div>
<div class="col-xs-10 col-sm-10 col-md-8 b-l b-r-md">
<section class="heading-title">
<h1 translate>{{ 'fablab_courses_and_workshops' }}</h1>
<h1 translate>{{ 'fablab_events' }}</h1>
</section>
</div>
@ -21,57 +21,19 @@
<section class="m-lg">
<div class="row">
<div class="col-md-12">
<uib-tabset justified="true">
<uib-tab heading="{{ 'events_monitoring' | translate }}">
<ng-include src="'<%= asset_path 'admin/events/monitoring.html' %>'"></ng-include>
</uib-tab>
<div class="col-md-6 m-b">
<select ng-model="selectedTimezone" class="form-control">
<option value="" translate>{{ 'all_events' }}</option>
<option value="passed" translate>{{ 'passed_events' }}</option>
<option value="future" translate>{{ 'events_to_come' }}</option>
</select>
</div>
<uib-tab heading="{{ 'manage_filters' | translate }}">
<ng-include src="'<%= asset_path 'admin/events/filters.html' %>'"></ng-include>
</uib-tab>
<table class="table">
<thead>
<tr>
<th style="width:30%" translate>{{ 'title' }}</th>
<th style="width:30%" translate>{{ 'dates' }}</th>
<th style="width:40%"></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="event in filtered = (events | eventsReservationsFilter:selectedTimezone)">
<td>
<a ui-sref="app.public.events_show({id: event.id})">{{ event.title }} </a>
</td>
<td>
<span> {{ 'from_DATE' | translate:{DATE:(event.start_date | amDateFormat:'LL')} }} <span class="text-sm font-thin" translate>{{ 'to_date' }}</span> {{event.end_date | amDateFormat:'LL'}}</span>
<br/>
<span ng-if="event.all_day == 'true'" translate>{{ 'all_day' }}</span>
<span ng-if="event.all_day == 'false'">
{{ 'from_TIME' | translate:{TIME:(event.start_date | amDateFormat:'LT')} }}
<span class="text-sm font-thin" translate>{{ 'to_time' }}</span>
{{event.end_date | amDateFormat:'LT'}}
</span>
</td>
<td>
<div class="buttons">
<button class="btn btn-default" ui-sref="app.admin.event_reservations({id: event.id})">
<i class="fa fa-bookmark"></i> {{ 'view_reservations' | translate }}
</button>
<button class="btn btn-default" ui-sref="app.admin.events_edit({id: event.id})">
<i class="fa fa-edit"></i> {{ 'edit' | translate }}
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="row">
<div class="col-lg-12 text-center">
<a class="btn btn-warning" ng-click="loadMoreEvents()" ng-if="paginateActive" translate>{{ 'load_the_next_courses_and_workshops' }}</a>
<uib-tab heading="{{ 'manage_prices_categories' | translate }}">
<ng-include src="'<%= asset_path 'admin/events/prices.html' %>'"></ng-include>
</uib-tab>
</uib-tabset>
</div>
</div>
</section>

View File

@ -0,0 +1,50 @@
<div class="col-md-6 m-b m-t">
<select ng-model="selectedTimezone" class="form-control">
<option value="" translate>{{ 'all_events' }}</option>
<option value="passed" translate>{{ 'passed_events' }}</option>
<option value="future" translate>{{ 'events_to_come' }}</option>
</select>
</div>
<table class="table">
<thead>
<tr>
<th style="width:30%" translate>{{ 'title' }}</th>
<th style="width:30%" translate>{{ 'dates' }}</th>
<th style="width:40%"></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="event in filtered = (events | eventsReservationsFilter:selectedTimezone)">
<td>
<a ui-sref="app.public.events_show({id: event.id})">{{ event.title }} </a>
</td>
<td>
<span> {{ 'from_DATE' | translate:{DATE:(event.start_date | amDateFormat:'LL')} }} <span class="text-sm font-thin" translate>{{ 'to_date' }}</span> {{event.end_date | amDateFormat:'LL'}}</span>
<br/>
<span ng-if="event.all_day == 'true'" translate>{{ 'all_day' }}</span>
<span ng-if="event.all_day == 'false'">
{{ 'from_TIME' | translate:{TIME:(event.start_date | amDateFormat:'LT')} }}
<span class="text-sm font-thin" translate>{{ 'to_time' }}</span>
{{event.end_date | amDateFormat:'LT'}}
</span>
</td>
<td>
<div class="buttons">
<button class="btn btn-default" ui-sref="app.admin.event_reservations({id: event.id})">
<i class="fa fa-bookmark"></i> {{ 'view_reservations' | translate }}
</button>
<button class="btn btn-default" ui-sref="app.admin.events_edit({id: event.id})">
<i class="fa fa-edit"></i> {{ 'edit' | translate }}
</button>
</div>
</td>
</tr>
</tbody>
</table>
<div class="row">
<div class="col-lg-12 text-center">
<a class="btn btn-warning" ng-click="loadMoreEvents()" ng-if="paginateActive" translate>{{ 'load_the_next_events' }}</a>
</div>
</div>

View File

@ -0,0 +1,40 @@
<div class="modal-header">
<img ng-src="{{logoBlack.custom_asset_file_attributes.attachment_url}}" alt="{{logo.custom_asset_file_attributes.attachment}}" class="modal-logo"/>
<h1 translate>{{ 'price_category' }}</h1>
</div>
<div class="modal-body">
<form role="form" name="priceCategoryForm" class="form-horizontal" novalidate autocomplete="off" ng-keydown="priceCategoryForm.$valid && $event.which == 13 && ok()">
<div class="form-group" ng-class="{'has-error': priceCategoryForm.name.$dirty && priceCategoryForm.name.$invalid}">
<div class="col-sm-12">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-tag"></i></span>
<input type="text"
name="name"
ng-model="category.name"
class="form-control"
placeholder="{{ 'category_name' | translate }}"
required />
</div>
<span class="help-block" ng-show="priceCategoryForm.name.$dirty && priceCategoryForm.name.$error.required" translate>{{ 'category_name_is_required' }}</span>
</div>
</div>
<div class="form-group" ng-class="{'has-error': priceCategoryForm.conditions.$dirty && priceCategoryForm.conditions.$invalid}">
<div class="col-sm-12">
<textarea ng-model="category.conditions"
rows="10"
class="form-control"
id="conditions"
placeholder="{{ 'enter_here_the_conditions_under_which_this_price_is_applicable' | translate }}"
name="conditions"
required>
</textarea>
<span class="help-block" ng-show="priceCategoryForm.conditions.$dirty && priceCategoryForm.conditions.$error.required" translate>{{ 'conditions_are_required' }}</span>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-info" ng-click="ok()" ng-disabled="priceCategoryForm.$invalid" translate>{{ 'confirm' }}</button>
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'cancel' }}</button>
</div>

View File

@ -0,0 +1,31 @@
<div class="m-t">
<h3 translate>{{ 'prices_categories' }}</h3>
<button type="button" class="btn btn-warning m-b m-t" ng-click="newPriceCategory()" translate>{{ 'add_a_price_category' }}</button>
<table class="table">
<thead>
<tr>
<th style="width:40%" translate>{{ 'name' }}</th>
<th style="width:40%" translate>{{ 'usages_count' }}</th>
<th style="width:20%"></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="category in priceCategories">
<td>{{ category.name }}</td>
<td>{{ category.events }}</td>
<td>
<div class="buttons">
<button class="btn btn-default" ng-click="editPriceCategory(category.id, $index)">
<i class="fa fa-edit"></i> <span class="hidden-xs hidden-sm" translate>{{ 'edit' }}</span>
</button>
<button class="btn btn-danger" ng-click="removePriceCategory(category.id, $index)">
<i class="fa fa-trash-o"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>

View File

@ -32,7 +32,10 @@
<a ui-sref="app.logged.members_show({id: reservation.user_id})">{{ reservation.user_full_name }} </a>
</td>
<td>{{ reservation.created_at | amDateFormat:'LL LTS' }}</td>
<td><span ng-if="reservation.nb_reserve_places > 0">{{ 'full_price_' | translate }} {{reservation.nb_reserve_places}}<br/></span><span ng-if="reservation.nb_reserve_reduced_places > 0">{{ 'reduced_rate_' | translate }} {{reservation.nb_reserve_reduced_places}}</span></td>
<td>
<span ng-if="reservation.nb_reserve_places > 0">{{ 'full_price_' | translate }} {{reservation.nb_reserve_places}}<br/></span>
<span ng-repeat="ticket in reservation.tickets">{{ticket.event_price_category.price_category.name}} : {{ticket.booked}}</span>
</td>
<td>
<div class="buttons">
<button class="btn btn-default" ui-sref="app.public.events_show({id: event.id})">

View File

@ -210,6 +210,7 @@
<li ng-click="invoice.reference.help = 'addDay.html'">{{ 'day' | translate }}</li>
<li ng-click="invoice.reference.help = 'addInvoiceNumber.html'">{{ '#_of_invoice' | translate }}</li>
<li ng-click="invoice.reference.help = 'addOnlineInfo.html'">{{ 'online_sales' | translate }}</li>
<%# <li ng-click="invoice.reference.help = 'addWalletInfo.html'">{{ 'wallet' | translate }}</li> %>
<li ng-click="invoice.reference.help = 'addRefundInfo.html'">{{ 'refund' | translate }}</li>
</ul>
</div>
@ -279,6 +280,12 @@
</table>
</script>
<script type="text/ng-template" id="addWalletInfo.html">
<table class="invoice-element-legend">
<tr><td><strong>W[texte]</strong></td><td>{{ 'add_a_notice_regarding_the_wallet_only_if_the_invoice_is_concerned' | translate }} <mark translate>{{ 'this_will_never_be_added_when_a_refund_notice_is_present' }}</mark> {{ '(eg_W[/PM]_will_add_/PM_to_the_invoices_settled_with_wallet)' | translate }}</td></tr>
</table>
</script>
<script type="text/ng-template" id="addRefundInfo.html">
<table class="invoice-element-legend">
<tr><td><strong>R[texte]</strong></td><td>{{ 'add_a_notice_regarding_refunds_only_if_the_invoice_is_concerned' | translate }}<mark translate>{{ 'this_will_never_be_added_when_an_online_sales_notice_is_present' }}</mark> {{ '(eg_R[/A]_will_add_/A_to_the_refund_invoices)' | translate }}</td></tr>
@ -383,4 +390,4 @@
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'cancel' }}</button>
</div>
</div>
</script>
</script>

View File

@ -0,0 +1,39 @@
<div class="col-md-5 m-t-lg">
<div class="form-group">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-filter"></i></span>
<input type="text" ng-model="searchFilter" class="form-control" placeholder="{{ 'search_for_an_administrator' | translate }}">
</div>
</div>
</div>
<div class="col-md-12">
<button type="button" class="btn btn-warning m-t m-b" ui-sref="app.admin.admins_new" translate>{{ 'add_a_new_administrator' }}</button>
<table class="table">
<thead>
<tr>
<th style="width:15%"><a href="" ng-click="setOrderAdmin('profile_attributes.last_name')">{{ 'surname' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderAdmin =='profile_attributes.last_name', 'fa fa-sort-alpha-desc': orderAdmin =='-profile_attributes.last_name', 'fa fa-arrows-v': orderAdmin }"></i></a></th>
<th style="width:15%"><a href="" ng-click="setOrderAdmin('profile_attributes.first_name')">{{ 'first_name' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderAdmin =='profile_attributes.first_name', 'fa fa-sort-alpha-desc': orderAdmin =='-profile_attributes.first_name', 'fa fa-arrows-v': orderAdmin }"></i></a></th>
<th style="width:15%"><a href="" ng-click="setOrderAdmin('email')">{{ 'email' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderAdmin =='email', 'fa fa-sort-alpha-desc': orderAdmin =='-email', 'fa fa-arrows-v': orderAdmin }"></i></a></th>
<th style="width:10%"><a href="" ng-click="setOrderAdmin('profile_attributes.phone')">{{ 'phone' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': orderAdmin =='profile_attributes.phone', 'fa fa-sort-numeric-desc': orderAdmin =='-profile_attributes.phone', 'fa fa-arrows-v': orderAdmin }"></i></a></th>
<th style="width:10%"></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="admin in admins | filter:searchFilter | orderBy: orderAdmin">
<td class="text-c">{{ admin.profile_attributes.last_name }}</td>
<td class="text-c">{{ admin.profile_attributes.first_name }}</td>
<td>{{ admin.email }}</td>
<td>{{ admin.profile_attributes.phone }}</td>
<td>
<button class="btn btn-danger" ng-if="admin.id != currentUser.id" ng-click="destroyAdmin(admins, admin)">
<i class="fa fa-trash-o"></i>
</button>
</td>
</tr>
</tbody>
</table>
</div>

View File

@ -9,7 +9,8 @@
</div>
<div class="col-md-8 b-l b-r">
<section class="heading-title">
<h1>{{ 'user' | translate }} {{ user.name }}</h1>
<h1 class="inline">{{ 'user' | translate }} {{ user.name }}</h1>
<span class="label label-danger text-white" ng-show="user.need_completion" translate>{{ 'incomplete_profile' }}</span>
</section>
</div>
@ -33,7 +34,14 @@
<uib-tabset justified="true" class="m-t">
<uib-tab heading="Profil utilisateur">
<uib-tab heading="{{ 'user_profile' | translate }}">
<section class="panel panel-danger m-lg" ng-show="user.need_completion && activeProvider.providable_type !== 'DatabaseProvider'">
<div class="panel-body m-r" translate>
{{ 'warning_incomplete_user_profile_probably_imported_from_sso' }}
</div>
</section>
<form role="form" name="userForm" class="form-horizontal col-md-8" novalidate action="{{ actionUrl }}" ng-upload="submited(content)" upload-options-enable-rails-csrf="true">
<section class="panel panel-default bg-light m-lg">
@ -138,11 +146,11 @@
</div>
</uib-tab>
<uib-tab heading="{{ 'courses_and_workshops' | translate }}">
<uib-tab heading="{{ 'events' | translate }}">
<div class="col-md-6">
<div class="widget panel b-a m m-t-lg">
<div class="panel-heading b-b">
<h4 class="text-u-c"><i class="fa fa-tag m-r-xs"></i> {{ 'next_courses_and_workshops' | translate }}</h4>
<h4 class="text-u-c"><i class="fa fa-tag m-r-xs"></i> {{ 'next_events' | translate }}</h4>
</div>
<div class="widget-content bg-light wrapper r-b">
<ul class="list-unstyled" ng-if="user.events_reservations.length > 0">
@ -152,20 +160,20 @@
<br/>
<span translate translate-values="{ NUMBER: r.nb_reserve_places}" translate-interpolation="messageformat">{{ 'NUMBER_full_price_tickets_reserved' }}</span>
</span>
<span ng-if="r.nb_reserve_reduced_places > 0">
<span ng-repeat="ticket in r.tickets">
<br/>
<span translate translate-values="{ NUMBER: r.nb_reserve_reduced_places}" translate-interpolation="messageformat">{{ 'NUMBER_reduced_rate_tickets_reserved' }}</span>
<span translate translate-values="{ NUMBER: ticket.booked, NAME: ticket.price_category.name }" translate-interpolation="messageformat">{{ 'NUMBER_NAME_tickets_reserved' }}</span>
</span>
</li>
</ul>
<div ng-if="(user.events_reservations | eventsReservationsFilter:'future').length == 0" translate>{{ 'no_upcomning_courses_or_workshops'}}</div>
<div ng-if="(user.events_reservations | eventsReservationsFilter:'future').length == 0" translate>{{ 'no_upcoming_events' }}</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="widget panel b-a m m-t-lg">
<div class="panel-heading b-b">
<h4 class="text-u-c"><i class="fa fa-tag m-r-xs"></i> {{ 'passed_courses_and_workshops' | translate }}</h4>
<h4 class="text-u-c"><i class="fa fa-tag m-r-xs"></i> {{ 'passed_events' | translate }}</h4>
</div>
<div class="widget-content bg-light auto wrapper r-b">
<ul class="list-unstyled" ng-if="user.events_reservations.length > 0">
@ -173,7 +181,7 @@
<span class="font-sbold">{{r.reservable.title}}</span> - <span class="label label-info text-white wrapper-sm">{{ r.start_at | amDateFormat:'LLL' }} - {{ r.end_at | amDateFormat:'LT' }}</span>
</li>
</ul>
<div ng-if="(user.events_reservations | eventsReservationsFilter:'passed').length == 0" translate>{{ 'no_passed_courses_or_workshop' }}</div>
<div ng-if="(user.events_reservations | eventsReservationsFilter:'passed').length == 0" translate>{{ 'no_passed_events' }}</div>
</div>
</div>
</div>
@ -215,6 +223,22 @@
</div>
</uib-tab>
<uib-tab heading="{{ 'wallet' | translate }}">
<div class="col-md-12 m m-t-lg">
<ng-include src="'<%= asset_path 'wallet/show.html' %>'"></ng-include>
<div class="clearfix"></div>
<div class="col-sm-4 text-center">
<button type="button" class="btn btn-warning m-t m-b" ng-click="createWalletCreditModal(user, wallet)" translate>{{ 'to_credit' }}</button>
</div>
</div>
<div class="col-md-12 m m-t-lg">
<ng-include src="'<%= asset_path 'wallet/transactions.html' %>'"></ng-include>
</div>
</uib-tab>
</uib-tabset>
</div>

View File

@ -16,130 +16,29 @@
<section class="m-lg">
<div class="row">
<div class="col-md-12">
<uib-tabset justified="true">
<div class="col-md-12">
<uib-tabset justified="true">
<uib-tab heading="{{ 'members' | translate }}">
<div class="col-md-5 m-t-lg">
<div class="form-group">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-filter"></i></span>
<input type="text" ng-model="member.searchText" class="form-control" placeholder="{{ 'search_for_an_user' | translate }}" ng-change="updateTextSearch()">
</div>
</div>
</div>
<div class="col-md-12">
<button type="button" class="btn btn-warning m-t m-b" ui-sref="app.admin.members_new" translate>{{ 'add_a_new_member' }}</button>
<div class="pull-right">
<a class="btn btn-default" ng-href="api/members/export_members.xls" target="_blank">
<i class="fa fa-file-excel-o"></i> {{ 'members' | translate }}
</a>
<a class="btn btn-default" ng-href="api/members/export_subscriptions.xls" target="_blank" ng-if="!fablabWithoutPlans">
<i class="fa fa-file-excel-o"></i> {{ 'subscriptions' | translate }}
</a>
<a class="btn btn-default" ng-href="api/members/export_reservations.xls" target="_blank">
<i class="fa fa-file-excel-o"></i> {{ 'reservations' | translate }}
</a>
</div>
<uib-tab heading="{{ 'members' | translate }}">
<ng-include src="'<%= asset_path 'admin/members/members.html' %>'"></ng-include>
</uib-tab>
<uib-tab heading="{{ 'administrators' | translate }}">
<ng-include src="'<%= asset_path 'admin/members/administrators.html' %>'"></ng-include>
</uib-tab>
<table class="table">
<thead>
<tr>
<th style="width:15%"><a href="" ng-click="setOrderMember('last_name')">{{ 'surname' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': member.order=='last_name', 'fa fa-sort-alpha-desc': member.order=='-last_name', 'fa fa-arrows-v': member.order }"></i></a></th>
<uib-tab heading="{{ 'groups' | translate }}">
<div ui-view="groups"></div>
</uib-tab>
<th style="width:15%"><a href="" ng-click="setOrderMember('first_name')">{{ 'first_name' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': member.order=='first_name', 'fa fa-sort-alpha-desc': member.order=='-first_name', 'fa fa-arrows-v': member.order }"></i></a></th>
<uib-tab heading="{{ 'tags' | translate }}">
<div ui-view="tags"></div>
</uib-tab>
<th style="width:15%"><a href="" ng-click="setOrderMember('email')">{{ 'email' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': member.order=='email', 'fa fa-sort-alpha-desc': member.order=='-email', 'fa fa-arrows-v': member.order }"></i></a></th>
<th style="width:10%"><a href="" ng-click="setOrderMember('phone')">{{ 'phone' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': member.order=='phone', 'fa fa-sort-numeric-desc': member.order=='-phone', 'fa fa-arrows-v': member.order }"></i></a></th>
<th style="width:20%"><a href="" ng-click="setOrderMember('group')">{{ 'user_type' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': member.order=='group', 'fa fa-sort-alpha-desc': member.order=='-group', 'fa fa-arrows-v': member.order }"></i></a></th>
<th style="width:15%"><a href="" ng-click="setOrderMember('plan')">{{ 'subscription' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': member.order=='plan', 'fa fa-sort-alpha-desc': member.order=='-plan', 'fa fa-arrows-v': member.order }"></i></a></th>
<th style="width:10%"></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="m in members">
<td class="text-c">{{ m.profile.last_name }}</td>
<td class="text-c">{{ m.profile.first_name }}</td>
<td>{{ m.email }}</td>
<td>{{ m.profile.phone }}</td>
<td class="text-u-c text-sm">{{ m.group.name }}</td>
<td>{{ m.subscribed_plan | humanReadablePlanName }}</td>
<td>
<div class="buttons">
<button class="btn btn-default" ui-sref="app.admin.members_edit({id: m.id})">
<i class="fa fa-edit"></i> {{ 'edit' | translate }}
</button>
</div>
</td>
</tr>
</tbody>
</table>
<div class="text-center">
<button class="btn btn-warning" ng-click="showNextMembers()" ng-hide="member.noMore"><i class="fa fa-search-plus" aria-hidden="true"></i> {{ 'display_more_users' | translate }}</button>
</div>
</div>
</uib-tab>
<uib-tab heading="{{ 'administrators' | translate }}">
<div class="col-md-5 m-t-lg">
<div class="form-group">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-filter"></i></span>
<input type="text" ng-model="searchFilter" class="form-control" placeholder="{{ 'search_for_an_administrator' | translate }}">
</div>
</div>
</div>
<div class="col-md-12">
<button type="button" class="btn btn-warning m-t m-b" ui-sref="app.admin.admins_new" translate>{{ 'add_a_new_administrator' }}</button>
<table class="table">
<thead>
<tr>
<th style="width:15%"><a href="" ng-click="setOrderAdmin('profile_attributes.last_name')">{{ 'surname' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderAdmin =='profile_attributes.last_name', 'fa fa-sort-alpha-desc': orderAdmin =='-profile_attributes.last_name', 'fa fa-arrows-v': orderAdmin }"></i></a></th>
<th style="width:15%"><a href="" ng-click="setOrderAdmin('profile_attributes.first_name')">{{ 'first_name' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderAdmin =='profile_attributes.first_name', 'fa fa-sort-alpha-desc': orderAdmin =='-profile_attributes.first_name', 'fa fa-arrows-v': orderAdmin }"></i></a></th>
<th style="width:15%"><a href="" ng-click="setOrderAdmin('email')">{{ 'email' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderAdmin =='email', 'fa fa-sort-alpha-desc': orderAdmin =='-email', 'fa fa-arrows-v': orderAdmin }"></i></a></th>
<th style="width:10%"><a href="" ng-click="setOrderAdmin('profile_attributes.phone')">{{ 'phone' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': orderAdmin =='profile_attributes.phone', 'fa fa-sort-numeric-desc': orderAdmin =='-profile_attributes.phone', 'fa fa-arrows-v': orderAdmin }"></i></a></th>
<th style="width:10%"></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="admin in admins | filter:searchFilter | orderBy: orderAdmin">
<td class="text-c">{{ admin.profile_attributes.last_name }}</td>
<td class="text-c">{{ admin.profile_attributes.first_name }}</td>
<td>{{ admin.email }}</td>
<td>{{ admin.profile_attributes.phone }}</td>
<td>
<button class="btn btn-danger" ng-if="admin.id != currentUser.id" ng-click="destroyAdmin(admins, admin)">
<i class="fa fa-trash-o"></i>
</button>
</td>
</tr>
</tbody>
</table>
</div>
</uib-tab>
<uib-tab heading="{{ 'groups' | translate }}">
<div ui-view="groups"></div>
</uib-tab>
<uib-tab heading="{{ 'tags' | translate }}">
<div ui-view="tags"></div>
</uib-tab>
<uib-tab heading="{{ 'authentication' | translate }}">
<div ui-view="authentification"></div>
</uib-tab>
</uib-tabset>
</div>
<uib-tab heading="{{ 'authentication' | translate }}">
<div ui-view="authentification"></div>
</uib-tab>
</uib-tabset>
</div>
</div>
</section>

View File

@ -0,0 +1,59 @@
<div class="col-md-5 m-t-lg">
<div class="form-group">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-filter"></i></span>
<input type="text" ng-model="member.searchText" class="form-control" placeholder="{{ 'search_for_an_user' | translate }}" ng-change="updateTextSearch()">
</div>
</div>
</div>
<div class="col-md-12">
<button type="button" class="btn btn-warning m-t m-b" ui-sref="app.admin.members_new" translate>{{ 'add_a_new_member' }}</button>
<div class="pull-right">
<a class="btn btn-default" ng-href="api/members/export_members.xlsx" target="export-frame" ng-click="alertExport('members')">
<i class="fa fa-file-excel-o"></i> {{ 'members' | translate }}
</a>
<a class="btn btn-default" ng-href="api/members/export_subscriptions.xlsx" target="export-frame" ng-if="!fablabWithoutPlans" ng-click="alertExport('subscriptions')">
<i class="fa fa-file-excel-o"></i> {{ 'subscriptions' | translate }}
</a>
<a class="btn btn-default" ng-href="api/members/export_reservations.xlsx" target="export-frame" ng-click="alertExport('reservations')">
<i class="fa fa-file-excel-o"></i> {{ 'reservations' | translate }}
</a>
<iframe name="export-frame" height="0" width="0" class="none"></iframe>
</div>
<table class="table">
<thead>
<tr>
<th style="width:15%"><a href="" ng-click="setOrderMember('last_name')">{{ 'surname' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': member.order=='last_name', 'fa fa-sort-alpha-desc': member.order=='-last_name', 'fa fa-arrows-v': member.order }"></i></a></th>
<th style="width:15%"><a href="" ng-click="setOrderMember('first_name')">{{ 'first_name' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': member.order=='first_name', 'fa fa-sort-alpha-desc': member.order=='-first_name', 'fa fa-arrows-v': member.order }"></i></a></th>
<th style="width:15%" class="hidden-xs"><a href="" ng-click="setOrderMember('email')">{{ 'email' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': member.order=='email', 'fa fa-sort-alpha-desc': member.order=='-email', 'fa fa-arrows-v': member.order }"></i></a></th>
<th style="width:10%" class="hidden-xs hidden-sm hidden-md"><a href="" ng-click="setOrderMember('phone')">{{ 'phone' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': member.order=='phone', 'fa fa-sort-numeric-desc': member.order=='-phone', 'fa fa-arrows-v': member.order }"></i></a></th>
<th style="width:20%" class="hidden-xs hidden-sm"><a href="" ng-click="setOrderMember('group')">{{ 'user_type' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': member.order=='group', 'fa fa-sort-alpha-desc': member.order=='-group', 'fa fa-arrows-v': member.order }"></i></a></th>
<th style="width:15%" class="hidden-xs hidden-sm hidden-md"><a href="" ng-click="setOrderMember('plan')">{{ 'subscription' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': member.order=='plan', 'fa fa-sort-alpha-desc': member.order=='-plan', 'fa fa-arrows-v': member.order }"></i></a></th>
<th style="width:10%"></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="m in members">
<td class="text-c">{{ m.profile.last_name }}</td>
<td class="text-c">{{ m.profile.first_name }}</td>
<td class="hidden-xs">{{ m.email }}</td>
<td class="hidden-xs hidden-sm hidden-md">{{ m.profile.phone }}</td>
<td class="text-u-c text-sm hidden-xs hidden-sm">{{ m.group.name }}</td>
<td class="hidden-xs hidden-sm hidden-md">{{ m.subscribed_plan | humanReadablePlanName }}</td>
<td>
<div class="buttons">
<button class="btn btn-default" ui-sref="app.admin.members_edit({id: m.id})">
<i class="fa fa-edit"></i> {{ 'edit' | translate }}
</button>
<span class="label label-danger text-white" ng-show="m.need_completion" translate>{{ 'incomplete_profile' }}</span>
</div>
</td>
</tr>
</tbody>
</table>
<div class="text-center">
<button class="btn btn-warning" ng-click="showNextMembers()" ng-hide="member.noMore"><i class="fa fa-search-plus" aria-hidden="true"></i> {{ 'display_more_users' | translate }}</button>
</div>
</div>

View File

@ -47,7 +47,7 @@
<tbody>
<tr ng-repeat="price in plan.prices">
<td style="width: 60%;">{{ price.priceable_name }} (id {{ price.priceable_id }}) *</td>
<td style="width: 60%;">{{ getMachineName(price.priceable_id) }} (id {{ price.priceable_id }}) *</td>
<td>
<div class="input-group" ng-class="{'has-error': planForm['plan[prices_attributes][][amount]'].$dirty && planForm['plan[prices_attributes][][amount]'].$invalid}">
<span class="input-group-addon">{{currencySymbol}}</span>

View File

@ -23,7 +23,7 @@
<ng-include src="'<%= asset_path 'admin/plans/_form.html' %>'"></ng-include>
<div class="panel-footer no-padder">
<input type="submit" value="Enregistrer" class="r-b btn-valid btn btn-warning btn-block p-lg btn-lg text-u-c" ng-disabled="planForm.$invalid || !partnerIsValid()"/>
<input type="submit" value="{{ 'save' | translate }}" class="r-b btn-valid btn btn-warning btn-block p-lg btn-lg text-u-c" ng-disabled="planForm.$invalid || !partnerIsValid()"/>
</div>
</form>

View File

@ -0,0 +1,27 @@
<h2 translate>{{ 'list_of_the_coupons' }}</h2>
<button type="button" class="btn btn-warning m-t-lg m-b" ui-sref="app.admin.coupons_new" translate>{{ 'add_a_new_coupon' }}</button>
<table class="table">
<thead>
<tr>
<th translate>{{ 'name' }}</th>
<th translate>{{ 'percentage_off' }}</th>
<th translate>{{ 'nb_of_usages' }}</th>
<th translate>{{ 'status' }}</th>
<th></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="coupon in coupons">
<td>{{coupon.name}}</td>
<td>{{coupon.percent_off}} %</td>
<td>{{coupon.usages}}</td>
<td translate>{{coupon.status}}</td>
<td>
<button type="button" class="btn btn-default" ng-click="sendCouponToUser(coupon)"><i class="fa fa-send-o"></i> </button>
<button type="button" class="btn btn-default" ui-sref="app.admin.coupons_edit({id:coupon.id})"><i class="fa fa-pencil-square-o"></i></button>
<button type="button" class="btn btn-danger" ng-click="deleteCoupon(coupons, coupon.id)"><i class="fa fa-trash"></i></button>
</td>
</tr>
</tbody>
</table>

View File

@ -0,0 +1,97 @@
<h2 class="m-t-lg" translate>{{ 'trainings' }}</h2>
<table class="table">
<thead>
<tr>
<th style="width:20%" translate>{{ 'subscription' }}</th>
<th style="width:10%" translate>{{ 'credits' }}</th>
<th style="width:50%" translate>{{ 'related_trainings' }}</th>
<th style="width:20%"></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="(planId, trainingIds) in trainingCreditsGroups" ng-init="plan = getPlanFromId(planId)">
<td>
{{ plan | humanReadablePlanName: groups }}
</td>
<td>
<span editable-text="plan.training_credit_nb" e-form="rowform" e-name="training_credits" e-required>
{{ plan.training_credit_nb }}
</span>
</td>
<td>
<span editable-checklist="trainingIds" e-form="rowform" e-name="training_ids" e-ng-options="t.id as t.name for t in trainings" e-required>
{{ showTrainings(trainingIds) }}
</span>
</td>
<td>
<form editable-form name="rowform" onbeforesave="saveTrainingCredits($data, planId)" ng-show="rowform.$visible" class="form-buttons form-inline" shown="inserted == trainingIds">
<button type="submit" ng-disabled="rowform.$waiting" class="btn btn-warning">
<i class="fa fa-check"></i>
</button>
<button type="button" ng-disabled="rowform.$waiting" ng-click="cancelTrainingCredit(rowform)" class="btn btn-default">
<i class="fa fa-times"></i>
</button>
</form>
<div class="buttons" ng-show="!rowform.$visible">
<button class="btn btn-default" ng-click="rowform.$show()">
<i class="fa fa-edit"></i> {{ 'edit' | translate }}
</button>
</div>
</td>
</tr>
</tbody>
</table>
<h2 class="m-t-lg" translate>{{ 'machines' }}</h2>
<div class="btn-group m-t-md m-b-md">
<button type="button" class="btn btn-warning" ng-click="addMachineCredit($event)" translate>{{ 'add_a_machine_credit' }}</button>
</div>
<table class="table">
<thead>
<tr>
<th style="width:20%" translate>{{ 'machine' }}</th>
<th style="width:10%" translate>{{ 'hours' }}</th>
<th style="width:50%" translate>{{ 'related_subscriptions' }}</th>
<th style="width:20%"></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="mc in machineCredits">
<td>
<span editable-select="mc.creditable_id" e-name="creditable_id" e-form="rowform" e-ng-options="m.id as m.name+' ( id. '+m.id+' )' for m in machines" e-required>
{{ showCreditableName(mc) }}
</span>
</td>
<td>
<span editable-number="mc.hours" e-name="hours" e-form="rowform" e-required>
{{ mc.hours }}
</span>
</td>
<td>
<span editable-select="mc.plan_id" e-ng-options="p.id as humanReadablePlanName(p, groups, 'short') for p in plans" e-name="plan_id" e-form="rowform">
{{ getPlanFromId(mc.plan_id) | humanReadablePlanName: groups: 'short' }}
</span>
</td>
<td>
<form editable-form name="rowform" onbeforesave="saveMachineCredit($data, mc.id)" ng-show="rowform.$visible" class="form-buttons form-inline" shown="inserted == mc">
<button type="submit" ng-disabled="rowform.$waiting" class="btn btn-warning">
<i class="fa fa-check"></i>
</button>
<button type="button" ng-disabled="rowform.$waiting" ng-click="cancelMachineCredit(rowform, $index)" class="btn btn-default">
<i class="fa fa-times"></i>
</button>
</form>
<div class="buttons" ng-show="!rowform.$visible">
<button class="btn btn-default" ng-click="rowform.$show()">
<i class="fa fa-edit"></i> {{ 'edit' | translate }}
</button>
<button class="btn btn-danger" ng-click="removeMachineCredit($index)">
<i class="fa fa-trash-o"></i> {{ 'delete' | translate }} (!)
</button>
</div>
</td>
</tr>
</tbody>
</table>

View File

@ -22,195 +22,23 @@
<uib-tabset justified="true">
<uib-tab heading="{{ 'subscriptions' | translate }}">
<h2 translate>{{ 'list_of_the_subscription_plans' }}</h2>
<div ng-show="fablabWithoutPlans" class="alert alert-warning m-t">
{{ 'beware_the_subscriptions_are_disabled_on_this_application' | translate }}
{{ 'you_can_create_some_but_they_wont_be_available_until_the_project_is_redeployed_by_the_server_manager' | translate }}
<br>{{ 'for_safety_reasons_please_dont_create_subscriptions_if_you_dont_want_intend_to_use_them_later' | translate }}
</div>
<button type="button" class="btn btn-warning m-t-lg m-b" ui-sref="app.admin.plans.new" translate>{{ 'add_a_new_subscription_plan' }}</button>
<table class="table">
<thead>
<tr>
<th><a href="" ng-click="setOrderPlans('type')">{{ 'type' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderPlans=='type', 'fa fa-sort-alpha-desc': orderPlans=='-type', 'fa fa-arrows-v': orderPlans }"></i></a></th>
<th><a href="" ng-click="setOrderPlans('name')">{{ 'name' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderPlans=='name', 'fa fa-sort-alpha-desc': orderPlans=='-name', 'fa fa-arrows-v': orderPlans }"></i></a></th>
<th><a href="" ng-click="setOrderPlans('interval')">{{ 'duration' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-amount-asc': orderPlans=='interval', 'fa fa-sort-amount-desc': orderPlans=='-interval', 'fa fa-arrows-v': orderPlans }"></i></a></th>
<th><a href="" ng-click="setOrderPlans('group_id')">{{ 'group' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderPlans=='group_id', 'fa fa-sort-alpha-desc': orderPlans=='-group_id', 'fa fa-arrows-v': orderPlans }"></i></a></th>
<th><a href="" ng-click="setOrderPlans('ui_weight')">{{ 'prominence' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': orderPlans=='ui_weight', 'fa fa-sort-numeric-desc': orderPlans=='-ui_weight', 'fa fa-arrows-v': orderPlans }"></i></a></th>
<th><a href="" ng-click="setOrderPlans('amount')">{{ 'price' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': orderPlans=='amount', 'fa fa-sort-numeric-desc': orderPlans=='-amount', 'fa fa-arrows-v': orderPlans }"></i></a></th>
<th></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="plan in plans | orderBy:orderPlans">
<td>{{getPlanType(plan.type)}}</td>
<td>{{plan.base_name}}</td>
<td>{{ plan.interval | planIntervalFilter:plan.interval_count }}</td>
<td>{{getGroupFromId(groups, plan.group_id).name}}</td>
<td>{{plan.ui_weight}}</td>
<td>{{plan.amount | currency}}</td>
<td><button type="button" class="btn btn-default" ui-sref="app.admin.plans.edit({id:plan.id})"><i class="fa fa-pencil-square-o"></i></button> <button type="button" class="btn btn-danger" ng-click="deletePlan(plans, plan.id)"><i class="fa fa-trash"></i></button></td>
</tr>
</tbody>
</table>
<ng-include src="'<%= asset_path 'admin/pricing/subscriptions.html' %>'"></ng-include>
</uib-tab>
<uib-tab heading="{{ 'trainings' | translate }}">
<table class="table">
<thead>
<tr>
<th style="width:20%" translate>{{ 'trainings' }}</th>
<th style="width:20%" ng-repeat="group in groups">
<span class="text-u-c text-sm">{{group.name}}</span>
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="training in trainings">
<td>
{{ training.name }}
</td>
<td ng-repeat="group in groups">
<span editable-number="findTrainingsPricing(trainingsPricings, training.id, group.id).amount"
onbeforesave="updateTrainingsPricing($data, findTrainingsPricing(trainingsPricings, training.id, group.id))">
{{ findTrainingsPricing(trainingsPricings, training.id, group.id).amount | currency}}
</span>
</td>
</tr>
</tbody>
</table>
<ng-include src="'<%= asset_path 'admin/pricing/trainings.html' %>'"></ng-include>
</uib-tab>
<uib-tab heading="{{ 'machine_hours' | translate }}">
<div class="alert alert-warning m-t">
{{ 'these_prices_match_machine_hours_rates_' | translate }} <span class="font-bold" translate>{{ '_without_subscriptions' }}</span>.
</div>
<table class="table">
<thead>
<tr>
<th style="width:20%" translate>{{ 'machines' }}</th>
<th style="width:20%" ng-repeat="group in groups">
<span class="text-u-c text-sm">{{group.name}}</span>
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="machine in machines">
<td>
{{ machine.name }}
</td>
<td ng-repeat="group in groups">
<span editable-number="findPriceBy(machinesPrices, machine.id, group.id).amount"
onbeforesave="updatePrice($data, findPriceBy(machinesPrices, machine.id, group.id))">
{{ findPriceBy(machinesPrices, machine.id, group.id).amount | currency}}
</span>
</td>
</tr>
</tbody>
</table>
<ng-include src="'<%= asset_path 'admin/pricing/machine_hours.html' %>'"></ng-include>
</uib-tab>
<uib-tab heading="{{ 'credits' | translate }}">
<h2 class="m-t-lg" translate>{{ 'trainings' }}</h2>
<table class="table">
<thead>
<tr>
<th style="width:20%" translate>{{ 'subscription' }}</th>
<th style="width:10%" translate>{{ 'credits' }}</th>
<th style="width:50%" translate>{{ 'related_trainings' }}</th>
<th style="width:20%"></th>
</tr>
</thead>
<tbody>
<ng-include src="'<%= asset_path 'admin/pricing/credits.html' %>'"></ng-include>
</uib-tab>
<tr ng-repeat="(planId, trainingIds) in trainingCreditsGroups" ng-init="plan = getPlanFromId(planId)">
<td>
{{ plan | humanReadablePlanName: groups }}
</td>
<td>
<span editable-text="plan.training_credit_nb" e-form="rowform" e-name="training_credits" e-required>
{{ plan.training_credit_nb }}
</span>
</td>
<td>
<span editable-checklist="trainingIds" e-form="rowform" e-name="training_ids" e-ng-options="t.id as t.name for t in trainings" e-required>
{{ showTrainings(trainingIds) }}
</span>
</td>
<td>
<form editable-form name="rowform" onbeforesave="saveTrainingCredits($data, planId)" ng-show="rowform.$visible" class="form-buttons form-inline" shown="inserted == trainingIds">
<button type="submit" ng-disabled="rowform.$waiting" class="btn btn-warning">
<i class="fa fa-check"></i>
</button>
<button type="button" ng-disabled="rowform.$waiting" ng-click="cancelTrainingCredit(rowform)" class="btn btn-default">
<i class="fa fa-times"></i>
</button>
</form>
<div class="buttons" ng-show="!rowform.$visible">
<button class="btn btn-default" ng-click="rowform.$show()">
<i class="fa fa-edit"></i> {{ 'edit' | translate }}
</button>
</div>
</td>
</tr>
</tbody>
</table>
<h2 class="m-t-lg" translate>{{ 'machines' }}</h2>
<div class="btn-group m-t-md m-b-md">
<button type="button" class="btn btn-warning" ng-click="addMachineCredit($event)" translate>{{ 'add_a_machine_credit' }}</button>
</div>
<table class="table">
<thead>
<tr>
<th style="width:20%" translate>{{ 'machine' }}</th>
<th style="width:10%" translate>{{ 'hours' }}</th>
<th style="width:50%" translate>{{ 'related_subscriptions' }}</th>
<th style="width:20%"></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="mc in machineCredits">
<td>
<span editable-select="mc.creditable_id" e-name="creditable_id" e-form="rowform" e-ng-options="m.id as m.name+' ( id. '+m.id+' )' for m in machines" e-required>
{{ showCreditableName(mc) }}
</span>
</td>
<td>
<span editable-number="mc.hours" e-name="hours" e-form="rowform" e-required>
{{ mc.hours }}
</span>
</td>
<td>
<span editable-select="mc.plan_id" e-ng-options="p.id as humanReadablePlanName(p, groups, 'short') for p in plans" e-name="plan_id" e-form="rowform">
{{ getPlanFromId(mc.plan_id) | humanReadablePlanName: groups: 'short' }}
</span>
</td>
<td>
<form editable-form name="rowform" onbeforesave="saveMachineCredit($data, mc.id)" ng-show="rowform.$visible" class="form-buttons form-inline" shown="inserted == mc">
<button type="submit" ng-disabled="rowform.$waiting" class="btn btn-warning">
<i class="fa fa-check"></i>
</button>
<button type="button" ng-disabled="rowform.$waiting" ng-click="cancelMachineCredit(rowform, $index)" class="btn btn-default">
<i class="fa fa-times"></i>
</button>
</form>
<div class="buttons" ng-show="!rowform.$visible">
<button class="btn btn-default" ng-click="rowform.$show()">
<i class="fa fa-edit"></i> {{ 'edit' | translate }}
</button>
<button class="btn btn-danger" ng-click="removeMachineCredit($index)">
<i class="fa fa-trash-o"></i> {{ 'delete' | translate }} (!)
</button>
</div>
</td>
</tr>
</tbody>
</table>
<uib-tab heading="{{ 'coupons' | translate }}">
<ng-include src="'<%= asset_path 'admin/pricing/coupons.html' %>'"></ng-include>
</uib-tab>
</uib-tabset>
</div>

View File

@ -0,0 +1,26 @@
<div class="alert alert-warning m-t">
{{ 'these_prices_match_machine_hours_rates_' | translate }} <span class="font-bold" translate>{{ '_without_subscriptions' }}</span>.
</div>
<table class="table">
<thead>
<tr>
<th style="width:20%" translate>{{ 'machines' }}</th>
<th style="width:20%" ng-repeat="group in groups">
<span class="text-u-c text-sm">{{group.name}}</span>
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="machine in machines">
<td>
{{ machine.name }}
</td>
<td ng-repeat="group in groups">
<span editable-number="findPriceBy(machinesPrices, machine.id, group.id).amount"
onbeforesave="updatePrice($data, findPriceBy(machinesPrices, machine.id, group.id))">
{{ findPriceBy(machinesPrices, machine.id, group.id).amount | currency}}
</span>
</td>
</tr>
</tbody>
</table>

View File

@ -0,0 +1,30 @@
<div class="modal-header">
<h3 class="text-center red" translate>{{ 'send_a_coupon' }}</h3>
</div>
<div class="modal-body">
<select-member></select-member>
<div class="widget panel b-a m">
<div class="panel-heading b-b small">
<h3 class="panel-title" translate>{{ 'coupon' }}</h3>
</div>
<div class="widget-content no-bg auto wrapper">
<table>
<thead>
<tr><th style="width:60%"></th></tr>
</thead>
<tbody>
<tr><td translate>{{'code'}}</td><td>{{coupon.code}}</td></tr>
<tr><td translate>{{'percent_off'}}</td><td>{{coupon.percent_off}} %</td></tr>
<tr><td translate>{{'validity_per_user'}}</td><td translate>{{coupon.validity_per_user}}</td></tr>
<tr><td translate>{{'valid_until'}}</td><td>{{coupon.valid_until | amDateFormat:'L'}}</td></tr>
<tr><td translate>{{'usages'}}</td><td>{{coupon.usages}} / {{coupon.max_usages | maxCount}}</td></tr>
<tr><td translate>{{'enabled'}}</td><td>{{coupon.active | booleanFormat}}</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-warning" ng-click="ok()" ng-disabled="ctrl.member == null" translate>{{ 'confirm' }}</button>
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'cancel' }}</button>
</div>

View File

@ -0,0 +1,33 @@
<h2 translate>{{ 'list_of_the_subscription_plans' }}</h2>
<div ng-show="fablabWithoutPlans" class="alert alert-warning m-t">
{{ 'beware_the_subscriptions_are_disabled_on_this_application' | translate }}
{{ 'you_can_create_some_but_they_wont_be_available_until_the_project_is_redeployed_by_the_server_manager' | translate }}
<br>{{ 'for_safety_reasons_please_dont_create_subscriptions_if_you_dont_want_intend_to_use_them_later' | translate }}
</div>
<button type="button" class="btn btn-warning m-t-lg m-b" ui-sref="app.admin.plans.new" translate>{{ 'add_a_new_subscription_plan' }}</button>
<table class="table">
<thead>
<tr>
<th><a href="" ng-click="setOrderPlans('type')">{{ 'type' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderPlans=='type', 'fa fa-sort-alpha-desc': orderPlans=='-type', 'fa fa-arrows-v': orderPlans }"></i></a></th>
<th><a href="" ng-click="setOrderPlans('name')">{{ 'name' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderPlans=='name', 'fa fa-sort-alpha-desc': orderPlans=='-name', 'fa fa-arrows-v': orderPlans }"></i></a></th>
<th><a href="" ng-click="setOrderPlans('interval')">{{ 'duration' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-amount-asc': orderPlans=='interval', 'fa fa-sort-amount-desc': orderPlans=='-interval', 'fa fa-arrows-v': orderPlans }"></i></a></th>
<th><a href="" ng-click="setOrderPlans('group_id')">{{ 'group' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderPlans=='group_id', 'fa fa-sort-alpha-desc': orderPlans=='-group_id', 'fa fa-arrows-v': orderPlans }"></i></a></th>
<th class="hidden-xs"><a href="" ng-click="setOrderPlans('ui_weight')">{{ 'prominence' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': orderPlans=='ui_weight', 'fa fa-sort-numeric-desc': orderPlans=='-ui_weight', 'fa fa-arrows-v': orderPlans }"></i></a></th>
<th><a href="" ng-click="setOrderPlans('amount')">{{ 'price' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': orderPlans=='amount', 'fa fa-sort-numeric-desc': orderPlans=='-amount', 'fa fa-arrows-v': orderPlans }"></i></a></th>
<th></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="plan in plans | orderBy:orderPlans">
<td>{{getPlanType(plan.type)}}</td>
<td>{{plan.base_name}}</td>
<td>{{ plan.interval | planIntervalFilter:plan.interval_count }}</td>
<td>{{getGroupFromId(groups, plan.group_id).name}}</td>
<td class="hidden-xs">{{plan.ui_weight}}</td>
<td>{{plan.amount | currency}}</td>
<td><button type="button" class="btn btn-default" ui-sref="app.admin.plans.edit({id:plan.id})"><i class="fa fa-pencil-square-o"></i></button> <button type="button" class="btn btn-danger" ng-click="deletePlan(plans, plan.id)"><i class="fa fa-trash"></i></button></td>
</tr>
</tbody>
</table>

View File

@ -0,0 +1,23 @@
<table class="table">
<thead>
<tr>
<th style="width:20%" translate>{{ 'trainings' }}</th>
<th style="width:20%" ng-repeat="group in groups">
<span class="text-u-c text-sm">{{group.name}}</span>
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="training in trainings">
<td>
{{ training.name }}
</td>
<td ng-repeat="group in groups">
<span editable-number="findTrainingsPricing(trainingsPricings, training.id, group.id).amount"
onbeforesave="updateTrainingsPricing($data, findTrainingsPricing(trainingsPricings, training.id, group.id))">
{{ findTrainingsPricing(trainingsPricings, training.id, group.id).amount | currency}}
</span>
</td>
</tr>
</tbody>
</table>

View File

@ -21,130 +21,13 @@
<div class="col-md-12">
<uib-tabset justified="true">
<uib-tab heading="{{ 'materials' | translate }}">
<button type="button" class="btn btn-warning m-b m-t" ng-click="addComponent()" translate>{{ 'add_a_material' }}</button>
<table class="table">
<thead>
<tr>
<th style="width:80%" translate>{{ 'name' }}</th>
<th style="width:20%"></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="component in components">
<td>
<span editable-text="component.name" e-cols="100" e-name="name" e-form="rowform" e-required>
{{ component.name }}
</span>
</td>
<td>
<!-- form -->
<form editable-form name="rowform" onbeforesave="saveComponent($data, component.id)" ng-show="rowform.$visible" class="form-buttons form-inline" shown="inserted == component">
<button type="submit" ng-disabled="rowform.$waiting" class="btn btn-warning">
<i class="fa fa-check"></i>
</button>
<button type="button" ng-disabled="rowform.$waiting" ng-click="cancelComponent(rowform, $index)" class="btn btn-default">
<i class="fa fa-times"></i>
</button>
</form>
<div class="buttons" ng-show="!rowform.$visible">
<button class="btn btn-default" ng-click="rowform.$show()">
<i class="fa fa-edit"></i> <span class="hidden-xs hidden-sm" translate>{{ 'edit' }}</span>
</button>
<button class="btn btn-danger" ng-click="removeComponent($index)">
<i class="fa fa-trash-o"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
<ng-include src="'<%= asset_path 'admin/project_elements/materials.html' %>'"></ng-include>
</uib-tab>
<uib-tab heading="{{ 'themes' | translate }}">
<button type="button" class="btn btn-warning m-t m-b" ng-click="addTheme()" translate>{{ 'add_a_new_theme' }}</button>
<table class="table">
<thead>
<tr>
<th style="width:80%" translate>{{ 'name' }}</th>
<th style="width:20%"></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="theme in themes">
<td>
<span editable-text="theme.name" e-name="name" e-form="rowform" e-required>
{{ theme.name }}
</span>
</td>
<td>
<!-- form -->
<form editable-form name="rowform" onbeforesave="saveTheme($data, theme.id)" ng-show="rowform.$visible" class="form-buttons form-inline" shown="inserted == theme">
<button type="submit" ng-disabled="rowform.$waiting" class="btn btn-warning">
<i class="fa fa-check"></i>
</button>
<button type="button" ng-disabled="rowform.$waiting" ng-click="cancelTheme(rowform, $index)" class="btn btn-default">
<i class="fa fa-times"></i>
</button>
</form>
<div class="buttons" ng-show="!rowform.$visible">
<button class="btn btn-default" ng-click="rowform.$show()">
<i class="fa fa-edit"></i> <span class="hidden-xs hidden-sm" translate>{{ 'edit' }}</span>
</button>
<button class="btn btn-danger" ng-click="removeTheme($index)">
<i class="fa fa-trash-o"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
<ng-include src="'<%= asset_path 'admin/project_elements/themes.html' %>'"></ng-include>
</uib-tab>
<uib-tab heading="{{ 'licences' | translate }}">
<button type="button" class="btn btn-warning m-t m-b" ng-click="addLicence()" translate>{{ 'add_a_new_licence' }}</button>
<table class="table">
<thead>
<tr>
<th style="width:30%" translate>{{ 'name' }}</th>
<th style="width:50%" class="hidden-xs" translate>{{ 'description' }}</th>
<th style="width:20%"></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="licence in licences">
<td>
<span editable-textarea="licence.name" e-rows="5" e-cols="100" e-name="name" e-form="rowform" e-required>
{{ licence.name }}
</span>
</td>
<td class="hidden-xs">
<span editable-textarea="licence.description" e-rows="5" e-cols="100" e-name="description" e-form="rowform" e-required>
<div class="text-sm">{{ licence.description }}</div>
</span>
</td>
<td>
<!-- form -->
<form editable-form name="rowform" onbeforesave="saveLicence($data, licence.id)" ng-show="rowform.$visible" class="form-buttons form-inline" shown="inserted == licence">
<button type="submit" ng-disabled="rowform.$waiting" class="btn btn-warning">
<i class="fa fa-check"></i>
</button>
<button type="button" ng-disabled="rowform.$waiting" ng-click="cancelLicence(rowform, $index)" class="btn btn-default">
<i class="fa fa-times"></i>
</button>
</form>
<div class="buttons" ng-show="!rowform.$visible">
<button class="btn btn-default" ng-click="rowform.$show()">
<i class="fa fa-edit"></i> <span class="hidden-xs hidden-sm" translate>{{ 'edit' }}</span>
</button>
<button class="btn btn-danger" ng-click="removeLicence($index)">
<i class="fa fa-trash-o"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
<ng-include src="'<%= asset_path 'admin/project_elements/licences.html' %>'"></ng-include>
</uib-tab>
</uib-tabset>
</div>

View File

@ -0,0 +1,44 @@
<button type="button" class="btn btn-warning m-t m-b" ng-click="addLicence()" translate>{{ 'add_a_new_licence' }}</button>
<table class="table">
<thead>
<tr>
<th style="width:30%" translate>{{ 'name' }}</th>
<th style="width:50%" class="hidden-xs" translate>{{ 'description' }}</th>
<th style="width:20%"></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="licence in licences">
<td>
<span editable-textarea="licence.name" e-rows="5" e-cols="100" e-name="name" e-form="rowform" e-required>
{{ licence.name }}
</span>
</td>
<td class="hidden-xs">
<span editable-textarea="licence.description" e-rows="5" e-cols="100" e-name="description" e-form="rowform" e-required>
<div class="text-sm">{{ licence.description }}</div>
</span>
</td>
<td>
<!-- form -->
<form editable-form name="rowform" onbeforesave="saveLicence($data, licence.id)" ng-show="rowform.$visible" class="form-buttons form-inline" shown="inserted == licence">
<button type="submit" ng-disabled="rowform.$waiting" class="btn btn-warning">
<i class="fa fa-check"></i>
</button>
<button type="button" ng-disabled="rowform.$waiting" ng-click="cancelLicence(rowform, $index)" class="btn btn-default">
<i class="fa fa-times"></i>
</button>
</form>
<div class="buttons" ng-show="!rowform.$visible">
<button class="btn btn-default" ng-click="rowform.$show()">
<i class="fa fa-edit"></i> <span class="hidden-xs hidden-sm" translate>{{ 'edit' }}</span>
</button>
<button class="btn btn-danger" ng-click="removeLicence($index)">
<i class="fa fa-trash-o"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>

View File

@ -0,0 +1,38 @@
<button type="button" class="btn btn-warning m-b m-t" ng-click="addComponent()" translate>{{ 'add_a_material' }}</button>
<table class="table">
<thead>
<tr>
<th style="width:80%" translate>{{ 'name' }}</th>
<th style="width:20%"></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="component in components">
<td>
<span editable-text="component.name" e-cols="100" e-name="name" e-form="rowform" e-required>
{{ component.name }}
</span>
</td>
<td>
<!-- form -->
<form editable-form name="rowform" onbeforesave="saveComponent($data, component.id)" ng-show="rowform.$visible" class="form-buttons form-inline" shown="inserted == component">
<button type="submit" ng-disabled="rowform.$waiting" class="btn btn-warning">
<i class="fa fa-check"></i>
</button>
<button type="button" ng-disabled="rowform.$waiting" ng-click="cancelComponent(rowform, $index)" class="btn btn-default">
<i class="fa fa-times"></i>
</button>
</form>
<div class="buttons" ng-show="!rowform.$visible">
<button class="btn btn-default" ng-click="rowform.$show()">
<i class="fa fa-edit"></i> <span class="hidden-xs hidden-sm" translate>{{ 'edit' }}</span>
</button>
<button class="btn btn-danger" ng-click="removeComponent($index)">
<i class="fa fa-trash-o"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>

View File

@ -0,0 +1,38 @@
<button type="button" class="btn btn-warning m-t m-b" ng-click="addTheme()" translate>{{ 'add_a_new_theme' }}</button>
<table class="table">
<thead>
<tr>
<th style="width:80%" translate>{{ 'name' }}</th>
<th style="width:20%"></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="theme in themes">
<td>
<span editable-text="theme.name" e-name="name" e-form="rowform" e-required>
{{ theme.name }}
</span>
</td>
<td>
<!-- form -->
<form editable-form name="rowform" onbeforesave="saveTheme($data, theme.id)" ng-show="rowform.$visible" class="form-buttons form-inline" shown="inserted == theme">
<button type="submit" ng-disabled="rowform.$waiting" class="btn btn-warning">
<i class="fa fa-check"></i>
</button>
<button type="button" ng-disabled="rowform.$waiting" ng-click="cancelTheme(rowform, $index)" class="btn btn-default">
<i class="fa fa-times"></i>
</button>
</form>
<div class="buttons" ng-show="!rowform.$visible">
<button class="btn btn-default" ng-click="rowform.$show()">
<i class="fa fa-edit"></i> <span class="hidden-xs hidden-sm" translate>{{ 'edit' }}</span>
</button>
<button class="btn btn-danger" ng-click="removeTheme($index)">
<i class="fa fa-trash-o"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>

View File

@ -0,0 +1,35 @@
<div class="panel panel-default m-t-md">
<div class="panel-body">
<div class="row m-t-lg m-b-lg">
<div class="col-sm-offset-4 col-sm-4">
<h1 ng-model="aboutTitleSetting.value" medium-editor options='{"placeholder": "{{ "title_of_the_about_page" | translate }}", "disableToolbar": true, "disableReturn": false}' class="text-u-c"></h1>
<span class="help-block text-info text-xs"><i class="fa fa-lightbulb-o"></i> {{ 'shift_enter_to_force_carriage_return' | translate }}</span>
<button name="button" class="btn btn-warning" ng-click="save(aboutTitleSetting)" translate>{{ 'save' }}</button>
</div>
</div>
<div class="row">
<div class="col-md-4 col-md-offset-1">
<div class="text-justify" ng-model="aboutBodySetting.value" medium-editor options='{"placeholder": "{{ "input_the_main_content" | translate }}",
"buttons": ["bold", "italic", "anchor", "header1", "header2" ]
}'>
</div>
<button name="button" class="btn btn-warning" ng-click="save(aboutBodySetting)" translate>{{ 'save' }}</button>
</div>
<div class="col-md-4 col-md-offset-2">
<div ng-model="aboutContactsSetting.value" medium-editor options='{"placeholder": "{{ "input_the_fablab_contacts" | translate }}",
"buttons": ["bold", "italic", "anchor", "header1", "header2" ]
}'>
</div>
<span class="help-block text-info text-xs"><i class="fa fa-lightbulb-o"></i> {{ 'shift_enter_to_force_carriage_return' | translate }}</span>
<button name="button" class="btn btn-warning" ng-click="save(aboutContactsSetting)" translate>{{ 'save' }}</button>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,297 @@
<div class="panel panel-default m-t-md">
<div class="panel-heading">
<span class="font-sbold" translate>{{ 'title' }}</span>
</div>
<div class="panel-body">
<div class="row m-t-lg">
<div class="col-md-4">
<form role="form" novalidate>
<label for="fablabName" class="control-label m-r" translate>{{ 'fablab_title' }}</label>
<div class="form-group">
<div class="input-group">
<div class="input-group-addon"><i class="fa fa-font"></i></div>
<input type="text" id="fablabName" ng-model="fablabName.value" class="form-control" placeholder="{{ 'fablab_name' | translate }}"/>
</div>
</div>
<button name="button" class="btn btn-warning" ng-click="save(fablabName)" translate>{{ 'save' }}</button>
</form>
</div>
<div class="col-md-4 col-md-offset-1">
<form role="form" novalidate>
<h4 class="control-label m-r" translate>{{ 'title_concordance' }}</h4>
<div class="form-group">
<input type="radio" name="nameGenre" id="nameGenreMale" ng-model="nameGenre.value" ng-value="'male'" />
<label for="nameGenreMale">{{ 'male' | translate }} <span style="font-weight: normal">{{ 'eg' | translate }} <cite>{{ 'about' | translate }} <strong translate>{{ 'male_preposition' }}</strong> {{fablabName.value}}</cite></span></label>
<br/>
<input type="radio" name="nameGenre" id="nameGenreFemale" ng-model="nameGenre.value" ng-value="'female'" />
<label for="nameGenreFemale">{{ 'female' | translate }} <span style="font-weight: normal">{{ 'eg' | translate }} <cite>{{ 'about' | translate }} <strong translate>{{ 'female_preposition' }}</strong> {{fablabName.value}}</cite></span></label>
</div>
<button name="button" class="btn btn-warning" ng-click="save(nameGenre)" translate>{{ 'save' }}</button>
</form>
</div>
</div>
</div>
</div>
<div class="panel panel-default m-t-lg">
<div class="panel-heading">
<span class="font-sbold" translate>{{ 'customize_information_messages' }}</span>
</div>
<div class="panel-body">
<div class="row">
<div class="col-md-3">
<h4 translate>{{ 'message_of_the_machine_booking_page' }}</h4>
<div ng-model="machineExplicationsAlert.value" medium-editor options='{"placeholder": "{{ "type_the_message_content" | translate }}",
"buttons": ["bold", "italic", "unorderedlist", "header2" ]
}'>
</div>
<button name="button" class="btn btn-warning" ng-click="save(machineExplicationsAlert)" translate>{{ 'save' }}</button>
</div>
<div class="col-md-3">
<h4 translate>{{ 'warning_message_of_the_training_booking_page'}}</h4>
<div ng-model="trainingExplicationsAlert.value" medium-editor options='{"placeholder": "{{ "type_the_message_content" | translate }}",
"buttons": ["bold", "italic", "unorderedlist", "header2" ]
}'>
</div>
<button name="button" class="btn btn-warning" ng-click="save(trainingExplicationsAlert)" translate>{{ 'save' }}</button>
</div>
<div class="col-md-3">
<h4 translate>{{ 'information_message_of_the_training_reservation_page'}}</h4>
<div ng-model="trainingInformationMessage.value" medium-editor options='{"placeholder": "{{ "type_the_message_content" | translate }}",
"buttons": ["bold", "italic", "unorderedlist", "header2" ]
}'>
</div>
<button name="button" class="btn btn-warning" ng-click="save(trainingInformationMessage)" translate>{{ 'save' }}</button>
</div>
<div class="col-md-3">
<h4 translate>{{ 'message_of_the_subscriptions_page' }}</h4>
<div ng-model="subscriptionExplicationsAlert.value" medium-editor options='{"placeholder": "{{ "type_the_message_content" | translate }}",
"buttons": ["bold", "italic", "unorderedlist", "header2" ]
}'>
</div>
<button name="button" class="btn btn-warning" ng-click="save(subscriptionExplicationsAlert)" translate>{{ 'save' }}</button>
</div>
</div>
</div>
</div>
<div class="panel panel-default m-t-lg">
<div class="panel-heading">
<span class="font-sbold" translate>{{ 'legal_documents'}}</span>
</div>
<div class="panel-body">
<div class="alert alert-warning m-t" translate>
{{ 'if_these_documents_are_not_filled_no_consent_about_them_will_be_asked_to_the_user' }}
</div>
<div class="row">
<form class="col-md-6" method="post" action="{{actionUrl.cgv}}" novalidate name="cgvForm" ng-upload="submited(content)" ng-submit="addLoader('cgv')" upload-options-enable-rails-csrf="true" unsaved-warning-form>
<input type="hidden" name="custom_asset[name]" value="cgv-file">
<input name="_method" type="hidden" ng-value="methods.cgv">
<label for="tnc_file" class="control-label m-r" translate>{{ 'general_terms_and_conditions_(T&C)' }}</label>
<div class="form-group">
<div class="fileinput input-group" data-provides="fileinput" ng-class="fileinputClass(cgvFile.custom_asset_file_attributes.attachment)">
<div class="form-control" data-trigger="fileinput">
<i class="glyphicon glyphicon-file fileinput-exists"></i> <span class="fileinput-filename">{{cgvFile.custom_asset_file_attributes.attachment}}</span>
</div>
<span class="input-group-addon btn btn-default btn-file">
<span class="fileinput-new" translate>{{ 'browse' }}</span>
<span class="fileinput-exists" translate>{{ 'change' }}</span>
<input type="file"
id="tnc_file"
name="custom_asset[custom_asset_file_attributes][attachment]"
accept=".pdf"
required />
</span>
</div>
</div>
<button name="button" type="submit" ng-class="{'btn-loading':loader.cgv}" ng-disabled="cgvForm.$invalid" class="btn btn-warning" translate>{{ 'save' }}</button>
</form>
</div>
<div class="row m-t-xl">
<form class="col-md-6" method="post" action="{{actionUrl.cgu}}" novalidate name="cguForm" ng-upload="submited(content)" ng-submit="addLoader('cgu')" upload-options-enable-rails-csrf="true" unsaved-warning-form>
<input type="hidden" name="custom_asset[name]" value="cgu-file">
<input name="_method" type="hidden" ng-value="methods.cgu">
<label for="tos_file" class="control-label m-r" translate>{{ 'terms_of_service_(TOS)' }}</label>
<div class="form-group">
<div class="fileinput input-group" data-provides="fileinput" ng-class="fileinputClass(cguFile.custom_asset_file_attributes.attachment)">
<div class="form-control" data-trigger="fileinput">
<i class="glyphicon glyphicon-file fileinput-exists"></i> <span class="fileinput-filename">{{cguFile.custom_asset_file_attributes.attachment}}</span>
</div>
<span class="input-group-addon btn btn-default btn-file">
<span class="fileinput-new" translate>{{ 'browse' }}</span>
<span class="fileinput-exists" translate>{{ 'change' }}</span>
<input type="file"
id="tos_file"
name="custom_asset[custom_asset_file_attributes][attachment]"
accept=".pdf"
required />
</span>
</div>
</div>
<button name="button" type="submit" ng-class="{'btn-loading':loader.cgu}" ng-disabled="cguForm.$invalid" class="btn btn-warning" translate>{{ 'save' }}</button>
</form>
</div>
</div>
</div>
<div class="panel panel-default m-t-lg">
<div class="panel-heading">
<span class="font-sbold" translate>{{ 'customize_the_graphics' }}</span>
</div>
<div class="panel-body">
<div class="alert alert-warning m-t">
<span translate>{{ 'for_an_optimal_rendering_the_logo_image_must_be_at_the_PNG_format_with_a_transparent_background_and_with_an_aspect_ratio_3.5_times_wider_than_the_height' }}</span><br/>
<span translate>{{ 'concerning_the_favicon_it_must_be_at_ICO_format_with_a_size_of_16x16_pixels' }}</span><br/>
<br/>
<span translate>{{ 'remember_to_refresh_the_page_for_the_changes_to_take_effect' }}</span>
</div>
<div class="row">
<div class="col-md-4">
<form class="custom-logo-container" method="post" action="{{actionUrl.logo}}" novalidate name="logoForm" ng-upload="submited(content)" upload-options-enable-rails-csrf="true" unsaved-warning-form>
<input type="hidden" name="custom_asset[name]" value="logo-file">
<input name="_method" type="hidden" ng-value="methods.logo">
<h3 class="m-l" translate>{{ 'logo_(white_background)' }}</h3>
<div class="custom-logo" style="background-image: url({{customLogo}});">
<img src="data:image/png;base64," data-src="holder.js/100%x100%/text:&#xf03e;/font:FontAwesome/icon-xs" bs-holder ng-show="!customLogo" class="img-responsive">
<img base-sixty-four-image="customLogo" ng-show="customLogo && customLogo.base64">
<img ng-src="{{customLogo.custom_asset_file_attributes.attachment_url}}" alt="{{customLogo.custom_asset_file_attributes.attachment}}" ng-show="customLogo && customLogo.custom_asset_file_attributes" />
<div class="tools-box">
<div class="btn-group">
<div class="btn btn-default btn-file">
<i class="fa fa-edit"></i> {{ 'change_the_logo' | translate }}
<input type="file"
accept="image/png,image/x-png"
name="custom_asset[custom_asset_file_attributes][attachment]"
ng-model="customLogo"
base-sixty-four-input
required />
</div>
</div>
</div>
</div>
<button name="button" type="submit" class="btn btn-warning m-t m-l" ng-disabled="logoForm.$invalid" translate>{{ 'save' }}</button>
</form>
</div>
<div class="col-md-4">
<form class="custom-logo-container" method="post" action="{{actionUrl.logoBlack}}" novalidate name="logoBlackForm" ng-upload="submited(content)" upload-options-enable-rails-csrf="true" unsaved-warning-form>
<input type="hidden" name="custom_asset[name]" value="logo-black-file">
<input name="_method" type="hidden" ng-value="methods.logoBlack">
<h3 class="m-l" translate>{{ 'logo_(black_background)' }}</h3>
<div class="custom-logo bg-dark" style="background-image: url({{customLogoBlack}});">
<img src="data:image/png;base64," data-src="holder.js/100%x100%/text:&#xf03e;/font:FontAwesome/icon-black-xs" bs-holder ng-show="!customLogoBlack" class="img-responsive">
<img base-sixty-four-image="customLogoBlack" ng-show="customLogoBlack && customLogoBlack.base64">
<img ng-src="{{customLogoBlack.custom_asset_file_attributes.attachment_url}}" alt="{{customLogoBlack.custom_asset_file_attributes.attachment}}" ng-show="customLogoBlack && customLogoBlack.custom_asset_file_attributes" />
<div class="tools-box">
<div class="btn-group">
<div class="btn btn-default btn-file">
<i class="fa fa-edit"></i> {{ 'change_the_logo' | translate }}
<input type="file"
accept="image/png,image/x-png"
name="custom_asset[custom_asset_file_attributes][attachment]"
ng-model="customLogoBlack"
base-sixty-four-input
required />
</div>
</div>
</div>
</div>
<button name="button" type="submit" class="btn btn-warning m-t m-l" ng-disabled="logoBlackForm.$invalid" translate>{{ 'save' }}</button>
</form>
</div>
<div class="col-md-4">
<form class="custom-favicon-container" method="post" action="{{actionUrl.favicon}}" novalidate name="faviconForm" ng-upload="submited(content)" upload-options-enable-rails-csrf="true" unsaved-warning-form>
<input type="hidden" name="custom_asset[name]" value="favicon-file">
<input name="_method" type="hidden" ng-value="methods.favicon">
<h3 class="m-l" translate>{{ 'favicon' }}</h3>
<div class="custom-favicon" style="background-image: url({{customFavicon}});">
<img src="data:image/png;base64," data-src="holder.js/32x32/text:&#xf03e;/font:FontAwesome/icon-xs" bs-holder ng-show="!customFavicon" class="img-responsive">
<img base-sixty-four-image="customFavicon" ng-show="customFavicon && customFavicon.base64">
<img ng-src="{{customFavicon.custom_asset_file_attributes.attachment_url}}" alt="{{customFavicon.custom_asset_file_attributes.attachment}}" ng-show="customFavicon && customFavicon.custom_asset_file_attributes" />
<div class="tools-box">
<div class="btn-group">
<div class="btn btn-default btn-file">
<i class="fa fa-edit"></i> {{ 'change_the_favicon' | translate }}
<input type="file"
accept="image/png,image/x-png,image/x-icon,image/ico,image/vnd.microsoft.icon"
name="custom_asset[custom_asset_file_attributes][attachment]"
ng-model="customFavicon"
base-sixty-four-input
required />
</div>
</div>
</div>
</div>
<button name="button" type="submit" class="btn btn-warning m-t m-l" ng-disabled="faviconForm.$invalid" translate>{{ 'save' }}</button>
</form>
</div>
</div>
<div class="row m-t m-l-xs">
<div class="col-md-4">
<h4 translate>{{ 'main_colour' }}</h4>
<form role="form" class="form-inline" name="mainColorForm" novalidate>
<div class="form-group">
<div class="input-group">
<div class="input-group-addon">
<i class="fa fa-paint-brush"></i>
</div>
<input type="text" minicolors ng-model="mainColorSetting.value" class="form-control" placeholder="{{ 'primary' | translate}}"/>
</div>
</div>
<div class="form-group">
<button name="button" class="btn btn-warning" ng-click="save(mainColorSetting)" translate>{{ 'save' }}</button>
</div>
</form>
</div>
<div class="col-md-4">
<h4 translate>{{ 'secondary_colour' }}</h4>
<form role="form" class="form-inline" name="secondColorForm" novalidate>
<div class="form-group">
<div class="input-group">
<div class="input-group-addon">
<i class="fa fa-paint-brush"></i>
</div>
<input type="text" minicolors ng-model="secondColorSetting.value" class="form-control" placeholder="{{ 'secondary' | translate}}"/>
</div>
</div>
<div class="form-group">
<button name="button" class="btn btn-warning" ng-click="save(secondColorSetting)" translate>{{ 'save' }}</button>
</div>
</form>
</div>
</div>
<div class="row m-t">
<div class="col-md-4">
<form class="custom-profile-image-container" method="post" action="{{actionUrl.profileImage}}" novalidate name="profileImageForm" ng-upload="submited(content)" upload-options-enable-rails-csrf="true" unsaved-warning-form>
<input type="hidden" name="custom_asset[name]" value="profile-image-file">
<input name="_method" type="hidden" ng-value="methods.profileImage">
<h3 class="m-l" translate>{{ 'background_picture_of_the_profile_banner' }}</h3>
<div class="custom-profile-image" style="background-image: url({{profileImage}});">
<img src="data:image/png;base64," data-src="holder.js/100%x100%/text:&#xf03e;/font:FontAwesome/icon-xs" bs-holder ng-show="!profileImage" class="img-responsive">
<img base-sixty-four-image="profileImage" ng-show="profileImage && profileImage.base64">
<img ng-src="{{profileImage.custom_asset_file_attributes.attachment_url}}" alt="{{profileImage.custom_asset_file_attributes.attachment}}" ng-show="profileImage && profileImage.custom_asset_file_attributes" />
<div class="tools-box">
<div class="btn-group">
<div class="btn btn-default btn-file">
<i class="fa fa-edit"></i> {{ 'change_the_profile_banner' | translate }}
<input type="file"
accept="image/png,image/x-png"
name="custom_asset[custom_asset_file_attributes][attachment]"
ng-model="profileImage"
base-sixty-four-input
required />
</div>
</div>
</div>
</div>
<button name="button" type="submit" class="btn btn-warning m-t m-l" ng-disabled="profileImageForm.$invalid" translate>{{ 'save' }}</button>
</form>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,29 @@
<div class="panel panel-default m-t-md">
<div class="panel-body">
<div class="row">
<div class="col-md-6">
<h4 translate>{{ 'news_of_the_home_page' }}</h4>
<div ng-model="homeBlogpostSetting.value" class="well" medium-editor options='{"placeholder": "{{ "type_your_news_here" | translate }}",
"buttons": ["bold", "italic", "anchor", "header1", "header2" ]}'></div>
<span class="help-block text-info text-xs"><i class="fa fa-lightbulb-o"></i> {{ 'leave_it_empty_to_not_bring_up_any_news_on_the_home_page' | translate }}</span>
<button name="button" class="btn btn-warning" ng-click="save(homeBlogpostSetting)" translate>{{ 'save' }}</button>
</div>
<div class="col-md-6">
<h4 translate>{{ 'twitter_stream' }}</h4>
<form role="form" class="form-inline" name="twitterForm" novalidate>
<div class="form-group">
<div class="input-group">
<div class="input-group-addon">
<i class="fa fa-twitter"></i>
</div>
<input type="text" ng-model="twitterSetting.value" class="form-control" placeholder="{{ 'name_of_the_twitter_account' | translate }}"/>
</div>
</div>
<div class="form-group">
<button name="button" class="btn btn-warning" ng-click="save(twitterSetting)" translate>{{ 'save' }}</button>
</div>
</form>
</div>
</div>
</div>
</div>

View File

@ -1,491 +0,0 @@
<section class="heading b-b">
<div class="row no-gutter">
<div class="col-xs-2 col-sm-2 col-md-1">
<section class="heading-btn">
<a href="#" ng-click="backPrevLocation($event)"><i class="fa fa-long-arrow-left "></i></a>
</section>
</div>
<div class="col-xs-10 col-sm-10 col-md-8 b-l">
<section class="heading-title">
<h1 translate>{{ 'customize_the_application' }}</h1>
</section>
</div>
</div>
</section>
<section class="m-lg">
<div class="row">
<div class="col-md-12">
<uib-tabset justified="true">
<uib-tab heading="{{ 'general' | translate }}">
<div class="panel panel-default m-t-md">
<div class="panel-heading">
<span class="font-sbold" translate>{{ 'title' }}</span>
</div>
<div class="panel-body">
<div class="row m-t-lg">
<div class="col-md-4">
<form role="form" novalidate>
<label for="fablabName" class="control-label m-r" translate>{{ 'fablab_title' }}</label>
<div class="form-group">
<div class="input-group">
<div class="input-group-addon"><i class="fa fa-font"></i></div>
<input type="text" id="fablabName" ng-model="fablabName.value" class="form-control" placeholder="{{ 'fablab_name' | translate }}"/>
</div>
</div>
<button name="button" class="btn btn-warning" ng-click="save(fablabName)" translate>{{ 'save' }}</button>
</form>
</div>
<div class="col-md-4 col-md-offset-1">
<form role="form" novalidate>
<h4 class="control-label m-r" translate>{{ 'title_concordance' }}</h4>
<div class="form-group">
<input type="radio" name="nameGenre" id="nameGenreMale" ng-model="nameGenre.value" ng-value="'male'" />
<label for="nameGenreMale">{{ 'male' | translate }} <span style="font-weight: normal">{{ 'eg' | translate }} <cite>{{ 'about' | translate }} <strong translate>{{ 'male_preposition' }}</strong> {{fablabName.value}}</cite></span></label>
<br/>
<input type="radio" name="nameGenre" id="nameGenreFemale" ng-model="nameGenre.value" ng-value="'female'" />
<label for="nameGenreFemale">{{ 'female' | translate }} <span style="font-weight: normal">{{ 'eg' | translate }} <cite>{{ 'about' | translate }} <strong translate>{{ 'female_preposition' }}</strong> {{fablabName.value}}</cite></span></label>
</div>
<button name="button" class="btn btn-warning" ng-click="save(nameGenre)" translate>{{ 'save' }}</button>
</form>
</div>
</div>
</div>
</div>
<div class="panel panel-default m-t-lg">
<div class="panel-heading">
<span class="font-sbold" translate>{{ 'customize_information_messages' }}</span>
</div>
<div class="panel-body">
<div class="row">
<div class="col-md-3">
<h4 translate>{{ 'message_of_the_machine_booking_page' }}</h4>
<div ng-model="machineExplicationsAlert.value" medium-editor options='{"placeholder": "{{ "type_the_message_content" | translate }}",
"buttons": ["bold", "italic", "unorderedlist", "header2" ]
}'>
</div>
<button name="button" class="btn btn-warning" ng-click="save(machineExplicationsAlert)" translate>{{ 'save' }}</button>
</div>
<div class="col-md-3">
<h4 translate>{{ 'warning_message_of_the_training_booking_page'}}</h4>
<div ng-model="trainingExplicationsAlert.value" medium-editor options='{"placeholder": "{{ "type_the_message_content" | translate }}",
"buttons": ["bold", "italic", "unorderedlist", "header2" ]
}'>
</div>
<button name="button" class="btn btn-warning" ng-click="save(trainingExplicationsAlert)" translate>{{ 'save' }}</button>
</div>
<div class="col-md-3">
<h4 translate>{{ 'information_message_of_the_training_reservation_page'}}</h4>
<div ng-model="trainingInformationMessage.value" medium-editor options='{"placeholder": "{{ "type_the_message_content" | translate }}",
"buttons": ["bold", "italic", "unorderedlist", "header2" ]
}'>
</div>
<button name="button" class="btn btn-warning" ng-click="save(trainingInformationMessage)" translate>{{ 'save' }}</button>
</div>
<div class="col-md-3">
<h4 translate>{{ 'message_of_the_subscriptions_page' }}</h4>
<div ng-model="subscriptionExplicationsAlert.value" medium-editor options='{"placeholder": "{{ "type_the_message_content" | translate }}",
"buttons": ["bold", "italic", "unorderedlist", "header2" ]
}'>
</div>
<button name="button" class="btn btn-warning" ng-click="save(subscriptionExplicationsAlert)" translate>{{ 'save' }}</button>
</div>
</div>
<div class="row">
<div class="col-md-3">
<h4 translate>{{ 'message_of_the_event_page_relative_to_the_reduced_rate_availability_conditions' }}</h4>
<div ng-model="eventReducedAmountAlert.value" medium-editor options='{"placeholder": "{{ "type_the_message_content" | translate }}",
"buttons": ["bold", "italic", "unorderedlist", "header2" ]
}'>
</div>
<button name="button" class="btn btn-warning" ng-click="save(eventReducedAmountAlert)" translate>{{ 'save' }}</button>
</div>
</div>
</div>
</div>
<div class="panel panel-default m-t-lg">
<div class="panel-heading">
<span class="font-sbold" translate>{{ 'legal_documents'}}</span>
</div>
<div class="panel-body">
<div class="alert alert-warning m-t" translate>
{{ 'if_these_documents_are_not_filled_no_consent_about_them_will_be_asked_to_the_user' }}
</div>
<div class="row">
<form class="col-md-6" method="post" action="{{actionUrl.cgv}}" novalidate name="cgvForm" ng-upload="submited(content)" ng-submit="addLoader('cgv')" upload-options-enable-rails-csrf="true" unsaved-warning-form>
<input type="hidden" name="custom_asset[name]" value="cgv-file">
<input name="_method" type="hidden" ng-value="methods.cgv">
<label for="moveDelay" class="control-label m-r" translate>{{ 'general_terms_and_conditions_(T&C)' }}</label>
<div class="form-group">
<div class="fileinput input-group" data-provides="fileinput" ng-class="fileinputClass(cgvFile.custom_asset_file_attributes.attachment)">
<div class="form-control" data-trigger="fileinput">
<i class="glyphicon glyphicon-file fileinput-exists"></i> <span class="fileinput-filename">{{cgvFile.custom_asset_file_attributes.attachment}}</span>
</div>
<span class="input-group-addon btn btn-default btn-file">
<span class="fileinput-new" translate>{{ 'browse' }}</span>
<span class="fileinput-exists" translate>{{ 'change' }}</span>
<input type="file"
name="custom_asset[custom_asset_file_attributes][attachment]"
accept=".pdf"
required />
</span>
</div>
</div>
<button name="button" type="submit" ng-class="{'btn-loading':loader.cgv}" ng-disabled="cgvForm.$invalid" class="btn btn-warning" translate>{{ 'save' }}</button>
</form>
</div>
<div class="row m-t-xl">
<form class="col-md-6" method="post" action="{{actionUrl.cgu}}" novalidate name="cguForm" ng-upload="submited(content)" ng-submit="addLoader('cgu')" upload-options-enable-rails-csrf="true" unsaved-warning-form>
<input type="hidden" name="custom_asset[name]" value="cgu-file">
<input name="_method" type="hidden" ng-value="methods.cgu">
<label for="moveDelay" class="control-label m-r" translate>{{ 'terms_of_service_(TOS)' }}</label>
<div class="form-group">
<div class="fileinput input-group" data-provides="fileinput" ng-class="fileinputClass(cguFile.custom_asset_file_attributes.attachment)">
<div class="form-control" data-trigger="fileinput">
<i class="glyphicon glyphicon-file fileinput-exists"></i> <span class="fileinput-filename">{{cguFile.custom_asset_file_attributes.attachment}}</span>
</div>
<span class="input-group-addon btn btn-default btn-file">
<span class="fileinput-new" translate>{{ 'browse' }}</span>
<span class="fileinput-exists" translate>{{ 'change' }}</span>
<input type="file"
name="custom_asset[custom_asset_file_attributes][attachment]"
accept=".pdf"
required />
</span>
</div>
</div>
<button name="button" type="submit" ng-class="{'btn-loading':loader.cgu}" ng-disabled="cguForm.$invalid" class="btn btn-warning" translate>{{ 'save' }}</button>
</form>
</div>
</div>
</div>
<div class="panel panel-default m-t-lg">
<div class="panel-heading">
<span class="font-sbold" translate>{{ 'customize_the_graphics' }}</span>
</div>
<div class="panel-body">
<div class="alert alert-warning m-t">
<span translate>{{ 'for_an_optimal_rendering_the_logo_image_must_be_at_the_PNG_format_with_a_transparent_background_and_with_an_aspect_ratio_3.5_times_wider_than_the_height' }}</span><br/>
<span translate>{{ 'concerning_the_favicon_it_must_be_at_ICO_format_with_a_size_of_16x16_pixels' }}</span><br/>
<br/>
<span translate>{{ 'remember_to_refresh_the_page_for_the_changes_to_take_effect' }}</span>
</div>
<div class="row">
<div class="col-md-4">
<form class="custom-logo-container" method="post" action="{{actionUrl.logo}}" novalidate name="logoForm" ng-upload="submited(content)" upload-options-enable-rails-csrf="true" unsaved-warning-form>
<input type="hidden" name="custom_asset[name]" value="logo-file">
<input name="_method" type="hidden" ng-value="methods.logo">
<h3 class="m-l" translate>{{ 'logo_(white_background)' }}</h3>
<div class="custom-logo" style="background-image: url({{customLogo}});">
<img src="data:image/png;base64," data-src="holder.js/100%x100%/text:&#xf03e;/font:FontAwesome/icon-xs" bs-holder ng-show="!customLogo" class="img-responsive">
<img base-sixty-four-image="customLogo" ng-show="customLogo && customLogo.base64">
<img ng-src="{{customLogo.custom_asset_file_attributes.attachment_url}}" alt="{{customLogo.custom_asset_file_attributes.attachment}}" ng-show="customLogo && customLogo.custom_asset_file_attributes" />
<div class="tools-box">
<div class="btn-group">
<div class="btn btn-default btn-file">
<i class="fa fa-edit"></i> {{ 'change_the_logo' | translate }}
<input type="file"
accept="image/png,image/x-png"
name="custom_asset[custom_asset_file_attributes][attachment]"
ng-model="customLogo"
base-sixty-four-input
required />
</div>
</div>
</div>
</div>
<button name="button" type="submit" class="btn btn-warning m-t m-l" ng-disabled="logoForm.$invalid" translate>{{ 'save' }}</button>
</form>
</div>
<div class="col-md-4">
<form class="custom-logo-container" method="post" action="{{actionUrl.logoBlack}}" novalidate name="logoBlackForm" ng-upload="submited(content)" upload-options-enable-rails-csrf="true" unsaved-warning-form>
<input type="hidden" name="custom_asset[name]" value="logo-black-file">
<input name="_method" type="hidden" ng-value="methods.logoBlack">
<h3 class="m-l" translate>{{ 'logo_(black_background)' }}</h3>
<div class="custom-logo bg-dark" style="background-image: url({{customLogoBlack}});">
<img src="data:image/png;base64," data-src="holder.js/100%x100%/text:&#xf03e;/font:FontAwesome/icon-black-xs" bs-holder ng-show="!customLogoBlack" class="img-responsive">
<img base-sixty-four-image="customLogoBlack" ng-show="customLogoBlack && customLogoBlack.base64">
<img ng-src="{{customLogoBlack.custom_asset_file_attributes.attachment_url}}" alt="{{customLogoBlack.custom_asset_file_attributes.attachment}}" ng-show="customLogoBlack && customLogoBlack.custom_asset_file_attributes" />
<div class="tools-box">
<div class="btn-group">
<div class="btn btn-default btn-file">
<i class="fa fa-edit"></i> {{ 'change_the_logo' | translate }}
<input type="file"
accept="image/png,image/x-png"
name="custom_asset[custom_asset_file_attributes][attachment]"
ng-model="customLogoBlack"
base-sixty-four-input
required />
</div>
</div>
</div>
</div>
<button name="button" type="submit" class="btn btn-warning m-t m-l" ng-disabled="logoBlackForm.$invalid" translate>{{ 'save' }}</button>
</form>
</div>
<div class="col-md-4">
<form class="custom-favicon-container" method="post" action="{{actionUrl.favicon}}" novalidate name="faviconForm" ng-upload="submited(content)" upload-options-enable-rails-csrf="true" unsaved-warning-form>
<input type="hidden" name="custom_asset[name]" value="favicon-file">
<input name="_method" type="hidden" ng-value="methods.favicon">
<h3 class="m-l" translate>{{ 'favicon' }}</h3>
<div class="custom-favicon" style="background-image: url({{customFavicon}});">
<img src="data:image/png;base64," data-src="holder.js/32x32/text:&#xf03e;/font:FontAwesome/icon-xs" bs-holder ng-show="!customFavicon" class="img-responsive">
<img base-sixty-four-image="customFavicon" ng-show="customFavicon && customFavicon.base64">
<img ng-src="{{customFavicon.custom_asset_file_attributes.attachment_url}}" alt="{{customFavicon.custom_asset_file_attributes.attachment}}" ng-show="customFavicon && customFavicon.custom_asset_file_attributes" />
<div class="tools-box">
<div class="btn-group">
<div class="btn btn-default btn-file">
<i class="fa fa-edit"></i> {{ 'change_the_favicon' | translate }}
<input type="file"
accept="image/png,image/x-png,image/x-icon,image/ico,image/vnd.microsoft.icon"
name="custom_asset[custom_asset_file_attributes][attachment]"
ng-model="customFavicon"
base-sixty-four-input
required />
</div>
</div>
</div>
</div>
<button name="button" type="submit" class="btn btn-warning m-t m-l" ng-disabled="faviconForm.$invalid" translate>{{ 'save' }}</button>
</form>
</div>
</div>
<div class="row m-t m-l-xs">
<div class="col-md-4">
<h4 translate>{{ 'main_colour' }}</h4>
<form role="form" class="form-inline" name="mainColorForm" novalidate>
<div class="form-group">
<div class="input-group">
<div class="input-group-addon">
<i class="fa fa-paint-brush"></i>
</div>
<input type="text" minicolors ng-model="mainColorSetting.value" class="form-control" placeholder="{{ 'primary' | translate}}"/>
</div>
</div>
<div class="form-group">
<button name="button" class="btn btn-warning" ng-click="save(mainColorSetting)" translate>{{ 'save' }}</button>
</div>
</form>
</div>
<div class="col-md-4">
<h4 translate>{{ 'secondary_colour' }}</h4>
<form role="form" class="form-inline" name="secondColorForm" novalidate>
<div class="form-group">
<div class="input-group">
<div class="input-group-addon">
<i class="fa fa-paint-brush"></i>
</div>
<input type="text" minicolors ng-model="secondColorSetting.value" class="form-control" placeholder="{{ 'secondary' | translate}}"/>
</div>
</div>
<div class="form-group">
<button name="button" class="btn btn-warning" ng-click="save(secondColorSetting)" translate>{{ 'save' }}</button>
</div>
</form>
</div>
</div>
<div class="row m-t">
<div class="col-md-4">
<form class="custom-profile-image-container" method="post" action="{{actionUrl.profileImage}}" novalidate name="profileImageForm" ng-upload="submited(content)" upload-options-enable-rails-csrf="true" unsaved-warning-form>
<input type="hidden" name="custom_asset[name]" value="profile-image-file">
<input name="_method" type="hidden" ng-value="methods.profileImage">
<h3 class="m-l" translate>{{ 'background_picture_of_the_profile_banner' }}</h3>
<div class="custom-profile-image" style="background-image: url({{profileImage}});">
<img src="data:image/png;base64," data-src="holder.js/100%x100%/text:&#xf03e;/font:FontAwesome/icon-xs" bs-holder ng-show="!profileImage" class="img-responsive">
<img base-sixty-four-image="profileImage" ng-show="profileImage && profileImage.base64">
<img ng-src="{{profileImage.custom_asset_file_attributes.attachment_url}}" alt="{{profileImage.custom_asset_file_attributes.attachment}}" ng-show="profileImage && profileImage.custom_asset_file_attributes" />
<div class="tools-box">
<div class="btn-group">
<div class="btn btn-default btn-file">
<i class="fa fa-edit"></i> {{ 'change_the_profile_banner' | translate }}
<input type="file"
accept="image/png,image/x-png"
name="custom_asset[custom_asset_file_attributes][attachment]"
ng-model="profileImage"
base-sixty-four-input
required />
</div>
</div>
</div>
</div>
<button name="button" type="submit" class="btn btn-warning m-t m-l" ng-disabled="profileImageForm.$invalid" translate>{{ 'save' }}</button>
</form>
</div>
</div>
</div>
</div>
</uib-tab>
<uib-tab heading="{{ 'home_page' | translate }}">
<div class="panel panel-default m-t-md">
<div class="panel-body">
<div class="row">
<div class="col-md-6">
<h4 translate>{{ 'news_of_the_home_page' }}</h4>
<div ng-model="homeBlogpostSetting.value" class="well" medium-editor options='{"placeholder": "{{ "type_your_news_here" | translate }}",
"buttons": ["bold", "italic", "anchor", "header1", "header2" ]}'></div>
<span class="help-block text-info text-xs"><i class="fa fa-lightbulb-o"></i> {{ 'leave_it_empty_to_not_bring_up_any_news_on_the_home_page' | translate }}</span>
<button name="button" class="btn btn-warning" ng-click="save(homeBlogpostSetting)" translate>{{ 'save' }}</button>
</div>
<div class="col-md-6">
<h4 translate>{{ 'twitter_stream' }}</h4>
<form role="form" class="form-inline" name="twitterForm" novalidate>
<div class="form-group">
<div class="input-group">
<div class="input-group-addon">
<i class="fa fa-twitter"></i>
</div>
<input type="text" ng-model="twitterSetting.value" class="form-control" placeholder="{{ 'name_of_the_twitter_account' | translate }}"/>
</div>
</div>
<div class="form-group">
<button name="button" class="btn btn-warning" ng-click="save(twitterSetting)" translate>{{ 'save' }}</button>
</div>
</form>
</div>
</div>
</div>
</div>
</uib-tab>
<uib-tab heading="{{ 'about' | translate }}">
<div class="panel panel-default m-t-md">
<div class="panel-body">
<div class="row m-t-lg m-b-lg">
<div class="col-sm-offset-4 col-sm-4">
<h1 ng-model="aboutTitleSetting.value" medium-editor options='{"placeholder": "{{ "title_of_the_about_page" | translate }}", "disableToolbar": true, "disableReturn": false}' class="text-u-c"></h1>
<span class="help-block text-info text-xs"><i class="fa fa-lightbulb-o"></i> {{ 'shift_enter_to_force_carriage_return' | translate }}</span>
<button name="button" class="btn btn-warning" ng-click="save(aboutTitleSetting)" translate>{{ 'save' }}</button>
</div>
</div>
<div class="row">
<div class="col-md-4 col-md-offset-1">
<div class="text-justify" ng-model="aboutBodySetting.value" medium-editor options='{"placeholder": "{{ "input_the_main_content" | translate }}",
"buttons": ["bold", "italic", "anchor", "header1", "header2" ]
}'>
</div>
<button name="button" class="btn btn-warning" ng-click="save(aboutBodySetting)" translate>{{ 'save' }}</button>
</div>
<div class="col-md-4 col-md-offset-2">
<div ng-model="aboutContactsSetting.value" medium-editor options='{"placeholder": "{{ "input_the_fablab_contacts" | translate }}",
"buttons": ["bold", "italic", "anchor", "header1", "header2" ]
}'>
</div>
<span class="help-block text-info text-xs"><i class="fa fa-lightbulb-o"></i> {{ 'shift_enter_to_force_carriage_return' | translate }}</span>
<button name="button" class="btn btn-warning" ng-click="save(aboutContactsSetting)" translate>{{ 'save' }}</button>
</div>
</div>
</div>
</div>
</uib-tab>
<uib-tab heading="{{ 'reservations' | translate }}">
<div class="panel panel-default m-t-lg">
<div class="panel-heading">
<span class="font-sbold" translate>{{ 'reservations_parameters' }}</span>
</div>
<div class="panel-body">
<div>
<div class="row">
<h3 class="m-l" translate>{{ 'confine_the_booking_agenda' }}</h3>
<div class="col-md-2">
<h4 translate>{{ 'opening_time' }}</h4>
<uib-timepicker ng-model="windowStart.value" hour-step="timepicker.hstep" minute-step="timepicker.mstep" show-meridian="false"></uib-timepicker>
</div>
<div class="col-md-4 m-t">
<button name="button" class="btn btn-warning m-l" ng-click="save(windowStart)" translate>{{ 'save' }}</button>
</div>
<div class="col-md-2">
<h4 translate>{{ 'closing_time' }}</h4>
<uib-timepicker ng-model="windowEnd.value" hour-step="timepicker.hstep" minute-step="timepicker.mstep" show-meridian="false"></uib-timepicker>
</div>
<div class="col-md-4 m-t">
<button name="button" class="btn btn-warning m-l" ng-click="save(windowEnd)" translate>{{ 'save' }}</button>
</div>
</div>
<div class="row">
<h3 class="m-l" translate>{{ 'ability_for_the_users_to_move_their_reservations' }}</h3>
<div class="form-group m-l">
<label for="enableMove" class="control-label m-r" translate>{{ 'reservations_shifting' }}</label>
<input bs-switch
ng-model="enableMove.value"
id="enableMove"
type="checkbox"
class="form-control"
switch-on-text="{{ 'enabled' | translate }}"
switch-off-text="{{ 'disabled' | translate }}"
switch-animate="true"/>
<button name="button" class="btn btn-warning m-l" ng-click="save(enableMove)" translate>{{ 'save' }}</button>
</div>
</div>
<div class="row" ng-show="enableMove.value">
<div class="col-md-4">
<label for="moveDelay" class="control-label m-r" translate>{{ 'prior_period_(hours)' }}</label>
<div class="form-group">
<div class="input-group">
<div class="input-group-addon">
<i class="fa fa-clock-o"></i>
</div>
<input type="number" class="form-control" id="moveDelay" ng-model="moveDelay.value">
</div>
</div>
<button name="button" class="btn btn-warning" ng-click="save(moveDelay)" translate>{{ 'save' }}</button>
</div>
</div>
<div class="row">
<h3 class="m-l" translate>{{ 'ability_for_the_users_to_cancel_their_reservations' }}</h3>
<div class="form-group m-l">
<label for="enableCancel" class="control-label m-r" translate>{{ 'reservations_cancelling' }}</label>
<input bs-switch
ng-model="enableCancel.value"
id="enableCancel"
type="checkbox"
class="form-control"
switch-on-text="{{ 'enabled' | translate }}"
switch-off-text="{{ 'disabled' | translate }}"
switch-animate="true"/>
<button name="button" class="btn btn-warning m-l" ng-click="save(enableCancel)" translate>{{ 'save' }}</button>
</div>
</div>
<div class="row" ng-show="enableCancel.value">
<div class="col-md-4">
<label for="moveDelay" class="control-label m-r" translate>{{ 'prior_period_(hours)' }}</label>
<div class="form-group">
<div class="input-group">
<div class="input-group-addon">
<i class="fa fa-clock-o"></i>
</div>
<input type="number" class="form-control" id="cancelDelay" ng-model="cancelDelay.value">
</div>
</div>
<button name="button" class="btn btn-warning" ng-click="save(cancelDelay)" translate>{{ 'save' }}</button>
</div>
</div>
</div>
</div>
</div>
</uib-tab>
</uib-tabset>
</div>
</div>
</section>

View File

@ -0,0 +1,41 @@
<section class="heading b-b">
<div class="row no-gutter">
<div class="col-xs-2 col-sm-2 col-md-1">
<section class="heading-btn">
<a href="#" ng-click="backPrevLocation($event)"><i class="fa fa-long-arrow-left "></i></a>
</section>
</div>
<div class="col-xs-10 col-sm-10 col-md-8 b-l">
<section class="heading-title">
<h1 translate>{{ 'customize_the_application' }}</h1>
</section>
</div>
</div>
</section>
<section class="m-lg">
<div class="row">
<div class="col-md-12">
<uib-tabset justified="true">
<uib-tab heading="{{ 'general' | translate }}">
<ng-include src="'<%= asset_path 'admin/settings/general.html' %>'"></ng-include>
</uib-tab>
<uib-tab heading="{{ 'home_page' | translate }}">
<ng-include src="'<%= asset_path 'admin/settings/home_page.html' %>'"></ng-include>
</uib-tab>
<uib-tab heading="{{ 'about' | translate }}">
<ng-include src="'<%= asset_path 'admin/settings/about.html' %>'"></ng-include>
</uib-tab>
<uib-tab heading="{{ 'reservations' | translate }}">
<ng-include src="'<%= asset_path 'admin/settings/reservations.html' %>'"></ng-include>
</uib-tab>
</uib-tabset>
</div>
</div>
</section>

View File

@ -0,0 +1,124 @@
<div class="panel panel-default m-t-lg">
<div class="panel-heading">
<span class="font-sbold" translate>{{ 'reservations_parameters' }}</span>
</div>
<div class="panel-body">
<div>
<div class="row">
<h3 class="m-l" translate>{{ 'confine_the_booking_agenda' }}</h3>
<div class="col-md-2">
<h4 translate>{{ 'opening_time' }}</h4>
<uib-timepicker ng-model="windowStart.value" hour-step="timepicker.hstep" minute-step="timepicker.mstep" show-meridian="false"></uib-timepicker>
</div>
<div class="col-md-4 m-t">
<button name="button" class="btn btn-warning m-l" ng-click="save(windowStart)" translate>{{ 'save' }}</button>
</div>
<div class="col-md-2">
<h4 translate>{{ 'closing_time' }}</h4>
<uib-timepicker ng-model="windowEnd.value" hour-step="timepicker.hstep" minute-step="timepicker.mstep" show-meridian="false"></uib-timepicker>
</div>
<div class="col-md-4 m-t">
<button name="button" class="btn btn-warning m-l" ng-click="save(windowEnd)" translate>{{ 'save' }}</button>
</div>
</div>
<div class="row">
<h3 class="m-l" translate>{{ 'ability_for_the_users_to_move_their_reservations' }}</h3>
<div class="form-group m-l">
<label for="enableMove" class="control-label m-r" translate>{{ 'reservations_shifting' }}</label>
<input bs-switch
ng-model="enableMove.value"
id="enableMove"
type="checkbox"
class="form-control"
switch-on-text="{{ 'enabled' | translate }}"
switch-off-text="{{ 'disabled' | translate }}"
switch-animate="true"/>
<button name="button" class="btn btn-warning m-l" ng-click="save(enableMove)" translate>{{ 'save' }}</button>
</div>
</div>
<div class="row" ng-show="enableMove.value">
<form class="col-md-4" name="moveDelayForm">
<label for="moveDelay" class="control-label m-r" translate>{{ 'prior_period_(hours)' }}</label>
<div class="form-group">
<div class="input-group">
<div class="input-group-addon">
<i class="fa fa-clock-o"></i>
</div>
<input type="number" class="form-control" id="moveDelay" ng-model="moveDelay.value" min="0" ng-required="enableMove.value">
</div>
</div>
<button name="button" class="btn btn-warning" ng-click="save(moveDelay)" ng-disabled="moveDelayForm.$invalid" translate>{{ 'save' }}</button>
</form>
</div>
<div class="row">
<h3 class="m-l" translate>{{ 'ability_for_the_users_to_cancel_their_reservations' }}</h3>
<div class="form-group m-l">
<label for="enableCancel" class="control-label m-r" translate>{{ 'reservations_cancelling' }}</label>
<input bs-switch
ng-model="enableCancel.value"
id="enableCancel"
type="checkbox"
class="form-control"
switch-on-text="{{ 'enabled' | translate }}"
switch-off-text="{{ 'disabled' | translate }}"
switch-animate="true"/>
<button name="button" class="btn btn-warning m-l" ng-click="save(enableCancel)" translate>{{ 'save' }}</button>
</div>
</div>
<div class="row" ng-show="enableCancel.value">
<form class="col-md-4" name="cancelDelayForm">
<label for="cancelDelay" class="control-label m-r" translate>{{ 'prior_period_(hours)' }}</label>
<div class="form-group">
<div class="input-group">
<div class="input-group-addon">
<i class="fa fa-clock-o"></i>
</div>
<input type="number" class="form-control" id="cancelDelay" ng-model="cancelDelay.value" min="0" ng-required="enableCancel.value">
</div>
</div>
<button name="button" class="btn btn-warning" ng-click="save(cancelDelay)" ng-disabled="cancelDelayForm.$invalid" translate>{{ 'save' }}</button>
</form>
</div>
</div>
</div>
</div>
<div class="panel panel-default m-t-lg">
<div class="panel-heading">
<span class="font-sbold" translate>{{ 'reservations_reminders' }}</span>
</div>
<div class="panel-body">
<div class="row">
<h3 class="m-l" translate>{{ 'notification_sending_before_the_reservation_occurs' }}</h3>
<div class="form-group m-l">
<label for="enableReminder" class="control-label m-r" translate>{{ 'reservations_reminders' }}</label>
<input bs-switch
ng-model="enableReminder.value"
id="enableReminder"
type="checkbox"
class="form-control"
switch-on-text="{{ 'enabled' | translate }}"
switch-off-text="{{ 'disabled' | translate }}"
switch-animate="true"/>
<button name="button" class="btn btn-warning m-l" ng-click="save(enableReminder)" translate>{{ 'save' }}</button>
</div>
</div>
<div class="row" ng-show="enableReminder.value">
<form class="col-md-4" name="reminderDelayForm">
<label for="reminderDelay" class="control-label m-r" translate>{{ 'prior_period_(hours)' }}</label>
<div class="form-group">
<div class="input-group">
<div class="input-group-addon">
<i class="fa fa-clock-o"></i>
</div>
<input type="number" class="form-control" id="reminderDelay" ng-model="reminderDelay.value" min="0">
</div>
<span class="help-block text-info text-xs">
<i class="fa fa-lightbulb-o"></i> {{ 'default_value_is_24_hours' | translate }}
</span>
</div>
<button name="button" class="btn btn-warning" ng-click="save(reminderDelay)" ng-disabled="reminderDelayForm.$invalid" translate>{{ 'save' }}</button>
</form>
</div>
</div>
</div>

View File

@ -0,0 +1,76 @@
<div class="modal-header">
<img ng-src="{{logoBlack.custom_asset_file_attributes.attachment_url}}" alt="{{logo.custom_asset_file_attributes.attachment}}" class="modal-logo"/>
<h1 translate>{{ 'export_statistics_to_excel' }}</h1>
</div>
<div class="modal-body">
<form>
<div class="radio">
<label><input type="radio" name="scope" ng-model="export.type" value="global" ng-change="setRequest()">{{ 'export_all_statistics' | translate }}</label>
</div>
<div ng-show="export.type == 'global'">
<ul class="list-unstyled">
<li class="row">
<span class="col-md-4" translate>{{ 'start' }}</span>
<div class="input-group black col-md-7 m-r" id="date_pick_start">
<input type="text"
class="form-control"
uib-datepicker-popup="{{exportEnd.format}}"
ng-model="dates.start"
name="startDate"
is-open="exportStart.opened"
min-date="exportStart.minDate"
max-date="exportStart.maxDate"
datepicker-options="exportStart.options"
show-button-bar="false"
placeholder="{{ 'start' | translate }}"
ng-click="toggleStartDatePicker($event)"
ng-change="setRequest()"
required="required"/>
<span class="input-group-btn">
<button type="button" class="btn btn-default btn-search-datepicker" ng-click="toggleStartDatePicker($event)">
<i class="glyphicon glyphicon-calendar"></i>
</button>
</span>
</div>
</li>
<li class="row">
<span class="col-md-4" translate>{{ 'end' }}</span>
<div class="input-group black col-md-7 m-r" id="date_pick_end">
<input type="text"
class="form-control"
uib-datepicker-popup="{{exportEnd.format}}"
ng-model="dates.end"
name="endDate"
is-open="exportEnd.opened"
min-date="exportEnd.minDate"
max-date="exportEnd.maxDate"
datepicker-options="datePickerEnd.options"
show-button-bar="false"
placeholder="{{ 'end' | translate }}"
ng-click="toggleEndDatePicker($event)"
ng-change="setRequest()"
required="required"/>
<span class="input-group-btn">
<button type="button" class="btn btn-default btn-search-datepicker" ng-click="toggleEndDatePicker($event)">
<i class="glyphicon glyphicon-calendar"></i>
</button>
</span>
</div>
</li>
</ul>
</div>
<div class="radio">
<label><input type="radio" name="scope" ng-model="export.type" value="current" ng-change="setRequest()">{{ 'export_the_current_search_results' | translate }}</label>
</div>
</form>
</div>
<div class="modal-footer">
<form role="form" ng-submit="exportData()" name="exportForm" method="post" action="{{ actionUrl }}" class="inline">
<input name="authenticity_token" type="hidden" ng-value="csrfToken"/>
<input name="_method" type="hidden" ng-value="method"/>
<input name="type_key" type="hidden" ng-value="typeKey"/>
<input name="body" type="hidden" ng-value="query"/>
<input type="submit" class="btn btn-info" value="{{ 'export' | translate }}" formtarget="export-frame"/>
</form>
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'cancel' }}</button>
</div>

View File

@ -12,6 +12,8 @@
</div>
<div class="col-xs-12 col-sm-12 col-md-3 b-t hide-b-md">
<section class="heading-actions wrapper">
<a class="btn btn-lg btn-default rounded m-t-sm text-sm" ng-click="exportToExcel()"><i class="fa fa-file-excel-o"></i></a>
<iframe name="export-frame" height="0" width="0" class="none" id="stats-export-frame"></iframe>
<a class="btn btn-lg btn-warning bg-white b-2x rounded m-t-sm upper text-sm" ui-sref="app.admin.stats_graphs" role="button"><i class="fa fa-line-chart"></i> {{ 'evolution' | translate }}</a>
</section>
</div>
@ -26,7 +28,7 @@
<uib-tabset justified="true">
<uib-tab ng-repeat="stat in statistics" heading="{{stat.label}}" select="setActiveTab(stat)" ng-if="stat.table && !(stat.es_type_key == 'subscription' && fablabWithoutPlans)">
<form id="filters_form" name="filters_form" class="form-inline m-t-md m-b-lg" novalidate="novalidate">
<div id="agePickerPane" class="form-group datepicker-container">
<div id="agePickerPane" class="form-group datepicker-container" style="z-index:102;">
<button id="agePickerExpand" class="btn btn-default" type="button" ng-click="agePicker.show = !agePicker.show">
<i class="fa fa-birthday-cake"></i>
<span ng-show="agePicker.start || agePicker.end">
@ -67,7 +69,7 @@
<select ng-model="type.selected" ng-options="type.label for type in stat.types" class="form-control"> </select>
</div>
<div id="customFilterPane" class="form-group datepicker-container m-l-md">
<div id="customFilterPane" class="form-group datepicker-container m-l-md" style="z-index:101;">
<button id="customFilterExpand" class="btn btn-default customMenuButton" type="button" ng-click="customFilter.show = !customFilter.show">
<i class="fa fa-filter"></i>
<span ng-show="!customFilter.criterion.key" class="text-gray" translate>{{ 'custom_filter' }}</span>
@ -170,7 +172,7 @@
<i class="fa fa-caret-up" ng-show="datePicker.show"></i>
</button>
<div class="datepicker-dropdown" ng-show="datePicker.show">
<ul class="list-unstyled">
<ul class="list-unstyled p-xs">
<li class="row">
<span class="col-md-4" translate>{{ 'start' }}</span>
<div class="input-group black col-md-7 m-r" id="date_pick_start">
@ -233,6 +235,7 @@
<li ng-show="selectedIndex.ca">{{ 'revenue_' | translate }} {{sumCA | currency}}</li>
<li>{{ 'average_age' | translate }} {{averageAge}} {{ 'years_old' | translate }}</li>
<li ng-if="!type.active.simple">{{ 'total' | translate }} {{type.active.label}} : {{sumStat}}</li>
<li ng-repeat="custom in type.active.custom_aggregations">{{ custom.field | translate }} {{customAggs[custom.field]}}</li>
</ul>
</div>
@ -287,4 +290,3 @@
</div>
</section>

View File

@ -0,0 +1,122 @@
<form role="form"
name="trainingForm"
class="form-horizontal"
ng-attr-action="{{ actionUrl }}"
ng-upload="submited(content)"
upload-options-enable-rails-csrf="true"
unsaved-warning-form
novalidate>
<input name="_method" type="hidden" ng-value="method">
<section class="panel panel-default bg-light m-lg">
<div class="panel-body m-r">
<uib-alert ng-repeat="alert in alerts" type="{{alert.type}}" close="closeAlert($index)">{{alert.msg}}</uib-alert>
<div class="form-group m-b-lg" ng-class="{'has-error': trainingForm['training[name]'].$dirty && trainingForm['training[name]'].$invalid}">
<label for="name" class="col-sm-2 control-label">{{ 'name' | translate }} *</label>
<div class="col-sm-4">
<input name="training[name]"
ng-model="training.name"
type="text"
class="form-control"
id="training_name"
placeholder="{{'name' | translate}}"
required/>
<span class="help-block" ng-show="trainingForm['training[name]'].$dirty && trainingForm['training[name]'].$error.required" translate>{{ 'name_is_required' }}</span>
</div>
</div>
<div class="form-group m-b-lg">
<label for="training_image" class="col-sm-2 control-label">{{ 'illustration' | translate }} *</label>
<div class="col-sm-10">
<div class="fileinput" data-provides="fileinput" ng-class="fileinputClass(training.training_image)">
<div class="fileinput-new thumbnail" style="width: 334px; height: 250px;">
<img src="data:image/png;base64," data-src="holder.js/100%x100%/text:&#xf03e;/font:FontAwesome/icon" bs-holder ng-if="!training.training_image">
</div>
<div class="fileinput-preview fileinput-exists thumbnail" style="max-width: 334px;">
<img ng-src="{{ training.training_image }}" alt="" />
</div>
<div>
<span class="btn btn-default btn-file">
<span class="fileinput-new">{{ 'add_an_illustration' | translate }} <i class="fa fa-upload fa-fw"></i></span>
<span class="fileinput-exists" translate>{{ 'change' }}</span>
<input type="file"
ng-model="training.training_image"
name="training[training_image_attributes][attachment]"
accept="image/*"
required
bs-jasny-fileinput>
</span>
<a href="#" class="btn btn-danger fileinput-exists" data-dismiss="fileinput" translate>{{ 'delete' }}</a>
</div>
</div>
</div>
</div>
<div class="form-group m-b-xl" ng-class="{'has-error': trainingForm['training[description]'].$dirty && trainingForm['training[description]'].$invalid}">
<label for="training_description" class="col-sm-2 control-label">{{ 'description' | translate }} *</label>
<div class="col-sm-10">
<input type="hidden" name="training[description]" ng-value="training.description" />
<summernote ng-model="training.description" id="training_description" placeholder="" config="summernoteOpts" name="training[description]" required></summernote>
<span class="help-block" ng-show="trainingForm['training[description]'].$dirty && trainingForm['training[description]'].$error.required" translate>{{ 'description_is_required' }}</span>
</div>
</div>
<div class="form-group m-b-lg" ng-class="{'has-error': trainingForm['training[machine_ids]'].$dirty && trainingForm['training[machine_ids]'].$invalid}">
<label for="training_machines" class="col-sm-2 control-label">{{ 'associated_machines' | translate }}</label>
<div class="col-sm-4">
<ui-select multiple ng-model="training.machine_ids" class="form-control" id="training_machines">
<ui-select-match>
<span ng-bind="$item.name"></span>
<input type="hidden" name="training[machine_ids][]" value="{{$item.id}}" />
</ui-select-match>
<ui-select-choices repeat="m.id as m in (machines | filter: $select.search)">
<span ng-bind-html="m.name | highlight: $select.search"></span>
</ui-select-choices>
</ui-select>
</div>
</div>
<div class="form-group m-b-lg" ng-class="{'has-error': trainingForm['training[nb_total_places]'].$dirty && trainingForm['training[nb_total_places]'].$invalid}">
<label for="training_nb_total_places" class="col-sm-2 control-label">{{ 'number_of_tickets' | translate }}</label>
<div class="col-sm-4">
<input ng-model="training.nb_total_places"
type="number"
min="0"
name="training[nb_total_places]"
class="form-control"
id="training_nb_total_places">
</div>
</div>
<div class="form-group">
<label for="training[public_page]" class="control-label col-sm-2" translate>
{{ 'public_page' }}
</label>
<div class="col-sm-10">
<input bs-switch
ng-model="training.public_page"
name="training[public_page]"
type="checkbox"
class="form-control"
switch-on-text="{{ 'yes' | translate }}"
switch-off-text="{{ 'no' | translate }}"
switch-animate="true"/>
<input type="hidden" name="training[public_page]" value="{{training.public_page}}">
</div>
</div>
</div> <!-- ./panel-body -->
<div class="panel-footer no-padder">
<input type="submit"
value="{{ 'validate_your_training' | translate }}"
class="r-b btn-valid btn btn-warning btn-block p-lg btn-lg text-u-c"
ng-disabled="trainingForm.$invalid"/>
</div>
</section>
</form>

View File

@ -0,0 +1,27 @@
<section class="heading b-b">
<div class="row no-gutter">
<div class="col-xs-2 col-sm-2 col-md-1">
<section class="heading-btn">
<a ng-click="cancel()"><i class="fa fa-long-arrow-left"></i></a>
</section>
</div>
<div class="col-xs-10 col-sm-10 col-md-8 b-l b-r-md">
<section class="heading-title">
<h1>{{ training.name }}</h1>
</section>
</div>
<div class="col-xs-12 col-sm-12 col-md-3 b-t hide-b-md">
<section class="heading-actions wrapper">
<div class="btn btn-lg btn-block btn-default rounded m-t-xs" ng-click="cancel()" translate>{{ 'cancel' }}</div>
</section>
</div>
</div>
</section>
<div class="row no-gutter">
<div class="col-sm-12 col-md-12 col-lg-9 b-r-lg nopadding">
<ng-include src="'<%= asset_path 'admin/trainings/_form.html' %>'"></ng-include>
</div>
</div>

View File

@ -21,11 +21,7 @@
<div class="col-md-12">
<uib-tabset justified="true">
<uib-tab heading="{{ 'trainings' | translate }}">
<button type="button" class="btn btn-warning m-t m-b" ng-click="addTraining()" translate>{{ 'add_a_new_training' }}</button>
<div class="alert alert-warning" role="alert">
{{ 'beware_when_creating_a_training_its_reservation_prices_are_initialized_to_zero' | translate }}
{{ 'dont_forget_to_change_them_before_creating_slots_for_this_training' | translate }}
</div>
<button type="button" class="btn btn-warning m-t m-b" ui-sref="app.admin.trainings_new" translate>{{ 'add_a_new_training' }}</button>
<table class="table">
<thead>
@ -38,35 +34,12 @@
</thead>
<tbody>
<tr ng-repeat="training in trainings">
<td>{{ training.name }}</td>
<td>{{ showMachines(training) }}</td>
<td>{{ training.nb_total_places }}</td>
<td>
<span editable-text="training.name" e-name="name" e-form="rowform" e-required>
{{ training.name }}
</span>
</td>
<td>
<span editable-checklist="training.machine_ids" e-ng-options="m.id as m.name for m in machines" e-name="machine_ids" e-form="rowform">
{{ showMachines(training) }}
</span>
</td>
<td>
<span editable-number="training.nb_total_places" e-name="nb_total_places" e-form="rowform" e-required>
{{ training.nb_total_places }}
</span>
</td>
<td>
<form editable-form name="rowform" onbeforesave="saveTraining($data, training.id)" ng-show="rowform.$visible" class="form-buttons form-inline" shown="inserted == training">
<button type="submit" ng-disabled="rowform.$waiting" class="btn btn-warning">
<i class="fa fa-check"></i>
</button>
<button type="button" ng-disabled="rowform.$waiting" ng-click="cancelTraining(rowform, $index)" class="btn btn-default">
<i class="fa fa-times"></i>
</button>
</form>
<div class="buttons" ng-show="!rowform.$visible">
<button ng-click="openModalToSetDescription(training)" class="btn btn-default">
<i class="fa fa-comment-o"></i>
</button>
<button class="btn btn-default" ng-click="rowform.$show()">
<div class="buttons">
<button class="btn btn-default" ui-sref="app.admin.trainings_edit({id:training.id})">
<i class="fa fa-edit"></i> {{ 'edit' | translate }}
</button>
<button class="btn btn-danger" ng-click="removeTraining($index, training)">

View File

@ -0,0 +1,35 @@
<section class="heading b-b">
<div class="row no-gutter">
<div class="col-md-1 hidden-xs">
<section class="heading-btn">
<a href="#" ng-click="cancel()"><i class="fa fa-long-arrow-left "></i></a>
</section>
</div>
<div class="col-md-8 b-l b-r">
<section class="heading-title">
<h1 translate>{{ 'add_a_new_training' }}</h1>
</section>
</div>
</div>
</section>
<div class="row no-gutter" >
<div class="col-md-9 b-r nopadding">
<div class="alert alert-warning m-lg" role="alert">
{{ 'beware_when_creating_a_training_its_reservation_prices_are_initialized_to_zero' | translate }}
{{ 'dont_forget_to_change_them_before_creating_slots_for_this_training' | translate }}
</div>
<ng-include src="'<%= asset_path 'admin/trainings/_form.html' %>'"></ng-include>
</div>
<div class="col-md-3">
<!-- <button class="btn">TEST</button> -->
</div>
</div>

View File

@ -0,0 +1,62 @@
<section class="heading b-b">
<div class="row no-gutter">
<div class="col-xs-2 col-sm-2 col-md-1">
<section class="heading-btn">
<a href="#" ng-click="backPrevLocation($event)"><i class="fa fa-long-arrow-left "></i></a>
</section>
</div>
<div class="col-xs-10 col-sm-10 col-md-8 b-l b-r-md hide-b-r-lg">
<section class="heading-title">
<h1 translate>{{ 'calendar' }}</h1>
</section>
</div>
<div class="col-xs-12 col-sm-12 col-md-3 b-t hide-b-md hidden-lg">
<div class="heading-actions wrapper">
<button type="button" class="btn btn-default m-t m-b" ng-click="openFilterAside()">
<span class="fa fa-filter"></span> {{ 'filter-calendar' | translate }}
</button>
</div>
</div>
</div>
</section>
<section class="row no-gutter">
<div class="hidden-lg">
<div class="row">
<div class="col-sm-12 col-md-12">
</div>
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-9">
<div ui-calendar="calendarConfig" ng-model="eventSources" calendar="calendar" class="wrapper-lg public-calendar"></div>
</div>
<div class="col-lg-3 hidden-md hidden-sm hidden-xs">
<div class="widget panel b-a m m-t-lg">
<div class="panel-heading b-b small">
<h3 translate>{{ 'filter-calendar' }}</h3>
</div>
<div class="widget-content no-bg auto wrapper calendar-filter">
<ng-include src="'<%= asset_path 'calendar/filter.html' %>'"></ng-include>
</div>
</div>
</div>
</section>
<script type="text/ng-template" id="filterAside.html">
<div class="widget">
<div class="modal-header">
<button type="button" class="close" ng-click="close($event)"><span>&times;</span></button>
<h1 class="modal-title" translate>{{ 'filter-calendar' }}</h1>
</div>
<div class="modal-body widget-content calendar-filter calendar-filter-aside">
<ng-include src="'<%= asset_path 'calendar/filter.html' %>'"></ng-include>
</div>
</div>
</script>

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