1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-01-29 18:52:22 +01:00

Merge branch 'dev' of git.sleede.com:projets/fab-manager into dev

This commit is contained in:
Nicolas Florentin 2017-03-15 12:08:05 +01:00
commit 2255335727
300 changed files with 10521 additions and 4951 deletions

View File

@ -1 +1 @@
2.4.0-dev
2.5.0-dev

8
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,8 @@
This issue tracker is **reserved** for bug reports and feature requests.
The place to ask a question or call for help is at Fab-manager forums at https://forum.fab-manager.com/.
To report a bug, please describe:
- Expected behavior and actual behavior.
- Steps to reproduce the problem.
- Specifications like the version of the project, operating system, or hardware.

4
.gitignore vendored
View File

@ -15,6 +15,7 @@
# Ignore all logfiles and tempfiles.
/log/*.log
/tmp
/coverage
/public/uploads
/public/assets
@ -39,5 +40,8 @@
.vagrant
.docker
# Do not versionate coveralls token
.coveralls.yml
# Plugins are versioned is their own repository
/plugins/*

View File

@ -1,7 +1,119 @@
# Changelog Fab Manager
## next release
- Project images will show in full-size on a click
## next release (v2.5.0)
- Ability to remove an unused custom price for an event (#61)
- Prevent polling notifications when the application is in background
- Ability to export the availabilities and their reservation rate from the admin calendar
- Ability to create, manage and reserve spaces
- Improved admin's interface to create availabilities
- Complete rewrote of the reservation cart functionality with improved stability, performance and sustainability
- Replaced letter_opener by MailCatcher to preview e-mails in development environments
- Fix a bug: trainings reservations are not shown in the admin's calendar
- Fix a bug: unable to delete an administrator from the system
- Fix a bug: unable to delete an event with a linked custom price (#61)
- Fix a bug: navigation in client calendar is bogus when browsing months (#59)
- [TODO DEPLOY] `rake db:migrate`, then `rake db:seed`
- [TODO DEPLOY] add the `FABLAB_WITHOUT_SPACES` environment variable
- [TODO DEPLOY] `rake fablab:es_add_spaces`
## v2.4.10 2017 January 9
- Optimized notifications system
- Fix a bug: when many users with too many unread notifications are connected at the same time, the system kill the application due to memory overflow
- Fix a bug: ReservationReminderWorker crash with undefined method find_by
- Fix a bug: navigation to about page duplicates admin's links in left menu
- Fix a bug: changing the price of a plan lost its past statistics
- [TODO DEPLOY] `rake db:migrate`
- [TODO DEPLOY] `rake fablab:set_plans_slugs`
## v2.4.9 2017 January 4
- Mask new notifications alerts when more than 3
- Added an asterisk on group select in admin's member form
- Statistics custom aggregations can handle custom filtering
- Statistics about hours available for machine reservations and tickets available for training reservations, now handle custom filtering on date and type
- Fix a bug: display more than 15 unread notifications (number on the bell icon & full list)
- Fix a bug: in invoice configuration panel, VAT amount and total excl. taxes are inverted
- Fix a bug: unable to compute user's age when they were born on february 29th and current year is not a leap year
- Fix a bug: wrong statistics about hours available for machines reservation. Fix requires user action (1)
- Fix a bug: when regenerating statistics, previous values are not fully removed (only 10 firsts), resulting in wrong statistics generation (2)
- Fix a bug: when deleting an availability just after its creation, the indexer workers crash and retries for a month
- [TODO DEPLOY] remove possible value `application/` in `ALLOWED_MIME_TYPES` list, in environment variable
- [TODO DEPLOY] `rails runner StatisticCustomAggregation.destroy_all`, then `rake db:seed`, then `rake fablab:es_build_availabilities_index` (1)
- [TODO DEPLOY] `rake fablab:generate_stats[1095]` if you already has regenerated the statistics in the past, then they are very likely corrupted. Run this task to fix (2)
## v2.4.8 2016 December 15
- Added asterisks on mandatory fields in member's form
- Fixed wording on SSO screens
- Ability to send again the auth-system migration token by email
- Fix a bug: notification email about refund invoice tells about subscription while concerning wallet credit
## v2.4.7 2016 December 14
- Improved automated testing
- Added an information notice about the processing time of deleting an administrator
- Ability to change the expiration date of a coupon after its creation
- Ability to generate a refund invoice when crediting user's wallet
- Fix a bug: unable to run rake db:migrate on first install
- Fix a bug: unable to create or edit a coupon of type 'percentage'
## v2.4.6 2016 November 30
- Change display of message about coupon application status
- Fix a bug: compute price API return error 500 if reservable_id is not provided
## v2.4.5 2016 November 29
- Ability to create coupons with cash amounts (previously only percentages were allowed)
- Improved error messages when something wrong append when paying a machine reservation by stripe
- Ability to display optional information message on event reservation page
- Fix a bug: misconfigured Twitter's ENV variables results in HTTP error 500
- Fix a bug: wallet is not debited when paying locally with a user who have invoices disabled
- Fix a bug: wrong error message about rounding inconsistency is logged on invoice generation
- Fix a bug: reservation calendar of a specific training shows availabilities for all trainings
- [TODO DEPLOY] `rake db:migrate`
## v2.4.4 2016 November 24
- Fix a bug: unable to rollback migration 20160906145713
- Fix a bug: Title's translation for plan's forms is not loaded in French
- Fix a bug: invoice of reservation show payment by debit card when user pay with wallet
## v2.4.3 2016 November 21
- Export user's invoicing status in members' excel export
- Fix a bug: Next events descriptions, shown on the home page, display raw html
- Fix a bug: number of reserved seats for an event is always of 1 in the excel export of reservations
- Fix a bug: conflict between similar translations around "reservations"
- Fix a bug: later occurrences of recurrent events does not have the initially configured theme and age range
- Fix a bug: some graphs do not display: events, users, trainings and machine hours
- [TODO DEPLOY] delete the `exports/users/reservations` folder to prevent the usage of old invalid exports
## v2.4.2 2016 November 8
- Image max size is configurable, default size is 2 megabytes
- Allow add more pictures for project step
- Ability to use HTML in event's descriptions using a WYSIWYG editor
- Fix a bug: statistics graphs were not showing
- Fix a bug: On invoices, only starting date is shown for multi-days events
- Fix a bug: In the sign-up modal, the translation for 'i_accept_to_receive_information_from_the_fablab' was not loaded
- [TODO DEPLOY] add `MAX_IMAGE_SIZE` environment variable in `application.yml` and docker env
## v2.4.1 2016 October 11
- Fix a bug: unable to share a project/event without image on social networks
- Fix a bug: after creating an element in the admin calendar, browsing through the calendar and coming back cause the element to appear duplicated
- Fix a bug: after deleting an element in the admin calendar, the confirmation message is wrong and an error is logged in the console
- Fix a bug: erroneous syntax in docker env example file
## 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
@ -16,14 +128,14 @@
- 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"
- 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
- 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
@ -38,25 +150,29 @@
- 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 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: 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` environment variable in `application.yml`
- [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
@ -76,7 +192,7 @@
- [TODO DEPLOY] `bundle install` and `rake db:migrate`
## 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 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 an event
@ -92,7 +208,7 @@
- User public profile: UI re-design with possible admin's customization
- Admin: Invoices list and users list are now loaded per 10 items to improve pages load time
- Admin: select member (eg. to buy a subscription for a member) is now loading the user's list dynamically when you type
- Project collaborators selection is now using a list dynamically loaded as you type
- Project collaborators selection is now using a list dynamically loaded as you type
- Admin: select a training before monitoring its reservations -> improves page load time
- API: GET /api/trainings do not load nor send the associated availabilities until they are requested
- List of members is now loaded 10 members by 10, to improve page load time

View File

@ -13,9 +13,8 @@ patches and features.
## Using the issue tracker
The [issue tracker](https://github.com/LaCasemate/fab-manager/issues) is the preferred channel for [bug reports](#bugs),
[features requests](#features) and [submitting pull requests](#pull-requests), but please respect the following
restrictions:
The [issue tracker](https://github.com/LaCasemate/fab-manager/issues) is the preferred channel for [bug reports](#bugs)
and [submitting pull requests](#pull-requests), but please respect the following restrictions:
* Please **do not** use the issue tracker for personal support requests (use [the forum](https://forum.fab-manager.com)).
@ -70,6 +69,9 @@ Feature requests are welcome. But take a moment to find out whether your idea fi
project. It's up to *you* to make a strong case to convince the project's developers of the merits of this feature.
Please provide as much detail and context as possible.
Please note also that [the forum](https://forum.fab-manager.com) is probably a better place for discussing about feature
requests.
<a name="pull-requests"></a>
## Pull requests

View File

@ -40,7 +40,7 @@ end
group :development do
# Preview mail in the browser
gem 'letter_opener'
gem 'mailcatcher'
gem 'awesome_print'
gem "puma"
@ -52,6 +52,8 @@ group :development do
gem 'capistrano-maintenance', '0.0.5', require: false
gem 'active_record_query_trace'
gem 'coveralls', require: false
end
group :test do
@ -62,6 +64,7 @@ group :test do
gem 'webmock'
gem 'vcr'
gem 'byebug'
gem 'pdf-reader'
end
group :production do

View File

@ -1,6 +1,7 @@
GEM
remote: https://rubygems.org/
specs:
Ascii85 (1.0.2)
aasm (4.1.0)
actionmailer (4.2.5)
actionpack (= 4.2.5)
@ -41,6 +42,7 @@ GEM
thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1)
addressable (2.3.8)
afm (0.2.2)
ansi (1.5.0)
api-pagination (4.3.0)
apipie-rails (0.3.6)
@ -94,13 +96,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)
@ -118,8 +120,15 @@ GEM
sass-rails (<= 5.0.1)
sprockets (< 2.13)
connection_pool (2.2.0)
coveralls (0.8.16)
json (>= 1.8, < 3)
simplecov (~> 0.12.0)
term-ansicolor (~> 1.3.0)
thor (~> 0.19.1)
tins (>= 1.6.0, < 2)
crack (0.4.3)
safe_yaml (~> 1.0.0)
daemons (1.2.4)
database_cleaner (1.4.1)
debug_inspector (0.0.2)
descendants_tracker (0.0.4)
@ -133,6 +142,7 @@ GEM
warden (~> 1.2.3)
devise-async (0.9.0)
devise (~> 3.2)
docile (1.1.5)
domain_name (0.5.25)
unf (>= 0.0.5, < 1.0.0)
elasticsearch (1.0.12)
@ -157,7 +167,8 @@ GEM
multi_json
equalizer (0.0.11)
erubis (2.7.0)
execjs (2.4.0)
eventmachine (1.0.9.1)
execjs (2.7.0)
faker (1.4.3)
i18n (~> 0.5)
faraday (0.9.1)
@ -177,6 +188,7 @@ GEM
has_secure_token (1.0.0)
activerecord (>= 3.0)
hashdiff (0.3.0)
hashery (2.1.2)
hashie (3.4.2)
highline (1.7.1)
hike (1.2.3)
@ -207,23 +219,27 @@ GEM
actionpack (>= 3.0.0)
activesupport (>= 3.0.0)
kgio (2.9.3)
launchy (2.4.3)
addressable (~> 2.3)
letter_opener (1.3.0)
launchy (~> 2.2)
libv8 (3.16.14.11)
loofah (2.0.3)
nokogiri (>= 1.5.9)
mail (2.6.3)
mime-types (>= 1.16, < 3)
mailcatcher (0.6.5)
eventmachine (= 1.0.9.1)
mail (~> 2.3)
rack (~> 1.5)
sinatra (~> 1.2)
skinny (~> 0.2.3)
sqlite3 (~> 1.3)
thin (~> 1.5.0)
memoizable (0.4.2)
thread_safe (~> 0.3, >= 0.3.1)
message_format (0.0.3)
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
@ -241,8 +257,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)
@ -264,7 +281,14 @@ GEM
httparty (~> 0.13)
orm_adapter (0.5.0)
pdf-core (0.5.1)
pdf-reader (1.4.0)
Ascii85 (~> 1.0.0)
afm (~> 0.2.1)
hashery (~> 2.0)
ruby-rc4
ttfunk
pg (0.18.1)
pkg-config (1.1.7)
prawn (2.0.1)
pdf-core (~> 0.5.1)
ttfunk (~> 1.4.0)
@ -313,7 +337,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)
@ -333,6 +357,7 @@ GEM
netrc (~> 0.7)
rolify (4.0.0)
ruby-progressbar (1.7.5)
ruby-rc4 (0.1.5)
rubyzip (1.1.7)
rufus-scheduler (3.0.9)
tzinfo
@ -363,10 +388,18 @@ GEM
sidekiq (>= 2.17.3)
tilt (< 2.0.0)
simple_oauth (0.3.1)
simplecov (0.12.0)
docile (~> 1.1.0)
json (>= 1.8, < 3)
simplecov-html (~> 0.10.0)
simplecov-html (0.10.0)
sinatra (1.4.6)
rack (~> 1.4)
rack-protection (~> 1.4)
tilt (>= 1.3, < 3)
skinny (0.2.4)
eventmachine (~> 1.0.0)
thin (>= 1.5, < 1.7)
spring (1.3.5)
sprockets (2.12.4)
hike (~> 1.2)
@ -377,19 +410,27 @@ GEM
actionpack (>= 3.0)
activesupport (>= 3.0)
sprockets (>= 2.8, < 4.0)
sqlite3 (1.3.13)
stripe (1.30.2)
json (~> 1.8.1)
rest-client (~> 1.4)
term-ansicolor (1.3.2)
tins (~> 1.0)
test_after_commit (1.0.0)
activerecord (>= 3.2)
therubyracer (0.12.0)
libv8 (~> 3.16.14.0)
ref
thin (1.5.1)
daemons (>= 1.0.9)
eventmachine (>= 0.12.6)
rack (>= 1.0.0)
thor (0.19.1)
thread_safe (0.3.5)
tilt (1.4.1)
timers (4.0.1)
hitimes
tins (1.13.0)
ttfunk (1.4.0)
twitter (5.14.0)
addressable (~> 2.3)
@ -460,6 +501,7 @@ DEPENDENCIES
chroma
coffee-rails (~> 4.1.0)
compass-rails (= 2.0.4)
coveralls
database_cleaner
devise
devise-async
@ -477,7 +519,7 @@ DEPENDENCIES
jbuilder_cache_multi
jquery-rails
kaminari
letter_opener
mailcatcher
message_format
mini_magick
minitest-reporters
@ -486,6 +528,7 @@ DEPENDENCIES
omniauth
omniauth-oauth2
openlab_ruby
pdf-reader
pg
prawn
prawn-table
@ -520,4 +563,4 @@ DEPENDENCIES
webmock
BUNDLED WITH
1.12.5
1.13.1

View File

@ -14,39 +14,42 @@ Copyright (C) 2015 La Casemate
along with this program. If not, see <http://www.gnu.org/licenses/>.
FabManager uses some external components, which are licenced under the
terms of [Apache 2 license](http://www.apache.org/licenses/LICENSE-2.0):
Fab-Manager uses some external components, which are licenced under the
terms of the following licences:
- [jasny-bootstrap](https://github.com/jasny/bootstrap/)
- [elasticsearch](https://github.com/elasticsearch/bower-elasticsearch-js)
- [nvd3](https://github.com/novus/nvd3)
- [angular-bootstrap-switch](https://github.com/frapontillo/angular-bootstrap-switch)
- [elasticsearch-rails](https://github.com/elastic/elasticsearch-rails)
- [elasticsearch-model](https://github.com/elastic/elasticsearch-rails/tree/master/elasticsearch-model)
- [elasticsearch-persistence](https://github.com/elastic/elasticsearch-rails/tree/master/elasticsearch-persistence)
- font [Open Sans](http://www.fontsquirrel.com/fonts/open-sans)
- [Apache 2 license](http://www.apache.org/licenses/LICENSE-2.0):
- [jasny-bootstrap](https://github.com/jasny/bootstrap/)
- [elasticsearch](https://github.com/elasticsearch/bower-elasticsearch-js)
- [nvd3](https://github.com/novus/nvd3)
- [angular-bootstrap-switch](https://github.com/frapontillo/angular-bootstrap-switch)
- [elasticsearch-rails](https://github.com/elastic/elasticsearch-rails)
- [elasticsearch-model](https://github.com/elastic/elasticsearch-rails/tree/master/elasticsearch-model)
- [elasticsearch-persistence](https://github.com/elastic/elasticsearch-rails/tree/master/elasticsearch-persistence)
- font [Open Sans](http://www.fontsquirrel.com/fonts/open-sans)
- [General Public License version 2](http://www.gnu.org/licenses/old-licenses/gpl-2.0-faq.en.html):
- [railroady](https://github.com/preston/railroady)
- [unicorn](https://github.com/defunkt/unicorn)
- [prawn](https://github.com/prawnpdf/prawn)
- [prawn-table](https://github.com/prawnpdf/prawn-table)
Some other used libraries/components are licenced under the terms of the
[General Public License version 2](http://www.gnu.org/licenses/old-licenses/gpl-2.0-faq.en.html):
- [BSD-2-Clause](https://opensource.org/licenses/BSD-2-Clause)
- [ruby](https://www.ruby-lang.org)
- [rubyzip](https://github.com/rubyzip/rubyzip)
- [byebug](https://github.com/deivid-rodriguez/byebug)
- [ruby](https://www.ruby-lang.org)
- [railroady](https://github.com/preston/railroady)
- [unicorn](https://github.com/defunkt/unicorn)
- [prawn](https://github.com/prawnpdf/prawn)
- [prawn-table](https://github.com/prawnpdf/prawn-table)
- [MIT Licence](https://opensource.org/licenses/MIT)
- Errors and omissions excepted, all the other external libraries used
in this project.
Errors and omissions excepted, the other external libraries used in this
project are licenced under the terms of the [MIT Licence](https://opensource.org/licenses/MIT).
Please refer to the libraries documentation for more informations about
their licences.
Please refer to the libraries documentation for more information about
their licences.
Complete lists of used libraries are available in `bower.json` for the
EcmaScript libraries and in `Gemfile` for Ruby libraries.
JS/EcmaScript libraries and in `Gemfile` for Ruby libraries.
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
@ -666,4 +669,4 @@ an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
END OF TERMS AND CONDITIONS

View File

@ -1,2 +1,3 @@
web: bundle exec rails server puma -p $PORT -b0.0.0.0
worker: bundle exec sidekiq -C ./config/sidekiq.yml
mail: bundle exec mailcatcher --foreground

102
README.md
View File

@ -3,7 +3,7 @@
FabManager is the FabLab management solution. It is web-based, open-source and totally free.
##### Table of Contents
##### Table of Contents
1. [Software stack](#software-stack)
2. [Contributing](#contributing)
3. [Setup a production environment](#setup-a-production-environment)
@ -122,9 +122,12 @@ In you only intend to run fab-manager on your local machine for testing purposes
```
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.
**Warning**: **NO NOT** run `rake db:setup` instead of these commands, as this will not run some required raw SQL instructions.
```bash
rake db:setup
rake db:create
rake db:migrate
rake db:seed
```
9. Create the pids folder used by Sidekiq. If you want to use a different location, you can configure it in `config/sidekiq.yml`
@ -145,6 +148,9 @@ In you only intend to run fab-manager on your local machine for testing purposes
- user: admin@fab-manager.com
- password: adminadmin
13. Email notifications will be caught by MailCatcher.
To see the emails sent by the platform, open your web browser at `http://localhost:1080` to access the MailCatcher interface.
<a name="environment-configuration"></a>
### Environment Configuration
@ -159,7 +165,7 @@ This value is only used when deploying with Docker, otherwise this is configured
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.
Please see [Setup the FabManager database in PostgreSQL](#setup-fabmanager-in-postgresql) for information 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
@ -198,6 +204,12 @@ The PDF file name will be of the form "(INVOICE_PREFIX) - (invoice ID) _ (invoic
FABLAB_WITHOUT_PLANS
If set to 'true', the subscription plans will be fully disabled and invisible in the application.
It is not recommended to disable plans if at least one subscription was took on the platform.
FABLAB_WITHOUT_SPACES
If set to 'false', enable the spaces management and reservation in the application.
It is not recommended to disable spaces if at least one space reservation was made on the system.
DEFAULT_MAIL_FROM
@ -222,11 +234,12 @@ Identifier of your Google Analytics account.
Unique identifier of your [Disqus](http://www.disqus.com) forum.
Disqus forums are used to allow visitors to comment on projects.
See https://help.disqus.com/customer/portal/articles/466208-what-s-a-shortname- for more informations.
See https://help.disqus.com/customer/portal/articles/466208-what-s-a-shortname- for more information.
TWITTER_NAME
Identifier of the Twitter account, from witch the last tweet will be fetched and displayed on the home page.
Identifier of the Twitter account, from witch the last tweet will be fetched and displayed on the home page.
This value can be graphically overridden during the application's lifecycle in Admin/Customization/Home page/Twitter Feed.
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
@ -239,6 +252,35 @@ Retrieve them from https://apps.twitter.com
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.
MAX_IMAGE_SIZE
Maximum size (in bytes) allowed for image uploaded on the platform.
This parameter concerns events, plans, user's avatars, projects and steps of projects.
If this parameter is not specified the maximum size allowed will be 2MB.
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.
@ -295,7 +337,7 @@ Otherwise, please follow the official instructions on the project's website.
<a name="setup-fabmanager-in-postgresql"></a>
### Setup the FabManager database in PostgreSQL
Before running `rake db:setup`, you have to make sure that the user configured in [config/database.yml](config/database.yml.default) for the `development` environment exists.
Before running `rake db:create`, 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. Run the PostgreSQL administration command line interface, logged as the postgres user
@ -325,20 +367,14 @@ To create it, please follow these instructions:
ALTER ROLE sleede WITH CREATEDB;
```
4. Then, create the fabmanager_development and fabmanager_test databases
```sql
CREATE DATABASE fabmanager_development OWNER sleede;
CREATE DATABASE fabmanager_test OWNER sleede;
```
5. To finish, attribute a password to this user
4. Then, 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.
5. 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
@ -347,14 +383,14 @@ To create it, please follow these instructions:
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).
- 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.
- 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()`
@ -363,8 +399,8 @@ To create it, please follow these instructions:
- `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.
This also requires your user to have the _SUPERUSER_ role.
Please see the [known issues](#known-issues) section for more information about this.
<a name="elasticsearch"></a>
## ElasticSearch
@ -495,7 +531,7 @@ Back-end translations uses the [Ruby on Rails syntax](http://guides.rubyonrails.
In each cases, some inline comments are included in the localisation files.
They can be recognized as they start with the sharp character (#).
These comments are not required to be translated, they are intended to help the translator to have some context informations about the sentence to translate.
These comments are not required to be translated, they are intended to help the translator to have some context information about the sentence to translate.
<a name="i18n-configuration"></a>
@ -629,7 +665,7 @@ Fab-manager can be connected to a [Single Sign-On](https://en.wikipedia.org/wiki
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).
Developers may find information on how to implement their own authentication protocol in [sso_authentication.md](doc/sso_authentication.md).
<a name="known-issues"></a>
## Known issues
@ -648,17 +684,17 @@ Developers may find informations on how to implement their own authentication pr
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.
- Due to a stripe limitation, you won't be able to create plans longer than one year.
- When running the tests suite with `rake test`, all tests may fail with errors similar to the following:
Error:
...
ActiveRecord::InvalidForeignKey: PG::ForeignKeyViolation: ERROR: insert or update on table "..." violates foreign key constraint "fk_rails_..."
DETAIL: Key (group_id)=(1) is not present in table "groups".
: ...
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'
...
ActiveRecord::InvalidForeignKey: PG::ForeignKeyViolation: ERROR: insert or update on table "..." violates foreign key constraint "fk_rails_..."
DETAIL: Key (group_id)=(1) is not present in table "groups".
: ...
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.
@ -669,10 +705,10 @@ Developers may find informations on how to implement their own authentication pr
DO NOT do this in a production environment, unless you know what you're doing: this could lead to a serious security issue.
- With Ubuntu 16.04, ElasticSearch may refuse to start even after having configured the service with systemd.
- 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
```

View File

@ -71,7 +71,7 @@ config(['$httpProvider', 'AuthProvider', "growlProvider", "unsavedWarningsConfig
// Angular-xeditable (click-to-edit elements, used in admin backoffice)
editableOptions.theme = 'bs3';
// Alter the UI-Router's $state, registering into some informations concerning the previous $state.
// Alter the UI-Router's $state, registering into some information concerning the previous $state.
// This is used to allow the user to navigate to the previous state
$rootScope.$on('$stateChangeSuccess', function(event, toState, toParams, fromState, fromParams){
$state.prevState = fromState;
@ -80,6 +80,8 @@ config(['$httpProvider', 'AuthProvider', "growlProvider", "unsavedWarningsConfig
// Global config: if true, the whole 'Plans & Subscriptions' feature will be disabled in the application
$rootScope.fablabWithoutPlans = Fablab.withoutPlans;
// Global config: it true, the whole 'Spaces' features will be disabled in the application
$rootScope.fablabWithoutSpaces = Fablab.withoutSpaces;
// Global function to allow the user to navigate to the previous screen (ie. $state).
// If no previous $state were recorded, navigate to the home page

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', 'bookingWindowStart', 'bookingWindowEnd', 'machinesPromise', '_t', 'uiCalendarConfig', 'CalendarConfig'
($scope, $state, $uibModal, moment, Availability, Slot, Setting, growl, dialogs, bookingWindowStart, bookingWindowEnd, machinesPromise, _t, uiCalendarConfig, CalendarConfig) ->
Application.Controllers.controller "AdminCalendarController", ["$scope", "$state", "$uibModal", "moment", "Availability", 'Slot', 'Setting', 'Export', 'growl', 'dialogs', 'bookingWindowStart', 'bookingWindowEnd', 'machinesPromise', '_t', 'uiCalendarConfig', 'CalendarConfig'
($scope, $state, $uibModal, moment, Availability, Slot, Setting, Export, growl, dialogs, bookingWindowStart, bookingWindowEnd, machinesPromise, _t, uiCalendarConfig, CalendarConfig) ->
@ -50,6 +50,8 @@ Application.Controllers.controller "AdminCalendarController", ["$scope", "$state
calendarEventClickCb(event, jsEvent, view)
eventRender: (event, element, view) ->
eventRenderCb(event, element)
loading: (isLoading, view ) ->
loadingCb(isLoading, view)
@ -62,8 +64,8 @@ Application.Controllers.controller "AdminCalendarController", ["$scope", "$state
dialogs.confirm
resolve:
object: ->
title: _t('confirmation_required')
msg: _t("do_you_really_want_to_cancel_the_USER_s_reservation_the_DATE_at_TIME_concerning_RESERVATION"
title: _t('admin_calendar.confirmation_required')
msg: _t("admin_calendar.do_you_really_want_to_cancel_the_USER_s_reservation_the_DATE_at_TIME_concerning_RESERVATION"
, { GENDER:getGender($scope.currentUser), USER:slot.user.name, DATE:moment(slot.start_at).format('L'), TIME:moment(slot.start_at).format('LT'), RESERVATION:slot.reservable.name }
, 'messageformat')
, ->
@ -76,9 +78,9 @@ Application.Controllers.controller "AdminCalendarController", ["$scope", "$state
resa.canceled_at = data.canceled_at
break
# notify the admin
growl.success(_t('reservation_was_successfully_cancelled'))
growl.success(_t('admin_calendar.reservation_was_successfully_cancelled'))
, (data, status) -> # failed
growl.error(_t('reservation_cancellation_failed'))
growl.error(_t('admin_calendar.reservation_cancellation_failed'))
@ -89,16 +91,16 @@ Application.Controllers.controller "AdminCalendarController", ["$scope", "$state
##
$scope.removeMachine = (machine) ->
if $scope.availability.machine_ids.length == 1
growl.error(_t('unable_to_remove_the_last_machine_of_the_slot_delete_the_slot_rather'))
growl.error(_t('admin_calendar.unable_to_remove_the_last_machine_of_the_slot_delete_the_slot_rather'))
else
# open a confirmation dialog
dialogs.confirm
resolve:
object: ->
title: _t('confirmation_required')
msg: _t('do_you_really_want_to_remove_MACHINE_from_this_slot', {GENDER:getGender($scope.currentUser), MACHINE:machine.name}, "messageformat") + ' ' +
_t('this_will_prevent_any_new_reservation_on_this_slot_but_wont_cancel_those_existing') + ' ' +
_t('beware_this_cannot_be_reverted')
title: _t('admin_calendar.confirmation_required')
msg: _t('admin_calendar.do_you_really_want_to_remove_MACHINE_from_this_slot', {GENDER:getGender($scope.currentUser), MACHINE:machine.name}, "messageformat") + ' ' +
_t('admin_calendar.this_will_prevent_any_new_reservation_on_this_slot_but_wont_cancel_those_existing') + ' ' +
_t('admin_calendar.beware_this_cannot_be_reverted')
, ->
# the admin has confirmed, remove the machine
machines = $scope.availability.machine_ids
@ -113,9 +115,20 @@ Application.Controllers.controller "AdminCalendarController", ["$scope", "$state
$scope.availability.title = data.title
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
# notify the admin
growl.success(_t('the_machine_was_successfully_removed_from_the_slot'))
growl.success(_t('admin_calendar.the_machine_was_successfully_removed_from_the_slot'))
, (data, status) -> # failed
growl.error(_t('deletion_failed'))
growl.error(_t('admin_calendar.deletion_failed'))
##
# Callback to alert the admin that the export request was acknowledged and is
# processing right now.
##
$scope.alertExport = (type) ->
Export.status({category: 'availabilities', type: type}).then (res) ->
unless (res.data.exists)
growl.success _t('admin_calendar.export_is_running_you_ll_be_notified_when_its_ready')
@ -148,6 +161,15 @@ Application.Controllers.controller "AdminCalendarController", ["$scope", "$state
resolve:
start: -> start
end: -> end
machinesPromise: ['Machine', (Machine)->
Machine.query().$promise
]
trainingsPromise: ['Training', (Training)->
Training.query().$promise
]
spacesPromise: ['Space', (Space)->
Space.query().$promise
]
# when the modal is closed, we send the slot to the server for saving
modalInstance.result.then (availability) ->
uiCalendarConfig.calendars.calendar.fullCalendar 'renderEvent',
@ -181,13 +203,10 @@ Application.Controllers.controller "AdminCalendarController", ["$scope", "$state
if ($(jsEvent.target).hasClass('remove-event'))
Availability.delete id: 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)
growl.success(_t('the_slot_START-END_has_been_successfully_deleted', {START:+moment(event.start).format('LL LT'), END:moment(event.end).format('LT')}))
growl.success(_t('admin_calendar.the_slot_START-END_has_been_successfully_deleted', {START:moment(event.start).format('LL LT'), END:moment(event.end).format('LT')}))
,->
growl.error(_t('unable_to_delete_the_slot_START-END_because_it_s_already_reserved_by_a_member', {START:+moment(event.start).format('LL LT'), END:moment(event.end).format('LT')}))
growl.error(_t('admin_calendar.unable_to_delete_the_slot_START-END_because_it_s_already_reserved_by_a_member', {START:moment(event.start).format('LL LT'), END:moment(event.end).format('LT')}))
# if the user has only clicked on the event, display its reservations
else
Availability.reservations {id: event.id}, (reservations) ->
@ -207,8 +226,20 @@ Application.Controllers.controller "AdminCalendarController", ["$scope", "$state
for tag in event.tags
html += "<span class='label label-success text-white'>#{tag.name}</span> "
element.find('.fc-title').append("<br/>"+html)
# force return to prevent coffee-script auto-return to return random value (possiblity falsy)
return
##
# Triggered when resource fetching starts/stops.
# @see https://fullcalendar.io/docs/resource_data/loading/
##
loadingCb = (isLoading, view) ->
if (isLoading)
# we remove existing events when fetching starts to prevent duplicates
uiCalendarConfig.calendars.calendar.fullCalendar('removeEvents')
]
@ -216,7 +247,8 @@ Application.Controllers.controller "AdminCalendarController", ["$scope", "$state
##
# Controller used in the slot creation modal window
##
Application.Controllers.controller 'CreateEventModalController', ["$scope", "$uibModalInstance", "moment", "start", "end", "Machine", "Availability", "Training", 'Tag', 'growl', '_t', ($scope, $uibModalInstance, moment, start, end, Machine, Availability, Training, Tag, growl, _t) ->
Application.Controllers.controller 'CreateEventModalController', ["$scope", "$uibModalInstance", "moment", "start", "end", "machinesPromise", "Availability", "trainingsPromise", "spacesPromise", 'Tag', 'growl', '_t'
, ($scope, $uibModalInstance, moment, start, end, machinesPromise, Availability, trainingsPromise, spacesPromise, Tag, growl, _t) ->
## $uibModal parameter
$scope.start = start
@ -225,14 +257,26 @@ Application.Controllers.controller 'CreateEventModalController', ["$scope", "$ui
$scope.end = end
## machines list
$scope.machines = []
$scope.machines = machinesPromise
## trainings list
$scope.trainings = []
$scope.trainings = trainingsPromise
## spaces list
$scope.spaces = spacesPromise
## machines associated with the created slot
$scope.selectedMachines = []
## training associated with the created slot
$scope.selectedTraining = null
## space associated with the created slot
$scope.selectedSpace = null
## UI step
$scope.step = 1
## the user is not able to edit the ending time of the availability, unless he set the type to 'training'
$scope.endDateReadOnly = true
@ -270,14 +314,16 @@ Application.Controllers.controller 'CreateEventModalController', ["$scope", "$ui
# Callback for the modal window validation: save the slot and closes the modal
##
$scope.ok = ->
if $scope.availability.available_type == "machines"
if $scope.availability.available_type == 'machines'
if $scope.selectedMachines.length > 0
$scope.availability.machine_ids = $scope.selectedMachines.map (m) -> m.id
else
growl.error(_t('you_should_link_a_training_or_a_machine_to_this_slot'))
growl.error(_t('admin_calendar.you_should_select_at_least_a_machine'))
return
else
else if $scope.availability.available_type == 'training'
$scope.availability.training_ids = [$scope.selectedTraining.id]
else if $scope.availability.available_type == 'space'
$scope.availability.space_ids = [$scope.selectedSpace.id]
Availability.save
availability: $scope.availability
, (availability) ->
@ -285,6 +331,23 @@ Application.Controllers.controller 'CreateEventModalController', ["$scope", "$ui
##
# Move the modal UI to the next step
##
$scope.next = ->
$scope.setNbTotalPlaces() if $scope.step == 1
$scope.step++
##
# Move the modal UI to the next step
##
$scope.previous = ->
$scope.step--
##
# Callback to cancel the slot creation
##
@ -293,22 +356,14 @@ Application.Controllers.controller 'CreateEventModalController', ["$scope", "$ui
##
# Switches the slot type : machine availability or training availability
##
$scope.changeAvailableType = ->
if $scope.availability.available_type == "machines"
$scope.availability.available_type = "training"
else
$scope.availability.available_type = "machines"
##
# For training avaiabilities, set the maximum number of people allowed to register on this slot
##
$scope.setNbTotalPlaces = ->
$scope.availability.nb_total_places = $scope.selectedTraining.nb_total_places
if $scope.availability.available_type == 'training'
$scope.availability.nb_total_places = $scope.selectedTraining.nb_total_places
else if $scope.availability.available_type == 'space'
$scope.availability.nb_total_places = $scope.selectedSpace.default_places
### PRIVATE SCOPE ###
@ -317,18 +372,11 @@ Application.Controllers.controller 'CreateEventModalController', ["$scope", "$ui
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
Machine.query().$promise.then (data)->
$scope.machines = data.map (d) ->
id: d.id
name: d.name
Training.query().$promise.then (data)->
$scope.trainings = data.map (d) ->
id: d.id
name: d.name
nb_total_places: d.nb_total_places
if $scope.trainings.length > 0
$scope.selectedTraining = $scope.trainings[0]
$scope.setNbTotalPlaces()
if $scope.trainings.length > 0
$scope.selectedTraining = $scope.trainings[0]
if $scope.spaces.length > 0
$scope.selectedSpace = $scope.spaces[0]
Tag.query().$promise.then (data) ->
$scope.tags = data
@ -336,7 +384,7 @@ Application.Controllers.controller 'CreateEventModalController', ["$scope", "$ui
## time must be dividable by 60 minutes (base slot duration). For training availabilities, the user
## can configure any duration as it does not matters.
$scope.$watch 'availability.available_type', (newValue, oldValue, scope) ->
if newValue == 'machines'
if newValue == 'machines' or newValue == 'space'
$scope.endDateReadOnly = true
diff = moment($scope.end).diff($scope.start, 'hours') # the result is rounded down by moment.js
$scope.end = moment($scope.start).add(diff, 'hours').toDate()
@ -347,8 +395,8 @@ Application.Controllers.controller 'CreateEventModalController', ["$scope", "$ui
## When the start date is changed, if we are configuring a machine availability,
## maintain the relative length of the slot (ie. change the end time accordingly)
$scope.$watch 'start', (newValue, oldValue, scope) ->
# for machine availabilities, adjust the end time
if $scope.availability.available_type == 'machines'
# for machine or space availabilities, adjust the end time
if $scope.availability.available_type == 'machines' or $scope.availability.available_type == 'space'
end = moment($scope.end)
end.add(moment(newValue).diff(oldValue), 'milliseconds')
$scope.end = end.toDate()

View File

@ -8,12 +8,13 @@ userValidities = ['once', 'forever']
##
# Controller used in the coupon creation page
##
Application.Controllers.controller "NewCouponController", ["$scope", "$state",'Coupon', 'growl', '_t'
Application.Controllers.controller "NewCouponController", ["$scope", "$state", 'Coupon', 'growl', '_t'
, ($scope, $state, Coupon, growl, _t) ->
## Values for the coupon currently created
$scope.coupon =
active: true
type: 'percent_off'
## Options for the validity per user
$scope.validities = userValidities
@ -57,8 +58,8 @@ Application.Controllers.controller "NewCouponController", ["$scope", "$state",'C
##
# Controller used in the coupon edition page
##
Application.Controllers.controller "EditCouponController", ["$scope", "$state", 'Coupon', 'couponPromise', '_t'
, ($scope, $state, Coupon, couponPromise, _t) ->
Application.Controllers.controller "EditCouponController", ["$scope", "$state", 'Coupon', 'couponPromise', '_t', 'growl'
, ($scope, $state, Coupon, couponPromise, _t, growl) ->
### PUBLIC SCOPE ###
@ -72,6 +73,9 @@ Application.Controllers.controller "EditCouponController", ["$scope", "$state",
## Options for the validity per user
$scope.validities = userValidities
## Mapping for validation errors
$scope.errors = {}
## Default parameters for AngularUI-Bootstrap datepicker (used for coupon validity limit selection)
$scope.datePicker =
format: Fablab.uibDateFormat
@ -97,11 +101,12 @@ Application.Controllers.controller "EditCouponController", ["$scope", "$state",
# Callback to save the coupon's changes to the API
##
$scope.updateCoupon = ->
$scope.errors = {}
Coupon.update {id: $scope.coupon.id}, coupon: $scope.coupon, (coupon) ->
$state.go('app.admin.pricing')
, (err)->
growl.error(_t('unable_to_update_the_coupon_an_error_occurred'))
console.error(err)
$scope.errors = err.data

View File

@ -16,6 +16,8 @@
# - $scope.toggleStartDatePicker($event)
# - $scope.toggleEndDatePicker($event)
# - $scope.toggleRecurrenceEnd(e)
# - $scope.addPrice()
# - $scope.removePrice(price, $event)
#
# Requires :
# - $scope.event.event_files_attributes = []
@ -137,6 +139,21 @@ class EventsController
##
# Remove the price or mark it as 'to delete'
##
$scope.removePrice = (price, event) ->
event.preventDefault()
event.stopPropagation()
if price.id
price._destroy = true
else
index = $scope.event.prices.indexOf(price)
$scope.event.prices.splice(index, 1)
##
# Controller used in the events listing page (admin view)
##
@ -382,8 +399,8 @@ Application.Controllers.controller "ShowEventReservationsController", ["$scope",
##
# 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) ->
Application.Controllers.controller "NewEventController", ["$scope", "$state", 'CSRF', 'categoriesPromise', 'themesPromise', 'ageRangesPromise', 'priceCategoriesPromise', '_t'
, ($scope, $state, CSRF, categoriesPromise, themesPromise, ageRangesPromise, priceCategoriesPromise, _t) ->
CSRF.setMetaTags()
## API URL where the form will be posted
@ -425,10 +442,6 @@ Application.Controllers.controller "NewEventController", ["$scope", "$state", "$
{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)
]
@ -438,8 +451,8 @@ Application.Controllers.controller "NewEventController", ["$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) ->
Application.Controllers.controller "EditEventController", ["$scope", "$state", "$stateParams", 'CSRF', 'eventPromise', 'categoriesPromise', 'themesPromise', 'ageRangesPromise', 'priceCategoriesPromise'
, ($scope, $state, $stateParams, CSRF, eventPromise, categoriesPromise, themesPromise, ageRangesPromise, priceCategoriesPromise) ->
### PUBLIC SCOPE ###
@ -454,9 +467,6 @@ Application.Controllers.controller "EditEventController", ["$scope", "$state", "
## 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

View File

@ -217,7 +217,7 @@ Application.Controllers.controller "GraphsController", ["$scope", "$state", "$ro
for it_st in [0.. cur_type.subtypes.length-1] by 1 # when we've found it, iterate over its subtypes ...
cur_subtype = cur_type.subtypes[it_st]
if subgroup.key == cur_subtype.key # ... which match $SUBTYPE
# then we construct NVD3 dataSource according to these informations
# then we construct NVD3 dataSource according to these information
dataSource =
values: []
key: cur_subtype.label
@ -362,6 +362,10 @@ Application.Controllers.controller "GraphsController", ["$scope", "$state", "$ro
"index": "stats"
"type": esType
"searchType": "count"
"stat-type": statType
"custom-query": ''
"start-date": moment($scope.datePickerStart.selected).format()
"end-date": moment($scope.datePickerEnd.selected).format()
"body": buildElasticAggregationsQuery(statType, $scope.display.interval, moment($scope.datePickerStart.selected), moment($scope.datePickerEnd.selected))
, (error, response) ->
if (error)

View File

@ -125,7 +125,7 @@ Application.Controllers.controller "InvoicesController", ["$scope", "$state", 'I
sample = sample.replace(/y+(?![^\[]*])/g, (match, offset, string) ->
padWithZeros(8, match.length)
)
# date informations
# date information
sample = sample.replace(/[YMD]+(?![^\[]*])/g, (match, offset, string) ->
$scope.today.format(match)
)
@ -133,9 +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 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
@ -164,7 +163,7 @@ Application.Controllers.controller "InvoicesController", ["$scope", "$state", 'I
sample = sample.replace(/d+(?![^\[]*])/g, (match, offset, string) ->
padWithZeros(2, match.length)
)
# date informations
# date information
sample = sample.replace(/[YMD]+(?![^\[]*])/g, (match, offset, string) ->
$scope.today.format(match)
)
@ -335,7 +334,7 @@ Application.Controllers.controller "InvoicesController", ["$scope", "$state", 'I
##
# Callback to save the value of the legal informations zone when editing is done
# Callback to save the value of the legal information zone when editing is done
##
$scope.legalsEditEnd = (event) ->
parsed = parseHtml($scope.invoice.legals.content)

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', 'Export'
, ($scope, membersPromise, adminsPromise, growl, Admin, dialogs, _t, Member, Export) ->
Application.Controllers.controller "AdminMembersController", ["$scope","$sce", 'membersPromise', 'adminsPromise', 'growl', 'Admin', 'dialogs', '_t', 'Member', 'Export'
, ($scope, $sce, membersPromise, adminsPromise, growl, Admin, dialogs, _t, Member, Export) ->
@ -177,7 +177,7 @@ Application.Controllers.controller "AdminMembersController", ["$scope", 'members
resolve:
object: ->
title: _t('confirmation_required')
msg: _t('do_you_really_want_to_delete_this_administrator_this_cannot_be_undone')
msg: $sce.trustAsHtml(_t('do_you_really_want_to_delete_this_administrator_this_cannot_be_undone') + '<br/><br/>' +_t('this_may_take_a_while_please_wait'))
, -> # cancel confirmed
Admin.delete id: admin.id, ->
admins.splice(findAdminIdxById(admins, admin.id), 1)
@ -421,16 +421,42 @@ Application.Controllers.controller "EditMemberController", ["$scope", "$state",
modalInstance = $uibModal.open
animation: true,
templateUrl: '<%= asset_path "wallet/credit_modal.html" %>'
controller: ['$scope', '$uibModalInstance', 'Wallet', '$locale', ($scope, $uibModalInstance, Wallet, $locale) ->
controller: ['$scope', '$uibModalInstance', 'Wallet', ($scope, $uibModalInstance, Wallet) ->
## currency symbol for the current locale (cf. angular-i18n)
$scope.currencySymbol = $locale.NUMBER_FORMATS.CURRENCY_SYM
# default: do not generate a refund invoice
$scope.generate_avoir = false
# date of the generated refund invoice
$scope.avoir_date = null
# optional description shown on the refund invoice
$scope.description = ''
# default configuration for the avoir date selector widget
$scope.datePicker =
format: Fablab.uibDateFormat
opened: false
options:
startingDay: Fablab.weekStartingDay
##
# Callback to open/close the date picker
##
$scope.toggleDatePicker = ($event) ->
$event.preventDefault()
$event.stopPropagation()
$scope.datePicker.opened = !$scope.datePicker.opened
##
# Modal dialog validation callback
##
$scope.ok = ->
Wallet.credit { id: wallet.id }, { amount: $scope.amount }, (_wallet)->
Wallet.credit { id: wallet.id },
amount: $scope.amount
avoir: $scope.generate_avoir
avoir_date: $scope.avoir_date
avoir_description: $scope.description
, (_wallet)->
growl.success(_t('wallet_credit_successfully'))
$uibModalInstance.close(_wallet)

View File

@ -6,7 +6,7 @@
class PlanController
constructor: ($scope, groups, plans, machines, prices, partners, CSRF) ->
constructor: ($scope, groups, prices, partners, CSRF) ->
# protection against request forgery
CSRF.setMetaTags()
@ -15,12 +15,6 @@ class PlanController
## groups list
$scope.groups = groups
## plans list
$scope.plans = plans
## machines list
$scope.machines = machines
## users with role 'partner', notifiables for a partner plan
$scope.partners = partners.users
@ -48,38 +42,11 @@ class PlanController
##
# Retrieve a plan from its numeric identifier
# @param id {number} plan ID
# @returns {Object} Plan, inherits from $resource
##
$scope.getPlanFromId = (id) ->
for plan in $scope.plans
if plan.id == id
return plan
##
# 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
##
# Controller used in the plan creation form
##
Application.Controllers.controller 'NewPlanController', ['$scope', '$uibModal', 'groups', 'plans', 'machines', 'prices', 'partners', 'CSRF', '$state', 'growl', '_t', '$locale'
, ($scope, $uibModal, groups, plans, machines, prices, partners, CSRF, $state, growl, _t, $locale) ->
Application.Controllers.controller 'NewPlanController', ['$scope', '$uibModal', 'groups', 'prices', 'partners', 'CSRF', '$state', 'growl', '_t'
, ($scope, $uibModal, groups, prices, partners, CSRF, $state, growl, _t) ->
@ -119,10 +86,6 @@ Application.Controllers.controller 'NewPlanController', ['$scope', '$uibModal',
$scope.method = 'POST'
## currency symbol for the current locale (cf. angular-i18n)
$scope.currencySymbol = $locale.NUMBER_FORMATS.CURRENCY_SYM;
##
# Checks if the partner contact is a valid data. Used in the form validation process
@ -150,7 +113,7 @@ Application.Controllers.controller 'NewPlanController', ['$scope', '$uibModal',
$scope.partner.name = "#{user.first_name} #{user.last_name}"
$uibModalInstance.close($scope.partner)
, (error)->
growl.error(_t('unable_to_save_this_user_check_that_there_isnt_an_already_a_user_with_the_same_name'))
growl.error(_t('new_plan.unable_to_save_this_user_check_that_there_isnt_an_already_a_user_with_the_same_name'))
$scope.cancel = ->
$uibModalInstance.dismiss('cancel')
]
@ -168,9 +131,9 @@ Application.Controllers.controller 'NewPlanController', ['$scope', '$uibModal',
##
$scope.afterSubmit = (content) ->
if !content.id? and !content.plan_ids?
growl.error(_t('unable_to_create_the_subscription_please_try_again'))
growl.error(_t('new_plan.unable_to_create_the_subscription_please_try_again'))
else
growl.success(_t('successfully_created_subscription(s)_dont_forget_to_redefine_prices'))
growl.success(_t('new_plan.successfully_created_subscription(s)_dont_forget_to_redefine_prices'))
if content.plan_ids?
$state.go('app.admin.pricing')
else
@ -179,7 +142,7 @@ Application.Controllers.controller 'NewPlanController', ['$scope', '$uibModal',
new PlanController($scope, groups, plans, machines, prices, partners, CSRF)
new PlanController($scope, groups, prices, partners, CSRF)
]
@ -187,13 +150,25 @@ Application.Controllers.controller 'NewPlanController', ['$scope', '$uibModal',
##
# Controller used in the plan edition form
##
Application.Controllers.controller 'EditPlanController', ['$scope', 'groups', 'plans', 'planPromise', 'machines', 'prices', 'partners', 'CSRF', '$state', '$stateParams', 'growl', '$filter', '_t', '$locale', 'Plan'
, ($scope, groups, plans, planPromise, machines, prices, partners, CSRF, $state, $stateParams, growl, $filter, _t, $locale, Plan) ->
Application.Controllers.controller 'EditPlanController', ['$scope', 'groups', 'plans', 'planPromise', 'machines', 'spaces', 'prices', 'partners', 'CSRF', '$state', '$stateParams', 'growl', '$filter', '_t', 'Plan'
, ($scope, groups, plans, planPromise, machines, spaces, prices, partners, CSRF, $state, $stateParams, growl, $filter, _t, Plan) ->
### PUBLIC SCOPE ###
## List of spaces
$scope.spaces = spaces
## List of plans
$scope.plans = plans
## List of machines
$scope.machines = machines
## List of groups
$scope.groups = groups
## current form is used for edition mode
$scope.mode = 'edition'
@ -208,10 +183,6 @@ Application.Controllers.controller 'EditPlanController', ['$scope', 'groups', 'p
$scope.method = 'PATCH'
## currency symbol for the current locale (cf. angular-i18n)
$scope.currencySymbol = $locale.NUMBER_FORMATS.CURRENCY_SYM;
##
# If a parent plan was set ($scope.plan.parent), the prices will be copied from this parent plan into
@ -239,9 +210,9 @@ Application.Controllers.controller 'EditPlanController', ['$scope', 'groups', 'p
##
$scope.afterSubmit = (content) ->
if !content.id? and !content.plan_ids?
growl.error(_t('unable_to_save_subscription_changes_please_try_again'))
growl.error(_t('edit_plan.unable_to_save_subscription_changes_please_try_again'))
else
growl.success(_t('subscription_successfully_changed'))
growl.success(_t('edit_plan.subscription_successfully_changed'))
$state.go('app.admin.pricing')
@ -259,6 +230,30 @@ Application.Controllers.controller 'EditPlanController', ['$scope', 'groups', 'p
##
# 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
##
# Retrieve the name of a space from its ID
# @param space_id {number} space identifier
# @returns {string} Space's name
##
$scope.getSpaceName = (space_id) ->
for space in $scope.spaces
if space.id == space_id
return space.name
### PRIVATE SCOPE ###
##
@ -266,7 +261,7 @@ Application.Controllers.controller 'EditPlanController', ['$scope', 'groups', 'p
##
initialize = ->
# Using the PlansController
new PlanController($scope, groups, plans, machines, prices, partners, CSRF)
new PlanController($scope, groups, prices, partners, CSRF)
## !!! MUST BE CALLED AT THE END of the controller
initialize()

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', '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) ->
Application.Controllers.controller "EditPricingController", ["$scope", "$state", '$uibModal', '$filter', 'TrainingsPricing', 'Credit', 'Pricing', 'Plan', 'Coupon', 'plans', 'groups', 'growl', 'machinesPricesPromise', 'Price', 'dialogs', 'trainingsPricingsPromise', 'trainingsPromise', 'machineCreditsPromise', 'machinesPromise', 'trainingCreditsPromise', 'couponsPromise', 'spacesPromise', 'spacesPricesPromise', 'spacesCreditsPromise', '_t'
, ($scope, $state, $uibModal, $filter, TrainingsPricing, Credit, Pricing, Plan, Coupon, plans, groups, growl, machinesPricesPromise, Price, dialogs, trainingsPricingsPromise, trainingsPromise, machineCreditsPromise, machinesPromise, trainingCreditsPromise, couponsPromise, spacesPromise, spacesPricesPromise, spacesCreditsPromise, _t) ->
### PUBLIC SCOPE ###
## List of machines prices (not considering any plan)
@ -37,6 +37,15 @@ Application.Controllers.controller "EditPricingController", ["$scope", "$state",
## List of coupons
$scope.coupons = couponsPromise
## List of spaces
$scope.spaces = spacesPromise
## Associate free space hours with subscriptions
$scope.spaceCredits = spacesCreditsPromise
## List of spaces prices (not considering any plan)
$scope.spacesPrices = spacesPricesPromise
## The plans list ordering. Default: by group
$scope.orderPlans = 'group_id'
@ -56,7 +65,7 @@ Application.Controllers.controller "EditPricingController", ["$scope", "$state",
if data?
TrainingsPricing.update({ id: trainingsPricing.id }, { trainings_pricing: { amount: data } }).$promise
else
_t('please_specify_a_number')
_t('pricing.please_specify_a_number')
##
# Retrieve a plan from its given identifier and returns it
@ -89,13 +98,13 @@ Application.Controllers.controller "EditPricingController", ["$scope", "$state",
##
$scope.showTrainings = (trainings) ->
unless angular.isArray(trainings) and trainings.length > 0
return _t('none')
return _t('pricing.none')
selected = []
angular.forEach $scope.trainings, (t) ->
if trainings.indexOf(t.id) >= 0
selected.push t.name
return if selected.length then selected.join(' | ') else _t('none')
return if selected.length then selected.join(' | ') else _t('pricing.none')
@ -110,7 +119,7 @@ Application.Controllers.controller "EditPricingController", ["$scope", "$state",
training_credit_nb: newdata.training_credits
, angular.noop() # do nothing in case of success
, (error) ->
growl.error(_t('an_error_occurred_while_saving_the_number_of_credits'))
growl.error(_t('pricing.an_error_occurred_while_saving_the_number_of_credits'))
# save the associated trainings
angular.forEach $scope.trainingCreditsGroups, (original, key) ->
@ -126,9 +135,9 @@ Application.Controllers.controller "EditPricingController", ["$scope", "$state",
$scope.trainingCredits.splice($scope.trainingCredits.indexOf(tc), 1)
$scope.trainingCreditsGroups[planId].splice($scope.trainingCreditsGroups[planId].indexOf(tc.id), 1)
, (error) ->
growl.error(_t('an_error_occurred_while_deleting_credit_with_the_TRAINING', {TRAINING:tc.creditable.name}))
growl.error(_t('pricing.an_error_occurred_while_deleting_credit_with_the_TRAINING', {TRAINING:tc.creditable.name}))
else
growl.error(_t('an_error_occurred_unable_to_find_the_credit_to_revoke'))
growl.error(_t('pricing.an_error_occurred_unable_to_find_the_credit_to_revoke'))
# iterate through the new credits to add
angular.forEach newdata.training_ids, (newTrainingId) ->
@ -143,7 +152,7 @@ Application.Controllers.controller "EditPricingController", ["$scope", "$state",
$scope.trainingCreditsGroups[newTc.plan_id].push(newTc.creditable_id)
, (error) -> # failed
training = getTrainingFromId(newTrainingId)
growl.error(_t('an_error_occurred_while_creating_credit_with_the_TRAINING', {TRAINING: training.name}))
growl.error(_t('pricing.an_error_occurred_while_creating_credit_with_the_TRAINING', {TRAINING: training.name}))
console.error(error)
@ -177,11 +186,16 @@ Application.Controllers.controller "EditPricingController", ["$scope", "$state",
# @param credit {Object} credit object, inherited from $resource
##
$scope.showCreditableName = (credit) ->
selected = _t('not_set')
selected = _t('pricing.not_set')
if credit and credit.creditable_id
angular.forEach $scope.machines, (m)->
if m.id == credit.creditable_id
selected = m.name+' ( id. '+m.id+' )'
if credit.creditable_type == 'Machine'
angular.forEach $scope.machines, (m)->
if m.id == credit.creditable_id
selected = m.name + ' ( id. ' + m.id + ' )'
else if credit.creditable_type == 'Space'
angular.forEach $scope.spaces, (s)->
if s.id == credit.creditable_id
selected = s.name
return selected
@ -195,27 +209,27 @@ Application.Controllers.controller "EditPricingController", ["$scope", "$state",
$scope.saveMachineCredit = (data, id) ->
for mc in $scope.machineCredits
if mc.plan_id == data.plan_id and mc.creditable_id == data.creditable_id and (id == null or mc.id != id)
growl.error(_t('error_a_credit_linking_this_machine_with_that_subscription_already_exists'))
growl.error(_t('pricing.error_a_credit_linking_this_machine_with_that_subscription_already_exists'))
unless id
$scope.machineCredits.pop()
return false
if id?
Credit.update {id: id}, credit: data, ->
growl.success(_t('changes_have_been_successfully_saved'))
growl.success(_t('pricing.changes_have_been_successfully_saved'))
else
data.creditable_type = 'Machine'
Credit.save
credit: data
, (resp) ->
$scope.machineCredits[$scope.machineCredits.length-1].id = resp.id
growl.success(_t('credit_was_successfully_saved'))
growl.success(_t('pricing.credit_was_successfully_saved'))
##
# Removes the newly inserted but not saved machine credit / Cancel the current machine credit modification
# @param rowform {Object} see http://vitalets.github.io/angular-xeditable/
# @param index {number} theme index in the $scope.machineCredits array
# @param index {number} credit index in the $scope.machineCredits array
##
$scope.cancelMachineCredit = (rowform, index) ->
if $scope.machineCredits[index].id?
@ -235,6 +249,70 @@ Application.Controllers.controller "EditPricingController", ["$scope", "$state",
##
# Create a new empty entry in the $scope.spaceCredits array
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
##
$scope.addSpaceCredit = (e)->
e.preventDefault()
e.stopPropagation()
$scope.inserted =
creditable_type: 'Space'
$scope.spaceCredits.push($scope.inserted)
$scope.status.isopen = !$scope.status.isopen
##
# Validation callback when editing space's credits. Save the changes.
# This will prevent the creation of two credits associated with the same space and plan.
# @param data {Object} space, associated plan and number of credit hours.
# @param [id] {number} credit id for edition, create a new credit object if not provided
##
$scope.saveSpaceCredit = (data, id) ->
for sc in $scope.spaceCredits
if sc.plan_id == data.plan_id and sc.creditable_id == data.creditable_id and (id == null or sc.id != id)
growl.error(_t('pricing.error_a_credit_linking_this_space_with_that_subscription_already_exists'))
unless id
$scope.spaceCredits.pop()
return false
if id?
Credit.update {id: id}, credit: data, ->
growl.success(_t('pricing.changes_have_been_successfully_saved'))
else
data.creditable_type = 'Space'
Credit.save
credit: data
, (resp) ->
$scope.spaceCredits[$scope.spaceCredits.length - 1].id = resp.id
growl.success(_t('pricing.credit_was_successfully_saved'))
##
# Removes the newly inserted but not saved space credit / Cancel the current space credit modification
# @param rowform {Object} see http://vitalets.github.io/angular-xeditable/
# @param index {number} credit index in the $scope.spaceCredits array
##
$scope.cancelSpaceCredit = (rowform, index) ->
if $scope.spaceCredits[index].id?
rowform.$cancel()
else
$scope.spaceCredits.splice(index, 1)
##
# Deletes the space credit at the specified index
# @param index {number} space credit index in the $scope.spaceCredits array
##
$scope.removeSpaceCredit = (index) ->
Credit.delete $scope.spaceCredits[index]
$scope.spaceCredits.splice(index, 1)
##
# If the plan does not have a type, return a default value for display purposes
# @param type {string|undefined|null} plan's type (eg. 'partner')
@ -242,8 +320,8 @@ Application.Controllers.controller "EditPricingController", ["$scope", "$state",
##
$scope.getPlanType = (type) ->
if type == 'PartnerPlan'
return _t('partner')
else return _t('standard')
return _t('pricing.partner')
else return _t('pricing.standard')
##
# Change the plans ordering criterion to the one provided
@ -270,7 +348,7 @@ Application.Controllers.controller "EditPricingController", ["$scope", "$state",
if data?
Price.update({ id: price.id }, { price: { amount: data } }).$promise
else
_t('please_specify_a_number')
_t('pricing.please_specify_a_number')
##
# Delete the specified subcription plan
@ -284,17 +362,17 @@ Application.Controllers.controller "EditPricingController", ["$scope", "$state",
dialogs.confirm
resolve:
object: ->
title: _t('confirmation_required')
msg: _t('do_you_really_want_to_delete_this_subscription_plan')
title: _t('pricing.confirmation_required')
msg: _t('pricing.do_you_really_want_to_delete_this_subscription_plan')
, ->
# the admin has confirmed, delete the plan
Plan.delete {id: id}, (res) ->
growl.success(_t('subscription_plan_was_successfully_deleted'))
growl.success(_t('pricing.subscription_plan_was_successfully_deleted'))
$scope.plans.splice(findItemIdxById(plans, id), 1)
, (error) ->
console.error('[EditPricingController::deletePlan] Error: '+error.statusText) if error.statusText
growl.error(_t('unable_to_delete_the_specified_subscription_an_error_occurred'))
growl.error(_t('pricing.unable_to_delete_the_specified_subscription_an_error_occurred'))
@ -324,8 +402,8 @@ Application.Controllers.controller "EditPricingController", ["$scope", "$state",
dialogs.confirm
resolve:
object: ->
title: _t('confirmation_required')
msg: _t('do_you_really_want_to_delete_this_coupon')
title: _t('pricing.confirmation_required')
msg: _t('pricing.do_you_really_want_to_delete_this_coupon')
, ->
# the admin has confirmed, delete the coupon
Coupon.delete {id: id}, (res) ->
@ -335,9 +413,9 @@ Application.Controllers.controller "EditPricingController", ["$scope", "$state",
, (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'))
growl.error(_t('pricing.unable_to_delete_the_specified_coupon_already_in_use'))
else
growl.error(_t('unable_to_delete_the_specified_coupon_an_unexpected_error_occurred'))
growl.error(_t('pricing.unable_to_delete_the_specified_coupon_an_unexpected_error_occurred'))
@ -363,10 +441,10 @@ Application.Controllers.controller "EditPricingController", ["$scope", "$state",
## 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}))
growl.success(_t('pricing.coupon_successfully_sent_to_USER', {USER: $scope.ctrl.member.name}))
$uibModalInstance.close({user_id: $scope.ctrl.member.id})
, (err) ->
growl.error(_t('an_error_occurred_unable_to_send_the_coupon'))
growl.error(_t('pricing.an_error_occurred_unable_to_send_the_coupon'))
## Callback to close the modal and cancel the sending process
$scope.cancel = ->

View File

@ -45,6 +45,8 @@ 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.eventExplicationsAlert = {name: 'event_explications_alert', value: settingsPromise.event_explications_alert }
$scope.spaceExplicationsAlert = { name: 'space_explications_alert', value: settingsPromise.space_explications_alert }
$scope.windowStart = { name: 'booking_window_start', value: settingsPromise.booking_window_start }
$scope.windowEnd = { name: 'booking_window_end', value: settingsPromise.booking_window_end }
$scope.mainColorSetting = { name: 'main_color', value: settingsPromise.main_color }
@ -73,7 +75,7 @@ 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')
@ -115,7 +117,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:_t(setting.name)}))
growl.success(_t('settings.customization_of_SETTING_successfully_saved', { SETTING:_t('settings.' + setting.name) }))
, (error)->
console.log(error)
@ -134,7 +136,7 @@ Application.Controllers.controller "SettingsController", ["$scope", 'Setting', '
angular.forEach v, (err)->
growl.error(err)
else
growl.success(_t('file_successfully_updated'))
growl.success(_t('settings.file_successfully_updated'))
if content.custom_asset.name is 'cgu-file'
$scope.cguFile = content.custom_asset
$scope.methods.cgu = 'put'

View File

@ -144,7 +144,7 @@ Application.Controllers.controller "StatisticsController", ["$scope", "$state",
##
# Callback called when the active tab is changed.
# recover the current tab and store its value in $scope.selectedIndex
# @param tab {Object} elasticsearch statistic structure
# @param tab {Object} elasticsearch statistic structure (from statistic_indices table)
##
$scope.setActiveTab = (tab) ->
$scope.selectedIndex = tab
@ -160,6 +160,23 @@ Application.Controllers.controller "StatisticsController", ["$scope", "$state",
##
# Returns true if the provided tab must be hidden due to some global or local configuration
# @param tab {Object} elasticsearch statistic structure (from statistic_indices table)
##
$scope.hiddenTab = (tab) ->
if tab.table
if tab.es_type_key == 'subscription' && $rootScope.fablabWithoutPlans
true
else if tab.es_type_key == 'space' && $rootScope.fablabWithoutSpaces
true
else
false
else
true
##
# Callback to validate the filters and send a new request to elastic
##
@ -380,6 +397,7 @@ Application.Controllers.controller "StatisticsController", ["$scope", "$state",
"size": RESULTS_PER_PAGE
"scroll": ES_SCROLL_TIME+'m'
"stat-type": type
"custom-query": if custom then JSON.stringify(Object.assign({exclude: custom.exclude}, buildElasticCustomCriterion(custom))) else ''
"start-date": moment($scope.datePickerStart.selected).format()
"end-date": moment($scope.datePickerEnd.selected).format()
"body": buildElasticDataQuery(type, custom, $scope.agePicker.start, $scope.agePicker.end, moment($scope.datePickerStart.selected), moment($scope.datePickerEnd.selected), $scope.sorting)
@ -427,15 +445,7 @@ Application.Controllers.controller "StatisticsController", ["$scope", "$state",
"lte": ageMax
# optional criterion
if custom
criterion = {
"match" : {}
}
switch $scope.getCustomValueInputType($scope.customFilter.criterion)
when 'input_date' then criterion.match[custom.key] = moment(custom.value).format('YYYY-MM-DD')
when 'input_select' then criterion.match[custom.key] = custom.value.key
when 'input_list' then criterion.match[custom.key+".name"] = custom.value
else criterion.match[custom.key] = custom.value
criterion = buildElasticCustomCriterion(custom)
if (custom.exclude)
q = "query": {
"filtered": {
@ -470,6 +480,27 @@ Application.Controllers.controller "StatisticsController", ["$scope", "$state",
##
# Build the elasticSearch query DSL to match the selected cutom filter
# @param custom {Object} if custom is empty or undefined, an empty string will be returned
# @returns {{match:{*}}|string}
##
buildElasticCustomCriterion = (custom) ->
if (custom)
criterion = {
"match" : {}
}
switch $scope.getCustomValueInputType($scope.customFilter.criterion)
when 'input_date' then criterion.match[custom.key] = moment(custom.value).format('YYYY-MM-DD')
when 'input_select' then criterion.match[custom.key] = custom.value.key
when 'input_list' then criterion.match[custom.key+".name"] = custom.value
else criterion.match[custom.key] = custom.value
criterion
else
''
##
# Parse the provided criteria array and return the corresponding elasticSearch syntax
# @param criteria {Array} array of {key_to_sort:order}

View File

@ -298,6 +298,13 @@ Application.Controllers.controller "TrainingsAdminController", ["$scope", "$stat
$scope.selectTrainingToMonitor = ->
Training.availabilities {id: $scope.monitoring.training.id}, (training) ->
$scope.groupedAvailabilities = groupAvailabilities([training])
# we open current year/month by default
now = moment()
$scope.accordions[training.name] = {}
$scope.accordions[training.name][now.year()] =
isOpenFirst: true
$scope.accordions[training.name][now.year()][now.month()] =
isOpenFirst: true

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', 'Version'
, ($rootScope, $scope, $window, Session, AuthService, Auth, $uibModal, $state, growl, Notification, $interval, Setting, _t, Version) ->
Application.Controllers.controller 'ApplicationController', ["$rootScope", "$scope", "$window", '$locale', "Session", "AuthService", "Auth", "$uibModal", "$state", 'growl', 'Notification', '$interval', "Setting", '_t', 'Version'
, ($rootScope, $scope, $window, $locale, Session, AuthService, Auth, $uibModal, $state, growl, Notification, $interval, Setting, _t, Version) ->
@ -18,19 +18,24 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
$scope.version =
version: ''
## currency symbol for the current locale (cf. angular-i18n)
$rootScope.currencySymbol = $locale.NUMBER_FORMATS.CURRENCY_SYM;
##
# Set the current user to the provided value and initialize the session
# @param user {Object} Rails/Devise user
##
$scope.setCurrentUser = (user) ->
$rootScope.currentUser = user
Session.create(user);
getNotifications()
# fab-manager's app-version
if user.role == 'admin'
$scope.version = Version.get()
else
$scope.version = {version: ''}
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: ''}
##
@ -55,7 +60,9 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
Session.destroy()
$rootScope.currentUser = null
$rootScope.toCheckNotifications = false
$scope.notifications = []
$scope.notifications =
total: 0
unread: 0
$state.go('app.public.home')
, (error) ->
# An error occurred logging out.
@ -235,6 +242,10 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
# user is not logged in
openLoginModal(toState, toParams)
# we stop polling notifications when the page is not in foreground
onPageVisible (state) ->
$rootScope.toCheckNotifications = (state is 'visible')
Setting.get { name: 'fablab_name' }, (data)->
$scope.fablabName = data.setting.value
Setting.get { name: 'name_genre' }, (data)->
@ -243,8 +254,9 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
# shorthands
$scope.isAuthenticated = Auth.isAuthenticated;
$scope.isAuthorized = AuthService.isAuthorized;
$scope.isAuthenticated = Auth.isAuthenticated
$scope.isAuthorized = AuthService.isAuthorized
$rootScope.login = $scope.login
@ -255,29 +267,31 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
getNotifications = ->
$rootScope.toCheckNotifications = true
unless $rootScope.checkNotificationsIsInit or !$rootScope.currentUser
$scope.notifications = Notification.query {is_read: false}
$scope.$watch 'notifications', (newValue, oldValue) ->
diff = []
angular.forEach newValue, (value) ->
find = false
for i in [0..oldValue.length] by 1
if oldValue[i] and (value.id is oldValue[i].id)
find = true
break
setTimeout ->
# we request the most recent notifications
Notification.last_unread (notifications) ->
$rootScope.lastCheck = new Date()
$scope.notifications = notifications.totals
unless find
diff.push(value)
toDisplay = []
angular.forEach notifications.notifications, (n) ->
toDisplay.push(n)
if toDisplay.length < notifications.totals.unread
toDisplay.push({message: {description: _t('and_NUMBER_other_notifications', {NUMBER: notifications.totals.unread - toDisplay.length}, "messageformat")}})
angular.forEach diff, (notification, key) ->
growl.info(notification.message.description)
, true
angular.forEach toDisplay, (notification) ->
growl.info(notification.message.description)
, 2000
checkNotifications = ->
if $rootScope.toCheckNotifications
Notification.query({is_read: false}).$promise.then (data) ->
$scope.notifications = data;
Notification.polling({last_poll: $rootScope.lastCheck}).$promise.then (data) ->
$rootScope.lastCheck = new Date()
$scope.notifications = data.totals
angular.forEach data.notifications, (notification) ->
growl.info(notification.message.description)
$interval(checkNotifications, NOTIFICATIONS_CHECK_PERIOD)
$rootScope.checkNotificationsIsInit = true
@ -361,6 +375,52 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
##
# Detect if the current page (tab/window) is active of put as background.
# When the status changes, the callback is triggered with the new status as parameter
# Inspired by http://stackoverflow.com/questions/1060008/is-there-a-way-to-detect-if-a-browser-window-is-not-currently-active#answer-1060034
##
onPageVisible = (callback) ->
hidden = 'hidden'
onchange = (evt) ->
v = 'visible'
h = 'hidden'
evtMap =
focus: v
focusin: v
pageshow: v
blur: h
focusout: h
pagehide: h
evt = evt or window.event
if evt.type of evtMap
if typeof callback == 'function' then callback(evtMap[evt.type])
else
if typeof callback == 'function' then callback(if @[hidden] then 'hidden' else 'visible')
return
# Standards:
if hidden of document
document.addEventListener 'visibilitychange', onchange
else if (hidden = 'mozHidden') of document
document.addEventListener 'mozvisibilitychange', onchange
else if (hidden = 'webkitHidden') of document
document.addEventListener 'webkitvisibilitychange', onchange
else if (hidden = 'msHidden') of document
document.addEventListener 'msvisibilitychange', onchange
# IE 9 and lower
else if 'onfocusin' of document
document.onfocusin = document.onfocusout = onchange
# All others
else
window.onpageshow = window.onpagehide = window.onfocus = window.onblur = onchange
# set the initial state (but only if browser supports the Page Visibility API)
if document[hidden] != undefined
onchange type: if document[hidden] then 'blur' else 'focus'
## !!! MUST BE CALLED AT THE END of the controller
initialize()
]

View File

@ -4,14 +4,15 @@
# 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) ->
Application.Controllers.controller "CalendarController", ["$scope", "$state", "$aside", "moment", "Availability", 'Slot', 'Setting', 'growl', 'dialogs', 'bookingWindowStart', 'bookingWindowEnd', '_t', 'uiCalendarConfig', 'CalendarConfig', 'trainingsPromise', 'machinesPromise', 'spacesPromise',
($scope, $state, $aside, moment, Availability, Slot, Setting, growl, dialogs, bookingWindowStart, bookingWindowEnd, _t, uiCalendarConfig, CalendarConfig, trainingsPromise, machinesPromise, spacesPromise) ->
### PRIVATE STATIC CONSTANTS ###
currentMachineEvent = null
machinesPromise.forEach((m) -> m.checked = true)
trainingsPromise.forEach((t) -> t.checked = true)
spacesPromise.forEach((s) -> s.checked = true)
## check all formation/machine is select in filter
isSelectAll = (type, scope) ->
@ -25,6 +26,9 @@ Application.Controllers.controller "CalendarController", ["$scope", "$state", "$
## List of machines
$scope.machines = machinesPromise
## List of spaces
$scope.spaces = spacesPromise
## add availabilities source to event sources
$scope.eventSources = []
@ -34,6 +38,7 @@ Application.Controllers.controller "CalendarController", ["$scope", "$state", "$
scope.filter = $scope.filter =
trainings: isSelectAll('trainings', scope)
machines: isSelectAll('machines', scope)
spaces: isSelectAll('spaces', scope)
evt: filter.evt
dispo: filter.dispo
$scope.calendarConfig.events = availabilitySourceUrl()
@ -43,6 +48,7 @@ Application.Controllers.controller "CalendarController", ["$scope", "$state", "$
$scope.filter =
trainings: isSelectAll('trainings', $scope)
machines: isSelectAll('machines', $scope)
spaces: isSelectAll('spaces', $scope)
evt: true
dispo: true
@ -62,15 +68,18 @@ Application.Controllers.controller "CalendarController", ["$scope", "$state", "$
$scope.trainings
machines: ->
$scope.machines
spaces: ->
$scope.spaces
filter: ->
$scope.filter
toggleFilter: ->
$scope.toggleFilter
filterAvailabilities: ->
$scope.filterAvailabilities
controller: ['$scope', '$uibModalInstance', 'trainings', 'machines', 'filter', 'toggleFilter', 'filterAvailabilities', ($scope, $uibModalInstance, trainings, machines, filter, toggleFilter, filterAvailabilities) ->
controller: ['$scope', '$uibModalInstance', 'trainings', 'machines', 'spaces', 'filter', 'toggleFilter', 'filterAvailabilities', ($scope, $uibModalInstance, trainings, machines, spaces, filter, toggleFilter, filterAvailabilities) ->
$scope.trainings = trainings
$scope.machines = machines
$scope.spaces = spaces
$scope.filter = filter
$scope.toggleFilter = (type, filter) ->
@ -94,13 +103,19 @@ Application.Controllers.controller "CalendarController", ["$scope", "$state", "$
currentMachineEvent = event
calendar.fullCalendar('changeView', 'agendaDay')
calendar.fullCalendar('gotoDate', event.start)
else if event.available_type == 'space'
calendar.fullCalendar('changeView', 'agendaDay')
calendar.fullCalendar('gotoDate', event.start)
else if event.available_type == 'event'
$state.go('app.public.events_show', {id: event.event_id})
else if event.available_type == 'training'
$state.go('app.public.training_show', {id: event.training_id})
else
if event.available_type == 'event'
$state.go('app.public.events_show', {id: event.event_id})
else if event.available_type == 'training'
$state.go('app.public.training_show', {id: event.training_id})
else
if event.machine_id
$state.go('app.public.machines_show', {id: event.machine_id})
else if event.space_id
$state.go('app.public.space_show', {id: event.space_id})
## agendaDay view: disable slotEventOverlap
## agendaWeek view: enable slotEventOverlap
@ -109,10 +124,10 @@ Application.Controllers.controller "CalendarController", ["$scope", "$state", "$
# 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
if today > view.intervalStart and today < view.intervalEnd and today != view.intervalStart
$scope.calendarConfig.defaultDate = today
else
$scope.calendarConfig.defaultDate = view.start
$scope.calendarConfig.defaultDate = view.intervalStart
if view.type == 'agendaDay'
$scope.calendarConfig.slotEventOverlap = false
else
@ -136,7 +151,8 @@ Application.Controllers.controller "CalendarController", ["$scope", "$state", "$
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}
s = $scope.spaces.filter((s) -> s.checked).map((s) -> s.id)
{t: t, m: m, s: s, evt: $scope.filter.evt, dispo: $scope.filter.dispo}
availabilitySourceUrl = ->
"/api/availabilities/public?#{$.param(getFilter())}"

View File

@ -154,6 +154,7 @@ Application.Controllers.controller "ShowEventController", ["$scope", "$state", "
tickets: {}
toReserve: false
amountTotal : 0
totalNoCoupon: 0
totalSeats: 0
## Discount coupon to apply to the basket, if any
@ -172,6 +173,9 @@ Application.Controllers.controller "ShowEventController", ["$scope", "$state", "
## Global config: delay in hours before a booking while changing the booking slot is forbidden
$scope.moveBookingDelay = parseInt(settingsPromise.booking_move_delay)
## Message displayed to the end user about rules that applies to events reservations
$scope.eventExplicationsAlert = settingsPromise.event_explications_alert
##
@ -400,6 +404,7 @@ Application.Controllers.controller "ShowEventController", ["$scope", "$state", "
r = mkReservation($scope.ctrl.member, $scope.reserve, $scope.event)
Price.compute mkRequestParams(r, $scope.coupon.applied), (res) ->
$scope.reserve.amountTotal = res.price
$scope.reserve.totalNoCoupon = res.price_without_coupon
else
$scope.reserve.amountTotal = null
@ -560,9 +565,9 @@ Application.Controllers.controller "ShowEventController", ["$scope", "$state", "
member: $scope.ctrl.member
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
controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'cgv', 'Auth', 'Reservation', 'growl', 'wallet', 'helpers', '$filter', 'coupon',
($scope, $uibModalInstance, $state, reservation, price, cgv, Auth, Reservation, growl, wallet, helpers, $filter, coupon) ->
# User's wallet amount
$scope.walletAmount = wallet.amount
# Price
@ -574,8 +579,7 @@ Application.Controllers.controller "ShowEventController", ["$scope", "$state", "
# Reservation
$scope.reservation = reservation
$scope.currencySymbol = $locale.NUMBER_FORMATS.CURRENCY_SYM
# Used in wallet info template to interpolate some translations
$scope.numberFilter = $filter('number')
# Callback for the stripe payment authorization
@ -616,9 +620,9 @@ Application.Controllers.controller "ShowEventController", ["$scope", "$state", "
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
controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'Auth', 'Reservation', 'wallet', 'helpers', '$filter', 'coupon',
($scope, $uibModalInstance, $state, reservation, price, Auth, Reservation, wallet, helpers, $filter, coupon) ->
# User's wallet amount
$scope.walletAmount = wallet.amount
# Price
@ -630,8 +634,7 @@ Application.Controllers.controller "ShowEventController", ["$scope", "$state", "
# Reservation
$scope.reservation = reservation
$scope.currencySymbol = $locale.NUMBER_FORMATS.CURRENCY_SYM
# Used in wallet info template to interpolate some translations
$scope.numberFilter = $filter('number')
# Button label
@ -675,6 +678,7 @@ Application.Controllers.controller "ShowEventController", ["$scope", "$state", "
$scope.event.nb_free_places = $scope.event.nb_free_places - reservation.total_booked_seats
resetEventReserve()
$scope.reserveSuccess = true
$scope.coupon.applied = null
$scope.reservations.push reservation
if $scope.currentUser.role == 'admin'
$scope.ctrl.member = null

View File

@ -93,7 +93,7 @@ _reserveMachine = (machine, e) ->
# if the currently logged'in user has completed the training for this machine, or this machine does not require
# a prior training, just redirect him to the machine's booking page
if machine.current_user_is_training or machine.trainings.length == 0
_this.$state.go('app.logged.machines_reserve', {id: machine.id})
_this.$state.go('app.logged.machines_reserve', {id: machine.slug})
else
# otherwise, if a user is authenticated ...
if _this.$scope.isAuthenticated()
@ -231,10 +231,10 @@ 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
## 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,20 +274,20 @@ 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', '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) ->
Application.Controllers.controller "ReserveMachineController", ["$scope", '$stateParams', '_t', "moment", 'Auth', '$timeout', 'Member', 'Availability', 'plansPromise', 'groupsPromise', 'machinePromise', 'settingsPromise', 'uiCalendarConfig', 'CalendarConfig',
($scope, $stateParams, _t, moment, Auth, $timeout, Member, Availability, plansPromise, groupsPromise, machinePromise, settingsPromise, uiCalendarConfig, CalendarConfig) ->
### PRIVATE STATIC CONSTANTS ###
# Slot already booked by the current user
# Slot free to be booked
FREE_SLOT_BORDER_COLOR = '<%= AvailabilityHelper::MACHINE_COLOR %>'
# Slot already booked by another user
UNAVAILABLE_SLOT_BORDER_COLOR = '<%= AvailabilityHelper::MACHINE_IS_RESERVED_BY_USER %>'
# Slot free to be booked
# Slot already booked by the current user
BOOKED_SLOT_BORDER_COLOR = '<%= AvailabilityHelper::IS_RESERVED_BY_CURRENT_USER %>'
@ -291,30 +297,31 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat
## bind the machine availabilities with full-Calendar events
$scope.eventSources = []
## fullCalendar event. The last selected slot that the user want to book
$scope.slotToPlace = null
## fullCalendar event. An already booked slot that the user want to modify
$scope.slotToModify = null
## indicates the state of the current view : calendar or plans informations
## indicates the state of the current view : calendar or plans information
$scope.plansAreShown = false
## will store the user's plan if he choosed to buy one
$scope.selectedPlan = null
## array of fullCalendar events. Slots where the user want to book
$scope.eventsReserved = []
## the moment when the plan selection changed for the last time, used to trigger changes in the cart
$scope.planSelectionTime = null
## total amount of the bill to pay
$scope.amountTotal = 0
## mapping of fullCalendar events.
$scope.events =
reserved: [] # Slots that the user wants to book
modifiable: null # Slot that the user wants to change
placable: null # Destination slot for the change
paid: [] # Slots that were just booked by the user (transaction ok)
moved: null # Slots that were just moved by the user (change done) -> {newSlot:* oldSlot: *}
## Discount coupon to apply to the basket, if any
$scope.coupon =
applied: null
## the moment when the slot selection changed for the last time, used to trigger changes in the cart
$scope.selectionTime = null
## is the user allowed to change the date of his booking
$scope.enableBookingMove = true
## the last clicked event in the calender
$scope.selectedEvent = null
## the application global settings
$scope.settings = settingsPromise
## list of plans, classified by group
$scope.plansClassifiedByGroup = []
@ -343,86 +350,87 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat
## Global config: message to the end user concerning the subscriptions rules
$scope.subscriptionExplicationsAlert = settingsPromise.subscription_explications_alert
## Gloabl config: message to the end user concerning the machine bookings
## Global config: message to the end user concerning the machine bookings
$scope.machineExplicationsAlert = settingsPromise.machine_explications_alert
## Global config: is the user authorized to change his bookings slots?
$scope.enableBookingMove = (settingsPromise.booking_move_enable == "true")
## Global config: delay in hours before a booking while changing the booking slot is forbidden
$scope.moveBookingDelay = parseInt(settingsPromise.booking_move_delay)
## Global config: is the user authorized to cancel his bookings?
$scope.enableBookingCancel = (settingsPromise.booking_cancel_enable == "true")
## Global config: delay in hours before a booking while the cancellation is forbidden
$scope.cancelBookingDelay = parseInt(settingsPromise.booking_cancel_delay)
##
# Change the last selected slot's appearence to looks like 'added to cart'
##
$scope.markSlotAsAdded = ->
$scope.selectedEvent.backgroundColor = FREE_SLOT_BORDER_COLOR
$scope.selectedEvent.title = _t('i_reserve')
updateCalendar()
##
# Cancel the current booking modification, removing the previously booked slot from the selection
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
# Change the last selected slot's appearence to looks like 'never added to cart'
##
$scope.removeSlotToModify = (e) ->
e.preventDefault()
if $scope.slotToPlace
$scope.slotToPlace.backgroundColor = 'white'
$scope.slotToPlace.title = ''
$scope.slotToPlace = null
$scope.slotToModify.title = if $scope.currentUser.role isnt 'admin' then _t('i_ve_reserved') else _t('not_available')
$scope.slotToModify.backgroundColor = 'white'
$scope.slotToModify = null
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
$scope.markSlotAsRemoved = (slot) ->
slot.backgroundColor = 'white'
slot.borderColor = FREE_SLOT_BORDER_COLOR
slot.title = ''
slot.isValid = false
slot.id = null
slot.is_reserved = false
slot.can_modify = false
slot.offered = false
updateCalendar()
##
# When modifying an already booked reservation, cancel the choice of the new slot
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
# Callback when a slot was successfully cancelled. Reset the slot style as 'ready to book'
##
$scope.removeSlotToPlace = (e)->
e.preventDefault()
$scope.slotToPlace.backgroundColor = 'white'
$scope.slotToPlace.title = ''
$scope.slotToPlace = null
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
$scope.slotCancelled = ->
$scope.markSlotAsRemoved($scope.selectedEvent)
##
# When modifying an already booked reservation, confirm the modification.
# Change the last selected slot's appearence to looks like 'currently looking for a new destination to exchange'
##
$scope.markSlotAsModifying = ->
$scope.selectedEvent.backgroundColor = '#eee'
$scope.selectedEvent.title = _t('i_change')
updateCalendar()
##
# Change the last selected slot's appearence to looks like 'the slot being exchanged will take this place'
##
$scope.changeModifyMachineSlot = ->
if $scope.events.placable
$scope.events.placable.backgroundColor = 'white'
$scope.events.placable.title = ''
if !$scope.events.placable or $scope.events.placable._id != $scope.selectedEvent._id
$scope.selectedEvent.backgroundColor = '#bbb'
$scope.selectedEvent.title = _t('i_shift')
updateCalendar()
##
# When modifying an already booked reservation, callback when the modification was successfully done.
##
$scope.modifyMachineSlot = ->
Slot.update {id: $scope.slotToModify.id},
slot:
start_at: $scope.slotToPlace.start
end_at: $scope.slotToPlace.end
availability_id: $scope.slotToPlace.availability_id
, -> # success
$scope.modifiedSlots =
newReservedSlot: $scope.slotToPlace
oldReservedSlot: $scope.slotToModify
$scope.slotToPlace.title = if $scope.currentUser.role isnt 'admin' then _t('i_ve_reserved') else _t('not_available')
$scope.slotToPlace.backgroundColor = 'white'
$scope.slotToPlace.borderColor = $scope.slotToModify.borderColor
$scope.slotToPlace.id = $scope.slotToModify.id
$scope.slotToPlace.is_reserved = true
$scope.slotToPlace.can_modify = true
$scope.slotToPlace = null
$scope.events.placable.title = if $scope.currentUser.role isnt 'admin' then _t('i_ve_reserved') else _t('not_available')
$scope.events.placable.backgroundColor = 'white'
$scope.events.placable.borderColor = $scope.events.modifiable.borderColor
$scope.events.placable.id = $scope.events.modifiable.id
$scope.events.placable.is_reserved = true
$scope.events.placable.can_modify = true
$scope.slotToModify.backgroundColor = 'white'
$scope.slotToModify.title = ''
$scope.slotToModify.borderColor = FREE_SLOT_BORDER_COLOR
$scope.slotToModify.id = null
$scope.slotToModify.is_reserved = false
$scope.slotToModify.can_modify = false
$scope.slotToModify = null
$scope.events.modifiable.backgroundColor = 'white'
$scope.events.modifiable.title = ''
$scope.events.modifiable.borderColor = FREE_SLOT_BORDER_COLOR
$scope.events.modifiable.id = null
$scope.events.modifiable.is_reserved = false
$scope.events.modifiable.can_modify = false
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
, (err) -> # failure
growl.error(_t('unable_to_change_the_reservation'))
console.error(err)
updateCalendar()
@ -430,14 +438,13 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat
# Cancel the current booking modification, reseting the whole process
##
$scope.cancelModifyMachineSlot = ->
$scope.slotToPlace.backgroundColor = 'white'
$scope.slotToPlace.title = ''
$scope.slotToPlace = null
$scope.slotToModify.title = if $scope.currentUser.role isnt 'admin' then _t('i_ve_reserved') else _t('not_available')
$scope.slotToModify.backgroundColor = 'white'
$scope.slotToModify = null
if $scope.events.placable
$scope.events.placable.backgroundColor = 'white'
$scope.events.placable.title = ''
$scope.events.modifiable.title = if $scope.currentUser.role isnt 'admin' then _t('i_ve_reserved') else _t('not_available')
$scope.events.modifiable.backgroundColor = 'white'
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
updateCalendar()
@ -446,67 +453,10 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat
# reservations. (admins only)
##
$scope.updateMember = ->
$scope.paidMachineSlots = null
$scope.plansAreShown = false
$scope.selectedPlan = null
Member.get {id: $scope.ctrl.member.id}, (member) ->
$scope.ctrl.member = member
updateCartPrice()
##
# Add the provided slot to the shopping cart (state transition from free to 'about to be reserved')
# and increment the total amount of the cart if needed.
# @param machineSlot {Object} fullCalendar event object
##
$scope.validMachineSlot = (machineSlot)->
machineSlot.isValid = true
updateCartPrice()
##
# Remove the provided slot from the shopping cart (state transition from 'about to be reserved' to free)
# and decrement the total amount of the cart if needed.
# @param machineSlot {Object} fullCalendar event object
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
##
$scope.removeMachineSlot = (machineSlot, e)->
e.preventDefault() if e
machineSlot.backgroundColor = 'white'
machineSlot.borderColor = FREE_SLOT_BORDER_COLOR
machineSlot.title = ''
machineSlot.isValid = false
if machineSlot.machine.is_reduced_amount
angular.forEach $scope.ctrl.member.machine_credits, (credit)->
if credit.machine_id = machineSlot.machine.id
credit.hours_used--
machineSlot.machine.is_reduced_amount = false
index = $scope.eventsReserved.indexOf(machineSlot)
$scope.eventsReserved.splice(index, 1)
if $scope.eventsReserved.length == 0
if $scope.plansAreShown
$scope.selectedPlan = null
$scope.plansAreShown = false
updateCartPrice()
$timeout ->
uiCalendarConfig.calendars.calendar.fullCalendar 'refetchEvents'
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
##
# Checks that every selected slots were added to the shopping cart. Ie. will return false if
# any checked slot was not validated by the user.
##
$scope.machineSlotsValid = ->
isValid = true
angular.forEach $scope.eventsReserved, (m)->
isValid = false if !m.isValid
isValid
@ -518,30 +468,10 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat
e.preventDefault()
$scope.plansAreShown = false
$scope.selectPlan($scope.selectedPlan)
$scope.planSelectionTime = new Date()
##
# Validates the shopping chart and redirect the user to the payment step
##
$scope.payMachine = ->
# first, we check that a user was selected
if Object.keys($scope.ctrl.member).length > 0
reservation = mkReservation($scope.ctrl.member, $scope.eventsReserved, $scope.selectedPlan)
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'))
##
# Switch the user's view from the reservation agenda to the plan subscription
##
@ -555,34 +485,41 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat
# @param plan {Object} the plan to subscribe
##
$scope.selectPlan = (plan) ->
if $scope.isAuthenticated()
angular.forEach $scope.eventsReserved, (machineSlot)->
angular.forEach $scope.ctrl.member.machine_credits, (credit)->
if credit.machine_id = machineSlot.machine.id
credit.hours_used = 0
machineSlot.machine.is_reduced_amount = false
if $scope.selectedPlan != plan
$scope.selectedPlan = plan
else
$scope.selectedPlan = null
updateCartPrice()
# toggle selected plan
if $scope.selectedPlan != plan
$scope.selectedPlan = plan
else
$scope.login null, ->
$scope.selectedPlan = plan
updateCartPrice()
$scope.selectedPlan = null
$scope.planSelectionTime = new Date()
##
# Checks if $scope.slotToModify and $scope.slotToPlace have tag incompatibilities
# @returns {boolean} true in case of incompatibility
# Once the reservation is booked (payment process successfully completed), change the event style
# in fullCalendar, update the user's subscription and free-credits if needed
# @param reservation {Object}
##
$scope.tagMissmatch = ->
for tag in $scope.slotToModify.tags
if tag.id not in $scope.slotToPlace.tag_ids
return true
false
$scope.afterPayment = (reservation)->
angular.forEach $scope.events.reserved, (machineSlot, key) ->
machineSlot.is_reserved = true
machineSlot.can_modify = true
if $scope.currentUser.role isnt 'admin'
machineSlot.title = _t('i_ve_reserved')
machineSlot.borderColor = BOOKED_SLOT_BORDER_COLOR
updateMachineSlot(machineSlot, reservation, $scope.currentUser)
else
machineSlot.title = _t('not_available')
machineSlot.borderColor = UNAVAILABLE_SLOT_BORDER_COLOR
updateMachineSlot(machineSlot, reservation, $scope.ctrl.member)
machineSlot.backgroundColor = 'white'
if $scope.selectedPlan
$scope.ctrl.member.subscribed_plan = angular.copy($scope.selectedPlan)
Auth._currentUser.subscribed_plan = angular.copy($scope.selectedPlan)
$scope.plansAreShown = false
$scope.selectedPlan = null
refetchCalendar()
@ -600,74 +537,6 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat
if $scope.currentUser.role isnt 'admin'
$scope.ctrl.member = $scope.currentUser
# 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()
##
# Create an hash map implementing the Reservation specs
# @param member {Object} User as retreived from the API: current user / selected user if current is admin
# @param slots {Array<Object>} Array of fullCalendar events: slots selected on the calendar
# @param [plan] {Object} Plan as retrived from the API: plan to buy with the current reservation
# @return {{user_id:Number, reservable_id:Number, reservable_type:String, slots_attributes:Array<Object>, plan_id:Number|null}}
##
mkReservation = (member, slots, plan = null) ->
reservation =
user_id: member.id
reservable_id: (slots[0].machine.id if slots.length > 0)
reservable_type: 'Machine'
slots_attributes: []
plan_id: (plan.id if plan)
angular.forEach slots, (slot, key) ->
reservation.slots_attributes.push
start_at: slot.start
end_at: slot.end
availability_id: slot.availability_id
offered: slot.offered || false
reservation
##
# 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 mkRequestParams(r, $scope.coupon.applied), (res) ->
$scope.amountTotal = res.price
setSlotsDetails(res.details)
else
# otherwise we alert, this error musn't occur when the current user is not admin
growl.warning(_t('please_select_a_member_first'))
$scope.amountTotal = null
setSlotsDetails = (details) ->
angular.forEach $scope.eventsReserved, (slot) ->
angular.forEach details.slots, (s) ->
if moment(s.start_at).isSame(slot.start)
slot.promo = s.promo
slot.price = s.price
##
@ -677,67 +546,8 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat
# if it's too late).
##
calendarEventClickCb = (event, jsEvent, view) ->
if !event.is_reserved && !$scope.slotToModify
index = $scope.eventsReserved.indexOf(event)
if index == -1
event.backgroundColor = FREE_SLOT_BORDER_COLOR
event.title = _t('i_reserve')
$scope.eventsReserved.push event
else
$scope.removeMachineSlot(event)
$scope.paidMachineSlots = null
$scope.selectedPlan = null
$scope.modifiedSlots = null
else if !event.is_reserved && $scope.slotToModify
if $scope.slotToPlace
$scope.slotToPlace.backgroundColor = 'white'
$scope.slotToPlace.title = ''
$scope.slotToPlace = event
event.backgroundColor = '#bbb'
event.title = _t('i_shift')
else if event.is_reserved and (slotCanBeModified(event) or slotCanBeCanceled(event)) and !$scope.slotToModify and $scope.eventsReserved.length == 0
event.movable = slotCanBeModified(event)
event.cancelable = slotCanBeCanceled(event)
dialogs.confirm
templateUrl: '<%= asset_path "shared/confirm_modify_slot_modal.html" %>'
resolve:
object: -> event
, (type) ->
if type == 'move'
$scope.modifiedSlots = null
$scope.slotToModify = event
event.backgroundColor = '#eee'
event.title = _t('i_change')
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
else if type == 'cancel'
dialogs.confirm
resolve:
object: ->
title: _t('confirmation_required')
msg: _t('do_you_really_want_to_cancel_this_reservation')
, -> # cancel confirmed
Slot.cancel {id: event.id}, -> # successfully canceled
growl.success _t('reservation_was_cancelled_successfully')
$scope.canceledSlot = event
$scope.canceledSlot.backgroundColor = 'white'
$scope.canceledSlot.title = ''
$scope.canceledSlot.borderColor = FREE_SLOT_BORDER_COLOR
$scope.canceledSlot.id = null
$scope.canceledSlot.is_reserved = false
$scope.canceledSlot.can_modify = false
$scope.canceledSlot = null
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
, -> # error while canceling
growl.error _t('cancellation_failed')
, ->
$scope.paidMachineSlots = null
$scope.selectedPlan = null
$scope.modifiedSlots = null
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
updateCartPrice()
$scope.selectedEvent = event
$scope.selectionTime = new Date()
@ -757,196 +567,6 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat
##
# Open a modal window that allows the user to process a credit card payment for his current shopping cart.
##
payByStripe = (reservation) ->
$uibModal.open
templateUrl: '<%= asset_path "stripe/payment_modal.html" %>'
size: 'md'
resolve:
reservation: ->
reservation
price: ->
Price.compute(mkRequestParams(reservation, $scope.coupon.applied)).$promise
wallet: ->
Wallet.getWalletByUser({user_id: reservation.user_id}).$promise
cgv: ->
CustomAsset.get({name: 'cgv-file'}).$promise
coupon: ->
$scope.coupon.applied
controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'cgv', 'Auth', 'Reservation', 'wallet', 'helpers', '$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 = helpers.getAmountToPay(price.price, wallet.amount)
# CGV
$scope.cgv = cgv.custom_asset
# 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
##
$scope.payment = (status, response) ->
if response.error
growl.error(response.error.message)
else
$scope.attempting = true
$scope.reservation.card_token = response.id
Reservation.save mkRequestParams($scope.reservation, coupon), (reservation) ->
$uibModalInstance.close(reservation)
, (response)->
$scope.alerts = []
$scope.alerts.push
msg: response.data.card[0]
type: 'danger'
$scope.attempting = false
]
.result['finally'](null).then (reservation)->
afterPayment(reservation)
##
# Open a modal window that allows the user to process a local payment for his current shopping cart (admin only).
##
payOnSite = (reservation) ->
$uibModal.open
templateUrl: '<%= asset_path "shared/valid_reservation_modal.html" %>'
size: 'sm'
resolve:
reservation: ->
reservation
price: ->
Price.compute(mkRequestParams(reservation, $scope.coupon.applied)).$promise
wallet: ->
Wallet.getWalletByUser({user_id: reservation.user_id}).$promise
coupon: ->
$scope.coupon.applied
controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'Auth', 'Reservation', 'wallet', 'helpers', '$filter', '$locale', 'coupon',
($scope, $uibModalInstance, $state, reservation, price, Auth, Reservation, wallet, helpers, $filter, $locale, coupon) ->
# user wallet amount
$scope.walletAmount = wallet.amount
# Global price (total of all items)
$scope.price = price.price
# Price to pay (wallet deducted)
$scope.amount = helpers.getAmountToPay(price.price, wallet.amount)
# Reservation
$scope.reservation = reservation
# 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_of_html', {ROLE:$scope.currentUser.role, AMOUNT:$filter('currency')($scope.amount)}, "messageformat")
else
if price.price > 0 and $scope.walletAmount == 0
$scope.validButtonName = _t('confirm_payment_of_html', {ROLE:$scope.currentUser.role, AMOUNT:$filter('currency')(price.price)}, "messageformat")
else
$scope.validButtonName = _t('confirm')
##
# Callback to process the local payment, triggered on button click
##
$scope.ok = ->
$scope.attempting = true
Reservation.save mkRequestParams($scope.reservation, coupon), (reservation) ->
$uibModalInstance.close(reservation)
$scope.attempting = true
, (response)->
$scope.alerts = []
$scope.alerts.push({msg: _t('a_problem_occured_during_the_payment_process_please_try_again_later'), type: 'danger' })
$scope.attempting = false
$scope.cancel = ->
$uibModalInstance.dismiss('cancel')
]
.result['finally'](null).then (reservation)->
afterPayment(reservation)
##
# Determines if the provided booked slot is able to be modified by the user.
# @param slot {Object} fullCalendar event object
##
slotCanBeModified = (slot)->
return true if $scope.currentUser.role is 'admin'
slotStart = moment(slot.start)
now = moment()
if slot.can_modify and $scope.enableBookingMove and slotStart.diff(now, "hours") >= $scope.moveBookingDelay
return true
else
return false
##
# Determines if the provided booked slot is able to be canceled by the user.
# @param slot {Object} fullCalendar event object
##
slotCanBeCanceled = (slot) ->
return true if $scope.currentUser.role is 'admin'
slotStart = moment(slot.start)
now = moment()
if slot.can_modify and $scope.enableBookingCancel and slotStart.diff(now, "hours") >= $scope.cancelBookingDelay
return true
else
return false
##
# Once the reservation is booked (payment process successfully completed), change the event style
# in fullCalendar, update the user's subscription and free-credits if needed
# @param reservation {Object}
##
afterPayment = (reservation)->
angular.forEach $scope.eventsReserved, (machineSlot, key) ->
machineSlot.is_reserved = true
machineSlot.can_modify = true
if $scope.currentUser.role isnt 'admin'
machineSlot.title = _t('i_ve_reserved')
machineSlot.borderColor = BOOKED_SLOT_BORDER_COLOR
updateMachineSlot(machineSlot, reservation, $scope.currentUser)
else
machineSlot.title = _t('not_available')
machineSlot.borderColor = UNAVAILABLE_SLOT_BORDER_COLOR
updateMachineSlot(machineSlot, reservation, $scope.ctrl.member)
machineSlot.backgroundColor = 'white'
$scope.paidMachineSlots = $scope.eventsReserved
$scope.eventsReserved = []
if $scope.selectedPlan
$scope.ctrl.member.subscribed_plan = angular.copy($scope.selectedPlan)
Auth._currentUser.subscribed_plan = angular.copy($scope.selectedPlan)
$scope.plansAreShown = false
uiCalendarConfig.calendars.calendar.fullCalendar 'refetchEvents'
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
##
# After payment, update the id of the newly reserved slot with the id returned by the server.
# This will allow the user to modify the reservation he just booked. The associated user will also be registered
@ -964,15 +584,20 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat
##
# Search for the requested plan in the provided array and return its price.
# @param plansArray {Array} full list of plans
# @param planId {Number} plan identifier
# @returns {Number|null} price of the given plan or null if not found
# Update the calendar's display to render the new attributes of the events
##
findAmountByPlanId = (plansArray, planId)->
for plan in plansArray
return plan.amount if plan.plan_id == planId
return null
updateCalendar = ->
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
##
# Asynchronously fetch the events from the API and refresh the calendar's view with these new events
##
refetchCalendar = ->
$timeout ->
uiCalendarConfig.calendars.calendar.fullCalendar 'refetchEvents'
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
## !!! MUST BE CALLED AT THE END of the controller

View File

@ -48,9 +48,16 @@ Application.Controllers.controller "MainNavController", ["$scope", "$location",
linkIcon: 'credit-card'
})
unless Fablab.withoutSpaces
$scope.navLinks.splice(3, 0, {
state: 'app.public.spaces_list'
linkText: 'reserve_a_space'
linkIcon: 'rocket'
})
Fablab.adminNavLinks = Fablab.adminNavLinks || []
Fablab.adminNavLinks = [
adminNavLinks = [
{
state: 'app.admin.trainings'
linkText: 'trainings_monitoring'
@ -108,5 +115,12 @@ Application.Controllers.controller "MainNavController", ["$scope", "$location",
}
].concat(Fablab.adminNavLinks)
$scope.adminNavLinks = Fablab.adminNavLinks
$scope.adminNavLinks = adminNavLinks
unless Fablab.withoutSpaces
$scope.adminNavLinks.splice(7, 0, {
state: 'app.public.spaces_list'
linkText: 'manage_the_spaces'
linkIcon: 'rocket'
})
]

View File

@ -14,10 +14,10 @@ Application.Controllers.controller "MembersController", ["$scope", 'Member', 'me
### PUBLIC SCOPE ###
## currently displayed page of members
$scope.page = 1
## members list
$scope.members = membersPromise
@ -31,12 +31,12 @@ Application.Controllers.controller "MembersController", ["$scope", 'Member', 'me
$scope.showNextMembers = ->
$scope.page += 1
Member.query {
requested_attributes:'[profile]',
page: $scope.page,
requested_attributes:'[profile]',
page: $scope.page,
size: MEMBERS_PER_PAGE
}, (members) ->
$scope.members = $scope.members.concat(members)
if (!members[0] || members[0].maxMembers <= $scope.members.length)
$scope.noMoreResults = true
@ -225,7 +225,9 @@ Application.Controllers.controller "EditProfileController", ["$scope", "$rootSco
Session.destroy()
$rootScope.currentUser = null
$rootScope.toCheckNotifications = false
$scope.notifications = []
$scope.notifications =
total: 0
unread: 0
$window.location.href = $scope.activeProvider.link_to_sso_connect
@ -260,7 +262,7 @@ Application.Controllers.controller "EditProfileController", ["$scope", "$rootSco
##
Application.Controllers.controller "ShowProfileController", ["$scope", 'memberPromise', 'SocialNetworks', ($scope, memberPromise, SocialNetworks) ->
## Selected user's informations
## Selected user's information
$scope.user = memberPromise # DEPENDENCY WITH NAVINUM GAMIFICATION PLUGIN !!!!
## List of social networks associated with this user and toggle 'show all' state

View File

@ -2,7 +2,7 @@
##
# Controller used in notifications page
# inherits $scope.$parent.notifications (unread notifications) from ApplicationController
# inherits $scope.$parent.notifications (global notifications state) from ApplicationController
##
Application.Controllers.controller "NotificationsController", ["$scope", 'Notification', ($scope, Notification) ->
@ -20,6 +20,15 @@ Application.Controllers.controller "NotificationsController", ["$scope", 'Notifi
## Array containg the archived notifications (already read)
$scope.notificationsRead = []
## Array containg the new notifications (not read)
$scope.notificationsUnread = []
## Total number of notifications for the current user
$scope.total = 0
## Total number of unread notifications for the current user
$scope.totalUnread = 0
## By default, the pagination mode is activated to limit the page size
$scope.paginateActive = true
@ -39,10 +48,15 @@ Application.Controllers.controller "NotificationsController", ["$scope", 'Notifi
Notification.update {id: notification.id},
id: notification.id
is_read: true
, ->
index = $scope.$parent.notifications.indexOf(notification)
$scope.$parent.notifications.splice(index,1)
$scope.notificationsRead.push notification
, (updatedNotif) ->
# remove notif from unreads
index = $scope.notificationsUnread.indexOf(notification)
$scope.notificationsUnread.splice(index,1)
# add update notif to read
$scope.notificationsRead.push updatedNotif
# update counters
$scope.$parent.notifications.unread -= 1
$scope.totalUnread -= 1
@ -52,21 +66,32 @@ Application.Controllers.controller "NotificationsController", ["$scope", 'Notifi
$scope.markAllAsRead = ->
Notification.update {}
, -> # success
angular.forEach $scope.$parent.notifications, (n)->
# add notifs to read
angular.forEach $scope.notificationsUnread, (n)->
n.is_read = true
$scope.notificationsRead.push n
$scope.$parent.notifications.splice(0, $scope.$parent.notifications.length)
# clear unread
$scope.notificationsUnread = []
# update counters
$scope.$parent.notifications.unread = 0
$scope.totalUnread = 0
##
# Request the server to retrieve the next undisplayed notifications and add them
# to the archived notifications list.
# Request the server to retrieve the next notifications and add them
# to their corresponding notifications list (read or unread).
##
$scope.addMoreNotificationsReaded = ->
Notification.query {is_read: true, page: $scope.page}, (notifications) ->
$scope.notificationsRead = $scope.notificationsRead.concat notifications
$scope.paginateActive = false if notifications.length < NOTIFICATIONS_PER_PAGE
$scope.addMoreNotifications = ->
Notification.query {page: $scope.page}, (notifications) ->
$scope.total = notifications.totals.total
$scope.totalUnread = notifications.totals.unread
angular.forEach notifications.notifications, (notif) ->
if notif.is_read
$scope.notificationsRead.push(notif)
else
$scope.notificationsUnread.push(notif)
$scope.paginateActive = (notifications.totals.total > ($scope.notificationsRead.length + $scope.notificationsUnread.length))
$scope.page += 1
@ -78,7 +103,7 @@ Application.Controllers.controller "NotificationsController", ["$scope", 'Notifi
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
$scope.addMoreNotificationsReaded()
$scope.addMoreNotifications()

View File

@ -177,11 +177,14 @@ Application.Controllers.controller "PlansIndexController", ["$scope", "$rootScop
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
$scope.cart.total = $scope.selectedPlan.amount
# apply the coupon if any
if $scope.coupon.applied
if $scope.coupon.applied.type == 'percent_off'
discount = $scope.cart.total * $scope.coupon.applied.percent_off / 100
$scope.cart.total -= discount
else if $scope.coupon.applied.type == 'amount_off'
discount = $scope.coupon.applied.amount_off
$scope.cart.total -= discount
else
$scope.reserve.amountTotal = null
@ -200,9 +203,9 @@ Application.Controllers.controller "PlansIndexController", ["$scope", "$rootScop
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
controller: ['$scope', '$uibModalInstance', '$state', 'selectedPlan', 'member', 'price', 'Subscription', 'CustomAsset', 'wallet', 'helpers', '$filter', 'coupon',
($scope, $uibModalInstance, $state, selectedPlan, member, price, Subscription, CustomAsset, wallet, helpers, $filter, coupon) ->
# User's wallet amount
$scope.walletAmount = wallet.amount
# Final price to pay by the user
@ -211,9 +214,6 @@ Application.Controllers.controller "PlansIndexController", ["$scope", "$rootScop
# 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')
@ -249,6 +249,7 @@ Application.Controllers.controller "PlansIndexController", ["$scope", "$rootScop
Auth._currentUser.subscribed_plan = angular.copy($scope.selectedPlan)
$scope.paid.plan = angular.copy($scope.selectedPlan)
$scope.selectedPlan = null
$scope.coupon.applied = null
@ -266,8 +267,8 @@ Application.Controllers.controller "PlansIndexController", ["$scope", "$rootScop
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) ->
controller: ['$scope', '$uibModalInstance', '$state', 'selectedPlan', 'member', 'price', 'Subscription', 'wallet', 'helpers', '$filter', 'coupon',
($scope, $uibModalInstance, $state, selectedPlan, member, price, Subscription, wallet, helpers, $filter, coupon) ->
# user wallet amount
$scope.walletAmount = wallet.amount
@ -277,9 +278,6 @@ Application.Controllers.controller "PlansIndexController", ["$scope", "$rootScop
# 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')
@ -329,6 +327,7 @@ Application.Controllers.controller "PlansIndexController", ["$scope", "$rootScop
$scope.ctrl.member = null
$scope.paid.plan = angular.copy($scope.selectedPlan)
$scope.selectedPlan = null
$scope.coupon.applied = null

View File

@ -1,8 +1,8 @@
'use strict'
Application.Controllers.controller "CompleteProfileController", ["$scope", "$rootScope", "$state", "$window", "_t", "growl", "CSRF", "Auth", "Member", "settingsPromise", "activeProviderPromise", "groupsPromise", "cguFile", "memberPromise", "Session"
, ($scope, $rootScope, $state, $window, _t, growl, CSRF, Auth, Member, settingsPromise, activeProviderPromise, groupsPromise, cguFile, memberPromise, Session) ->
Application.Controllers.controller "CompleteProfileController", ["$scope", "$rootScope", "$state", "$window", "_t", "growl", "CSRF", "Auth", "Member", "settingsPromise", "activeProviderPromise", "groupsPromise", "cguFile", "memberPromise", "Session", "dialogs", "AuthProvider"
, ($scope, $rootScope, $state, $window, _t, growl, CSRF, Auth, Member, settingsPromise, activeProviderPromise, groupsPromise, cguFile, memberPromise, Session, dialogs, AuthProvider) ->
@ -20,13 +20,13 @@ Application.Controllers.controller "CompleteProfileController", ["$scope", "$roo
## name of the current fablab application (eg. "Fablab de la Casemate")
$scope.fablabName = settingsPromise.fablab_name
## informations from the current SSO provider
## information from the current SSO provider
$scope.activeProvider = activeProviderPromise
## list of user's groups (student/standard/...)
$scope.groups = groupsPromise
## current user, contains informations retrieved from the SSO
## current user, contains information retrieved from the SSO
$scope.user = memberPromise
## disallow the user to change his password as he connect from SSO
@ -141,6 +141,27 @@ Application.Controllers.controller "CompleteProfileController", ["$scope", "$roo
##
# Ask for email confirmation and send the SSO merging token again
# @param $event {Object} jQuery event object
##
$scope.resendCode = (event) ->
event.preventDefault()
event.stopPropagation()
dialogs.confirm
templateUrl: '<%= asset_path "profile/resend_code_modal.html" %>'
resolve:
object: ->
email: memberPromise.email
, (email) ->
# Request the server to send an auth-migration email to the current user
AuthProvider.send_code {email: email}, (res) ->
growl.info(_t('code_successfully_sent_again'))
, (err) ->
growl.error(err.data.error)
##
# Disconnect and re-connect the user to the SSO to force the synchronisation of the profile's data
##
@ -149,7 +170,9 @@ Application.Controllers.controller "CompleteProfileController", ["$scope", "$roo
Session.destroy()
$rootScope.currentUser = null
$rootScope.toCheckNotifications = false
$scope.notifications = []
$scope.notifications =
total: 0
unread: 0
$window.location.href = activeProviderPromise.link_to_sso_connect
@ -177,4 +200,4 @@ Application.Controllers.controller "CompleteProfileController", ["$scope", "$roo
## !!! MUST BE CALLED AT THE END of the controller
initialize()
]
]

View File

@ -12,6 +12,7 @@
# - $scope.components = [{Component}]
# - $scope.themes = [{Theme}]
# - $scope.licences = [{Licence}]
# - $scope.allowedExtensions = [{String}]
# - $scope.submited(content)
# - $scope.cancel()
# - $scope.addFile()
@ -26,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, dialogs, _t)->
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)->
@ -52,8 +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
##
@ -119,7 +124,7 @@ class ProjectsController
##
$scope.addStep = ->
$scope.totalSteps += 1
$scope.project.project_steps_attributes.push { step_nb: $scope.totalSteps }
$scope.project.project_steps_attributes.push { step_nb: $scope.totalSteps, project_step_images_attributes: [] }
@ -180,6 +185,26 @@ class ProjectsController
console.error(error)
##
# This will create a single new empty entry into the project's step image list.
##
$scope.addProjectStepImage = (step)->
step.project_step_images_attributes.push {}
##
# This will remove the given image from the project's step image list.
# @param step {Object} the project step has images
# @param image {Object} the image to delete
##
$scope.deleteProjectStepImage = (step, image) ->
index = step.project_step_images_attributes.indexOf(image)
if image.id?
image._destroy = true
else
step.project_step_images_attributes.splice(index, 1)
##
# Controller used on projects listing page
@ -192,10 +217,25 @@ Application.Controllers.controller "ProjectsController", ["$scope", "$state", 'P
# Number of projects added to the page when the user clicks on 'load more projects'
PROJECTS_PER_PAGE = 16
$scope.openlabAppId = Fablab.openlabAppId
### PUBLIC SCOPE ###
$scope.search = { q: ($location.$$search.q || ""), from: ($location.$$search.from || undefined), machine_id: (parseInt($location.$$search.machine_id) || undefined), component_id: (parseInt($location.$$search.component_id) || undefined), theme_id: (parseInt($location.$$search.theme_id) || undefined) }
## Fab-manager's instance ID in the openLab network
$scope.openlabAppId = Fablab.openlabAppId
## Is openLab enabled on the instance?
$scope.openlab =
projectsActive: Fablab.openlabProjectsActive
searchOverWholeNetwork: false
## default search parameters
$scope.search =
q: ($location.$$search.q || "")
from: ($location.$$search.from || undefined)
machine_id: (parseInt($location.$$search.machine_id) || undefined)
component_id: (parseInt($location.$$search.component_id) || undefined)
theme_id: (parseInt($location.$$search.theme_id) || undefined)
## list of projects to display
$scope.projects = []
@ -209,32 +249,14 @@ Application.Controllers.controller "ProjectsController", ["$scope", "$state", 'P
## list of components / used for filtering
$scope.components = componentsPromise
$scope.openlab = {}
$scope.openlab.projectsActive = Fablab.openlabProjectsActive
if $location.$$search.whole_network is 'f'
$scope.openlab.searchOverWholeNetwork = false
else
$scope.openlab.searchOverWholeNetwork = $scope.openlab.projectsActive || false
normalizeProjectsAttrs = (projects)->
projects.map((project)->
project.project_image = project.image_url
return project
)
$scope.searchOverWholeNetworkChanged = ->
setTimeout ->
$scope.resetFiltersAndTriggerSearch()
, 150
loadMoreCallback = (projectsPromise)->
$scope.projects = $scope.projects.concat(projectsPromise.projects)
updateUrlParam('page', $scope.projectsPagination.currentPage)
loadMoreOpenlabCallback = (projectsPromise)->
$scope.projects = $scope.projects.concat(normalizeProjectsAttrs(projectsPromise.projects))
updateUrlParam('page', $scope.projectsPagination.currentPage)
$scope.loadMore = ->
if $scope.openlab.searchOverWholeNetwork is true
@ -243,6 +265,7 @@ Application.Controllers.controller "ProjectsController", ["$scope", "$state", 'P
$scope.projectsPagination.loadMore(search: $scope.search)
$scope.resetFiltersAndTriggerSearch = ->
$scope.search.q = ""
$scope.search.from = undefined
@ -252,6 +275,8 @@ Application.Controllers.controller "ProjectsController", ["$scope", "$state", 'P
$scope.setUrlQueryParams($scope.search)
$scope.triggerSearch()
$scope.triggerSearch = ->
currentPage = parseInt($location.$$search.page) || 1
if $scope.openlab.searchOverWholeNetwork is true
@ -273,6 +298,8 @@ Application.Controllers.controller "ProjectsController", ["$scope", "$state", 'P
$scope.projectsPagination.totalCount = projectsPromise.meta.total
$scope.projects = projectsPromise.projects
##
# Callback to switch the user's view to the detailled project page
# @param project {{slug:string}} The project to display
@ -284,6 +311,8 @@ Application.Controllers.controller "ProjectsController", ["$scope", "$state", 'P
else
$state.go('app.public.projects_show', {id: project.slug})
##
# function to set all url query search parameters from search object
##
@ -295,6 +324,21 @@ Application.Controllers.controller "ProjectsController", ["$scope", "$state", 'P
updateUrlParam('component_id', search.component_id)
updateUrlParam('machine_id', search.machine_id)
### PRIVATE SCOPE ###
##
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
if $location.$$search.whole_network is 'f'
$scope.openlab.searchOverWholeNetwork = false
else
$scope.openlab.searchOverWholeNetwork = $scope.openlab.projectsActive || false
$scope.triggerSearch()
##
# function to update url query param, little hack to turn off reloadOnSearch and re-enable it after setting the params
# params example: 'q' , 'presse-purée'
@ -305,9 +349,30 @@ Application.Controllers.controller "ProjectsController", ["$scope", "$state", 'P
$timeout ->
$state.current.reloadOnSearch = undefined
## initialization
$scope.triggerSearch()
loadMoreCallback = (projectsPromise)->
$scope.projects = $scope.projects.concat(projectsPromise.projects)
updateUrlParam('page', $scope.projectsPagination.currentPage)
loadMoreOpenlabCallback = (projectsPromise)->
$scope.projects = $scope.projects.concat(normalizeProjectsAttrs(projectsPromise.projects))
updateUrlParam('page', $scope.projectsPagination.currentPage)
normalizeProjectsAttrs = (projects)->
projects.map((project)->
project.project_image = project.image_url
return project
)
## !!! MUST BE CALLED AT THE END of the controller
initialize()
]
@ -315,8 +380,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', 'dialogs', '_t'
, ($scope, $state, Project, Machine, Member, Component, Theme, Licence, $document, CSRF, Diacritics, dialogs, _t) ->
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
@ -333,7 +398,7 @@ Application.Controllers.controller "NewProjectController", ["$scope", "$state",
$scope.matchingMembers = []
## Using the ProjectsController
new ProjectsController($scope, $state, Project, Machine, Member, Component, Theme, Licence, $document, Diacritics, dialogs, _t)
new ProjectsController($scope, $state, Project, Machine, Member, Component, Theme, Licence, $document, Diacritics, dialogs, allowedExtensions, _t)
]
@ -341,8 +406,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', 'dialogs', '_t'
, ($scope, $state, $stateParams, Project, Machine, Member, Component, Theme, Licence, $document, CSRF, projectPromise, Diacritics, dialogs, _t) ->
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
@ -359,7 +424,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, dialogs, _t)
new ProjectsController($scope, $state, Project, Machine, Member, Component, Theme, Licence, $document, Diacritics, dialogs, allowedExtensions, _t)
]

View File

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

View File

@ -12,13 +12,13 @@ Application.Controllers.controller "TrainingsController", ['$scope', '$state', '
# Callback for the 'reserve' button
##
$scope.reserveTraining = (training, event) ->
$state.go('app.logged.trainings_reserve', {id: training.id})
$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.id})
$state.go('app.public.training_show', {id: training.slug})
]
@ -26,17 +26,43 @@ Application.Controllers.controller "TrainingsController", ['$scope', '$state', '
##
# Public view of a specific training
##
Application.Controllers.controller "ShowTrainingController", ['$scope', '$state', 'trainingPromise', ($scope, $state, trainingPromise) ->
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)
##
@ -51,8 +77,8 @@ Application.Controllers.controller "ShowTrainingController", ['$scope', '$state'
# 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', '$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) ->
Application.Controllers.controller "ReserveTrainingController", ["$scope", '$stateParams', 'Auth', '$timeout', 'Availability', 'Member', 'availabilityTrainingsPromise', 'plansPromise', 'groupsPromise', 'settingsPromise', 'trainingPromise', '_t', 'uiCalendarConfig', 'CalendarConfig'
($scope, $stateParams, Auth, $timeout, Availability, Member, availabilityTrainingsPromise, plansPromise, groupsPromise, settingsPromise, trainingPromise, _t, uiCalendarConfig, CalendarConfig) ->
@ -61,7 +87,7 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
# Color of the selected event backgound
SELECTED_EVENT_BG_COLOR = '#ffdd00'
# Slot already booked by the current user
# Slot free to be booked
FREE_SLOT_BORDER_COLOR = '<%= AvailabilityHelper::TRAINING_COLOR %>'
@ -83,33 +109,34 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
groupObj.plans.push(plan) if plan.group_id == group.id
$scope.plansClassifiedByGroup.push(groupObj)
## indicates the state of the current view : calendar or plans informations
## mapping of fullCalendar events.
$scope.events =
reserved: [] # Slots that the user wants to book
modifiable: null # Slot that the user wants to change
placable: null # Destination slot for the change
paid: [] # Slots that were just booked by the user (transaction ok)
moved: null # Slots that were just moved by the user (change done) -> {newSlot:* oldSlot: *}
## the moment when the slot selection changed for the last time, used to trigger changes in the cart
$scope.selectionTime = null
## the last clicked event in the calender
$scope.selectedEvent = null
## indicates the state of the current view : calendar or plans information
$scope.plansAreShown = false
## indicates if the selected training was validated (ie. added to the shopping cart)
$scope.trainingIsValid = false
## contains the selected training once it was payed, allows to display a firendly end-of-shopping message
$scope.paidTraining = null
## will store the user's plan if he choosed to buy one
$scope.selectedPlan = null
## fullCalendar event. Training slot that the user want to book
$scope.selectedTraining = null
## the moment when the plan selection changed for the last time, used to trigger changes in the cart
$scope.planSelectionTime = null
## fullCalendar event. An already booked slot that the user want to modify
$scope.slotToModify = null
## Once a training reservation was modified, will contains {newReservedSlot:{}, oldReservedSlot:{}}
$scope.modifiedSlots = null
## Selected training unless 'all' trainings are displayed
## Selected training
$scope.training = trainingPromise
## Discount coupon to apply to the basket, if any
$scope.coupon =
applied: null
## 'all' OR training's slug
$scope.mode = $stateParams.id
## fullCalendar (v2) configuration
$scope.calendarConfig = CalendarConfig
@ -117,19 +144,113 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
maxTime: moment.duration(moment(settingsPromise.booking_window_end).format('HH:mm:ss'))
eventClick: (event, jsEvent, view) ->
calendarEventClickCb(event, jsEvent, view)
eventAfterAllRender: (view)->
$scope.events = uiCalendarConfig.calendars.calendar.fullCalendar 'clientEvents'
eventRender: (event, element, view) ->
eventRenderCb(event, element, view)
## Custom settings
## Application global settings
$scope.settings = settingsPromise
## Global config: message to the end user concerning the subscriptions rules
$scope.subscriptionExplicationsAlert = settingsPromise.subscription_explications_alert
## Global config: message to the end user concerning the training reservation
$scope.trainingExplicationsAlert = settingsPromise.training_explications_alert
## Global config: message to the end user giving advice about the training reservation
$scope.trainingInformationMessage = settingsPromise.training_information_message
$scope.enableBookingMove = (settingsPromise.booking_move_enable == "true")
$scope.moveBookingDelay = parseInt(settingsPromise.booking_move_delay)
$scope.enableBookingCancel = (settingsPromise.booking_cancel_enable == "true")
$scope.cancelBookingDelay = parseInt(settingsPromise.booking_cancel_delay)
##
# Change the last selected slot's appearence to looks like 'added to cart'
##
$scope.markSlotAsAdded = ->
$scope.selectedEvent.backgroundColor = SELECTED_EVENT_BG_COLOR
updateCalendar()
##
# Change the last selected slot's appearence to looks like 'never added to cart'
##
$scope.markSlotAsRemoved = (slot) ->
slot.backgroundColor = 'white'
slot.title = slot.training.name
slot.borderColor = FREE_SLOT_BORDER_COLOR
slot.id = null
slot.isValid = false
slot.is_reserved = false
slot.can_modify = false
slot.offered = false
slot.is_completed = false if slot.is_completed
updateCalendar()
##
# Callback when a slot was successfully cancelled. Reset the slot style as 'ready to book'
##
$scope.slotCancelled = ->
$scope.markSlotAsRemoved($scope.selectedEvent)
##
# Change the last selected slot's appearence to looks like 'currently looking for a new destination to exchange'
##
$scope.markSlotAsModifying = ->
$scope.selectedEvent.backgroundColor = '#eee'
$scope.selectedEvent.title = $scope.selectedEvent.training.name + ' - ' + _t('i_change')
updateCalendar()
##
# Change the last selected slot's appearence to looks like 'the slot being exchanged will take this place'
##
$scope.changeModifyTrainingSlot = ->
if $scope.events.placable
$scope.events.placable.backgroundColor = 'white'
$scope.events.placable.title = $scope.events.placable.training.name
if !$scope.events.placable or $scope.events.placable._id != $scope.selectedEvent._id
$scope.selectedEvent.backgroundColor = '#bbb'
$scope.selectedEvent.title = $scope.selectedEvent.training.name + ' - ' + _t('i_shift')
updateCalendar()
##
# When modifying an already booked reservation, callback when the modification was successfully done.
##
$scope.modifyTrainingSlot = ->
$scope.events.placable.title = if $scope.currentUser.role isnt 'admin' then $scope.events.placable.training.name + " - " + _t('i_ve_reserved') else $scope.events.placable.training.name
$scope.events.placable.backgroundColor = 'white'
$scope.events.placable.borderColor = $scope.events.modifiable.borderColor
$scope.events.placable.id = $scope.events.modifiable.id
$scope.events.placable.is_reserved = true
$scope.events.placable.can_modify = true
$scope.events.modifiable.backgroundColor = 'white'
$scope.events.modifiable.title = $scope.events.modifiable.training.name
$scope.events.modifiable.borderColor = FREE_SLOT_BORDER_COLOR
$scope.events.modifiable.id = null
$scope.events.modifiable.is_reserved = false
$scope.events.modifiable.can_modify = false
$scope.events.modifiable.is_completed = false if $scope.events.modifiable.is_completed
updateCalendar()
##
# Cancel the current booking modification, reseting the whole process
##
$scope.cancelModifyTrainingSlot = ->
if $scope.events.placable
$scope.events.placable.backgroundColor = 'white'
$scope.events.placable.title = $scope.events.placable.training.name
$scope.events.modifiable.title = if $scope.currentUser.role isnt 'admin' then $scope.events.modifiable.training.name + " - " + _t('i_ve_reserved') else $scope.events.modifiable.training.name
$scope.events.modifiable.backgroundColor = 'white'
updateCalendar()
@ -141,67 +262,17 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
if $scope.ctrl.member
Member.get {id: $scope.ctrl.member.id}, (member) ->
$scope.ctrl.member = member
Availability.trainings {trainingId: $stateParams.id, member_id: $scope.ctrl.member.id}, (trainings) ->
id = if $stateParams.id is 'all' then $stateParams.id else $scope.training.id
Availability.trainings {trainingId: id, member_id: $scope.ctrl.member.id}, (trainings) ->
uiCalendarConfig.calendars.calendar.fullCalendar 'removeEvents'
$scope.eventSources.push
$scope.eventSources.splice(0, 1,
events: trainings
textColor: 'black'
$scope.trainingIsValid = false
$scope.paidTraining = null
$scope.plansAreShown = false
)
# as the events are re-fetched for the new user, we must re-init the cart
$scope.events.reserved = []
$scope.selectedPlan = null
$scope.selectedTraining = null
$scope.slotToModify = null
$scope.modifiedSlots = null
##
# Callback to mark the selected training as validated (add it to the shopping cart).
##
$scope.validTraining = ->
$scope.trainingIsValid = true
$scope.updatePrices()
##
# Remove the training from the shopping cart
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
##
$scope.removeTraining = (e) ->
e.preventDefault()
$scope.selectedTraining.backgroundColor = 'white'
$scope.selectedTraining = null
$scope.plansAreShown = false
$scope.selectedPlan = null
$scope.trainingIsValid = false
$timeout ->
uiCalendarConfig.calendars.fullCalendar 'refetchEvents'
uiCalendarConfig.calendars.fullCalendar 'rerenderEvents'
##
# Validates the shopping chart and redirect the user to the payment step
##
$scope.payTraining = ->
# first, we check that a user was selected
if Object.keys($scope.ctrl.member).length > 0
reservation = mkReservation($scope.ctrl.member, $scope.selectedTraining, $scope.selectedPlan)
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'))
@ -210,17 +281,12 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
# @param plan {Object} the plan to subscribe
##
$scope.selectPlan = (plan) ->
if $scope.isAuthenticated()
if $scope.selectedPlan != plan
$scope.selectedPlan = plan
$scope.updatePrices()
else
$scope.selectedPlan = null
$scope.updatePrices()
# toggle selected plan
if $scope.selectedPlan != plan
$scope.selectedPlan = plan
else
$scope.login null, ->
$scope.selectedPlan = plan
$scope.updatePrices()
$scope.selectedPlan = null
$scope.planSelectionTime = new Date()
@ -232,7 +298,9 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
e.preventDefault()
$scope.plansAreShown = false
$scope.selectedPlan = null
$scope.updatePrices()
$scope.planSelectionTime = new Date()
##
# Switch the user's view from the reservation agenda to the plan subscription
@ -240,95 +308,33 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
$scope.showPlans = ->
$scope.plansAreShown = true
##
# Cancel the current booking modification, removing the previously booked slot from the selection
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
##
$scope.removeSlotToModify = (e) ->
e.preventDefault()
if $scope.slotToPlace
$scope.slotToPlace.backgroundColor = 'white'
$scope.slotToPlace.title = $scope.slotToPlace.training.name
$scope.slotToPlace = null
$scope.slotToModify.title = if $scope.currentUser.role isnt 'admin' then $scope.slotToModify.training.name + " - " + _t('i_ve_reserved') else $scope.slotToModify.training.name
$scope.slotToModify.backgroundColor = 'white'
$scope.slotToModify = null
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
##
# When modifying an already booked reservation, cancel the choice of the new slot
# @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
# Once the reservation is booked (payment process successfully completed), change the event style
# in fullCalendar, update the user's subscription and free-credits if needed
# @param reservation {Object}
##
$scope.removeSlotToPlace = (e)->
e.preventDefault()
$scope.slotToPlace.backgroundColor = 'white'
$scope.slotToPlace.title = $scope.slotToPlace.training.name
$scope.slotToPlace = null
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
$scope.afterPayment = (reservation)->
$scope.events.paid[0].backgroundColor = 'white'
$scope.events.paid[0].is_reserved = true
$scope.events.paid[0].can_modify = true
updateTrainingSlotId($scope.events.paid[0], reservation)
$scope.events.paid[0].borderColor = '#b2e774'
$scope.events.paid[0].title = $scope.events.paid[0].training.name + " - " + _t('i_ve_reserved')
if $scope.selectedPlan
$scope.ctrl.member.subscribed_plan = angular.copy($scope.selectedPlan)
Auth._currentUser.subscribed_plan = angular.copy($scope.selectedPlan)
$scope.plansAreShown = false
$scope.selectedPlan = null
$scope.ctrl.member.training_credits = angular.copy(reservation.user.training_credits)
$scope.ctrl.member.machine_credits = angular.copy(reservation.user.machine_credits)
Auth._currentUser.training_credits = angular.copy(reservation.user.training_credits)
Auth._currentUser.machine_credits = angular.copy(reservation.user.machine_credits)
##
# When modifying an already booked reservation, confirm the modification.
##
$scope.modifyTrainingSlot = ->
Slot.update {id: $scope.slotToModify.slot_id},
slot:
start_at: $scope.slotToPlace.start
end_at: $scope.slotToPlace.end
availability_id: $scope.slotToPlace.id
, -> # success
$scope.modifiedSlots =
newReservedSlot: $scope.slotToPlace
oldReservedSlot: $scope.slotToModify
$scope.slotToPlace.title = if $scope.currentUser.role isnt 'admin' then $scope.slotToPlace.training.name + " - " + _t('i_ve_reserved') else $scope.slotToPlace.training.name
$scope.slotToPlace.backgroundColor = 'white'
$scope.slotToPlace.borderColor = $scope.slotToModify.borderColor
$scope.slotToPlace.slot_id = $scope.slotToModify.slot_id
$scope.slotToPlace.is_reserved = true
$scope.slotToPlace.can_modify = true
$scope.slotToPlace = null
$scope.slotToModify.backgroundColor = 'white'
$scope.slotToModify.title = $scope.slotToModify.training.name
$scope.slotToModify.borderColor = FREE_SLOT_BORDER_COLOR
$scope.slotToModify.slot_id = null
$scope.slotToModify.is_reserved = false
$scope.slotToModify.can_modify = false
$scope.slotToModify.is_completed = false if $scope.slotToModify.is_completed
$scope.slotToModify = null
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
, -> # failure
growl.error('an_error_occured_preventing_the_booked_slot_from_being_modified')
##
# Cancel the current booking modification, reseting the whole process
##
$scope.cancelModifyMachineSlot = ->
$scope.slotToPlace.backgroundColor = 'white'
$scope.slotToPlace.title = $scope.slotToPlace.training.name
$scope.slotToPlace = null
$scope.slotToModify.title = if $scope.currentUser.role isnt 'admin' then $scope.slotToModify.training.name + " - " + _t('i_ve_reserved') else $scope.slotToModify.training.name
$scope.slotToModify.backgroundColor = 'white'
$scope.slotToModify = null
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
##
# Update the prices, based on the current selection
##
$scope.updatePrices = ->
if Object.keys($scope.ctrl.member).length > 0
r = mkReservation($scope.ctrl.member, $scope.selectedTraining, $scope.selectedPlan)
Price.compute mkRequestParams(r, $scope.coupon.applied), (res) ->
$scope.amountTotal = res.price
else
$scope.amountTotal = null
refetchCalendar()
@ -342,51 +348,6 @@ 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()
##
# Create an hash map implementing the Reservation specs
# @param member {Object} User as retreived from the API: current user / selected user if current is admin
# @param training {Object} fullCalendar event: training slot selected on the calendar
# @param [plan] {Object} Plan as retrived from the API: plan to buy with the current reservation
# @return {{user_id:Number, reservable_id:Number, reservable_type:String, slots_attributes:Array<Object>, plan_id:Number|null}}
##
mkReservation = (member, training, plan = null) ->
reservation =
user_id: member.id
reservable_id: training.training.id
reservable_type: 'Training'
slots_attributes: []
plan_id: (plan.id if plan)
reservation.slots_attributes.push
start_at: training.start
end_at: training.end
availability_id: training.id
offered: training.offered || false
reservation
##
# 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
##
@ -397,310 +358,26 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
# @see http://fullcalendar.io/docs/mouse/eventClick/
##
calendarEventClickCb = (event, jsEvent, view) ->
if $scope.ctrl.member
# reserve a training if this training will not be reserved and is not about to move and not is completed
if !event.is_reserved && !$scope.slotToModify && !event.is_completed
$scope.coupon.applied = null
if event != $scope.selectedTraining
$scope.selectedTraining = event
$scope.selectedTraining.offered = false
event.backgroundColor = SELECTED_EVENT_BG_COLOR
computeTrainingAmount($scope.selectedTraining)
else
$scope.selectedTraining = null
event.backgroundColor = 'white'
$scope.trainingIsValid = false
$scope.paidTraining = null
$scope.selectedPlan = null
$scope.modifiedSlots = null
# clean all others events background
angular.forEach $scope.events, (e)->
if event.id != e.id
e.backgroundColor = 'white'
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)
if $scope.slotToPlace
$scope.slotToPlace.backgroundColor = 'white'
$scope.slotToPlace.title = event.training.name
$scope.slotToPlace = event
event.backgroundColor = '#bbb'
event.title = event.training.name + ' - ' + _t('i_shift')
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)
event.cancelable = slotCanBeCanceled(event)
if $scope.currentUser.role is 'admin'
event.user =
name: $scope.ctrl.member.name
dialogs.confirm
templateUrl: '<%= asset_path "shared/confirm_modify_slot_modal.html" %>'
resolve:
object: -> event
, (type) -> # success
if type == 'move'
$scope.modifiedSlots = null
$scope.slotToModify = event
event.backgroundColor = '#eee'
event.title = event.training.name + ' - ' + _t('i_change')
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
else if type == 'cancel'
dialogs.confirm
resolve:
object: ->
title: _t('confirmation_required')
msg: _t('do_you_really_want_to_cancel_this_reservation')
, -> # cancel confirmed
Slot.cancel {id: event.slot_id}, -> # successfully canceled
growl.success _t('reservation_was_successfully_cancelled')
$scope.canceledSlot = event
$scope.canceledSlot.backgroundColor = 'white'
$scope.canceledSlot.title = event.training.name
$scope.canceledSlot.borderColor = FREE_SLOT_BORDER_COLOR
$scope.canceledSlot.slot_id = null
$scope.canceledSlot.is_reserved = false
$scope.canceledSlot.can_modify = false
$scope.canceledSlot.is_completed = false if event.is_completed
$scope.canceledSlot = null
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
, -> # error while canceling
growl.error _t('cancellation_failed')
, -> # canceled
$scope.paidMachineSlots = null
$scope.selectedPlan = null
$scope.modifiedSlots = null
$scope.selectedEvent = event
if $stateParams.id is 'all'
$scope.training = event.training
$scope.selectionTime = new Date()
##
# When events are rendered, adds attributes for popover and compile
# Triggered when fullCalendar tries to graphicaly render an event block.
# Append the event tag into the block, just after the event title.
# @see http://fullcalendar.io/docs/event_rendering/eventRender/
##
eventRenderCb = (event, element, view)->
# 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)
##
# Open a modal window that allows the user to process a credit card payment for his current shopping cart.
##
payByStripe = (reservation) ->
$uibModal.open
templateUrl: '<%= asset_path "stripe/payment_modal.html" %>'
size: 'md'
resolve:
reservation: ->
reservation
price: ->
Price.compute(mkRequestParams(reservation, $scope.coupon.applied)).$promise
wallet: ->
Wallet.getWalletByUser({user_id: reservation.user_id}).$promise
cgv: ->
CustomAsset.get({name: 'cgv-file'}).$promise
coupon: ->
$scope.coupon.applied
controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', '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 = helpers.getAmountToPay(price.price, wallet.amount)
# CGV
$scope.cgv = cgv.custom_asset
# 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
##
$scope.payment = (status, response) ->
if response.error
growl.error(response.error.message)
else
$scope.attempting = true
$scope.reservation.card_token = response.id
Reservation.save mkRequestParams($scope.reservation, coupon), (reservation) ->
$uibModalInstance.close(reservation)
, (response)->
$scope.alerts = []
if response.data.card
$scope.alerts.push
msg: response.data.card[0]
type: 'danger'
else
$scope.alerts.push({msg: _t('a_problem_occured_during_the_payment_process_please_try_again_later'), type: 'danger' })
$scope.attempting = false
]
.result['finally'](null).then (reservation)->
afterPayment(reservation)
##
# Open a modal window that allows the user to process a local payment for his current shopping cart (admin only).
##
payOnSite = (reservation) ->
$uibModal.open
templateUrl: '<%= asset_path "shared/valid_reservation_modal.html" %>'
size: 'sm'
resolve:
reservation: ->
reservation
price: ->
Price.compute(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.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_of_html', {ROLE:$scope.currentUser.role, AMOUNT:$filter('currency')($scope.amount)}, "messageformat")
else
if price.price > 0 and $scope.walletAmount == 0
$scope.validButtonName = _t('confirm_payment_of_html', {ROLE:$scope.currentUser.role, AMOUNT:$filter('currency')(price.price)}, "messageformat")
else
$scope.validButtonName = _t('confirm')
##
# Callback to process the local payment, triggered on button click
##
$scope.ok = ->
$scope.attempting = true
Reservation.save mkRequestParams($scope.reservation, coupon), (reservation) ->
$uibModalInstance.close(reservation)
$scope.attempting = true
, (response)->
$scope.alerts = []
$scope.alerts.push({msg: _t('a_problem_occured_during_the_payment_process_please_try_again_later'), type: 'danger' })
$scope.attempting = false
$scope.cancel = ->
$uibModalInstance.dismiss('cancel')
]
.result['finally'](null).then (reservation)->
afterPayment(reservation)
##
# Computes the training amount depending of the member's credit
# @param training {Object} training slot
##
computeTrainingAmount = (training)->
# first we check that a user was selected
if Object.keys($scope.ctrl.member).length > 0
r = mkReservation($scope.ctrl.member, training) # reservation without any Plan -> we get the training price
Price.compute mkRequestParams(r, $scope.coupon.applied), (res) ->
$scope.selectedTrainingAmount = res.price
else
$scope.selectedTrainingAmount = null
##
# Once the reservation is booked (payment process successfully completed), change the event style
# in fullCalendar, update the user's subscription and free-credits if needed
# @param reservation {Object}
##
afterPayment = (reservation)->
$scope.paidTraining = $scope.selectedTraining
$scope.paidTraining.backgroundColor = 'white'
$scope.paidTraining.is_reserved = true
$scope.paidTraining.can_modify = true
updateTrainingSlotId($scope.paidTraining, reservation)
$scope.paidTraining.borderColor = '#b2e774'
$scope.paidTraining.title = $scope.paidTraining.training.name + " - " + _t('i_ve_reserved')
$scope.selectedTraining = null
$scope.trainingIsValid = false
if $scope.selectedPlan
$scope.ctrl.member.subscribed_plan = angular.copy($scope.selectedPlan)
Auth._currentUser.subscribed_plan = angular.copy($scope.selectedPlan)
$scope.plansAreShown = false
$scope.selectedPlan = null
$scope.ctrl.member.training_credits = angular.copy(reservation.user.training_credits)
$scope.ctrl.member.machine_credits = angular.copy(reservation.user.machine_credits)
Auth._currentUser.training_credits = angular.copy(reservation.user.training_credits)
Auth._currentUser.machine_credits = angular.copy(reservation.user.machine_credits)
uiCalendarConfig.calendars.calendar.fullCalendar 'refetchEvents'
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
##
# Determines if the provided booked slot is able to be modified by the user.
# @param slot {Object} fullCalendar event object
##
slotCanBeModified = (slot)->
return true if $scope.currentUser.role is 'admin'
slotStart = moment(slot.start)
now = moment(new Date())
if slot.can_modify and $scope.enableBookingMove and slotStart.diff(now, "hours") >= $scope.moveBookingDelay
return true
else
return false
##
# Determines if the provided booked slot is able to be canceled by the user.
# @param slot {Object} fullCalendar event object
##
slotCanBeCanceled = (slot) ->
return true if $scope.currentUser.role is 'admin'
slotStart = moment(slot.start)
now = moment()
if slot.can_modify and $scope.enableBookingCancel and slotStart.diff(now, "hours") >= $scope.cancelBookingDelay
return true
else
return false
##
# For booking modifications, checks that the newly selected slot is valid
# @param slot {Object} fullCalendar event object
##
slotCanBePlaced = (slot)->
if slot.training.id == $scope.slotToModify.training.id and !slot.is_completed
return true
else
return false
if $scope.currentUser.role is 'admin' and event.tags.length > 0
html = ''
for tag in event.tags
html += "<span class='label label-success text-white' title='#{tag.name}'>#{tag.name}</span>"
element.find('.fc-time').append(html)
return
@ -713,11 +390,29 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
updateTrainingSlotId = (slot, reservation)->
angular.forEach reservation.slots, (s)->
if slot.start_at == slot.start_at
slot.slot_id = s.id
slot.id = s.id
## !!! MUST BE CALLED AT THE END of the controller
##
# Update the calendar's display to render the new attributes of the events
##
updateCalendar = ->
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
##
# Asynchronously fetch the events from the API and refresh the calendar's view with these new events
##
refetchCalendar = ->
$timeout ->
uiCalendarConfig.calendars.calendar.fullCalendar 'refetchEvents'
uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
## !!! MUST BE CALLED AT THE END of the controller
initialize()
]

View File

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

View File

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

View File

@ -270,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
]
@ -296,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
]
@ -367,7 +373,7 @@ 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',
'app.shared.wallet', 'app.shared.coupon_input']).$promise
'app.shared.wallet', 'app.shared.coupon_input', 'app.shared.cart']).$promise
]
.state 'app.admin.machines_edit',
url: '/machines/:id/edit'
@ -382,6 +388,97 @@ angular.module('application.router', ['ui.router']).
translations: [ 'Translations', (Translations) ->
Translations.query(['app.admin.machines_edit', 'app.shared.machine']).$promise
]
# spaces
.state 'app.public.spaces_list',
url: '/spaces'
abstract: Fablab.withoutSpaces
views:
'main@':
templateUrl: '<%= asset_path "spaces/index.html" %>'
controller: 'SpacesController'
resolve:
spacesPromise: ['Space', (Space)->
Space.query().$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query(['app.public.spaces_list']).$promise
]
.state 'app.admin.space_new',
url: '/spaces/new'
abstract: Fablab.withoutSpaces
views:
'main@':
templateUrl: '<%= asset_path "spaces/new.html" %>'
controller: 'NewSpaceController'
resolve:
translations: [ 'Translations', (Translations) ->
Translations.query(['app.admin.space_new', 'app.shared.space']).$promise
]
.state 'app.public.space_show',
url: '/spaces/:id'
abstract: Fablab.withoutSpaces
views:
'main@':
templateUrl: '<%= asset_path "spaces/show.html" %>'
controller: 'ShowSpaceController'
resolve:
spacePromise: ['Space', '$stateParams', (Space, $stateParams)->
Space.get(id: $stateParams.id).$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query(['app.public.space_show']).$promise
]
.state 'app.admin.space_edit',
url: '/spaces/:id/edit'
abstract: Fablab.withoutSpaces
views:
'main@':
templateUrl: '<%= asset_path "spaces/edit.html" %>'
controller: 'EditSpaceController'
resolve:
spacePromise: ['Space', '$stateParams', (Space, $stateParams)->
Space.get(id: $stateParams.id).$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query(['app.admin.space_edit', 'app.shared.space']).$promise
]
.state 'app.logged.space_reserve',
url: '/spaces/:id/reserve'
abstract: Fablab.withoutSpaces
views:
'main@':
templateUrl: '<%= asset_path "spaces/reserve.html" %>'
controller: 'ReserveSpaceController'
resolve:
spacePromise: ['Space', '$stateParams', (Space, $stateParams)->
Space.get(id: $stateParams.id).$promise
]
availabilitySpacesPromise: ['Availability', '$stateParams', (Availability, $stateParams)->
Availability.spaces({spaceId: $stateParams.id}).$promise
]
plansPromise: ['Plan', (Plan)->
Plan.query().$promise
]
groupsPromise: ['Group', (Group)->
Group.query().$promise
]
settingsPromise: ['Setting', (Setting)->
Setting.query(names: "['booking_window_start',
'booking_window_end',
'booking_move_enable',
'booking_move_delay',
'booking_cancel_enable',
'booking_cancel_delay',
'subscription_explications_alert',
'space_explications_alert']").$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query(['app.logged.space_reserve', 'app.shared.plan_subscribe', 'app.shared.member_select',
'app.shared.stripe', 'app.shared.valid_reservation_modal', 'app.shared.confirm_modify_slot_modal',
'app.shared.wallet', 'app.shared.coupon_input', 'app.shared.cart']).$promise
]
# trainings
.state 'app.public.trainings_list',
url: '/trainings'
@ -445,7 +542,7 @@ 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',
'app.shared.wallet', 'app.shared.coupon_input']).$promise
'app.shared.wallet', 'app.shared.coupon_input', 'app.shared.cart']).$promise
]
# notifications
.state 'app.logged.notifications',
@ -516,7 +613,7 @@ angular.module('application.router', ['ui.router']).
PriceCategory.query().$promise
]
settingsPromise: ['Setting', (Setting)->
Setting.query(names: "['booking_move_enable', 'booking_move_delay']").$promise
Setting.query(names: "['booking_move_enable', 'booking_move_delay', 'event_explications_alert']").$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query(['app.public.events_show', 'app.shared.member_select', 'app.shared.stripe',
@ -543,6 +640,9 @@ angular.module('application.router', ['ui.router']).
machinesPromise: ['Machine', (Machine)->
Machine.query().$promise
]
spacesPromise: ['Space', (Space) ->
Space.query().$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query(['app.public.calendar']).$promise
]
@ -764,6 +864,15 @@ angular.module('application.router', ['ui.router']).
couponsPromise: ['Coupon', (Coupon) ->
Coupon.query().$promise
]
spacesPromise: ['Space', (Space) ->
Space.query().$promise
]
spacesPricesPromise: ['Price', (Price)->
Price.query(priceable_type: 'Space', plan_id: 'null').$promise
]
spacesCreditsPromise: ['Credit', (Credit) ->
Credit.query({creditable_type: 'Space'}).$promise
]
# plans
.state 'app.admin.plans',
@ -772,15 +881,9 @@ angular.module('application.router', ['ui.router']).
prices: ['Pricing', (Pricing) ->
Pricing.query().$promise
]
machines: ['Machine', (Machine) ->
Machine.query().$promise
]
groups: ['Group', (Group) ->
Group.query().$promise
]
plans: ['Plan', (Plan) ->
Plan.query().$promise
]
partners: ['User', (User) ->
User.query({role: 'partner'}).$promise
]
@ -801,6 +904,15 @@ angular.module('application.router', ['ui.router']).
templateUrl: '<%= asset_path "admin/plans/edit.html" %>'
controller: 'EditPlanController'
resolve:
spaces: ['Space', (Space) ->
Space.query().$promise
]
machines: ['Machine', (Machine) ->
Machine.query().$promise
]
plans: ['Plan', (Plan) ->
Plan.query().$promise
]
planPromise: ['Plan', '$stateParams', (Plan, $stateParams) ->
Plan.get({id: $stateParams.id}).$promise
]
@ -1031,6 +1143,8 @@ angular.module('application.router', ['ui.router']).
'training_explications_alert',
'training_information_message',
'subscription_explications_alert',
'event_explications_alert',
'space_explications_alert',
'booking_window_start',
'booking_window_end',
'booking_move_enable',

View File

@ -11,4 +11,7 @@ Application.Services.factory 'AuthProvider', ["$resource", ($resource)->
active:
method: 'GET'
url: '/api/auth_providers/active'
send_code:
method: 'POST'
url: '/api/auth_providers/send_code'
]

View File

@ -17,6 +17,11 @@ Application.Services.factory 'Availability', ["$resource", ($resource)->
url: '/api/availabilities/trainings/:trainingId'
params: {trainingId: "@trainingId"}
isArray: true
spaces:
method: 'GET'
url: '/api/availabilities/spaces/:spaceId'
params: {spaceId: "@spaceId"}
isArray: true
update:
method: 'PUT'
]

View File

@ -3,6 +3,14 @@
Application.Services.factory 'Notification', ["$resource", ($resource)->
$resource "/api/notifications/:id",
{id: "@id"},
query:
isArray: false
update:
method: 'PUT'
polling:
url: '/api/notifications/polling'
method: 'GET'
last_unread:
url: '/api/notifications/last_unread'
method: 'GET'
]

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

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

View File

@ -5,6 +5,7 @@
//.bg-yellow { background-color: $yellow !important; }
.bg-token { background-color: rgba(230, 208, 137, 0.49); }
.bg-machine { background-color: $beige; }
.bg-space { background-color: $cyan }
.bg-formation { background-color: $violet; }
.bg-event { background-color: $japonica; }
.bg-atelier { background-color: $blue; }
@ -39,4 +40,5 @@
.text-purple { color: $violet !important; }
.text-japonica { color: $japonica !important; }
.text-beige { color: $beige !important; }
.text-cyan { color: $cyan !important; }
.text-green, .green { color: #79C84A !important; }

View File

@ -15,6 +15,20 @@
.note-editor .note-editable {
background-color: white;
}
.note-editor {
.form-group {
margin-left: 0px;
margin-right: 0px;
}
.modal-header {
padding: 15px;
}
.note-group-select-from-files {
display: none;
}
}
// Growl

View File

@ -56,6 +56,7 @@ p, .widget p {
.block.hide{display: none;}
.inline{display:inline-block !important;}
.none{display: none;}
.pull-left{float: left;}
.pull-right-lg{float: right;}
.pull-none{float: none;}
.rounded{border-radius: 500px;}
@ -102,6 +103,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;}
@ -127,6 +129,7 @@ p, .widget p {
.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}
@ -173,6 +176,7 @@ p, .widget p {
.r-n { border-radius: 0 0 0 0; }
.p-xs { padding: 5px;}
.p-s { padding: 10px;}
.p-lg { padding: 30px; }
.p-l { padding: 16px; }

View File

@ -43,6 +43,7 @@ $blue: $brand-info;
$green: $brand-success;
$beige: #e4cd78;
$violet: #bd7ae9;
$cyan: #3fc7ff;
$japonica: #dd7e6b;
$border-color: #dddddd;
@ -767,7 +768,7 @@ $panel-footer-padding: $panel-heading-padding !default;
$panel-border-radius: $border-radius-large !default;
// add sleede
$panel-border: $border-color !default;
$panel-border: $border-color !default;
$panel-heading-bg: #fff !default;
$panel-footer-bg: #fff !default;

View File

@ -7,14 +7,15 @@
</div>
<div class="col-xs-10 col-sm-10 col-md-8 b-l b-r-md">
<section class="heading-title">
<h1 translate>{{ 'calendar_management' }}</h1>
<h1 translate>{{ 'admin_calendar.calendar_management' }}</h1>
</section>
</div>
<div class="col-xs-12 col-sm-12 col-md-3 b-t hide-b-md">
<section class="heading-actions wrapper">
<span class="badge text-sm bg-formation m-t-sm" translate>{{ 'trainings' }}</span><br>
<span class="badge text-sm bg-machine" translate>{{ 'machines' }}</span>
<section class="heading-actions wrapper" ng-class="{'p-s': !fablabWithoutSpaces}">
<span class="badge text-sm bg-formation" ng-class="{'m-t-sm': fablabWithoutSpaces}" translate>{{ 'admin_calendar.trainings' }}</span><br>
<span class="badge text-sm bg-machine" translate>{{ 'admin_calendar.machines' }}</span><br>
<span class="badge text-sm bg-space" ng-hide="fablabWithoutSpaces" translate>{{ 'admin_calendar.spaces' }}</span>
</section>
</div>
@ -29,9 +30,21 @@
</div>
<div class="col-sm-12 col-md-12 col-lg-3">
<div class="m text-center">
<a class="btn btn-default"
ng-href="api/availabilities/export_index.xlsx"
target="export-frame"
ng-click="alertExport('index')"
uib-popover="{{ 'admin_calendar.availabilities_notice' | translate}}"
popover-trigger="mouseenter"
popover-placement="bottom">
<i class="fa fa-file-excel-o"></i> {{ 'admin_calendar.availabilities' | translate }}
</a>
<iframe name="export-frame" height="0" width="0" class="none"></iframe>
</div>
<div class="widget panel b-a m m-t-lg">
<div class="panel-heading b-b small">
<h3 translate>{{ 'ongoing_reservations' }}</h3>
<h3 translate>{{ 'admin_calendar.ongoing_reservations' }}</h3>
</div>
<div class="widget-content no-bg auto wrapper">
<ul class="list-unstyled" ng-if="reservations.length > 0">
@ -42,7 +55,7 @@
<span class="btn btn-warning btn-xs" ng-click="cancelBooking(r)" ng-if="!r.canceled_at"><i class="fa fa-times red"></i></span>
</li>
</ul>
<div ng-if="reservations.length == 0" translate>{{ 'no_reservations' }}</div>
<div ng-if="reservations.length == 0" translate>{{ 'admin_calendar.no_reservations' }}</div>
</div>
</div>
</div>
@ -50,7 +63,7 @@
<div class="col-sm-12 col-md-12 col-lg-3" ng-if="availability.machine_ids.length > 0">
<div class="widget panel b-a m m-t-lg">
<div class="panel-heading b-b small">
<h3 translate>{{ 'machines' }}</h3>
<h3 translate>{{ 'admin_calendar.machines' }}</h3>
</div>
<div class="widget-content no-bg auto wrapper">
<ul class="list-unstyled">

View File

@ -1,22 +1,41 @@
<div class="modal-header">
<h3 class="text-center red">
{{ 'DATE_slot' | translate:{DATE:(start | amDateFormat: 'LL')} }} {{start | amDateFormat:'LT'}} - {{end | amDateFormat:'LT'}}
{{ 'admin_calendar.DATE_slot' | translate:{DATE:(start | amDateFormat: 'LL')} }} {{start | amDateFormat:'LT'}} - {{end | amDateFormat:'LT'}}
</h3>
</div>
<div class="modal-body">
<p class="text-center font-sbold" translate>{{ 'you_can_define_a_training_on_that_slot' }}</p>
<div>
<label class="checkbox-inline">
<input type="checkbox" ng-model="available_type" ng-change="changeAvailableType()"> {{ 'link_a_training' | translate }}
</label>
<div class="modal-body" ng-show="step === 1">
<label class="m-t-sm" translate>{{ 'admin_calendar.what_kind_of_slot_do_you_want_to_create' }}</label>
<div class="form-group">
<div class="radio">
<label>
<input type="radio" id="training" name="available_type" value="training" ng-model="availability.available_type">
<span translate>{{ 'admin_calendar.training' }}</span>
</label>
</div>
<div class="radio">
<label>
<input type="radio" id="machine" name="available_type" value="machines" ng-model="availability.available_type">
<span translate>{{ 'admin_calendar.machine' }}</span>
</label>
</div>
<div class="radio" ng-hide="fablabWithoutSpaces">
<label>
<input type="radio" id="space" name="available_type" value="space" ng-model="availability.available_type" ng-disabled="spaces.length === 0">
<span translate>{{ 'admin_calendar.space' }}</span>
</label>
</div>
</div>
</div>
<div class="modal-body" ng-show="step === 2">
<p class="text-center font-sbold m-t" ng-show="availability.available_type == 'machines'"><span class="underline" translate>{{ 'or_' }}</span> {{ '_select_some_machines' | translate }}</p>
<div ng-show="availability.available_type == 'machines'">
<p class="text-center font-sbold m-t-sm">{{ 'admin_calendar.select_some_machines' | translate }}</p>
<div class="checkbox" ng-show="availability.available_type == 'machines'">
<label class="checkbox" ng-repeat="machine in machines">
<input type="checkbox" ng-click="toggleSelection(machine)"> {{machine.name}} <span class="text-xs">(id {{machine.id}})</span>
</label>
<div class="form-group m-l-xl">
<label class="checkbox" ng-repeat="machine in machines">
<input type="checkbox" ng-click="toggleSelection(machine)"> {{machine.name}} <span class="text-xs">(id {{machine.id}})</span>
</label>
</div>
</div>
<div ng-show="availability.available_type == 'training'">
@ -24,27 +43,42 @@
</select>
<div class="row m-t">
<div class="form-group">
<label class="col-sm-6 control-label" translate>{{ 'number_of_tickets' }}</label>
<label class="col-sm-6 control-label" for="nb_places_training" translate>{{ 'admin_calendar.number_of_tickets' }}</label>
<div class="col-sm-6">
<input type="number" class="form-control" ng-model="availability.nb_total_places">
<input type="number" id="nb_places_training" class="form-control" ng-model="availability.nb_total_places">
</div>
</div>
</div>
</div>
<div id="timeAdjust" class="m-t-lg">
<p class="text-center font-sbold" translate>{{ 'adjust_the_opening_hours' }}</p>
<div ng-show="availability.available_type == 'space'">
<select ng-model="selectedSpace" class="form-control m-t-sm" ng-options="t.name for t in spaces" ng-change="setNbTotalPlaces()">
</select>
<div class="row m-t">
<div class="form-group">
<label class="col-sm-6 control-label" for="nb_places_space" translate>{{ 'admin_calendar.number_of_tickets' }}</label>
<div class="col-sm-6">
<input type="number" id="nb_places_space" class="form-control" ng-model="availability.nb_total_places">
</div>
</div>
</div>
</div>
</div>
<div class="modal-body" ng-show="step === 3">
<div id="timeAdjust" class="m-t-sm">
<p class="text-center font-sbold" translate>{{ 'admin_calendar.adjust_the_opening_hours' }}</p>
<div class="row">
<div class="col-md-3 col-md-offset-2">
<uib-timepicker ng-model="start" hour-step="timepickers.start.hstep" readonly-input="true" minute-step="timepickers.start.mstep" show-meridian="false"></uib-timepicker>
</div>
<span class="col-md-1 m-t-xl m-l" translate>{{ 'to_time' }}</span>
<span class="col-md-1 m-t-xl m-l" translate>{{ 'admin_calendar.to_time' }}</span>
<fieldset ng-disabled="endDateReadOnly" class="col-md-5">
<uib-timepicker ng-model="end" hour-step="timepickers.end.hstep" readonly-input="true" minute-step="timepickers.end.mstep" show-meridian="false"></uib-timepicker>
</fieldset>
</div>
</div>
<div id="tagAssociate" class="m-t-lg">
<p class="text-center font-sbold" translate>{{ 'restrict_this_slot_with_labels_(optional)' }}</p>
<p class="text-center font-sbold" translate>{{ 'admin_calendar.restrict_this_slot_with_labels_(optional)' }}</p>
<div class="row">
<div class="col-sm-12">
<ui-select multiple ng-model="availability.tag_ids" class="form-control">
@ -59,7 +93,13 @@
</div>
</div>
</div>
<div class="modal-footer">
<div class="modal-footer" ng-show="step < 3">
<button class="btn btn-info" ng-click="previous()" ng-disabled="step === 1" translate>{{ 'admin_calendar.previous' }}</button>
<button class="btn btn-info" ng-click="next()" translate>{{ 'admin_calendar.next' }}</button>
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'cancel' }}</button>
</div>
<div class="modal-footer" ng-show="step === 3">
<button class="btn btn-info" ng-click="previous()" translate>{{ 'admin_calendar.previous' }}</button>
<button class="btn btn-warning" ng-click="ok()" translate>{{ 'confirm' }}</button>
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'cancel' }}</button>
</div>

View File

@ -21,7 +21,20 @@
<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}">
<div class="form-group">
<label for="coupon[type]">{{ 'kind_of_coupon' | translate }} *</label>
<select id="coupon[type]"
name="coupon[type]"
class="form-control"
ng-model="coupon.type"
ng-disabled="mode == 'EDIT'"
required="required">
<option value="percent_off" translate>{{ 'percentage' }}</option>
<option value="amount_off" translate>{{ 'amount' }}</option>
</select>
</div>
<div class="form-group" ng-class="{'has-error': couponForm['coupon[percent_off]'].$dirty && couponForm['coupon[percent_off]'].$invalid}" ng-show="coupon.type == 'percent_off'">
<label for="coupon[percent_off]">{{ 'percent_off' | translate }} *</label>
<div class="input-group">
<input type="number" id="coupon[percent_off]"
@ -31,13 +44,30 @@
min="0"
max="100"
ng-disabled="mode == 'EDIT'"
required="required"/>
ng-required="coupon.type == 'percent_off'"/>
<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[amount_off]'].$dirty && couponForm['coupon[amount_off]'].$invalid}" ng-show="coupon.type == 'amount_off'">
<label for="coupon[amount_off]">{{ 'amount_off' | translate }} *</label>
<div class="input-group">
<span class="input-group-addon">{{currencySymbol}}</span>
<input type="number" id="coupon[amount_off]"
name="coupon[amount_off]"
class="form-control"
ng-model="coupon.amount_off"
min="0"
ng-disabled="mode == 'EDIT'"
ng-required="coupon.type == 'amount_off'"/>
</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]"
@ -51,7 +81,7 @@
<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">
<div class="form-group" ng-class="{'has-error': errors['valid_until']}">
<label for="coupon[valid_until]" translate>{{ 'valid_until' }}</label>
<div class="input-group">
<input type="text" id="coupon[valid_until]"
@ -62,16 +92,16 @@
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 error" ng-show="errors['valid_until']">{{ errors['valid_until'].join(' ; ') }}</span>
<span class="help-block text-info text-xs">
<i class="fa fa-lightbulb-o"></i> {{ 'leave_empty_for_no_limit' | translate }}
</span>
<span class="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}">
@ -84,7 +114,7 @@
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">
<span class="text-info text-xs">
<i class="fa fa-lightbulb-o"></i> {{ 'leave_empty_for_no_limit' | translate }}
</span>
</div>

View File

@ -7,7 +7,7 @@
</div>
<div class="col-xs-10 col-sm-10 col-md-8 b-l">
<section class="heading-title">
<h1>{{ 'reservations' | translate }} {{event.title}}</h1>
<h1>{{ 'the_reservations' | translate }} {{event.title}}</h1>
</section>
</div>
</div>

View File

@ -157,11 +157,11 @@
<tr class="invoice-vat invoice-editable vat-line italic" ng-click="openEditVAT()" ng-show="invoice.VAT.active">
<td>{{ 'including_VAT' | translate }} {{invoice.VAT.rate}} %</td>
<td>{{30/(invoice.VAT.rate/100+1) | currency}}</td>
<td>{{30-(30/(invoice.VAT.rate/100+1)) | currency}}</td>
</tr>
<tr class="invoice-ht vat-line italic" ng-show="invoice.VAT.active">
<td translate>{{ 'including_total_excluding_taxes' }}</td>
<td>{{30-(30/(invoice.VAT.rate/100+1)) | currency}}</td>
<td>{{30/(invoice.VAT.rate/100+1) | currency}}</td>
</tr>
<tr class="invoice-payed vat-line bold" ng-show="invoice.VAT.active">
<td translate>{{ 'including_amount_payed_on_ordering' }}</td>

View File

@ -1,5 +1,8 @@
<div class="form-group" ng-class="{'has-error': userForm['user[group_id]'].$dirty && userForm['user[group_id]'].$invalid}">
<label for="user_group_id" class="col-sm-3 control-label" translate>{{ 'group' }}</label>
<label for="user_group_id" class="col-sm-3 control-label">
<span translate>{{ 'group' }}</span>
<span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
</label>
<div class="col-sm-9">
<select ng-model="user.group_id" ng-disabled="user.subscribed_plan" class="form-control" name="user[group_id]" id="user_group_id" ng-options="g.id as g.name for g in groups" required>
</select>

View File

@ -1,19 +1,19 @@
<h2 translate>{{ 'general_informations' }}</h2>
<h2 translate>{{ 'plan_form.general_information' }}</h2>
<input type="hidden" name="_method" value="{{method}}">
<div class="form-group" ng-class="{'has-error': planForm['plan[base_name]'].$dirty && planForm['plan[base_name]'].$invalid}">
<label for="plan[base_name]">{{ 'name' | translate }} *</label>
<label for="plan[base_name]">{{ 'plan_form.name' | translate }} *</label>
<input type="text" id="plan[base_name]"
name="plan[base_name]"
class="form-control"
ng-maxlength="24"
ng-model="plan.base_name"
required="required"/>
<span class="help-block error" ng-show="planForm['plan[base_name]'].$dirty && planForm['plan[base_name]'].$error.required" translate>{{ 'name_is_required' }}</span>
<span class="help-block error" ng-show="planForm['plan[base_name]'].$dirty && planForm['plan[base_name]'].$error.maxlength" translate>{{ 'name_length_must_be_less_than_24_characters' }}</span>
<span class="help-block error" ng-show="planForm['plan[base_name]'].$dirty && planForm['plan[base_name]'].$error.required" translate>{{ 'plan_form.name_is_required' }}</span>
<span class="help-block error" ng-show="planForm['plan[base_name]'].$dirty && planForm['plan[base_name]'].$error.maxlength" translate>{{ 'plan_form.name_length_must_be_less_than_24_characters' }}</span>
</div>
<div class="form-group" ng-class="{'has-error': planForm['plan[type]'].$dirty && planForm['plan[type]'].$invalid}">
<label for="plan[type]">{{ 'type' | translate }} *</label>
<label for="plan[type]">{{ 'plan_form.type' | translate }} *</label>
<select id="plan[type]"
name="plan[type]"
class="form-control"
@ -23,40 +23,40 @@
<option value="Plan" ng-selected="plan.type == 'Plan'" translate>{{ 'standard' }}</option>
<option value="PartnerPlan" ng-selected="plan.type == 'PartnerPlan'" translate>{{ 'partner' }}</option>
</select>
<span class="help-block error" ng-show="planForm['plan[type]'].$dirty && planForm['plan[type]'].$error.required" translate>{{ 'type_is_required' }}</span>
<span class="help-block error" ng-show="planForm['plan[type]'].$dirty && planForm['plan[type]'].$error.required" translate>{{ 'plan_form.type_is_required' }}</span>
</div>
<div class="form-group" ng-class="{'has-error': planForm['plan[group_id]'].$dirty && planForm['plan[group_id]'].$invalid}">
<label for="plan[group_id]">{{ 'group' | translate }} *</label>
<label for="plan[group_id]">{{ 'plan_form.group' | translate }} *</label>
<select id="plan[group_id]"
name="plan[group_id]"
class="form-control"
ng-model="plan.group_id"
required="required"
ng-disabled="method == 'PATCH'">
<option value="all" translate>{{ 'transversal_(all_groups)' }}</option>
<option value="all" translate>{{ 'plan_form.transversal_(all_groups)' }}</option>
<optgroup label="Groupes">
<option ng-repeat="group in groups" ng-value="group.id" ng-selected="plan.group_id == group.id">{{group.name}}</option>
</optgroup>
</select>
<span class="help-block" ng-show="planForm['plan[group_id]'].$dirty && planForm['plan[group_id]'].$error.required" translate>{{ 'group_is_required' }}</span>
<span class="help-block" ng-show="planForm['plan[group_id]'].$dirty && planForm['plan[group_id]'].$error.required" translate>{{ 'plan_form.group_is_required' }}</span>
</div>
<div class="form-group" ng-class="{'has-error': planForm['plan[interval]'].$dirty && planForm['plan[interval]'].$invalid}">
<label for="plan[interval]">{{ 'period' | translate }} *</label>
<label for="plan[interval]">{{ 'plan_form.period' | translate }} *</label>
<select id="plan[interval]"
name="plan[interval]"
class="form-control"
ng-model="plan.interval"
ng-disabled="method == 'PATCH'"
required="required">
<option value="month" ng-selected="plan.interval == 'month'" translate>{{ 'month' }}</option>
<option value="year" ng-selected="plan.interval == 'year'" translate>{{ 'year' }}</option>
<option value="month" ng-selected="plan.interval == 'month'" translate>{{ 'plan_form.month' }}</option>
<option value="year" ng-selected="plan.interval == 'year'" translate>{{ 'plan_form.year' }}</option>
</select>
<span class="help-block" ng-show="planForm['plan[interval]'].$dirty && planForm['plan[interval]'].$error.required" translate>{{ 'period_is_required' }}</span>
<span class="help-block" ng-show="planForm['plan[interval]'].$dirty && planForm['plan[interval]'].$error.required" translate>{{ 'plan_form.period_is_required' }}</span>
</div>
<div class="form-group" ng-class="{'has-error': planForm['plan[interval_count]'].$dirty && planForm['plan[interval_count]'].$invalid}">
<label for="plan[interval]">{{ 'number_of_periods' | translate }} *</label>
<label for="plan[interval]">{{ 'plan_form.number_of_periods' | translate }} *</label>
<input id="plan[interval_count]"
name="plan[interval_count]"
class="form-control"
@ -65,12 +65,12 @@
ng-disabled="method == 'PATCH'"
required="required"
min="1"/>
<span class="help-block" ng-show="planForm['plan[interval_count]'].$dirty && planForm['plan[interval_count]'].$error.required" translate>{{ 'number_of_periods_is_required' }}</span>
<span class="help-block" ng-show="planForm['plan[interval_count]'].$dirty && planForm['plan[interval_count]'].$error.required" translate>{{ 'plan_form.number_of_periods_is_required' }}</span>
</div>
<div class="form-group">
<div class="input-group" ng-class="{'has-error': planForm['plan[amount]'].$dirty && planForm['plan[amount]'].$invalid}">
<label for="plan[amount]">{{ 'subscription_price' | translate }} *</label>
<label for="plan[amount]">{{ 'plan_form.subscription_price' | translate }} *</label>
<div class="input-group">
<span class="input-group-addon">{{currencySymbol}}</span>
<input id="plan[amount]"
@ -80,24 +80,24 @@
ng-required="true"
ng-model="plan.amount"/>
</div>
<span class="help-block" ng-show="planForm['plan[amount]'].$dirty && planForm['plan[amount]'].$error.required" translate>{{ 'price_is_required' }}</span>
<span class="help-block" ng-show="planForm['plan[amount]'].$dirty && planForm['plan[amount]'].$error.required" translate>{{ 'plan_form.price_is_required' }}</span>
</div>
</div>
<div class="form-group">
<label translate>{{ 'visual_prominence_of_the_subscription' }}</label>
<label translate>{{ 'plan_form.visual_prominence_of_the_subscription' }}</label>
<input ng-model="plan.ui_weight"
type="number"
name="plan[ui_weight]"
class="form-control">
<span class="help-block">
{{ 'on_the_subscriptions_page_the_most_prominent_subscriptions_will_be_placed_at_the_top_of_the_list' | translate }}
{{ 'an_evelated_number_means_a_higher_prominence' | translate }}
{{ 'plan_form.on_the_subscriptions_page_the_most_prominent_subscriptions_will_be_placed_at_the_top_of_the_list' | translate }}
{{ 'plan_form.an_evelated_number_means_a_higher_prominence' | translate }}
</span>
</div>
<div class="input-group m-t-md">
<label for="plan[is_rolling]" class="control-label m-r-md">{{ 'rolling_subscription' | translate }} *</label>
<label for="plan[is_rolling]" class="control-label m-r-md">{{ 'plan_form.rolling_subscription' | translate }} *</label>
<input bs-switch
ng-model="plan.isRolling"
id="plan[is_rolling]"
@ -112,8 +112,8 @@
<span ng-if="method == 'PATCH'">{{ (plan.is_rolling ? 'yes' : 'no') | translate }}</span>
<input type="hidden" name="plan[is_rolling]" value="{{plan.isRolling}}"/>
<span class="help-block">
{{ 'a_rolling_subscription_will_begin_the_day_of_the_first_training' | translate }}
{{ 'otherwise_it_will_begin_as_soon_as_it_is_bought' | translate }}
{{ 'plan_form.a_rolling_subscription_will_begin_the_day_of_the_first_training' | translate }}
{{ 'plan_form.otherwise_it_will_begin_as_soon_as_it_is_bought' | translate }}
</span>
</div>
@ -121,12 +121,12 @@
<!-- PDF description attachement -->
<input type="hidden" ng-model="plan.plan_file_attributes.id" name="plan[plan_file_attributes][id]" ng-value="plan.plan_file_attributes.id" />
<input type="hidden" ng-model="plan.plan_file_attributes._destroy" name="plan[plan_file_attributes][_destroy]" ng-value="plan.plan_file_attributes._destroy"/>
<label class="m-t-md" translate>{{ 'information_sheet' }}</label>
<label class="m-t-md" translate>{{ 'plan_form.information_sheet' }}</label>
<div class="fileinput input-group" data-provides="fileinput" ng-class="fileinputClass(plan.plan_file_attributes)">
<div class="form-control" data-trigger="fileinput">
<i class="glyphicon glyphicon-file fileinput-exists"></i> <span class="fileinput-filename">{{file.attachment || plan.plan_file_attributes.attachment_identifier}}</span>
</div>
<span class="input-group-addon btn btn-default btn-file"><span class="fileinput-new" translate>{{ 'attach_an_information_sheet' }}</span>
<span class="input-group-addon btn btn-default btn-file"><span class="fileinput-new" translate>{{ 'plan_form.attach_an_information_sheet' }}</span>
<span class="fileinput-exists" translate>{{ 'change' }}</span><input type="file"
name="plan[plan_file_attributes][attachment]"
accept="image/*, application/pdf"></span>
@ -135,7 +135,7 @@
<div class="form-group m-t-md" ng-show="plan.type == 'PartnerPlan' && method != 'PATCH'">
<input type="hidden" ng-model="plan.partnerId" name="plan[partner_id]" ng-value="plan.partnerId" />
<label for="plan[partner_id]">{{ 'notified_partner' | translate }} *</label>
<label for="plan[partner_id]">{{ 'plan_form.notified_partner' | translate }} *</label>
<div class="input-group">
<select class="form-control"
ng-model="plan.partnerId"
@ -144,10 +144,10 @@
<option value=""></option>
</select>
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="openPartnerNewModal()"><i class="fa fa-user-plus"></i> {{ 'new_user' | translate }}</button>
<button class="btn btn-default" type="button" ng-click="openPartnerNewModal()"><i class="fa fa-user-plus"></i> {{ 'plan_form.new_user' | translate }}</button>
</span>
</div>
<span class="help-block" translate>{{ 'as_part_of_a_partner_subscription_some_notifications_may_be_sent_to_this_user' }}</span>
<span class="help-block" translate>{{ 'plan_form.as_part_of_a_partner_subscription_some_notifications_may_be_sent_to_this_user' }}</span>
</div>
<div class="form-group" ng-show="plan.partners">
@ -155,4 +155,4 @@
<span ng-repeat="partner in plan.partners">
<input type="text" class="form-control" disabled value="{{ partner.first_name}} {{partner.last_name }}">
</span>
</div>
</div>

View File

@ -7,7 +7,7 @@
</div>
<div class="col-xs-10 col-sm-10 col-md-8 b-l">
<section class="heading-title">
<h1>{{ 'subscription_plan' | translate }} {{ plan.base_name }}</h1>
<h1>{{ 'edit_plan.subscription_plan' | translate }} {{ plan.base_name }}</h1>
</section>
</div>
@ -30,23 +30,23 @@
<ng-include src="'<%= asset_path 'admin/plans/_form.html' %>'"></ng-include>
<h2 class="m-t-xl" translate>{{ 'prices' }}</h2>
<h2 class="m-t-xl" translate>{{ 'edit_plan.prices' }}</h2>
<div class="form-group col-md-6 col-lg-offset-6">
<input type="hidden" ng-model="plan.parent" name="plan[parent_id]" ng-value="plan.parent"/>
<label for="parentPlan" translate>{{ 'copy_prices_from' }}</label>
<label for="parentPlan" translate>{{ 'edit_plan.copy_prices_from' }}</label>
<select id="parentPlan" ng-options="plan.id as humanReadablePlanName(plan, groups) for plan in plans" ng-model="plan.parent" ng-change="copyPricesFromPlan()" class="form-control">
<option value=""></option>
</select>
</div>
<h3 translate>{{ 'machines' }}</h3>
<h3 translate>{{ 'edit_plan.machines' }}</h3>
<table class="table">
<thead>
<th translate>{{ 'machine' }}</th>
<th translate>{{ 'hourly_rate' }}</th>
<th translate>{{ 'edit_plan.machine' }}</th>
<th translate>{{ 'edit_plan.hourly_rate' }}</th>
</thead>
<tbody>
<tr ng-repeat="price in plan.prices">
<tr ng-repeat="price in plan.prices" ng-if="price.priceable_type === 'Machine'">
<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}">
@ -59,6 +59,27 @@
</tbody>
</table>
<h3 ng-hide="fablabWithoutSpaces" translate>{{ 'edit_plan.spaces' }}</h3>
<table class="table" ng-hide="fablabWithoutSpaces">
<thead>
<th translate>{{ 'edit_plan.space' }}</th>
<th translate>{{ 'edit_plan.hourly_rate' }}</th>
</thead>
<tbody>
<tr ng-repeat="price in plan.prices" ng-if="price.priceable_type === 'Space'">
<td style="width: 60%;">{{ getSpaceName(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>
<input type="number" class="form-control" name="plan[prices_attributes][][amount]" ng-value="price.amount" required="required"/>
<input type="hidden" class="form-control" name="plan[prices_attributes][][id]" ng-value="price.id"/>
</div>
</td>
</tr>
</tbody>
</table>
<div class="panel-footer no-padder">
<input type="submit" value="{{ 'confirm_changes' | translate }}" class="r-b btn-valid btn btn-warning btn-block p-lg btn-lg text-u-c" ng-disabled="planForm.$invalid"/>
</div>

View File

@ -7,7 +7,7 @@
</div>
<div class="col-xs-10 col-sm-10 col-md-8 b-l">
<section class="heading-title">
<h1 translate>{{ 'add_a_subscription_plan' }}</h1>
<h1 translate>{{ 'new_plan.add_a_subscription_plan' }}</h1>
</section>
</div>

View File

@ -1,22 +1,25 @@
<h2 translate>{{ 'list_of_the_coupons' }}</h2>
<h2 translate>{{ 'pricing.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>
<button type="button" class="btn btn-warning m-t-lg m-b" ui-sref="app.admin.coupons_new" translate>{{ 'pricing.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 translate>{{ 'pricing.name' }}</th>
<th translate>{{ 'pricing.discount' }}</th>
<th translate>{{ 'pricing.nb_of_usages' }}</th>
<th translate>{{ 'pricing.status' }}</th>
<th></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="coupon in coupons">
<td>{{coupon.name}}</td>
<td>{{coupon.percent_off}} %</td>
<td>
<span ng-show="coupon.type == 'percent_off'">{{coupon.percent_off}} %</span>
<span ng-show="coupon.type == 'amount_off'">{{coupon.amount_off}} {{currencySymbol}}</span>
</td>
<td>{{coupon.usages}}</td>
<td translate>{{coupon.status}}</td>
<td translate>{{'pricing.'+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>

View File

@ -1,10 +1,10 @@
<h2 class="m-t-lg" translate>{{ 'trainings' }}</h2>
<h2 class="m-t-lg" translate>{{ 'pricing.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%" translate>{{ 'pricing.subscription' }}</th>
<th style="width:10%" translate>{{ 'pricing.credits' }}</th>
<th style="width:50%" translate>{{ 'pricing.related_trainings' }}</th>
<th style="width:20%"></th>
</tr>
</thead>
@ -43,17 +43,17 @@
</tbody>
</table>
<h2 class="m-t-lg" translate>{{ 'machines' }}</h2>
<h2 class="m-t-lg" translate>{{ 'pricing.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>
<button type="button" class="btn btn-warning" ng-click="addMachineCredit($event)" translate>{{ 'pricing.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%" translate>{{ 'pricing.machine' }}</th>
<th style="width:10%" translate>{{ 'pricing.hours' }}</th>
<th style="width:50%" translate>{{ 'pricing.related_subscriptions' }}</th>
<th style="width:20%"></th>
</tr>
</thead>
@ -94,4 +94,56 @@
</td>
</tr>
</tbody>
</table>
<h2 class="m-t-lg" translate>{{ 'pricing.spaces' }}</h2>
<div class="btn-group m-t-md m-b-md">
<button type="button" class="btn btn-warning" ng-click="addSpaceCredit($event)" translate>{{ 'pricing.add_a_space_credit' }}</button>
</div>
<table class="table">
<thead>
<tr>
<th style="width:20%" translate>{{ 'pricing.space' }}</th>
<th style="width:10%" translate>{{ 'pricing.hours' }}</th>
<th style="width:50%" translate>{{ 'pricing.related_subscriptions' }}</th>
<th style="width:20%"></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="sc in spaceCredits">
<td>
<span editable-select="sc.creditable_id" e-name="creditable_id" e-form="rowform" e-ng-options="s.id as s.name for s in spaces" e-required>
{{ showCreditableName(sc) }}
</span>
</td>
<td>
<span editable-number="sc.hours" e-name="hours" e-form="rowform" e-required>
{{ sc.hours }}
</span>
</td>
<td>
<span editable-select="sc.plan_id" e-ng-options="p.id as humanReadablePlanName(p, groups, 'short') for p in plans" e-name="plan_id" e-form="rowform">
{{ getPlanFromId(sc.plan_id) | humanReadablePlanName: groups: 'short' }}
</span>
</td>
<td>
<form editable-form name="rowform" onbeforesave="saveSpaceCredit($data, sc.id)" ng-show="rowform.$visible" class="form-buttons form-inline" shown="inserted == sc">
<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="cancelSpaceCredit(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="removeSpaceCredit($index)">
<i class="fa fa-trash-o"></i> {{ 'delete' | translate }} (!)
</button>
</div>
</td>
</tr>
</tbody>
</table>

View File

@ -7,7 +7,7 @@
</div>
<div class="col-xs-10 col-sm-10 col-md-8 b-l">
<section class="heading-title">
<h1 translate>{{ 'pricing_management' }}</h1>
<h1 translate>{{ 'pricing.pricing_management' }}</h1>
</section>
</div>
@ -21,23 +21,27 @@
<div class="col-md-12">
<uib-tabset justified="true">
<uib-tab heading="{{ 'subscriptions' | translate }}">
<uib-tab heading="{{ 'pricing.subscriptions' | translate }}">
<ng-include src="'<%= asset_path 'admin/pricing/subscriptions.html' %>'"></ng-include>
</uib-tab>
<uib-tab heading="{{ 'trainings' | translate }}">
<uib-tab heading="{{ 'pricing.trainings' | translate }}">
<ng-include src="'<%= asset_path 'admin/pricing/trainings.html' %>'"></ng-include>
</uib-tab>
<uib-tab heading="{{ 'machine_hours' | translate }}">
<uib-tab heading="{{ 'pricing.machine_hours' | translate }}">
<ng-include src="'<%= asset_path 'admin/pricing/machine_hours.html' %>'"></ng-include>
</uib-tab>
<uib-tab heading="{{ 'credits' | translate }}">
<uib-tab heading="{{ 'pricing.spaces' | translate }}" ng-hide="fablabWithoutSpaces">
<ng-include src="'<%= asset_path 'admin/pricing/spaces.html' %>'"></ng-include>
</uib-tab>
<uib-tab heading="{{ 'pricing.credits' | translate }}">
<ng-include src="'<%= asset_path 'admin/pricing/credits.html' %>'"></ng-include>
</uib-tab>
<uib-tab heading="{{ 'coupons' | translate }}">
<uib-tab heading="{{ 'pricing.coupons' | translate }}">
<ng-include src="'<%= asset_path 'admin/pricing/coupons.html' %>'"></ng-include>
</uib-tab>
</uib-tabset>

View File

@ -1,10 +1,10 @@
<div class="alert alert-warning m-t">
{{ 'these_prices_match_machine_hours_rates_' | translate }} <span class="font-bold" translate>{{ '_without_subscriptions' }}</span>.
{{ 'pricing.these_prices_match_machine_hours_rates_' | translate }} <span class="font-bold" translate>{{ 'pricing._without_subscriptions' }}</span>.
</div>
<table class="table">
<thead>
<tr>
<th style="width:20%" translate>{{ 'machines' }}</th>
<th style="width:20%" translate>{{ 'pricing.machines' }}</th>
<th style="width:20%" ng-repeat="group in groups">
<span class="text-u-c text-sm">{{group.name}}</span>
</th>

View File

@ -1,11 +1,11 @@
<div class="modal-header">
<h3 class="text-center red" translate>{{ 'send_a_coupon' }}</h3>
<h3 class="text-center red" translate>{{ 'pricing.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>
<h3 class="panel-title" translate>{{ 'pricing.coupon' }}</h3>
</div>
<div class="widget-content no-bg auto wrapper">
<table>
@ -13,12 +13,12 @@
<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>
<tr><td translate>{{'pricing.code'}}</td> <td>{{coupon.code}}</td></tr>
<tr><td translate>{{'pricing.discount'}}</td> <td><span ng-show="coupon.type == 'percent_off'">{{coupon.percent_off}} %</span><span ng-show="coupon.type == 'amount_off'">{{coupon.amount_off}} {{currencySymbol}}</span></td></tr>
<tr><td translate>{{'pricing.validity_per_user'}}</td> <td translate>{{'pricing.'+coupon.validity_per_user}}</td></tr>
<tr><td translate>{{'pricing.valid_until'}}</td> <td>{{coupon.valid_until | amDateFormat:'L'}}</td></tr>
<tr><td translate>{{'pricing.usages'}}</td> <td>{{coupon.usages}} / {{coupon.max_usages | maxCount}}</td></tr>
<tr><td translate>{{'pricing.enabled'}}</td> <td>{{coupon.active | booleanFormat}}</td></tr>
</tbody>
</table>
</div>

View File

@ -0,0 +1,26 @@
<div class="alert alert-warning m-t">
{{ 'pricing.these_prices_match_space_hours_rates_' | translate }} <span class="font-bold" translate>{{ 'pricing._without_subscriptions' }}</span>.
</div>
<table class="table">
<thead>
<tr>
<th style="width:20%" translate>{{ 'pricing.spaces' }}</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="space in spaces">
<td>
{{ space.name }}
</td>
<td ng-repeat="group in groups">
<span editable-number="findPriceBy(spacesPrices, space.id, group.id).amount"
onbeforesave="updatePrice($data, findPriceBy(spacesPrices, space.id, group.id))">
{{ findPriceBy(spacesPrices, space.id, group.id).amount | currency}}
</span>
</td>
</tr>
</tbody>
</table>

View File

@ -1,21 +1,21 @@
<h2 translate>{{ 'list_of_the_subscription_plans' }}</h2>
<h2 translate>{{ 'pricing.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 }}
{{ 'pricing.beware_the_subscriptions_are_disabled_on_this_application' | translate }}
{{ 'pricing.you_can_create_some_but_they_wont_be_available_until_the_project_is_redeployed_by_the_server_manager' | translate }}
<br>{{ 'pricing.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>
<button type="button" class="btn btn-warning m-t-lg m-b" ui-sref="app.admin.plans.new" translate>{{ 'pricing.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><a href="" ng-click="setOrderPlans('type')">{{ 'pricing.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')">{{ 'pricing.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')">{{ 'pricing.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')">{{ 'pricing.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('pricing.ui_weight')">{{ 'pricing.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')">{{ 'pricing.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>

View File

@ -1,7 +1,7 @@
<table class="table">
<thead>
<tr>
<th style="width:20%" translate>{{ 'trainings' }}</th>
<th style="width:20%" translate>{{ 'pricing.trainings' }}</th>
<th style="width:20%" ng-repeat="group in groups">
<span class="text-u-c text-sm">{{group.name}}</span>
</th>

View File

@ -3,8 +3,8 @@
<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>
<h1 ng-model="aboutTitleSetting.value" medium-editor options='{"placeholder": "{{ "settings.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> {{ 'settings.shift_enter_to_force_carriage_return' | translate }}</span>
<button name="button" class="btn btn-warning" ng-click="save(aboutTitleSetting)" translate>{{ 'save' }}</button>
</div>
@ -12,7 +12,7 @@
<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 }}",
<div class="text-justify" ng-model="aboutBodySetting.value" medium-editor options='{"placeholder": "{{ "settings.input_the_main_content" | translate }}",
"buttons": ["bold", "italic", "anchor", "header1", "header2" ]
}'>
@ -20,12 +20,12 @@
<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 }}",
<div ng-model="aboutContactsSetting.value" medium-editor options='{"placeholder": "{{ "settings.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>
<span class="help-block text-info text-xs"><i class="fa fa-lightbulb-o"></i> {{ 'settings.shift_enter_to_force_carriage_return' | translate }}</span>
<button name="button" class="btn btn-warning" ng-click="save(aboutContactsSetting)" translate>{{ 'save' }}</button>
</div>

View File

@ -1,16 +1,16 @@
<div class="panel panel-default m-t-md">
<div class="panel-heading">
<span class="font-sbold" translate>{{ 'title' }}</span>
<span class="font-sbold" translate>{{ 'settings.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>
<label for="fablabName" class="control-label m-r" translate>{{ 'settings.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 }}"/>
<input type="text" id="fablabName" ng-model="fablabName.value" class="form-control" placeholder="{{ 'settings.fablab_name' | translate }}"/>
</div>
</div>
<button name="button" class="btn btn-warning" ng-click="save(fablabName)" translate>{{ 'save' }}</button>
@ -19,13 +19,13 @@
<div class="col-md-4 col-md-offset-1">
<form role="form" novalidate>
<h4 class="control-label m-r" translate>{{ 'title_concordance' }}</h4>
<h4 class="control-label m-r" translate>{{ 'settings.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>
<label for="nameGenreMale">{{ 'settings.male' | translate }} <span style="font-weight: normal">{{ 'settings.eg' | translate }} <cite>{{ 'settings.about' | translate }} <strong translate>{{ 'settings.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>
<label for="nameGenreFemale">{{ 'settings.female' | translate }} <span style="font-weight: normal">{{ 'settings.eg' | translate }} <cite>{{ 'settings.about' | translate }} <strong translate>{{ 'settings.female_preposition' }}</strong> {{fablabName.value}}</cite></span></label>
</div>
<button name="button" class="btn btn-warning" ng-click="save(nameGenre)" translate>{{ 'save' }}</button>
</form>
@ -36,15 +36,15 @@
<div class="panel panel-default m-t-lg">
<div class="panel-heading">
<span class="font-sbold" translate>{{ 'customize_information_messages' }}</span>
<span class="font-sbold" translate>{{ 'settings.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 }}",
<h4 translate>{{ 'settings.message_of_the_machine_booking_page' }}</h4>
<div ng-model="machineExplicationsAlert.value" medium-editor options='{"placeholder": "{{ "settings.type_the_message_content" | translate }}",
"buttons": ["bold", "italic", "unorderedlist", "header2" ]
}'>
@ -52,8 +52,8 @@
<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 }}",
<h4 translate>{{ 'settings.warning_message_of_the_training_booking_page'}}</h4>
<div ng-model="trainingExplicationsAlert.value" medium-editor options='{"placeholder": "{{ "settings.type_the_message_content" | translate }}",
"buttons": ["bold", "italic", "unorderedlist", "header2" ]
}'>
@ -61,8 +61,8 @@
<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 }}",
<h4 translate>{{ 'settings.information_message_of_the_training_reservation_page'}}</h4>
<div ng-model="trainingInformationMessage.value" medium-editor options='{"placeholder": "{{ "settings.type_the_message_content" | translate }}",
"buttons": ["bold", "italic", "unorderedlist", "header2" ]
}'>
@ -70,30 +70,46 @@
<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 }}",
<h4 translate>{{ 'settings.message_of_the_subscriptions_page' }}</h4>
<div ng-model="subscriptionExplicationsAlert.value" medium-editor options='{"placeholder": "{{ "settings.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 class="col-md-3">
<h4 translate>{{ 'settings.message_of_the_events_page' }}</h4>
<div ng-model="eventExplicationsAlert.value" medium-editor options='{"placeholder": "{{ "settings.type_the_message_content" | translate }}",
"buttons": ["bold", "italic", "unorderedlist", "header2" ]
}'>
</div>
<button name="button" class="btn btn-warning" ng-click="save(eventExplicationsAlert)" translate>{{ 'save' }}</button>
</div>
<div class="col-md-3" ng-hide="fablabWithoutSpaces">
<h4 translate>{{ 'settings.message_of_the_spaces_page' }}</h4>
<div ng-model="spaceExplicationsAlert.value" medium-editor options='{"placeholder": "{{ "settings.type_the_message_content" | translate }}",
"buttons": ["bold", "italic", "unorderedlist", "header2" ]
}'>
</div>
<button name="button" class="btn btn-warning" ng-click="save(spaceExplicationsAlert)" 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>
<span class="font-sbold" translate>{{ 'settings.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' }}
{{ 'settings.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>
<label for="tnc_file" class="control-label m-r" translate>{{ 'settings.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">
@ -117,7 +133,7 @@
<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>
<label for="tos_file" class="control-label m-r" translate>{{ 'settings.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">
@ -142,21 +158,21 @@
<div class="panel panel-default m-t-lg">
<div class="panel-heading">
<span class="font-sbold" translate>{{ 'customize_the_graphics' }}</span>
<span class="font-sbold" translate>{{ 'settings.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/>
<span translate>{{ 'settings.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>{{ 'settings.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>
<span translate>{{ 'settings.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>
<h3 class="m-l" translate>{{ 'settings.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">
@ -164,7 +180,7 @@
<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 }}
<i class="fa fa-edit"></i> {{ 'settings.change_the_logo' | translate }}
<input type="file"
accept="image/png,image/x-png"
name="custom_asset[custom_asset_file_attributes][attachment]"
@ -182,7 +198,7 @@
<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>
<h3 class="m-l" translate>{{ 'settings.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">
@ -190,7 +206,7 @@
<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 }}
<i class="fa fa-edit"></i> {{ 'settings.change_the_logo' | translate }}
<input type="file"
accept="image/png,image/x-png"
name="custom_asset[custom_asset_file_attributes][attachment]"
@ -208,7 +224,7 @@
<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>
<h3 class="m-l" translate>{{ 'settings.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">
@ -216,7 +232,7 @@
<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 }}
<i class="fa fa-edit"></i> {{ 'settings.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]"
@ -233,14 +249,14 @@
</div>
<div class="row m-t m-l-xs">
<div class="col-md-4">
<h4 translate>{{ 'main_colour' }}</h4>
<h4 translate>{{ 'settings.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}}"/>
<input type="text" minicolors ng-model="mainColorSetting.value" class="form-control" placeholder="{{ 'settings.primary' | translate}}"/>
</div>
</div>
<div class="form-group">
@ -249,14 +265,14 @@
</form>
</div>
<div class="col-md-4">
<h4 translate>{{ 'secondary_colour' }}</h4>
<h4 translate>{{ 'settings.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}}"/>
<input type="text" minicolors ng-model="secondColorSetting.value" class="form-control" placeholder="{{ 'settings.secondary' | translate}}"/>
</div>
</div>
<div class="form-group">
@ -270,7 +286,7 @@
<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>
<h3 class="m-l" translate>{{ 'settings.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">
@ -278,7 +294,7 @@
<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 }}
<i class="fa fa-edit"></i> {{ 'settings.change_the_profile_banner' | translate }}
<input type="file"
accept="image/png,image/x-png"
name="custom_asset[custom_asset_file_attributes][attachment]"

View File

@ -2,21 +2,21 @@
<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 }}",
<h4 translate>{{ 'settings.news_of_the_home_page' }}</h4>
<div ng-model="homeBlogpostSetting.value" class="well" medium-editor options='{"placeholder": "{{ "settings.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>
<span class="help-block text-info text-xs"><i class="fa fa-lightbulb-o"></i> {{ 'settings.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>
<h4 translate>{{ 'settings.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 }}"/>
<input type="text" ng-model="twitterSetting.value" class="form-control" placeholder="{{ 'settings.name_of_the_twitter_account' | translate }}"/>
</div>
</div>
<div class="form-group">

View File

@ -7,7 +7,7 @@
</div>
<div class="col-xs-10 col-sm-10 col-md-8 b-l">
<section class="heading-title">
<h1 translate>{{ 'customize_the_application' }}</h1>
<h1 translate>{{ 'settings.customize_the_application' }}</h1>
</section>
</div>
@ -20,18 +20,18 @@
<div class="col-md-12">
<uib-tabset justified="true">
<uib-tab heading="{{ 'general' | translate }}">
<uib-tab heading="{{ 'settings.general' | translate }}">
<ng-include src="'<%= asset_path 'admin/settings/general.html' %>'"></ng-include>
</uib-tab>
<uib-tab heading="{{ 'home_page' | translate }}">
<uib-tab heading="{{ 'settings.home_page' | translate }}">
<ng-include src="'<%= asset_path 'admin/settings/home_page.html' %>'"></ng-include>
</uib-tab>
<uib-tab heading="{{ 'about' | translate }}">
<uib-tab heading="{{ 'settings.about' | translate }}">
<ng-include src="'<%= asset_path 'admin/settings/about.html' %>'"></ng-include>
</uib-tab>
<uib-tab heading="{{ 'reservations' | translate }}">
<uib-tab heading="{{ 'settings.reservations' | translate }}">
<ng-include src="'<%= asset_path 'admin/settings/reservations.html' %>'"></ng-include>
</uib-tab>
</uib-tabset>

View File

@ -1,20 +1,20 @@
<div class="panel panel-default m-t-lg">
<div class="panel-heading">
<span class="font-sbold" translate>{{ 'reservations_parameters' }}</span>
<span class="font-sbold" translate>{{ 'settings.reservations_parameters' }}</span>
</div>
<div class="panel-body">
<div>
<div class="row">
<h3 class="m-l" translate>{{ 'confine_the_booking_agenda' }}</h3>
<h3 class="m-l" translate>{{ 'settings.confine_the_booking_agenda' }}</h3>
<div class="col-md-2">
<h4 translate>{{ 'opening_time' }}</h4>
<h4 translate>{{ 'settings.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>
<h4 translate>{{ 'settings.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">
@ -22,23 +22,23 @@
</div>
</div>
<div class="row">
<h3 class="m-l" translate>{{ 'ability_for_the_users_to_move_their_reservations' }}</h3>
<h3 class="m-l" translate>{{ 'settings.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>
<label for="enableMove" class="control-label m-r" translate>{{ 'settings.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-on-text="{{ 'settings.enabled' | translate }}"
switch-off-text="{{ 'settings.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>
<label for="moveDelay" class="control-label m-r" translate>{{ 'settings.prior_period_(hours)' }}</label>
<div class="form-group">
<div class="input-group">
<div class="input-group-addon">
@ -51,23 +51,23 @@
</form>
</div>
<div class="row">
<h3 class="m-l" translate>{{ 'ability_for_the_users_to_cancel_their_reservations' }}</h3>
<h3 class="m-l" translate>{{ 'settings.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>
<label for="enableCancel" class="control-label m-r" translate>{{ 'settings.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-on-text="{{ 'settings.enabled' | translate }}"
switch-off-text="{{ 'settings.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>
<label for="cancelDelay" class="control-label m-r" translate>{{ 'settings.prior_period_(hours)' }}</label>
<div class="form-group">
<div class="input-group">
<div class="input-group-addon">
@ -85,27 +85,27 @@
<div class="panel panel-default m-t-lg">
<div class="panel-heading">
<span class="font-sbold" translate>{{ 'reservations_reminders' }}</span>
<span class="font-sbold" translate>{{ 'settings.reservations_reminders' }}</span>
</div>
<div class="panel-body">
<div class="row">
<h3 class="m-l" translate>{{ 'notification_sending_before_the_reservation_occurs' }}</h3>
<h3 class="m-l" translate>{{ 'settings.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>
<label for="enableReminder" class="control-label m-r" translate>{{ 'settings.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-on-text="{{ 'settings.enabled' | translate }}"
switch-off-text="{{ 'settings.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>
<label for="reminderDelay" class="control-label m-r" translate>{{ 'settings.prior_period_(hours)' }}</label>
<div class="form-group">
<div class="input-group">
<div class="input-group-addon">
@ -114,7 +114,7 @@
<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 }}
<i class="fa fa-lightbulb-o"></i> {{ 'settings.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>

View File

@ -26,7 +26,7 @@
<div class="col-md-12">
<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)">
<uib-tab ng-repeat="stat in statistics" heading="{{stat.label}}" select="setActiveTab(stat)" ng-hide="hiddenTab(stat)">
<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" style="z-index:102;">
<button id="agePickerExpand" class="btn btn-default" type="button" ng-click="agePicker.show = !agePicker.show">

View File

@ -7,14 +7,14 @@
</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>
<h1 translate>{{ 'calendar.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 }}
<span class="fa fa-filter"></span> {{ 'calendar.filter_calendar' | translate }}
</button>
</div>
</div>
@ -38,7 +38,7 @@
<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>
<h3 translate>{{ 'calendar.filter_calendar' }}</h3>
</div>
<div class="widget-content no-bg auto wrapper calendar-filter">
<ng-include src="'<%= asset_path 'calendar/filter.html' %>'"></ng-include>
@ -53,7 +53,7 @@
<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>
<h1 class="modal-title" translate>{{ 'calendar.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>

View File

@ -1,6 +1,6 @@
<div>
<div class="row">
<h3 class="col-md-11 col-sm-11 col-xs-11 text-purple" translate>{{ 'trainings' }}</h3>
<h3 class="col-md-11 col-sm-11 col-xs-11 text-purple" translate>{{ 'calendar.trainings' }}</h3>
<input class="col-md-1 col-sm-1 col-xs-1" type="checkbox" ng-model="filter.trainings" ng-change="toggleFilter('trainings', filter)">
</div>
<div ng-repeat="t in trainings" class="row">
@ -10,7 +10,7 @@
</div>
<div class="m-t">
<div class="row">
<h3 class="col-md-11 col-sm-11 col-xs-11 text-beige" translate>{{ 'machines' }}</h3>
<h3 class="col-md-11 col-sm-11 col-xs-11 text-beige" translate>{{ 'calendar.machines' }}</h3>
<input class="col-md-1 col-sm-1 col-xs-1" type="checkbox" ng-model="filter.machines" ng-change="toggleFilter('machines', filter)">
</div>
<div ng-repeat="m in machines" class="row">
@ -18,11 +18,21 @@
<input class="col-md-1 col-sm-1 col-xs-1" type="checkbox" ng-model="m.checked" ng-change="filterAvailabilities(filter)">
</div>
</div>
<div class="m-t">
<div class="row">
<h3 class="col-md-11 col-sm-11 col-xs-11 text-cyan" translate>{{ 'calendar.spaces' }}</h3>
<input class="col-md-1 col-sm-1 col-xs-1" type="checkbox" ng-model="filter.spaces" ng-change="toggleFilter('spaces', filter)">
</div>
<div ng-repeat="s in spaces" class="row">
<span class="col-md-11 col-sm-11 col-xs-11">{{::s.name}}</span>
<input class="col-md-1 col-sm-1 col-xs-1" type="checkbox" ng-model="s.checked" ng-change="filterAvailabilities(filter)">
</div>
</div>
<div class="m-t row">
<h3 class="col-md-11 col-sm-11 col-xs-11 text-japonica" translate>{{ 'events' }}</h3>
<h3 class="col-md-11 col-sm-11 col-xs-11 text-japonica" translate>{{ 'calendar.events' }}</h3>
<input class="col-md-1 col-sm-1 col-xs-1" type="checkbox" ng-model="filter.evt" ng-change="filterAvailabilities(filter)">
</div>
<div class="m-t row">
<h3 class="col-md-11 col-sm-11 col-xs-11 text-black" translate>{{ 'show_no_disponible' }}</h3>
<h3 class="col-md-11 col-sm-11 col-xs-11 text-black" translate>{{ 'calendar.show_unavailables' }}</h3>
<input class="col-md-1 col-sm-1 col-xs-1" type="checkbox" ng-model="filter.dispo" ng-change="filterAvailabilities(filter)">
</div>

View File

@ -40,7 +40,16 @@
<div class="form-group" ng-class="{'has-error': eventForm['event[description]'].$dirty && eventForm['event[description]'].$invalid}">
<label for="description" class="col-sm-3 control-label">{{ 'description' | translate }} *</label>
<div class="col-sm-9">
<textarea ng-model="event.description" rows="16" class="form-control" id="event_description" placeholder="" name="event[description]" required></textarea>
<input type="hidden"
name="event[description]"
ng-value="event.description" />
<summernote ng-model="event.description"
id="event_description"
placeholder=""
config="summernoteOpts"
name="event[description]"
required>
</summernote>
<span class="help-block" ng-show="eventForm['event[description]'].$dirty && eventForm['event[description]'].$error.required" translate>{{ 'description_is_required' }}</span>
</div>
</div>
@ -237,7 +246,7 @@
<span class="help-block" translate>{{ '0_=_free' }}</span>
</div>
</div>
<div class="form-group" ng-repeat="price in event.prices">
<div class="form-group" ng-repeat="price in event.prices" ng-show="!price._destroy">
<div class="col-sm-5">
<input type="hidden" name="event[event_price_categories_attributes][][id]" ng-value="price.id">
<select class="form-control"
@ -255,6 +264,10 @@
<div class="input-group-addon">{{currencySymbol}}</div>
</div>
</div>
<div class="col-sm-1">
<input type="hidden" name="event[event_price_categories_attributes][][_destroy]" ng-value="price._destroy">
<a class="btn" ng-click="removePrice(price, $event)" href="#"><i class="fa fa-times text-danger"></i></a>
</div>
</div>
<div class="link-icon m-b" ng-hide="event.prices.length == priceCategories.length">
<div class="col-sm-offset-5">

View File

@ -36,7 +36,7 @@
</div>
<h3 translate>{{ 'event_description' }}</h3>
<p ng-bind-html="event.description | breakFilter"></p>
<p ng-bind-html="event.description | toTrusted"></p>
</div>
@ -68,7 +68,7 @@
<section class="widget panel b-a m m-t-lg">
<div class="panel-heading b-b small">
<h3 translate>{{ 'informations_and_booking' }}</h3>
<h3 translate>{{ 'information_and_booking' }}</h3>
</div>
<div class="panel-content wrapper">
@ -164,7 +164,7 @@
<button class="btn btn-warning-full rounded btn-block text-sm" ng-click="reserveEvent()" ng-show="event.nb_free_places > 0 && !reserve.toReserve">{{ 'book' | translate }}</button>
<coupon show="reserve.totalSeats > 0 && ctrl.member" coupon="coupon.applied" user-id="{{ctrl.member.id}}"></coupon>
<coupon show="reserve.totalSeats > 0 && ctrl.member" coupon="coupon.applied" total="reserve.totalNoCoupon" user-id="{{ctrl.member.id}}"></coupon>
</div>
</div>
@ -178,6 +178,13 @@
</section>
<uib-alert type="warning m" ng-if="eventExplicationsAlert.length > 0">
<p class="text-sm pull-left">
<i class="fa fa-warning"></i>
<div class="m-l-lg" ng-bind-html="eventExplicationsAlert"></div>
</p>
</uib-alert>
</div>
</div>

View File

@ -100,7 +100,7 @@
<span class="v-middle badge text-xs" ng-class="'bg-{{event.category.name | lowercase}}'">{{event.category.name}}</span>
</div>
</div>
<p>{{event.description | humanize : 500 }}</p>
<p ng-bind-html="event.description | simpleText | humanize : 500 | breakFilter"></p>
<hr/>
<div class="row">

View File

@ -29,178 +29,25 @@
<select-member></select-member>
</div>
<div class="widget panel b-a m m-t-lg" ng-show="!ctrl.member && currentUser.role == 'admin' && eventsReserved.length == 0 && (!paidMachineSlots || paidMachineSlots.length == 0) && !slotToModify && !modifiedSlots">
<div class="panel-heading b-b small">
<h3 translate>{{ 'summary' }}</h3>
</div>
<div class="widget-content no-bg auto wrapper">
<p class="font-felt fleche-left text-lg"><%= image_tag("fleche-left.png", class: 'fleche-left visible-lg') %>
{{ 'select_one_or_more_slots_in_the_calendar' | translate }}</p>
</div>
</div>
<div class="widget panel b-a m m-t-lg" ng-if="ctrl.member && !slotToModify && !modifiedSlots">
<div class="panel-heading b-b small">
<h3 translate>{{ 'summary' }}</h3>
</div>
<div class="widget-content no-bg auto wrapper" ng-show="eventsReserved.length == 0 && (!paidMachineSlots || paidMachineSlots.length == 0)">
<p class="font-felt fleche-left text-lg"><%= image_tag("fleche-left.png", class: 'fleche-left visible-lg') %>
{{ 'select_one_or_more_slots_in_the_calendar' | translate }}</p>
</div>
<div class="widget-content no-bg auto wrapper" ng-if="eventsReserved.length > 0">
<div class="font-sbold m-b-sm " translate>{{ 'you_ve_just_selected_the_slot' }}</div>
<div class="panel panel-default bg-light" ng-repeat="machineSlot in eventsReserved">
<div class="panel-body">
<div class="font-sbold text-u-c">{{ 'datetime_to_time' | translate:{START_DATETIME:(machineSlot.start | amDateFormat:'LLLL'), END_TIME:(machineSlot.end | amDateFormat:'LT') } }}</div>
<div class="text-base">{{ 'cost_of_a_machine_hour' | translate }} <span ng-class="{'text-blue': !machineSlot.promo, 'red': machineSlot.promo}">{{machineSlot.price | currency}}</span></div>
<div ng-show="currentUser.role == 'admin'" class="m-t">
<label for="offerSlot" class="control-label m-r" translate>{{ 'offer_this_slot' }}</label>
<input bs-switch
ng-model="machineSlot.offered"
id="offerSlot"
type="checkbox"
class="form-control"
switch-on-text="{{ 'yes' | translate}}"
switch-off-text="{{ 'no' | translate}}"
switch-animate="true"
switch-readonly="{{machineSlot.isValid}}"/>
</div>
</div>
<div>
<button class="btn btn-valid btn-warning btn-block text-u-c r-b" ng-click="validMachineSlot(machineSlot)" ng-if="!machineSlot.isValid" translate>{{ 'confirm_this_slot' }}</button>
</div>
<div class="clear"><a class="pull-right m-b-sm text-u-l ng-scope m-r-sm" href="#" ng-click="removeMachineSlot(machineSlot, $event)" ng-if="machineSlot.isValid" translate>{{ 'remove_this_slot' }}</a></div>
</div>
<coupon show="machineSlotsValid() && (!plansAreShown || selectedPlan)" coupon="coupon.applied" has-select-slot="machineSlotsValid()" user-id="{{ctrl.member.id}}"></coupon>
<span ng-hide="fablabWithoutPlans">
<div ng-if="machineSlotsValid() && !ctrl.member.subscribed_plan" ng-show="!plansAreShown">
<p class="font-sbold text-base l-h-2x" translate>{{ 'to_benefit_from_attractive_prices' }}</p>
<div><button class="btn btn-warning-full rounded btn-block text-xs" ng-click="showPlans()" translate>{{ 'view_our_subscriptions' }}</button></div>
<p class="font-bold text-base text-u-c text-center m-b-xs" translate>{{ 'or' }}</p>
</div>
<div ng-if="selectedPlan">
<div class="m-t-md m-b-sm text-base">{{ 'you_ve_just_selected_a_' | translate }} <br> <span class="font-sbold" translate>{{ '_subscription' }}</span> :</div>
<div class="panel panel-default bg-light m-n">
<div class="panel-body m-b-md">
<div class="font-sbold text-u-c">{{selectedPlan | humanReadablePlanName }}</div>
<div class="text-base">{{ 'cost_of_the_subscription' | translate }} <span class="text-blue">{{selectedPlan.amount | currency}}</span></div>
</div>
</div>
</div>
</span>
</div>
<div class="panel-footer no-padder" ng-if="eventsReserved.length > 0">
<button class="btn btn-valid btn-info btn-block p-l btn-lg text-u-c r-b text-base" ng-click="payMachine()" ng-if="machineSlotsValid() && (!plansAreShown || selectedPlan)">{{ 'confirm_and_pay' | translate }} {{amountTotal | currency}}</button>
</div>
<div class="widget-content no-bg auto wrapper" ng-if="paidMachineSlots">
{{ 'you_have_settled_the_following_machine_hours' | translate }} <strong>{{machine.name}}</strong>:
<div class="well well-warning m-t-sm" ng-repeat="paidSlot in paidMachineSlots">
<i class="font-sbold text-u-c">{{ 'datetime_to_time' | translate:{START_DATETIME:(paidSlot.start | amDateFormat:'LLLL'), END_TIME:(paidSlot.end | amDateFormat:'LT') } }}</i>
<div class="font-sbold">{{ 'cost_of_a_machine_hour' | translate }} {{paidSlot.machine.amount() | currency}}</div>
</div>
<div ng-if="selectedPlan">
<div class="m-t-md m-b-sm text-base">{{ 'you_have_settled_a_' | translate }} <br> <span class="font-sbold" translate>{{ '_subscription' }}</span> :</div>
<div class="well well-warning m-t-sm">
<i class="font-sbold text-u-c">{{selectedPlan | humanReadablePlanName }}</i>
<div class="font-sbold">{{ 'cost_of_the_subscription' | translate }} {{selectedPlan.amount | currency}}</div>
</div>
</div>
<div class="m-t-md font-sbold">{{ 'total_' | translate }} {{amountTotal | currency}}</div>
<div class="alert alert-success" ng-if="ctrl.member.subscribed_plan">{{ 'thank_you_your_payment_has_been_successfully_registered' | translate }}<br>
{{ 'your_invoice_will_be_available_soon_from_your_' | translate }} <a ui-sref="app.logged.dashboard.invoices" translate>{{ 'dashboard' }}</a>
</div>
</div>
</div>
<div class="widget panel b-a m m-t-lg" ng-if="slotToModify || modifiedSlots">
<div class="panel-heading b-b small">
<h3 translate>{{ 'summary' }}</h3>
</div>
<div class="widget-content no-bg auto wrapper" ng-if="slotToModify">
<div class="font-sbold m-b-sm " translate>{{ 'i_want_to_change_the_following_reservation' }}</div>
<div class="panel panel-warning bg-yellow">
<div class="panel-body">
<div class="font-sbold text-u-c">{{ 'datetime_to_time' | translate:{START_DATETIME:(slotToModify.start | amDateFormat:'LLLL'), END_TIME:(slotToModify.end | amDateFormat:'LT') } }}</div>
</div>
<div class="clear"><a class="pull-right m-b-sm text-u-l ng-scope m-r-sm" href="#" ng-click="removeSlotToModify($event)" translate>{{ 'cancel_my_modification' }}</a></div>
</div>
<div class="widget-content no-bg">
<p class="font-felt fleche-left text-lg"><%= image_tag("fleche-left.png", class: 'fleche-left visible-lg') %>
{{ 'select_a_new_slot_in_the_calendar' | translate }}</p>
</div>
<div class="panel panel-info bg-info text-white" ng-if="slotToPlace">
<div class="panel-body">
<div class="font-sbold text-u-c">{{ 'datetime_to_time' | translate:{START_DATETIME:(slotToPlace.start | amDateFormat:'LLLL'), END_TIME:(slotToPlace.end | amDateFormat:'LT') } }}</div>
</div>
<div class="clear"><a class="pull-right m-b-sm text-u-l ng-scope m-r-sm" href="#" ng-click="removeSlotToPlace($event)" translate>{{ 'cancel_my_selection' }}</a></div>
</div>
<div ng-if="slotToPlace && slotToModify.tags.length > 0 && slotToPlace.tags.length > 0" ng-class="{'panel panel-danger bg-red': tagMissmatch()}">
<div class="panel-body">
<div id="fromTags">
{{ 'tags_of_the_original_slot' | translate }}<br/>
<span ng-repeat="tag in slotToModify.tags">
<span class='label label-success text-white' title="{{tag.name}}">{{tag.name}}</span>
</span>
</div><br/>
<div id="toTags">
{{ 'tags_of_the_destination_slot' | translate }}<br/>
<span ng-repeat="tag in slotToPlace.tags">
<span class='label label-success text-white' title="{{tag.name}}">{{tag.name}}</span>
</span>
</div>
</div>
</div>
</div>
<div class="panel-footer no-padder" ng-if="slotToModify && slotToPlace">
<button class="btn btn-invalid btn-default btn-block p-l btn-lg text-u-c r-n text-base" ng-click="cancelModifyMachineSlot()" translate>{{ 'cancel' }}</button>
<div>
<button class="btn btn-valid btn-info btn-block p-l btn-lg text-u-c r-b text-base" ng-click="modifyMachineSlot()" translate>{{ 'confirm_my_modification' }}</button>
</div>
</div>
<div class="widget-content no-bg auto wrapper" ng-if="modifiedSlots">
<div class="font-sbold m-b-sm " translate>{{ 'your_booking_slot_was_successfully_moved_from_' }}</div>
<div class="panel panel-default bg-light">
<div class="panel-body">
<div class="font-sbold text-u-c">{{ 'datetime_to_time' | translate:{START_DATETIME:(modifiedSlots.oldReservedSlot.start | amDateFormat:'LLLL'), END_TIME:(modifiedSlots.oldReservedSlot.end | amDateFormat:'LT') } }}</div>
</div>
</div>
<p class="text-center font-bold m-b-sm text-u-c" translate>{{ 'to_date' }}</p>
<div class="panel panel-success bg-success bg-light">
<div class="panel-body">
<div class="font-sbold text-u-c">{{ 'datetime_to_time' | translate:{START_DATETIME:(modifiedSlots.newReservedSlot.start | amDateFormat:'LLLL'), END_TIME:(modifiedSlots.newReservedSlot.end | amDateFormat:'LT') } }}</div>
</div>
</div>
</div>
</div>
<cart slot="selectedEvent"
slot-selection-time="selectionTime"
events="events"
user="ctrl.member"
mode-plans="plansAreShown"
plan="selectedPlan"
plan-selection-time="planSelectionTime"
settings="settings"
on-slot-added-to-cart="markSlotAsAdded"
on-slot-removed-from-cart="markSlotAsRemoved"
on-slot-start-to-modify="markSlotAsModifying"
on-slot-modify-success="modifyMachineSlot"
on-slot-modify-cancel="cancelModifyMachineSlot"
on-slot-modify-unselect="changeModifyMachineSlot"
on-slot-cancel-success="slotCancelled"
after-payment="afterPayment"
reservable-id="{{machine.id}}"
reservable-type="Machine"
reservable-name="{{machine.name}}"></cart>
<uib-alert type="warning m">
<p class="text-sm">

View File

@ -19,7 +19,7 @@
<div class="row">
<div class="col-md-12">
<button type="button" class="btn btn-warning m-t-sm m-b" ng-click="markAllAsRead()" ng-disabled="notifications.length == 0">{{ 'mark_all_as_read' | translate }} ({{notifications.length}})</button>
<button type="button" class="btn btn-warning m-t-sm m-b" ng-click="markAllAsRead()" ng-disabled="totalUnread == 0">{{ 'mark_all_as_read' | translate }} ({{totalUnread}})</button>
<table class="table">
<thead>
@ -31,7 +31,7 @@
</tr>
</thead>
<tbody>
<tr ng-repeat="notification in notifications" ng-if="notifications.length > 0">
<tr ng-repeat="notification in notificationsUnread" ng-if="notificationsUnread.length > 0">
<td>
<button class="btn btn-sm btn-warning" ng-click="markAsRead(notification, $event)">
<i class="fa fa-check"></i>
@ -41,45 +41,47 @@
<td ng-bind-html="notification.message.description"></td>
</tr>
<tr ng-if="notifications.length == 0">
<tr ng-if="notificationsUnread.length == 0">
<td colspan="3" translate>{{ 'no_new_notifications' }}</td>
</tr>
</tbody>
</table>
<h5 translate>{{ 'archives' }}</h5>
<div ng-hide="notificationsRead.length == 0 && notificationsUnread.length < total">
<h5 translate>{{ 'archives' }}</h5>
<table class="table">
<thead>
<tr>
<th style="width:10%"></th>
<th style="width:20%"></th>
<th style="width:70%"></th>
<table class="table">
<thead>
<tr>
<th style="width:10%"></th>
<th style="width:20%"></th>
<th style="width:70%"></th>
</tr>
</thead>
<tbody>
</tr>
</thead>
<tbody>
<tr class="read" ng-repeat="n in notificationsRead | orderBy:'created_at':true" ng-if="notificationsRead.length > 0">
<td>
</td>
<td>{{ n.created_at | amDateFormat:'LLL' }}</td>
<td ng-bind-html="n.message.description"></td>
<tr class="read" ng-repeat="n in notificationsRead | orderBy:'created_at':true" ng-if="notificationsRead.length > 0">
<td>
</td>
<td>{{ n.created_at | amDateFormat:'LLL' }}</td>
<td ng-bind-html="n.message.description"></td>
</tr>
</tr>
<tr ng-if="notificationsRead.length == 0">
<td colspan="3" translate>{{ 'no_archived_notifications' }}</td>
</tr>
<tr ng-if="notificationsRead.length == 0">
<td colspan="3" translate>{{ 'no_archived_notifications' }}</td>
</tr>
</tbody>
</table>
</tbody>
</table>
</div>
<a class="btn btn-default" ng-click="addMoreNotificationsReaded()" ng-if="paginateActive" translate>{{ 'load_the_next_notifications' }}</a>
<a class="btn btn-default" ng-click="addMoreNotifications()" ng-if="paginateActive" translate>{{ 'load_the_next_notifications' }}</a>
</div>

View File

@ -61,7 +61,7 @@
</div>
<br ng-show="!plan.plan_file_url"> <!-- TODO Refacto with CSS -->
<a ng-href="{{ plan.plan_file_url }}" ng-show="plan.plan_file_url" target="_blank" translate>{{ 'more_informations' }}</a>
<a ng-href="{{ plan.plan_file_url }}" ng-show="plan.plan_file_url" target="_blank" translate>{{ 'more_information' }}</a>
</div>
</div>
@ -141,7 +141,7 @@
<div class="font-sbold">{{ 'subscription_price' | translate }} {{selectedPlan.amount | currency}}</div>
</div>
<coupon show="!ctrl.member.subscribed_plan" coupon="coupon.applied" user-id="{{ctrl.member.id}}"></coupon>
<coupon show="!ctrl.member.subscribed_plan" coupon="coupon.applied" total="selectedPlan.amount" user-id="{{ctrl.member.id}}"></coupon>
</div>
<div class="widget-footer">

View File

@ -3,6 +3,7 @@
<h3 translate>{{ 'do_you_already_have_an_account' }}</h3>
<p ng-hide="hasDuplicate()" translate>{{ 'do_not_fill_the_form_beside_but_specify_here_the_code_you_ve_received_by_email_to_recover_your_access' }}</p>
<p ng-show="hasDuplicate()" translate>{{ 'just_specify_code_here_to_recover_access' }}</p>
<p class="pull-right"><a href="#" ng-click="resendCode($event)" translate>{{ 'i_did_not_receive_the_code' }}</a></p>
<div class="row">
<div class="col-lg-3 col-lg-offset-1 hidden-md col-sm-3 col-sm-offset-1"></div>
<div class="col-lg-offset-1 col-lg-6 col-md-12 col-sm-offset-1 col-sm-6">

View File

@ -28,7 +28,7 @@
{{ 'you_ve_just_created_a_new_account_on_the_fablab_by_logging_from' | translate:{ GENDER: nameGenre, NAME: fablabName }:"messageformat" }}<br/>
<img class="m-l v-middle" height="16" width="16" src='https://www.google.com/s2/favicons?domain={{activeProvider.domain}}' />
<strong class="v-middle">{{activeProvider.name}} <span ng-if="ssoEmail()">({{ssoEmail()}})</span></strong><br/>
<p class="m-t-md" ng-hide="hasDuplicate()" translate>{{ 'before_letting_you_use_the_application_we_need_some_more_details' }}.</p>
<p class="m-t-md" ng-hide="hasDuplicate()" translate>{{ 'we_need_some_more_details' }}.</p>
<p class="m-t-md" ng-show="hasDuplicate()" translate>{{ 'your_email_is_already_used_by_another_account_on_the_platform' }}</p>
</div>
</section>

View File

@ -0,0 +1,26 @@
<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>{{ 'send_code_again' }}</h1>
</div>
<div class="modal-body">
<form name="emailForm">
<label for="email" class="beforeAmount" translate>{{ 'email_address_associated_with_your_account' }}</label>
<div class="input-group" ng-class="{'has-error': emailForm.email.$dirty && emailForm.email.$invalid }">
<span class="input-group-addon"><i class="fa fa-envelope"></i> </span>
<input class="form-control"
type="email"
id="email"
name="email"
ng-model="object.email"
required>
</div>
<span class="help-block error" ng-show="emailForm['email'].$dirty && emailForm['email'].$error.required" translate>{{'email_is_required'}}</span>
<span class="help-block error" ng-show="emailForm['email'].$dirty && emailForm['email'].$error.email" translate>{{'email_format_is_incorrect'}}</span>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-info" ng-click="ok(object.email)" ng-disabled="emailForm.$invalid" translate>{{ 'confirm' }}</button>
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'cancel' }}</button>
</div>

View File

@ -42,18 +42,22 @@
<label class="col-sm-2 control-label" translate>{{ 'CAD_file' }}</label>
<div class="col-sm-10">
<div ng-repeat="file in project.project_caos_attributes" ng-show="!file._destroy">
<input type="hidden" name="project[project_caos_attributes][][id]" ng-value="file.id" />
<input type="hidden" name="project[project_caos_attributes][][_destroy]" ng-value="file._destroy" />
<div class="col-md-11 m-l-n">
<input type="hidden" name="project[project_caos_attributes][][id]" ng-value="file.id" />
<input type="hidden" name="project[project_caos_attributes][][_destroy]" ng-value="file._destroy" />
<div class="fileinput input-group" data-provides="fileinput" ng-class="fileinputClass(file.attachment)">
<div class="form-control" data-trigger="fileinput">
<i class="glyphicon glyphicon-file fileinput-exists"></i> <span class="fileinput-filename">{{file.attachment}}</span>
<div class="fileinput input-group" data-provides="fileinput" ng-class="fileinputClass(file.attachment)">
<div class="form-control" data-trigger="fileinput">
<i class="glyphicon glyphicon-file fileinput-exists"></i> <span class="fileinput-filename">{{file.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="project[project_caos_attributes][][attachment]"></span>
<a class="input-group-addon btn btn-danger fileinput-exists" data-dismiss="fileinput" ng-click="deleteFile(file)"><i class="fa fa-trash-o"></i></a>
</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="project[project_caos_attributes][][attachment]"></span>
<a class="input-group-addon btn btn-danger fileinput-exists" data-dismiss="fileinput" ng-click="deleteFile(file)"><i class="fa fa-trash-o"></i></a>
</div>
<div class="col-md-1 m-t-xs">
<i class="fa fa-info-circle" aria-hidden="true" uib-tooltip="{{ 'allowed_extensions' | translate }} {{allowedExtensions.join(', ')}}" tooltip-placement="bottom" tooltip-class="media-lg"></i>
</div>
</div>
<a class="btn btn-default" ng-click="addFile()" role="button">{{ 'add_a_new_file' | translate }} <i class="fa fa-file-o fa-fw"></i></a>
</div>
@ -96,15 +100,32 @@
<input type="hidden" name="project[project_steps_attributes][][description]" ng-value="step.description" />
<summernote ng-model="step.description" placeholder="" config="summernoteOpts" name=project[project_steps_attributes][][description]></summernote>
<div class="fileinput" data-provides="fileinput" ng-class="fileinputClass(step.project_step_image)">
<span class="btn btn-default btn-file"><span class="fileinput-new">{{ 'add_a_picture' | translate }} <i class="fa fa-file-image-o m-l-sm" aria-hidden="true"></i></span><span class="fileinput-exists" translate>{{ 'change_the_picture' }}</span>
<input type="file"
name="project[project_steps_attributes][][project_step_image_attributes][attachment]"></span>
<span class="fileinput-filename">{{step.project_step_image}}</span>
<a class="close fileinput-exists" data-dismiss="fileinput" style="float: none"><i class="fa fa-trash-o"></i></a>
<div class="row">
<div ng-repeat-start="image in step.project_step_images_attributes" class="clearfix" ng-if="$index % 3 == 0"></div>
<div class="col-md-4" ng-repeat-end ng-show="!image._destroy">
<input type="hidden" name="project[project_steps_attributes][][project_step_images_attributes][][id]" ng-value="image.id" />
<input type="hidden" name="project[project_steps_attributes][][project_step_images_attributes][][_destroy]" ng-value="image._destroy" />
<div class="fileinput" data-provides="fileinput" ng-class="fileinputClass(image.attachment)" style="width: 100%;">
<div class="fileinput-new thumbnail" style="width: 100%; height: 200px;">
<img src="data:image/png;base64," data-src="holder.js/100%x100%/text:&#xf03e;/font:FontAwesome/icon" bs-holder ng-if="!image.attachment">
</div>
<div class="fileinput-preview fileinput-exists thumbnail" data-trigger="fileinput" style="max-width: 334px;">
<img ng-src="{{ image.attachment_url }}" alt="{{image.attachment}}" />
</div>
<div>
<span class="btn btn-default btn-file"><span class="fileinput-new">{{ 'browse' | translate }} <i class="fa fa-upload fa-fw"></i></span><span class="fileinput-exists" translate>{{ 'change' }}</span>
<input type="file" name="project[project_steps_attributes][][project_step_images_attributes][][attachment]"></span>
<a class="btn btn-danger fileinput-exists" data-dismiss="fileinput" ng-click="deleteProjectStepImage(step, image)" translate>{{ 'delete' }}</a>
</div>
</div>
</div>
</div>
<div>
<a class="btn btn-default" ng-click="addProjectStepImage(step)" role="button">{{ 'add_a_picture' | translate }} <i class="fa fa-file-o fa-fw"></i></a>
</div>
<div>
<div class="m-t">
<a class="btn btn-sm btn-danger" ng-click="deleteStep(step)" role="button"><i class="fa fa-trash-o m-r-xs"></i> {{ 'delete_the_step' | translate }}</a>
</div>
</div>

View File

@ -90,7 +90,7 @@
<div class="card-header-bg" style="background-image: url({{project.project_image}});">
<img src="data:image/png;base64," data-src="holder.js/100%x100%/text:&#xf03e;/font:FontAwesome/icon" bs-holder ng-if="!project.project_image">
</div>
</div>
<div class="card-block">

View File

@ -42,10 +42,11 @@
<div class="col-md-12 m-b-xs">
<h3 class="well well-simple step-title">{{ 'step_N' | translate:{INDEX:step.step_nb} }} : {{step.title}}</h3>
</div>
<div class="col-md-4" ng-if="step.project_step_image">
<a href="{{step.project_step_full_image_url}}" target="_blank"><img class="img-responsive m-b" ng-src="{{step.project_step_image_url}}" alt="{{step.title}}" ></a>
<div ng-repeat-start="image in step.project_step_images_attributes" class="clearfix" ng-if="$index % 3 == 0"></div>
<div class="col-md-4" ng-repeat-end>
<a href="{{image.attachment_full_url}}" target="_blank"><img class="img-responsive m-b" ng-src="{{image.attachment_url}}" alt="{{image.attachment}}" ></a>
</div>
<div class="col-md-8" ng-class="{'col-md-12' : step.project_step_image == undefined}">
<div class="col-md-8" ng-class="{'col-md-12' : step.project_step_images_attributes.length > 1 || step.project_step_images_attributes.length == 0}">
<p ng-bind-html="step.description | toTrusted"></p>
</div>

View File

@ -0,0 +1,167 @@
<div class="widget panel b-a m m-t-lg" ng-if="user && !events.modifiable && !events.moved">
<div class="panel-heading b-b small">
<h3 translate>{{ 'cart.summary' }}</h3>
</div>
<div class="widget-content no-bg auto wrapper" ng-show="events.reserved.length == 0 && (!events.paid || events.paid.length == 0)">
<p class="font-felt fleche-left text-lg"><%= image_tag('fleche-left.png', class: 'fleche-left visible-lg') %>
{{ 'cart.select_one_or_more_slots_in_the_calendar' | translate:{SINGLE:limitToOneSlot}:"messageformat" }}</p>
</div>
<div class="widget-content no-bg auto wrapper" ng-if="events.reserved.length > 0">
<div class="font-sbold m-b-sm " translate>{{ 'cart.you_ve_just_selected_the_slot' }}</div>
<div class="panel panel-default bg-light" ng-repeat="slot in events.reserved">
<div class="panel-body">
<div class="font-sbold text-u-c">{{ 'cart.datetime_to_time' | translate:{START_DATETIME:(slot.start | amDateFormat:'LLLL'), END_TIME:(slot.end | amDateFormat:'LT') } }}</div>
<div class="text-base">{{ 'cart.cost_of_TYPE' | translate:{TYPE:reservableType}:"messageformat" }} <span ng-class="{'text-blue': !slot.promo, 'red': slot.promo}">{{slot.price | currency}}</span></div>
<div ng-show="isAdmin()" class="m-t">
<label for="offerSlot" class="control-label m-r" translate>{{ 'cart.offer_this_slot' }}</label>
<input bs-switch
ng-model="slot.offered"
id="offerSlot"
type="checkbox"
class="form-control"
switch-on-text="{{ 'yes' | translate}}"
switch-off-text="{{ 'no' | translate}}"
switch-animate="true"
switch-readonly="{{slot.isValid}}"/>
</div>
</div>
<div>
<button class="btn btn-valid btn-warning btn-block text-u-c r-b" ng-click="validateSlot(slot)" ng-if="!slot.isValid" translate>{{ 'cart.confirm_this_slot' }}</button>
</div>
<div class="clear"><a class="pull-right m-b-sm text-u-l ng-scope m-r-sm" href="#" ng-click="removeSlot(slot, $index, $event)" ng-if="slot.isValid" translate>{{ 'cart.remove_this_slot' }}</a></div>
</div>
<coupon show="isSlotsValid() && (!modePlans || selectedPlan)" coupon="coupon.applied" total="totalNoCoupon" user-id="{{user.id}}"></coupon>
<div ng-hide="fablabWithoutPlans">
<div ng-if="isSlotsValid() && !user.subscribed_plan" ng-show="!modePlans">
<p class="font-sbold text-base l-h-2x" translate>{{ 'cart.to_benefit_from_attractive_prices' }}</p>
<div><button class="btn btn-warning-full rounded btn-block text-xs" ng-click="showPlans()" translate>{{ 'cart.view_our_subscriptions' }}</button></div>
<p class="font-bold text-base text-u-c text-center m-b-xs" translate>{{ 'cart.or' }}</p>
</div>
<div ng-if="selectedPlan">
<div class="m-t-md m-b-sm text-base">{{ 'cart.you_ve_just_selected_a_' | translate }} <br> <span class="font-sbold" translate>{{ 'cart._subscription' }}</span> :</div>
<div class="panel panel-default bg-light m-n">
<div class="panel-body m-b-md">
<div class="font-sbold text-u-c">{{selectedPlan | humanReadablePlanName }}</div>
<div class="text-base">{{ 'cart.cost_of_the_subscription' | translate }} <span class="text-blue">{{selectedPlan.amount | currency}}</span></div>
</div>
</div>
</div>
</div>
</div>
<div class="panel-footer no-padder" ng-if="events.reserved.length > 0">
<button class="btn btn-valid btn-info btn-block p-l btn-lg text-u-c r-b text-base" ng-click="payCart()" ng-if="isSlotsValid() && (!modePlans || selectedPlan)">{{ 'cart.confirm_and_pay' | translate }} {{amountTotal | currency}}</button>
</div>
<div class="widget-content no-bg auto wrapper" ng-show="events.paid && events.paid.length > 0">
{{ 'cart.you_have_settled_the_following_TYPE' | translate:{TYPE:reservableType}:"messageformat" }} <strong>{{reservableName}}</strong>:
<div class="well well-warning m-t-sm" ng-repeat="paidSlot in events.paid">
<i class="font-sbold text-u-c">{{ 'cart.datetime_to_time' | translate:{START_DATETIME:(paidSlot.start | amDateFormat:'LLLL'), END_TIME:(paidSlot.end | amDateFormat:'LT') } }}</i>
<div class="font-sbold">{{ 'cart.cost_of_TYPE' | translate:{TYPE:reservableType}:"messageformat" }} {{paidSlot.price | currency}}</div>
</div>
<div ng-if="selectedPlan">
<div class="m-t-md m-b-sm text-base">{{ 'cart.you_have_settled_a_' | translate }} <br> <span class="font-sbold" translate>{{ 'cart._subscription' }}</span> :</div>
<div class="well well-warning m-t-sm">
<i class="font-sbold text-u-c">{{selectedPlan | humanReadablePlanName }}</i>
<div class="font-sbold">{{ 'cart.cost_of_the_subscription' | translate }} {{selectedPlan.amount | currency}}</div>
</div>
</div>
<div class="m-t-md font-sbold">{{ 'cart.total_' | translate }} {{amountTotal | currency}}</div>
<div class="alert alert-success" ng-if="user.subscribed_plan">{{ 'cart.thank_you_your_payment_has_been_successfully_registered' | translate }}<br>
{{ 'cart.your_invoice_will_be_available_soon_from_your_' | translate }} <a ui-sref="app.logged.dashboard.invoices" translate>{{ 'cart.dashboard' }}</a>
</div>
</div>
</div>
<div class="widget panel b-a m m-t-lg" ng-if="events.modifiable || events.moved">
<div class="panel-heading b-b small">
<h3 translate>{{ 'cart.summary' }}</h3>
</div>
<div class="widget-content no-bg auto wrapper" ng-if="events.modifiable">
<div class="font-sbold m-b-sm " translate>{{ 'cart.i_want_to_change_the_following_reservation' }}</div>
<div class="panel panel-warning bg-yellow">
<div class="panel-body">
<div class="font-sbold text-u-c">{{ 'cart.datetime_to_time' | translate:{START_DATETIME:(events.modifiable.start | amDateFormat:'LLLL'), END_TIME:(events.modifiable.end | amDateFormat:'LT') } }}</div>
</div>
<div class="clear"><a class="pull-right m-b-sm text-u-l ng-scope m-r-sm" href="#" ng-click="cancelModifySlot($event)" translate>{{ 'cart.cancel_my_modification' }}</a></div>
</div>
<div class="widget-content no-bg">
<p class="font-felt fleche-left text-lg"><%= image_tag('fleche-left.png', class: 'fleche-left visible-lg') %>
{{ 'cart.select_a_new_slot_in_the_calendar' | translate }}</p>
</div>
<div class="panel panel-info bg-info text-white" ng-if="events.placable">
<div class="panel-body">
<div class="font-sbold text-u-c">{{ 'cart.datetime_to_time' | translate:{START_DATETIME:(events.placable.start | amDateFormat:'LLLL'), END_TIME:(events.placable.end | amDateFormat:'LT') } }}</div>
</div>
<div class="clear"><a class="pull-right m-b-sm text-u-l ng-scope m-r-sm" href="#" ng-click="removeSlotToPlace($event)" translate>{{ 'cart.cancel_my_selection' }}</a></div>
</div>
<div ng-if="events.placable && (events.modifiable.tags.length > 0 || events.placable.tags.length > 0)" ng-class="{'panel panel-danger bg-red': tagMissmatch()}">
<div class="panel-body">
<div id="fromTags">
{{ 'cart.tags_of_the_original_slot' | translate }}<br/>
<span ng-repeat="tag in events.modifiable.tags">
<span class='label label-success text-white' title="{{tag.name}}">{{tag.name}}</span>
</span>
<span ng-show="events.modifiable.tags.length == 0">
<span class='label label-warning text-white' title="{{ 'cart.none' | translate }}" translate>{{ 'cart.none' }}</span>
</span>
</div><br/>
<div id="toTags">
{{ 'cart.tags_of_the_destination_slot' | translate }}<br/>
<span ng-repeat="tag in events.placable.tags">
<span class='label label-success text-white' title="{{tag.name}}">{{tag.name}}</span>
</span>
<span ng-show="events.placable.tags.length == 0">
<span class='label label-warning text-white' title="{{ 'cart.none' | translate }}" translate>{{ 'cart.none' }}</span>
</span>
</div>
</div>
</div>
</div>
<div class="panel-footer no-padder" ng-if="events.modifiable && events.placable">
<button class="btn btn-invalid btn-default btn-block p-l btn-lg text-u-c r-n text-base" ng-click="cancelModifySlot()" translate>{{ 'cancel' }}</button>
<div>
<button class="btn btn-valid btn-info btn-block p-l btn-lg text-u-c r-b text-base" ng-click="modifySlot()" translate>{{ 'cart.confirm_my_modification' }}</button>
</div>
</div>
<div class="widget-content no-bg auto wrapper" ng-show="events.moved">
<div class="font-sbold m-b-sm " translate>{{ 'cart.your_booking_slot_was_successfully_moved_from_' }}</div>
<div class="panel panel-default bg-light">
<div class="panel-body">
<div class="font-sbold text-u-c">{{ 'cart.datetime_to_time' | translate:{START_DATETIME:(events.moved.oldSlot.start | amDateFormat:'LLLL'), END_TIME:(events.moved.oldSlot.end | amDateFormat:'LT') } }}</div>
</div>
</div>
<p class="text-center font-bold m-b-sm text-u-c" translate>{{ 'cart.to_date' }}</p>
<div class="panel panel-success bg-success bg-light">
<div class="panel-body">
<div class="font-sbold text-u-c">{{ 'cart.datetime_to_time' | translate:{START_DATETIME:(events.moved.newSlot.start | amDateFormat:'LLLL'), END_TIME:(events.moved.newSlot.end | amDateFormat:'LT') } }}</div>
</div>
</div>
</div>
</div>

View File

@ -3,7 +3,7 @@
<div ng-show="code.input">
<label for="coupon_code" translate>{{ 'code_' }}</label>
<div class="input-group">
<div class="input-group m-b">
<input type="text"
class="form-control"
name="coupon_code"
@ -17,5 +17,7 @@
<i class="fa fa-check" ng-show="status == 'valid'"></i>
</span>
</div>
<uib-alert ng-repeat="msg in messages" type="{{msg.type}}" close="closeMessage($index)">{{msg.message}}</uib-alert>
</div>
</div>

View File

@ -55,13 +55,16 @@
ng-disabled="preventField['profile.gender'] && user.profile.gender && !userForm['user[profile_attributes][gender]'].$dirty"/>
<i class="fa fa-female m-l-sm"></i> {{ 'woman' | translate }}
</label>
<span class="help-block" ng-show="userForm['user[profile_attributes][gender]'].$dirty && userForm['user[profile_attributes][gender]'].$error.required" translate>{{ 'gender_is_required' }}</span>
<span class="exponent m-l-xs"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
<span class="help-block" ng-show="userForm['user[profile_attributes][gender]'].$dirty && userForm['user[profile_attributes][gender]'].$error.required" translate>{{ 'gender_is_required' }}</span>
</div>
<div class="form-group" ng-class="{'has-error': userForm['user[username]'].$dirty && userForm['user[username]'].$invalid}">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-user"></i></span>
<span class="input-group-addon"><i class="fa fa-user"></i> <span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
</span>
<input type="text"
name="user[username]"
ng-model="user.username"
@ -78,7 +81,7 @@
<div class="form-group" ng-class="{'has-error': userForm['user[profile_attributes][last_name]'].$dirty && userForm['user[profile_attributes][last_name]'].$invalid}">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-user"></i></span>
<span class="input-group-addon"><i class="fa fa-user"></i> <span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span></span>
<input type="text"
name="user[profile_attributes][last_name]"
ng-model="user.profile.last_name"
@ -93,7 +96,7 @@
<div class="form-group" ng-class="{'has-error': userForm['user[profile_attributes][first_name]'].$dirty && userForm['user[profile_attributes][first_name]'].$invalid}">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-user"></i></span>
<span class="input-group-addon"><i class="fa fa-user"></i> <span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span></span>
<input type="text"
name="user[profile_attributes][first_name]"
ng-model="user.profile.first_name"
@ -108,7 +111,7 @@
<div class="form-group" ng-class="{'has-error': userForm['user[email]'].$dirty && userForm['user[email]'].$invalid}">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-envelope"></i> </span>
<span class="input-group-addon"><i class="fa fa-envelope"></i> <span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span></span>
<input type="email"
name="user[email]"
ng-model="user.email"
@ -130,7 +133,7 @@
<div class="form-group" ng-class="{'has-error': userForm['user[password]'].$dirty && userForm['user[password]'].$invalid}" ng-if="password.change">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-key"></i> </span>
<span class="input-group-addon"><i class="fa fa-key"></i> <span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span></span>
<input type="password"
name="user[password]"
ng-model="user.password"
@ -146,7 +149,7 @@
<div class="form-group" ng-class="{'has-error': userForm['user[password_confirmation]'].$dirty && userForm['user[password_confirmation]'].$invalid}" ng-if="password.change">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-key"></i> </span>
<span class="input-group-addon"><i class="fa fa-key"></i> <span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span></span>
<input type="password"
name="user[password_confirmation]"
ng-model="user.password_confirmation"
@ -164,7 +167,7 @@
<div class="form-group" ng-if="user.profile.organization" ng-class="{'has-error': userForm['user[profile_attributes][organization_attributes][name]'].$dirty && userForm['user[profile_attributes][organization_attributes][name]'].$invalid}">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-building-o"></i></span>
<span class="input-group-addon"><i class="fa fa-building-o"></i> <span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span></span>
<input type="hidden"
name="user[profile_attributes][organization_attributes][id]"
ng-value="user.profile.organization.id" />
@ -181,7 +184,7 @@
<div class="form-group" ng-if="user.profile.organization" ng-class="{'has-error': userForm['user[profile_attributes][organization_attributes][address_attributes][address]'].$dirty && userForm['user[profile_attributes][organization_attributes][address_attributes][address]'].$invalid}">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-map-marker"></i></span>
<span class="input-group-addon"><i class="fa fa-map-marker"></i> <span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span></span>
<input type="hidden"
name="user[profile_attributes][organization_attributes][address_attributes][id]"
ng-value="user.profile.organization.address.id" />
@ -198,7 +201,7 @@
<div class="form-group" ng-class="{'has-error': userForm['user[profile_attributes][birthday]'].$dirty && userForm['user[profile_attributes][birthday]'].$invalid}">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-calendar-o"></i> </span>
<span class="input-group-addon"><i class="fa fa-calendar-o"></i> <span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span></span>
<input type="text"
id="user_birthday"
class="form-control"
@ -235,7 +238,7 @@
<div class="form-group" ng-class="{'has-error': userForm['user[profile_attributes][phone]'].$dirty && userForm['user[profile_attributes][phone]'].$invalid}">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-phone"></i> </span>
<span class="input-group-addon"><i class="fa fa-phone"></i> <span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span></span>
<input type="text"
name="user[profile_attributes][phone]"
ng-model="user.profile.phone"
@ -313,7 +316,7 @@
<!-- allow receive newsletter -->
<div class="form-group">
<label for="allowNewsletter" translate>{{ 'i_accept_to_receive_informations_from_the_fablab' }}</label>
<label for="allowNewsletter" translate>{{ 'i_accept_to_receive_information_from_the_fablab' }}</label>
<input bs-switch
ng-model="user.is_allow_newsletter"
id="allowNewsletter"

View File

@ -3,7 +3,7 @@
<h1>{{object.title}}</h1>
</div>
<div class="modal-body">
<p>{{object.msg}}</p>
<p ng-bind-html="object.msg"></p>
</div>
<div class="modal-footer">
<button class="btn btn-info" ng-click="ok()" translate>{{ 'confirm' }}</button>

View File

@ -23,7 +23,7 @@
<!-- Top Nav -->
<ul class="nav navbar-nav navbar-right m-n hidden-xs nav-user user">
<li class="notification-open" ng-if="isAuthenticated()">
<a ui-sref="app.logged.notifications"><i class="fa fa-bell fa-2x black"></i> <span class="badge" ng-class="{'bg-red': notifications.length > 0}">{{notifications.length}}</span></a>
<a ui-sref="app.logged.notifications"><i class="fa fa-bell fa-2x black"></i> <span class="badge" ng-class="{'bg-red': notifications.unread > 0}">{{notifications.unread}}</span></a>
</li>
<li class="dropdown user-profile-nav" ng-if="isAuthenticated()" uib-dropdown>
<a href="#" class="dropdown-toggle" uib-dropdown-toggle>

View File

@ -217,7 +217,7 @@
id="is_allow_newsletter"
ng-model="user.is_allow_newsletter"
value="true"/>
<label for="is_allow_newsletter" translate>{{ 'i_accept_to_receive_informations_from_the_fablab' }}</label>
<label for="is_allow_newsletter" translate>{{ 'i_accept_to_receive_information_from_the_fablab' }}</label>
</div>
</div>

View File

@ -0,0 +1,109 @@
<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': spaceForm['space[name]'].$dirty && spaceForm['space[name]'].$invalid}">
<label for="space_name" class="col-sm-2 control-label">{{ 'space.name' | translate }} *</label>
<div class="col-sm-4">
<input ng-model="space.name"
type="text"
name="space[name]"
class="form-control"
id="space_name"
placeholder="{{'space.name' | translate}}"
required>
<span class="help-block" ng-show="spaceForm['space[name]'].$dirty && spaceForm['space[name]'].$error.required" translate>{{ 'space.name_is_required' }}</span>
</div>
</div>
<div class="form-group m-b-lg">
<label for="space_image" class="col-sm-2 control-label">{{ 'space.illustration' | translate }} *</label>
<div class="col-sm-10">
<div class="fileinput" data-provides="fileinput" ng-class="fileinputClass(space.space_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="!space.space_image">
</div>
<div class="fileinput-preview fileinput-exists thumbnail" style="max-width: 334px;">
<img ng-src="{{ space.space_image }}" alt="" />
</div>
<div>
<span class="btn btn-default btn-file">
<span class="fileinput-new">{{ 'space.add_an_illustration' | translate }} <i class="fa fa-upload fa-fw"></i></span>
<span class="fileinput-exists" translate>{{ 'change' }}</span>
<input type="file"
id="space_image"
ng-model="space.space_image"
name="space[space_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': spaceForm['space[default_places]'].$dirty && spaceForm['space[default_places]'].$invalid}">
<label for="default_places" class="col-sm-2 control-label">{{ 'space.default_places' | translate }} *</label>
<div class="col-sm-10">
<input type="number"
name="space[default_places]"
ng-model="space.default_places"
id="default_places"
class="form-control"
required>
<span class="help-block" ng-show="spaceForm['space[default_places]'].$dirty && spaceForm['space[default_places]'].$error.required" translate>{{ 'space.default_places_is_required' }}</span>
</div>
</div>
<div class="form-group m-b-xl">
<label for="space_description" class="col-sm-2 control-label" translate>{{ 'space.description' }}</label>
<div class="col-sm-10">
<input type="hidden"
name="space[description]"
ng-value="space.description" />
<summernote ng-model="space.description"
id="space_description"
placeholder=""
config="summernoteOpts"
name="space[description]">
</summernote>
</div>
</div>
<div class="form-group m-b-xl">
<label for="space_characteristics" class="col-sm-2 control-label" translate>{{ 'space.characteristics' }}</label>
<div class="col-sm-10">
<input type="hidden"
name="space[characteristics]"
ng-value="space.characteristics" />
<summernote ng-model="space.characteristics"
id="space_characteristics"
placeholder=""
config="summernoteOpts"
name="space[characteristics]">
</summernote>
</div>
</div>
<div class="form-group m-b-xl">
<label class="col-sm-2 control-label" translate>{{ 'space.attached_files_(pdf)' }}</label>
<div class="col-sm-10">
<div ng-repeat="file in space.space_files_attributes" ng-show="!file._destroy">
<input type="hidden" ng-model="file.id" name="space[space_files_attributes][][id]" ng-value="file.id" />
<input type="hidden" ng-model="file._destroy" name="space[space_files_attributes][][_destroy]" ng-value="file._destroy"/>
<div class="fileinput input-group" data-provides="fileinput" ng-class="fileinputClass(file.attachment)">
<div class="form-control" data-trigger="fileinput">
<i class="glyphicon glyphicon-file fileinput-exists"></i> <span class="fileinput-filename">{{file.attachment}}</span>
</div>
<span class="input-group-addon btn btn-default btn-file"><span class="fileinput-new" translate>{{ 'space.attach_a_file' }}</span>
<span class="fileinput-exists" translate>{{ 'change' }}</span><input type="file" name="space[space_files_attributes][][attachment]" accept=".pdf"></span>
<a class="input-group-addon btn btn-danger fileinput-exists" data-dismiss="fileinput" ng-click="deleteFile(file)"><i class="fa fa-trash-o"></i></a>
</div>
</div>
<a class="btn btn-default" ng-click="addFile()" role="button"> {{ 'space.add_an_attachment' | translate }} <i class="fa fa-file-o fa-fw"></i></a>
</div>
</div>

View File

@ -0,0 +1,50 @@
<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="backPrevLocation($event)"><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 translate-values="{NAME: space.name}">{{ 'space_edit.edit_the_space_NAME' }}</h1>
</section>
</div>
</div>
</section>
<div class="row no-gutter" >
<div class="col-md-9 b-r nopadding">
<form role="form"
name="spaceForm"
class="form-horizontal"
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">
<ng-include src="'<%= asset_path 'spaces/_form.html' %>'"></ng-include>
</div>
<div class="panel-footer no-padder">
<input type="submit"
value="{{ 'space_edit.validate_the_changes' | translate }}"
class="r-b btn-valid btn btn-warning btn-block p-lg btn-lg text-u-c"
ng-disabled="spaceForm.$invalid"/>
</div>
</section>
</form>
</div>
<div class="col-md-3"/>
</div>

View File

@ -0,0 +1,63 @@
<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">
<section class="heading-title">
<h1 translate>{{ 'the_spaces' }}</h1>
</section>
</div>
<div class="col-xs-12 col-sm-12 col-md-3 b-t hide-b-md" ng-if="isAuthorized('admin')">
<section class="heading-actions wrapper">
<a class="btn btn-lg btn-warning bg-white b-2x rounded m-t-xs" ui-sref="app.admin.space_new" role="button" translate>{{ 'add_a_space' }}</a>
</section>
</div>
</div>
</section>
<section class="m-lg">
<div class="row" ng-repeat="space in (spaces.length/3 | array)">
<div class="col-xs-12 col-sm-6 col-md-4" ng-repeat="space in spaces.slice(3*$index, 3*$index + 3)">
<div class="widget panel panel-default">
<div class="panel-heading picture" ng-if="!space.space_image" ng-click="showMachine(space)">
<img src="data:image/png;base64," data-src="holder.js/100%x100%/text:&#xf03e;/font:FontAwesome/icon" bs-holder class="img-responsive">
</div>
<div class="panel-heading picture" style="background-image:url({{space.space_image}})" ng-if="space.space_image" ng-click="showMachine(space)">
</div>
<div class="panel-body">
<h1 class="text-center m-b">{{space.name}}</h1>
</div>
<div class="panel-footer no-padder">
<div class="text-center clearfix">
<div class="col-sm-6 b-r no-padder">
<div class="btn btn-default btn-block no-b padder-v red" ng-click="reserveSpace(space, $event)">
<i class="fa fa-bookmark"></i> {{ 'book' | translate }}
</div>
</div>
<div class="col-sm-6 no-padder">
<div class="btn btn-default btn-block padder-v no-b red" ng-click="showSpace(space)">
<i class="fa fa-eye"></i> {{ 'consult' | translate }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>

View File

@ -0,0 +1,57 @@
<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="backPrevLocation($event)"><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>{{ 'space_new.add_a_new_space' }}</h1>
</section>
</div>
</div>
</section>
<div class="row no-gutter" >
<div class="col-md-9 b-r nopadding">
<div class="m-lg alert alert-warning" role="alert">
{{ 'space_new.watch_out_when_creating_a_new_space_its_prices_are_initialized_at_0_for_all_subscriptions' | translate}}
{{ 'space_new.consider_changing_its_prices_before_creating_any_reservation_slot' | translate }}
</div>
<form role="form"
name="spaceForm"
class="form-horizontal"
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">
<ng-include src="'<%= asset_path 'spaces/_form.html' %>'"></ng-include>
</div>
<div class="panel-footer no-padder">
<input type="submit"
value="{{ 'space_new.add_this_space' | translate }}"
class="r-b btn-valid btn btn-warning btn-block p-lg btn-lg text-u-c"
ng-disabled="spaceForm.$invalid"/>
</div>
</section>
</form>
</div>
<div class="col-md-3"/>
</div>

View File

@ -0,0 +1,59 @@
<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">
<section class="heading-title">
<h1 translate translate-values="{NAME:space.name}">{{ 'space_reserve.planning_of_space_NAME' }}</h1>
</section>
</div>
</div>
</section>
<div class="row no-gutter training-reserve">
<div class="col-sm-12 col-md-12 col-lg-9">
<div ui-calendar="calendarConfig" ng-model="eventSources" calendar="calendar" class="wrapper-lg" ng-show="!plansAreShown"></div>
<ng-include ng-if="!fablabWithoutPlans" src="'<%= asset_path 'plans/_plan.html' %>'"></ng-include>
</div>
<div class="col-sm-12 col-md-12 col-lg-3">
<div ng-if="currentUser.role === 'admin'">
<select-member></select-member>
</div>
<cart slot="selectedEvent"
slot-selection-time="selectionTime"
events="events"
user="ctrl.member"
mode-plans="plansAreShown"
plan="selectedPlan"
plan-selection-time="planSelectionTime"
settings="settings"
on-slot-added-to-cart="markSlotAsAdded"
on-slot-removed-from-cart="markSlotAsRemoved"
on-slot-start-to-modify="markSlotAsModifying"
on-slot-modify-success="modifyTrainingSlot"
on-slot-modify-cancel="cancelModifyTrainingSlot"
on-slot-modify-unselect="changeModifyTrainingSlot"
on-slot-cancel-success="slotCancelled"
after-payment="afterPayment"
reservable-id="{{space.id}}"
reservable-type="Space"
reservable-name="{{space.name}}"></cart>
<uib-alert type="warning m" ng-show="spaceExplicationsAlert">
<p class="text-sm pull-left">
<i class="fa fa-warning"></i>
<div class="m-l-lg" ng-bind-html="spaceExplicationsAlert"></div>
</p>
</uib-alert>
</div>
</div>

View File

@ -0,0 +1,85 @@
<div>
<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-7 b-l b-r-md">
<section class="heading-title">
<h1>{{ space.name }}</h1>
</section>
</div>
<div class="col-xs-12 col-sm-12 col-md-4 b-t hide-b-md">
<section class="heading-actions wrapper">
<a ng-click="reserveSpace($event)" class="btn btn-lg btn-warning bg-white b-2x rounded m-t-xs" ng-if="!isAuthorized('admin')" translate>{{ 'space_show.book_this_space' }}</a>
<a ui-sref="app.admin.space_edit({id:space.slug})" ng-if="isAuthorized('admin')" class="btn btn-lg btn-warning bg-white b-2x rounded m-t-xs"><i class="fa fa-edit"></i> {{ 'edit' | translate }}</a>
<a ng-click="deleteSpace($event)" ng-if="isAuthorized('admin')" class="btn btn-lg btn-danger b-2x rounded no-b m-t-xs"><i class="fa fa-trash-o"></i></a>
</section>
</div>
</div>
</section>
<div class="row no-gutter">
<div class="col-sm-12 col-md-12 col-lg-8 b-r-lg">
<div class="article wrapper-lg">
<div class="article-thumbnail" ng-if="space.space_image">
<img ng-src="{{space.space_image}}" alt="{{space.name}}" class="img-responsive">
</div>
<p class="intro" ng-bind-html="space.description | toTrusted"></p>
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-4">
<div class="widget panel b-a m m-t-lg">
<div class="panel-heading b-b small">
<h3 translate>{{ 'space_show.characteristics' }}</h3>
</div>
<div class="widget-content no-bg wrapper">
<h3></h3>
<p ng-bind-html="space.characteristics | toTrusted"></p>
</div>
</div>
<section class="widget panel b-a m" ng-if="space.space_files_attributes">
<div class="panel-heading b-b">
<span class="badge bg-warning pull-right">{{space.space_files_attributes.length}}</span>
<h3 translate>{{ 'space_show.files_to_download' }}</h3>
</div>
<ul class="widget-content list-group list-group-lg no-bg auto">
<li ng-repeat="file in space.space_files_attributes" class="list-group-item no-b clearfix">
<a target="_blank" ng-href="{{file.attachment_url}}"><i class="fa fa-arrow-circle-o-down"> </i> {{file.attachment | humanize : 25}}</a>
</li>
</ul>
</section>
<section class="widget panel b-a m" ng-if="space.space_projects">
<div class="panel-heading b-b">
<h3 translate>{{ 'space_show.projects_using_the_space' }}</h3>
</div>
<ul class="widget-content list-group list-group-lg no-bg auto">
<li ng-repeat="project in space.space_projects" class="list-group-item no-b clearfix">
<a ui-sref="app.public.projects_show({id:project.slug})"><i class="fa"> </i> {{project.name}}</a>
</li>
</ul>
</section>
</div>
</div>
</div>

View File

@ -7,15 +7,15 @@
</div>
<div class="col-xs-10 col-sm-10 col-md-8 b-l b-r-md">
<section class="heading-title">
<h1 ng-hide="training" translate>{{ 'trainings_planning' }}</h1>
<h1 ng-show="training"><span translate>{{ 'planning_of' }}</span> {{training.name}}</h1>
<h1 ng-show="mode == 'all'" translate>{{ 'trainings_planning' }}</h1>
<h1 ng-hide="mode == 'all'"><span translate>{{ 'planning_of' }}</span> {{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">
<a class="btn btn-lg btn-warning bg-white b-2x rounded m-t-xs"
ui-sref="app.logged.trainings_reserve({id:'all'})"
ng-show="training"
ng-hide="mode == 'all'"
role="button"
translate>{{ 'all_trainings' }}</a>
</section>
@ -33,165 +33,34 @@
<div class="col-sm-12 col-md-12 col-lg-3">
<div class="text-center m-t">
</div>
<div ng-if="currentUser.role === 'admin'">
<select-member></select-member>
</div>
<div class="widget panel b-a m m-t-lg" ng-if="ctrl.member && !slotToModify && !modifiedSlots">
<div class="panel-heading b-b small">
<h3 translate>{{ 'summary' }}</h3>
</div>
<div class="widget-content no-bg auto wrapper" ng-show="!selectedTraining && !paidTraining">
<p class="font-felt fleche-left text-lg"><%= image_tag("fleche-left.png", class: 'fleche-left visible-lg') %>
{{ 'select_a_slot_in_the_calendar' | translate }}</p>
</div>
<div class="widget-content no-bg auto wrapper" ng-if="selectedTraining">
<div class="font-sbold m-b-sm " translate>{{ 'you_ve_just_selected_the_slot' }}</div>
<cart slot="selectedEvent"
slot-selection-time="selectionTime"
events="events"
user="ctrl.member"
mode-plans="plansAreShown"
plan="selectedPlan"
plan-selection-time="planSelectionTime"
settings="settings"
on-slot-added-to-cart="markSlotAsAdded"
on-slot-removed-from-cart="markSlotAsRemoved"
on-slot-start-to-modify="markSlotAsModifying"
on-slot-modify-success="modifyTrainingSlot"
on-slot-modify-cancel="cancelModifyTrainingSlot"
on-slot-modify-unselect="changeModifyTrainingSlot"
on-slot-cancel-success="slotCancelled"
after-payment="afterPayment"
reservable-id="{{training.id}}"
reservable-type="Training"
reservable-name="{{training.name}}"
limit-to-one-slot="true"></cart>
<div class="panel panel-default bg-light m-n">
<div class="panel-body">
<div class="font-sbold text-u-c">{{ 'datetime_to_time' | translate:{START_DATETIME:(selectedTraining.start | amDateFormat:'LLLL'), END_TIME:(selectedTraining.end | amDateFormat:'LT')} }}</div>
<div class="text-base">{{ 'training_cost_' | translate }} <span ng-class="{'text-blue': selectedTraining.training.amount == selectedTrainingAmount, 'red': selectedTraining.training.amount != selectedTrainingAmount}">{{selectedTrainingAmount | currency}}</span></div>
<div ng-show="currentUser.role == 'admin'" class="m-t">
<label for="offerSlot" class="control-label m-r" translate>{{ 'offer_this_training' }}</label>
<input bs-switch
ng-model="selectedTraining.offered"
id="offerSlot"
type="checkbox"
class="form-control"
switch-on-text="{{ 'yes' | translate }}"
switch-off-text="{{ 'no' | translate }}"
switch-animate="true"
switch-readonly="{{trainingIsValid}}"
ng-change="updatePrices()"/>
</div>
</div>
<div class="panel-footer no-padder">
<button class="btn btn-valid btn-warning btn-block text-u-c r-b" ng-click="validTraining()" ng-if="!trainingIsValid" translate>{{ 'confirm_this_slot' }}</button>
</div>
</div>
<div class="clear">
<a class="pull-right m-t-xs text-u-l" href="#" ng-click="removeTraining($event)" ng-if="trainingIsValid" translate>{{ 'remove_this_slot' }}</a>
</div>
<coupon show="trainingIsValid && (!plansIsShow || selectedPlan)" coupon="coupon.applied" has-select-slot="trainingIsValid" user-id="{{ctrl.member.id}}"></coupon>
<span ng-hide="fablabWithoutPlans">
<div ng-if="trainingIsValid && !ctrl.member.subscribed_plan" ng-show="!plansIsShow">
<p class="font-sbold text-base l-h-2x" translate>{{ 'to_benefit_from_attractive_prices_and_a_free_training' }}</p>
<div><button class="btn btn-warning-full rounded btn-block text-xs" ng-click="showPlans()" translate>{{ 'view_our_subscriptions' }}</button></div>
<p class="font-bold text-base text-u-c text-center m-b-xs">ou</p>
</div>
<div ng-if="selectedPlan">
<div class="m-t-md m-b-sm text-base">{{ 'you_ve_just_selected_a_' | translate }} <br> <span class="font-sbold" translate>{{ '_subscription' }}</span> :</div>
<div class="panel panel-default bg-light m-n">
<div class="panel-body m-b-md">
<div class="font-sbold text-u-c">{{ selectedPlan | humanReadablePlanName }}</div>
<div class="text-base">{{ 'subscription_cost' | translate }} <span class="text-blue">{{selectedPlan.amount | currency}}</span></div>
</div>
</div>
</div>
</span>
</div>
<div class="panel-footer no-padder" ng-if="selectedTraining">
<button class="btn btn-valid btn-info btn-block p-l btn-lg text-u-c r-b text-base" ng-click="payTraining()" ng-if="trainingIsValid && (!plansIsShow || selectedPlan)">{{ 'confirm_and_pay' | translate }} {{amountTotal | currency}}</button>
</div>
<div class="widget-content no-bg auto wrapper" ng-if="paidTraining">
<div class="m-t-md m-b-sm text-base">{{ 'you_have_settled_the_training' | translate }} <br> <span class="font-sbold">{{paidTraining.training.name}}</span> :
</div>
<div class="well well-warning m-t-sm">
<i class="font-sbold text-u-c">{{ 'datetime_to_time' | translate:{START_DATETIME:(paidTraining.start | amDateFormat:'LLLL'), END_TIME:(paidTraining.end | amDateFormat:'LT') } }} </i>
<div class="font-sbold">{{ 'training_cost_' | translate }} {{paidTraining.training.amount | currency}}</div>
</div>
<div ng-if="selectedPlan">
<div class="m-t-md m-b-sm text-base">{{ 'you_have_settled_a_' | translate }} <br> <span class="font-sbold" translate>{{ '_subscription' }}</span> :</div>
<div class="well well-warning m-t-sm">
<i class="font-sbold text-u-c">{{selectedPlan | humanReadablePlanName }}</i>
<div class="font-sbold">{{ 'subscription_cost' | translate }} {{selectedPlan.amount | currency}}</div>
</div>
</div>
<div class="m-t-md font-sbold">{{ 'total_' | translate }} {{amountTotal | currency}}</div>
<div class="alert alert-success" ng-if="ctrl.member.subscribed_plan">{{ 'thank_you_your_payment_has_been_successfully_registered' | translate }}<br>
{{ 'your_invoice_will_be_available_soon_from_your_' | translate }} <a ui-sref="app.logged.dashboard.invoices" translate>{{ 'dashboard' }}</a>
</div>
</div>
</div>
<div class="widget panel b-a m m-t-lg" ng-if="slotToModify || modifiedSlots">
<div class="panel-heading b-b small">
<h3 translate>{{ 'summary' }}</h3>
</div>
<div class="widget-content no-bg auto wrapper" ng-if="slotToModify">
<div class="font-sbold m-b-sm " translate>{{ 'i_want_to_change_the_following_reservation' }}</div>
<div class="panel panel-warning bg-yellow">
<div class="panel-body">
<div class="font-sbold text-u-c">{{ 'datetime_to_time' | translate:{START_DATETIME:(slotToModify.start | amDateFormat:'LLLL'), END_TIME:(slotToModify.end | amDateFormat:'LT') } }}</div>
</div>
<div class="clear"><a class="pull-right m-b-sm text-u-l ng-scope m-r-sm" href="#" ng-click="removeSlotToModify($event)" translate>{{ 'cancel_my_modification' }}</a></div>
</div>
<div class="widget-content no-bg">
<p class="font-felt fleche-left text-lg"><%= image_tag("fleche-left.png", class: 'fleche-left visible-lg') %>
{{ 'select_a_new_slot_in_the_calendar' | translate }}</p>
</div>
<div class="panel panel-info bg-info text-white" ng-if="slotToPlace">
<div class="panel-body">
<div class="font-sbold text-u-c">{{ 'datetime_to_time' | translate:{START_DATETIME:(slotToPlace.start | amDateFormat:'LLLL'), END_TIME:(slotToPlace.end | amDateFormat:'LT') } }}</div>
</div>
<div class="clear"><a class="pull-right m-b-sm text-u-l ng-scope m-r-sm" href="#" ng-click="removeSlotToPlace($event)" translate>{{ 'cancel_my_selection' }}</a></div>
</div>
</div>
<div class="panel-footer no-padder" ng-if="slotToModify && slotToPlace">
<button class="btn btn-invalid btn-default btn-block p-l btn-lg text-u-c r-n text-base" ng-click="cancelModifyMachineSlot()" translate>{{ 'cancel' }}</button>
<div>
<button class="btn btn-valid btn-info btn-block p-l btn-lg text-u-c r-b text-base" ng-click="modifyTrainingSlot()" translate>{{ 'confirm_my_modification' }}</button>
</div>
</div>
<div class="widget-content no-bg auto wrapper" ng-if="modifiedSlots">
<div class="font-sbold m-b-sm " translate>{{ 'your_booking_slot_was_successfully_moved_from_' }}</div>
<div class="panel panel-default bg-light">
<div class="panel-body">
<div class="font-sbold text-u-c">{{ 'datetime_to_time' | translate:{START_DATETIME:(modifiedSlots.oldReservedSlot.start | amDateFormat:'LLLL'), END_TIME:(modifiedSlots.oldReservedSlot.end | amDateFormat:'LT') } }}</div>
</div>
</div>
<p class="text-center font-bold m-b-sm text-u-c" translate>{{ 'to_date' }}</p>
<div class="panel panel-success bg-success bg-light">
<div class="panel-body">
<div class="font-sbold text-u-c">{{ 'datetime_to_time' | translate:{START_DATETIME:(modifiedSlots.newReservedSlot.start | amDateFormat:'LLLL'), END_TIME:(modifiedSlots.newReservedSlot.end | amDateFormat:'LT') } }}</div>
</div>
</div>
</div>
</div>
<uib-alert type="info m">
<p class="text-sm font-bold">
<i class="fa fa-lightbulb-o"></i>

View File

@ -18,6 +18,7 @@
<a ng-click="reserveTraining(training, $event)" class="btn btn-lg btn-warning bg-white b-2x rounded m-t-xs" ng-if="!isAuthorized('admin')" translate>{{ 'book_this_training' }}</a>
<a ui-sref="app.admin.trainings_edit({id: training.id})" ng-if="isAuthorized('admin')" class="btn btn-lg btn-warning bg-white b-2x rounded m-t-xs"><i class="fa fa-edit"></i> {{ 'edit' | translate }}</a>
<a ng-click="delete(training)" ng-if="isAuthorized('admin')" class="btn btn-lg btn-danger b-2x rounded no-b m-t-xs"><i class="fa fa-trash-o"></i></a>
</section>
</div>
</div>

View File

@ -3,13 +3,86 @@
<h1 translate>{{ 'credit_title' }}</h1>
</div>
<div class="modal-body">
<div class="alert alert-warning m-b-md m-b-sm">
<i class="fa fa-warning m-sm inline" aria-hidden="true"></i>
<div class="inline pull-right width-90 m-t-n-xs" translate>{{ 'warning_uneditable_credit' }}</div>
</div>
<form name="walletForm" ng-class="{'has-error': walletForm.amount.$dirty && walletForm.amount.$invalid}">
<div class="text-center amountGroup">
<span class="beforeAmount" translate>{{ 'credit_label' }}</span>
<input class="form-control" type="number" name="amount" ng-model="amount" required min="1" step="any">
<div class="text-right amountGroup m-r-md">
<label for="amount" class="beforeAmount" translate>{{ 'credit_label' }}</label>
<input class="form-control m-l"
type="number"
id="amount"
name="amount"
ng-model="amount"
required min="1"
step="any">
<span class="afterAmount">{{currencySymbol}}</span>
<span class="help-block" ng-show="walletForm.amount.$dirty && walletForm.amount.$error.required" translate>{{'amount_is_required'}}</span>
<span class="help-block" ng-show="walletForm.amount.$dirty && walletForm.amount.$error.min">{{ 'amount_minimum_1' | translate }}{{currencySymbol}}</span>
<span class="help-block" ng-show="walletForm.amount.$dirty && walletForm.amount.$error.min">{{ 'amount_minimum_1' | translate }} {{currencySymbol}}.</span>
</div>
<div class="text-right amountGroup m-t m-r-md" ng-class="{'has-error': walletForm.amount_confirm.$dirty && walletForm.amount_confirm.$invalid }">
<label for="amount_confirm" class="beforeAmount" translate>{{ 'confirm_credit_label' }}</label>
<input class="form-control m-l"
type="number"
id="amount_confirm"
name="amount_confirm"
ng-model="amount_confirm"
required
min="1"
step="any"
ng-pattern="amount.toString()">
<span class="afterAmount">{{currencySymbol}}</span>
<span class="help-block" ng-show="walletForm.amount_confirm.$dirty && walletForm.amount_confirm.$error.required" translate>{{'amount_confirm_is_required'}}</span>
<span class="help-block" ng-show="walletForm.amount_confirm.$dirty && walletForm.amount_confirm.$error.pattern">{{ 'amount_confirm_does_not_match' | translate }}</span>
</div>
<hr/>
<div class="text-right m-t">
<label for="generate_avoir" translate>{{ 'generate_a_refund_invoice' }}</label>
<div class="inline m-l">
<input bs-switch
ng-model="generate_avoir"
id="generate_avoir"
name="generate_avoir"
type="checkbox"
switch-on-text="{{ 'yes' | translate }}"
switch-off-text="{{ 'no' | translate }}"
switch-animate="true"/>
</div>
</div>
<div ng-show="generate_avoir">
<div class="m-t" ng-class="{'has-error': walletForm.avoir_date.$dirty && walletForm.avoir_date.$invalid }">
<label for="avoir_date" translate>{{ 'creation_date_for_the_refund' }}</label>
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-calendar"></i></span>
<input type="text"
class="form-control"
id="avoir_date"
name="avoir_date"
ng-model="avoir_date"
uib-datepicker-popup="{{datePicker.format}}"
datepicker-options="datePicker.options"
is-open="datePicker.opened"
placeholder="{{datePicker.format}}"
ng-click="toggleDatePicker($event)"
ng-required="generate_avoir"/>
</div>
<span class="help-block" ng-show="walletForm.avoir_date.$dirty && walletForm.avoir_date.$error.required" translate>{{ 'creation_date_is_required' }}</span>
</div>
<div class="m-t">
<label for="description" translate>{{ 'description_(optional)' }}</label>
<p translate>{{ 'will_appear_on_the_refund_invoice' }}</p>
<textarea class="form-control m-t-sm"
id="description"
name="description"
ng-model="description">
</textarea>
</div>
</div>
</form>

View File

@ -1,7 +1,6 @@
class API::AuthProvidersController < API::ApiController
before_action :set_provider, only: [:show, :update, :destroy]
def index
@providers = policy_scope(AuthProvider)
end
@ -48,6 +47,25 @@ class API::AuthProvidersController < API::ApiController
@provider = AuthProvider.active
end
def send_code
authorize AuthProvider
user = User.find_by(email: params[:email])
if user&.auth_token
if AuthProvider.active.providable_type != DatabaseProvider.name
NotificationCenter.call type: 'notify_user_auth_migration',
receiver: user,
attached_object: user
render json: {status: 'processing'}, status: :ok
else
render json: {status: 'error', error: I18n.t('members.current_authentication_method_no_code')}, status: :bad_request
end
else
render json: {status: 'error', error: I18n.t('members.requested_account_does_not_exists')}, status: :bad_request
end
end
private
def set_provider

View File

@ -1,48 +1,78 @@
class API::AvailabilitiesController < API::ApiController
include FablabConfiguration
before_action :authenticate_user!, except: [:public]
before_action :set_availability, only: [:show, :update, :destroy, :reservations]
respond_to :json
## machine availabilities are divided in multiple slots of 60 minutes
SLOT_DURATION = 60
def index
authorize Availability
start_date = ActiveSupport::TimeZone[params[:timezone]].parse(params[:start])
end_date = ActiveSupport::TimeZone[params[:timezone]].parse(params[:end]).end_of_day
@availabilities = Availability.includes(:machines,:tags,:trainings).where.not(available_type: 'event')
@availabilities = Availability.includes(:machines, :tags, :trainings, :spaces).where.not(available_type: 'event')
.where('start_at >= ? AND end_at <= ?', start_date, end_date)
if fablab_spaces_deactivated?
@availabilities = @availabilities.where.not(available_type: 'space')
end
end
def public
start_date = ActiveSupport::TimeZone[params[:timezone]].parse(params[:start])
end_date = ActiveSupport::TimeZone[params[:timezone]].parse(params[:end]).end_of_day
@reservations = Reservation.includes(:slots, user: [:profile]).references(:slots, :user).where('slots.start_at >= ? AND slots.end_at <= ?', start_date, end_date)
# request for 1 single day
if in_same_day(start_date, end_date)
@training_and_event_availabilities = Availability.includes(:tags, :trainings, :event, :slots).where(available_type: ['training', 'event'])
# trainings, events
@training_and_event_availabilities = Availability.includes(:tags, :trainings, :event, :slots).where(available_type: %w(training event))
.where('start_at >= ? AND end_at <= ?', start_date, end_date)
# machines
@machine_availabilities = Availability.includes(:tags, :machines).where(available_type: 'machines')
.where('start_at >= ? AND end_at <= ?', start_date, end_date)
@machine_slots = []
@machine_availabilities.each do |a|
a.machines.each do |machine|
if params[:m] and params[:m].include?(machine.id.to_s)
((a.end_at - a.start_at)/SLOT_DURATION.minutes).to_i.times do |i|
slot = Slot.new(start_at: a.start_at + (i*SLOT_DURATION).minutes, end_at: a.start_at + (i*SLOT_DURATION).minutes + SLOT_DURATION.minutes, availability_id: a.id, availability: a, machine: machine, title: machine.name)
((a.end_at - a.start_at)/ApplicationHelper::SLOT_DURATION.minutes).to_i.times do |i|
slot = Slot.new(start_at: a.start_at + (i*ApplicationHelper::SLOT_DURATION).minutes, end_at: a.start_at + (i*ApplicationHelper::SLOT_DURATION).minutes + ApplicationHelper::SLOT_DURATION.minutes, availability_id: a.id, availability: a, machine: machine, title: machine.name)
slot = verify_machine_is_reserved(slot, @reservations, current_user, '')
@machine_slots << slot
end
end
end
end
@availabilities = [].concat(@training_and_event_availabilities).concat(@machine_slots)
else
@availabilities = Availability.includes(:tags, :machines, :trainings, :event, :slots)
# spaces
@space_availabilities = Availability.includes(:tags, :spaces).where(available_type: 'space')
.where('start_at >= ? AND end_at <= ?', start_date, end_date)
if params[:s]
@space_availabilities.where(available_id: params[:s])
end
@space_slots = []
@space_availabilities.each do |a|
space = a.spaces.first
((a.end_at - a.start_at)/ApplicationHelper::SLOT_DURATION.minutes).to_i.times do |i|
if (a.start_at + (i * ApplicationHelper::SLOT_DURATION).minutes) > Time.now
slot = Slot.new(start_at: a.start_at + (i*ApplicationHelper::SLOT_DURATION).minutes, end_at: a.start_at + (i*ApplicationHelper::SLOT_DURATION).minutes + ApplicationHelper::SLOT_DURATION.minutes, availability_id: a.id, availability: a, space: space, title: space.name)
slot = verify_space_is_reserved(slot, @reservations, current_user, '')
@space_slots << slot
end
end
end
@availabilities = [].concat(@training_and_event_availabilities).concat(@machine_slots).concat(@space_slots)
# request for many days (week or month)
else
@availabilities = Availability.includes(:tags, :machines, :trainings, :spaces, :event, :slots)
.where('start_at >= ? AND end_at <= ?', start_date, end_date)
@availabilities.each do |a|
if a.available_type != 'machines'
a = verify_training_event_is_reserved(a, @reservations)
if a.available_type == 'training' or a.available_type == 'event'
a = verify_training_event_is_reserved(a, @reservations, current_user)
elsif a.available_type == 'space'
a.is_reserved = is_reserved_availability(a, current_user.id)
end
end
end
@ -90,7 +120,7 @@ class API::AvailabilitiesController < API::ApiController
@user = current_user
end
@current_user_role = current_user.is_admin? ? 'admin' : 'user'
@machine = Machine.find(params[:machine_id])
@machine = Machine.friendly.find(params[:machine_id])
@slots = []
@reservations = Reservation.where('reservable_type = ? and reservable_id = ?', @machine.class.to_s, @machine.id).includes(:slots, user: [:profile]).references(:slots, :user).where('slots.start_at > ?', Time.now)
if @user.is_admin?
@ -101,9 +131,9 @@ class API::AvailabilitiesController < API::ApiController
@availabilities = @machine.availabilities.includes(:tags).where("end_at > ? AND end_at < ? AND available_type = 'machines'", Time.now, end_at).where('availability_tags.tag_id' => @user.tag_ids.concat([nil]))
end
@availabilities.each do |a|
((a.end_at - a.start_at)/SLOT_DURATION.minutes).to_i.times do |i|
if (a.start_at + (i * SLOT_DURATION).minutes) > Time.now
slot = Slot.new(start_at: a.start_at + (i*SLOT_DURATION).minutes, end_at: a.start_at + (i*SLOT_DURATION).minutes + SLOT_DURATION.minutes, availability_id: a.id, availability: a, machine: @machine, title: '')
((a.end_at - a.start_at)/ApplicationHelper::SLOT_DURATION.minutes).to_i.times do |i|
if (a.start_at + (i * ApplicationHelper::SLOT_DURATION).minutes) > Time.now
slot = Slot.new(start_at: a.start_at + (i*ApplicationHelper::SLOT_DURATION).minutes, end_at: a.start_at + (i*ApplicationHelper::SLOT_DURATION).minutes + ApplicationHelper::SLOT_DURATION.minutes, availability_id: a.id, availability: a, machine: @machine, title: '')
slot = verify_machine_is_reserved(slot, @reservations, current_user, @current_user_role)
@slots << slot
end
@ -126,8 +156,8 @@ class API::AvailabilitiesController < API::ApiController
# what is requested?
# 1) a single training
if params[:training_id].is_number?
@availabilities = Training.find(params[:training_id]).availabilities
if params[:training_id].is_number? or (params[:training_id].length > 0 and params[:training_id] != 'all')
@availabilities = Training.friendly.find(params[:training_id]).availabilities
# 2) all trainings
else
@availabilities = Availability.trainings
@ -135,7 +165,7 @@ class API::AvailabilitiesController < API::ApiController
# who made the request?
# 1) an admin (he can see all future availabilities)
if @user.is_admin?
if current_user.is_admin?
@availabilities = @availabilities.includes(:tags, :slots, trainings: [:machines]).where('availabilities.start_at > ?', Time.now)
# 2) an user (he cannot see availabilities further than 1 (or 3) months)
else
@ -146,13 +176,62 @@ class API::AvailabilitiesController < API::ApiController
# finally, we merge the availabilities with the reservations
@availabilities.each do |a|
a = verify_training_event_is_reserved(a, @reservations)
a = verify_training_event_is_reserved(a, @reservations, @user)
end
end
def spaces
if params[:member_id]
@user = User.find(params[:member_id])
else
@user = current_user
end
@current_user_role = current_user.is_admin? ? 'admin' : 'user'
@space = Space.friendly.find(params[:space_id])
@slots = []
@reservations = Reservation.where('reservable_type = ? and reservable_id = ?', @space.class.to_s, @space.id).includes(:slots, user: [:profile]).references(:slots, :user).where('slots.start_at > ?', Time.now)
if @user.is_admin?
@availabilities = @space.availabilities.includes(:tags).where("end_at > ? AND available_type = 'space'", Time.now)
else
end_at = 1.month.since
end_at = 3.months.since if is_subscription_year(@user)
@availabilities = @space.availabilities.includes(:tags).where("end_at > ? AND end_at < ? AND available_type = 'space'", Time.now, end_at).where('availability_tags.tag_id' => @user.tag_ids.concat([nil]))
end
@availabilities.each do |a|
((a.end_at - a.start_at)/ApplicationHelper::SLOT_DURATION.minutes).to_i.times do |i|
if (a.start_at + (i * ApplicationHelper::SLOT_DURATION).minutes) > Time.now
slot = Slot.new(start_at: a.start_at + (i*ApplicationHelper::SLOT_DURATION).minutes, end_at: a.start_at + (i*ApplicationHelper::SLOT_DURATION).minutes + ApplicationHelper::SLOT_DURATION.minutes, availability_id: a.id, availability: a, space: @space, title: '')
slot = verify_space_is_reserved(slot, @reservations, @user, @current_user_role)
@slots << slot
end
end
end
@slots.each do |s|
if s.is_complete? and not s.is_reserved
s.title = t('availabilities.not_available')
end
end
end
def reservations
authorize Availability
@reservation_slots = @availability.slots.includes(reservation: [user: [:profile]]).order('slots.start_at ASC')
@reservation_slots = @availability.slots.includes(reservations: [user: [:profile]]).order('slots.start_at ASC')
end
def export_availabilities
authorize :export
export = Export.where({category:'availabilities', export_type: 'index'}).where('created_at > ?', Availability.maximum('updated_at')).last
if export.nil? || !FileTest.exist?(export.file)
@export = Export.new({category:'availabilities', export_type: 'index', user: current_user})
if @export.save
render json: {export_id: @export.id}, status: :ok
else
render json: @export.errors, status: :unprocessable_entity
end
else
send_file File.join(Rails.root, export.file), :type => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', :disposition => 'attachment'
end
end
private
@ -161,10 +240,20 @@ class API::AvailabilitiesController < API::ApiController
end
def availability_params
params.require(:availability).permit(:start_at, :end_at, :available_type, :machine_ids, :training_ids, :nb_total_places, machine_ids: [], training_ids: [], tag_ids: [],
params.require(:availability).permit(:start_at, :end_at, :available_type, :machine_ids, :training_ids, :nb_total_places, machine_ids: [], training_ids: [], space_ids: [], tag_ids: [],
:machines_attributes => [:id, :_destroy])
end
def is_reserved_availability(availability, user_id)
reserved_slots = []
availability.slots.each do |s|
if s.canceled_at.nil?
reserved_slots << s
end
end
reserved_slots.map(&:reservations).flatten.map(&:user_id).include? user_id
end
def is_reserved(start_at, reservations)
is_reserved = false
reservations.each do |r|
@ -184,7 +273,7 @@ class API::AvailabilitiesController < API::ApiController
slot.is_reserved = true
slot.title = "#{slot.machine.name} - #{t('availabilities.not_available')}"
slot.can_modify = true if user_role === 'admin'
slot.reservation = r
slot.reservations.push r
end
if s.start_at == slot.start_at and r.user == user and s.canceled_at == nil
slot.title = "#{slot.machine.name} - #{t('availabilities.i_ve_reserved')}"
@ -197,8 +286,27 @@ class API::AvailabilitiesController < API::ApiController
slot
end
def verify_training_event_is_reserved(availability, reservations)
user = current_user
def verify_space_is_reserved(slot, reservations, user, user_role)
reservations.each do |r|
r.slots.each do |s|
if slot.space.id == r.reservable_id
if s.start_at == slot.start_at and s.canceled_at == nil
slot.can_modify = true if user_role === 'admin'
slot.reservations.push r
end
if s.start_at == slot.start_at and r.user == user and s.canceled_at == nil
slot.id = s.id
slot.title = t('availabilities.i_ve_reserved')
slot.can_modify = true
slot.is_reserved = true
end
end
end
end
slot
end
def verify_training_event_is_reserved(availability, reservations, user)
reservations.each do |r|
r.slots.each do |s|
if ((availability.available_type == 'training' and availability.trainings.first.id == r.reservable_id) or (availability.available_type == 'event' and availability.event.id == r.reservable_id)) and s.start_at == availability.start_at and s.canceled_at == nil
@ -239,6 +347,12 @@ class API::AvailabilitiesController < API::ApiController
availabilities_filtered << a
end
end
# space
if params[:s] and a.available_type == 'space'
if params[:s].include?(a.spaces.first.id.to_s)
availabilities_filtered << a
end
end
# machines
if params[:m] and a.available_type == 'machines'
if (params[:m].map(&:to_i) & a.machine_ids).any?

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