diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 08f52b2da..6f9669cf3 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,8 +1,14 @@ -This issue tracker is **reserved** for bug reports and feature requests. +This issue tracker is **reserved** for bug reports. -The place to ask a question or call for help is at Fab-manager forums at https://forum.fab-manager.com/. +The place to ask a question or call for help is at [Fab-manager forums](https://forum.fab-manager.com) + +The place to request or vote for new feature is on the [feedback website](https://feedback.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. + +The following elements may help to quickly resolve your issue: +- Server logs `tail -f /apps/fabmanager/log/app-stdout.log` on the server +- Client logs `Ctrl`+`Maj`+`i` > `Console` in the browser diff --git a/3rd-PARTY-LICENSES.md b/3rd-PARTY-LICENSES.md index e112be4c1..5fc887b81 100644 --- a/3rd-PARTY-LICENSES.md +++ b/3rd-PARTY-LICENSES.md @@ -1,5 +1,5 @@ -Fab-Manager uses some external components, which are licenced under the +Fab-manager uses some external components, which are licenced under the terms of the following licences: - [Apache 2 license](http://www.apache.org/licenses/LICENSE-2.0): diff --git a/CHANGELOG.md b/CHANGELOG.md index 784975ed6..1a3e3b895 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,78 @@ -# Changelog Fab Manager +# Changelog Fab-manager + +## v4.3.0 2020 March 04 + +- Ability to configure reservation slot restricted for plan subscribers +- Ability to configure the policy (allow or prevent) for members booking a machine/formation/event slot, if they already have a reservation the same day at the same time +- Ability to create and delete periodic calendar availabilities (recurrence) +- Ability to fully customize the home page +- Automated setup assistant +- An administrator can delete a member +- An event reservation can be cancelled, if reservation cancellation is enabled +- Delete multiple recurring events at one time +- Edit multiple recurring events at one time +- Ability to import iCalendar agendas in the public calendar, through URLs to ICS files (RFC 5545) +- Ability to configure the duration of a reservation slot, using `SLOT_DURATION`. Previously, only 60 minutes slots were allowed +- Ability to force the email validation when a new user registers. This is optionally configured with `USER_CONFIRMATION_NEEDED_TO_SIGN_IN` +- Display the scheduled events in the admin calendar, depending on `EVENTS_IN_CALENDAR` configuration. +- Display indications on required fields in new administrator form +- Administrators can to book machine/space/training slots, until 1 month in the past +- Filter members by non-validated emails or by inactive for 3 years +- Ability to customize the title of the link to the about page +- Automatic version check with security alerts +- Public endpoint to check the system health +- Configuration of phone number in members registration forms: can be required or optional, depending on `PHONE_REQUIRED` configuration +- Improved user experience in defining slots in the calendar management +- Improved notification email to the member when a rolling subscription is taken +- Notify all admins on the creation of a refund invoice +- Helper links between admin sections of the scheduling process +- Calendar management: improved legend display and visual behavior +- Reorganized left menu +- Create machine availabilities: select all/none in a click +- Prevent event reservation in the past [Taiga#127] +- Removed the need of twitter API keys to display the last tweet on the home page +- Various helper links to help newcomers creating their first items +- Handle Ctrl^C in upgrade scripts +- Updated moment-timezone +- Updated angular-ui-bootstrap from v0.14 to v1.2 +- Updated caxlsx to 3.0.1 and rails_axlsx to rails_caxlsx +- Updated sidekiq to 5.2.8 +- Option to disable developers analytics +- Added the a "cron" tab in Sidekiq web-ui to watch scheduled tasks +- Integration of Crowdin "in-context" translation management system +- Added freeCAD files as default allowed extensions +- Rake task to sync local users with Stripe +- Unified translations syntax to use ICU MessageFormat +- Refactored front-end translations keys with unified paths +- Updated and refactored README and documentations +- Harmonized Fab-manager typography and case +- Updated seeds file +- Fix a bug: unable to remove the picture from a training +- Fix a bug: no alerts on errors during admin creation +- Fix a bug: replaces all Time.now by DateTime.current to prevent time zones issues [Taiga#134] +- Fix a bug: logs are not printed in staging environment +- Fix a bug: theme colors must be selected twice before the changes became effective +- Fix a bug: datepicker does not work in profile completion screen +- Fix a bug: unable to select a group in profile completion screen +- Fix a bug: in some cases, bogus admin notification on profile completed +- Fix a bug: with Firefox browser, the texts in date inputs are shifted to the bottom +- Fix a bug: sometimes when browsing the invoices section, the translations are missing +- Fix a bug: first day of week is ignored in agendas (#169) +- Fix a bug: statistics page is bogus before the creation of the first plan +- Fix a bug: default invoice logo is broken and prevent invoice generation +- Fix a security issue: updated loofah to fix [CVE-2019-15587](https://nvd.nist.gov/vuln/detail/CVE-2019-15587) +- Fix a security issue: updated angular to 1.7.9 to fix [CVE-2019-10768](https://nvd.nist.gov/vuln/detail/CVE-2019-10768) +- Fix a security issue: updated puma to 3.12.4 to fix [GHSA-7xx3-m584-x994](https://github.com/advisories/GHSA-7xx3-m584-x994), [CVE-2020-5247](https://nvd.nist.gov/vuln/detail/CVE-2020-5247) and [CVE-2019-16254](https://nvd.nist.gov/vuln/detail/CVE-2020-5247) +- Fix a security issue: updated nokogiri to 1.10.8 to fix [CVE-2020-7595](https://nvd.nist.gov/vuln/detail/CVE-2020-7595) +- Fix a security issue: updated rack to 1.6.12 to fix [CVE-2019-16782](https://nvd.nist.gov/vuln/detail/CVE-2019-16782) +- [TODO DEPLOY] add the `SLOT_DURATION` environment variable (see [doc/environment.md](doc/environment.md#SLOT_DURATION) for configuration details) +- [TODO DEPLOY] add the `PHONE_REQUIRED` environment variable (see [doc/environment.md](doc/environment.md#PHONE_REQUIRED) for configuration details) +- [TODO DEPLOY] add the `EVENTS_IN_CALENDAR` environment variable (see [doc/environment.md](doc/environment.md#EVENTS_IN_CALENDAR) for configuration details) +- [TODO DEPLOY] add the `USER_CONFIRMATION_NEEDED_TO_SIGN_IN` environment variable (see [doc/environment.md](doc/environment.md#USER_CONFIRMATION_NEEDED_TO_SIGN_IN) for configuration details) +- [TODO DEPLOY] add the `BOOK_SLOT_AT_SAME_TIME` environment variable (see [doc/environment.md](doc/environment.md#BOOK_SLOT_AT_SAME_TIME) for configuration details) +- [TODO DEPLOY] -> (only dev) `bundle install && yarn install` +- [TODO DEPLOY] `rake db:migrate && rake db:seed` +- [TODO DEPLOY] `rake fablab:fix:name_stylesheet` ## v4.2.4 2019 October 30 @@ -58,6 +132,7 @@ - [TODO DEPLOY] add the `MAX_IMPORT_SIZE` environment variable (see [doc/environment.md](doc/environment.md#MAX_IMPORT_SIZE) for configuration details) - [TODO DEPLOY] add the `FABLAB_WITHOUT_INVOICES` environment variable (see [doc/environment.md](doc/environment.md#FABLAB_WITHOUT_INVOICES) for configuration details) - [TODO DEPLOY] add the `SMTP_TLS` environment variable (see [doc/environment.md](doc/environment.md#SMTP_TLS) for configuration details) +- [TODO DEPLOY] add the `FABLAB_WITHOUT_WALLET` environment variable (see [doc/environment.md](doc/environment.md#FABLAB_WITHOUT_WALLET) for configuration details) - [TODO DEPLOY] **IMPORTANT** Please read [postgres_upgrade.md](doc/postgres_upgrade.md) for instructions on upgrading PostgreSQL. ## v4.1.1 2019 September 20 @@ -91,7 +166,7 @@ ## v4.0.4 2019 August 14 - Fix a bug: #140 VAT rate is erroneous in invoices. - Note: this bug was introduced in v4.0.3 and requires (if you are on v4.0.3) to regenerate the invoices since August 1st (if + Note: this bug was introduced in v4.0.3 and requires (if you are on v4.0.3) to regenerate the invoices since August 1st - [TODO DEPLOY] `rake fablab:maintenance:regenerate_invoices[2019,8]` ## v4.0.3 2019 August 01 @@ -135,7 +210,7 @@ - Refactored user's profile to keep invoicing data after an user was deleted - Refactored user's profile to keep statistical data after an user was deleted - Ability to delete an user (fixes #129 and #120) -- Ask user acceptance before deposing analytics cookies +- Ask user acceptance before deposing analytics cookies - Fix a bug: (spanish) some translations are not loaded correctly - Fix a bug: some users may not appear in the admin's general listing - Fix a bug: Availabilities export report an erroneous number of reservations for machine availabilities (#131) @@ -171,7 +246,7 @@ - Improved date checks before closing an accounting period - Paginate list of coupons - Allow filtering coupons list -- Fix a bug: when VAT has changed during fab-manager's lifecycle, this may not be reflected in archives +- Fix a bug: when VAT has changed during Fab-manager's lifecycle, this may not be reflected in archives - Fix a bug: using a quote in event category's name results in angular $parse:syntax Error ## v3.0.1 2019 April 1st @@ -204,7 +279,7 @@ - [TODO DEPLOY] `rake fablab:setup:chain_invoices_records` - [TODO DEPLOY] `rake fablab:setup:chain_history_values_records` - [TODO DEPLOY] add `DISK_SPACE_MB_ALERT` and `SUPERADMIN_EMAIL` environment variables (see [doc/environment.md](doc/environment.md) for configuration details) -- [TODO DEPLOY] add the `accounting` volume to the fab-manager's image in [docker-compose.yml](docker/docker-compose.yml) +- [TODO DEPLOY] add the `accounting` volume to the Fab-manager's image in [docker-compose.yml](docker/docker-compose.yml) ## v2.8.4 2019 March 18 @@ -332,8 +407,8 @@ - Fix a security issue: sprockets < 2.12.5 has a security vulnerability as described in [CVE-2018-3760](https://nvd.nist.gov/vuln/detail/CVE-2018-3760) - Ensure elasticSearch indices are started with green status on new installations - Refactored User.to_json to remove code duplication -- Fixed syntax and typos in README -- [TODO DEPLOY] **IMPORTANT** Please read [elastic_upgrade.md](doc/elastic_upgrade.md) for instructions on upgrading ElasticSearch. +- Fixed syntax and typos in README +- [TODO DEPLOY] **IMPORTANT** Please read [elastic_upgrade.md](doc/elastic_upgrade.md) for instructions on upgrading ElasticSearch. - [TODO DEPLOY] `rake fablab:fix:categories_slugs` - [TODO DEPLOY] -> (only dev) `bundle install` - [TODO DEPLOY] `rake db:seed` @@ -343,10 +418,10 @@ - Ability to share trainings on social medias - Fix a bug: a reminder notification were sent for canceled reservations - Fix a bug: sharing an event on facebook has HTML tags in the description -- Set Stripe API version, all fab-managers has to use this version because codebase relies on it +- Set Stripe API version, all Fab-managers has to use this version because codebase relies on it - Fix a security issue: OmniAuth < 1.3.2 has a security vulnerability described in [CVE-2017-18076](https://nvd.nist.gov/vuln/detail/CVE-2017-18076) - Fix a security issue: rack-protection < 1.5.5 has a security vulnerability described in [CVE-2018-1000119](https://nvd.nist.gov/vuln/detail/CVE-2018-1000119) -- Fix a security issue: http gem < 0.7.3 has a security vulnerability described in [CVE-2015-1828](https://nvd.nist.gov/vuln/detail/CVE-2015-1828), updates twitter gem as a dependency +- Fix a security issue: http gem < 0.7.3 has a security vulnerability described in [CVE-2015-1828](https://nvd.nist.gov/vuln/detail/CVE-2015-1828), updates twitter gem as a dependency ## v2.6.3 2018 January 2 @@ -402,12 +477,12 @@ ## v2.5.13 2017 September 11 -- Fix a bug: ActiveRecord::RecordNotFound when running rake task fix:recursive_events_over_DST with recursive events which the initial event was deleted +- Fix a bug: ActiveRecord::RecordNotFound when running rake task fix:recursive_events_over_DST with recursive events which the initial event was deleted ## v2.5.12 2017 September 11 - Fix a bug: Long words overflow from homepage's events blocks -- Fix a bug: ActiveRecord::RecordNotFound when running rake task fix:recursive_events_over_DST with non-recursive events +- Fix a bug: ActiveRecord::RecordNotFound when running rake task fix:recursive_events_over_DST with non-recursive events ## v2.5.11 2017 September 7 @@ -614,7 +689,7 @@ - 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 +- Display Fab-manager's version in "Powered by" label, when logged as admin - Load translation locales from subdirectories - Add wallet to user, client can pay total/partial reservation or subscription by wallet - Public calendar for show all trainings/machines/events diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d7ee792fc..862ce5648 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ -# Contributing to FabManager +# Contributing to Fab-manager -♥ [FabManager](http://www.fab-manager.com) and want to get involved? +♥ [Fab-manager](http://www.fab-manager.com) and want to get involved? Thanks! There are plenty of ways you can help! Please take a moment to review this document in order to make the contribution process easy and effective for everyone diff --git a/Gemfile b/Gemfile index b69b1c8f9..2420d2e1a 100644 --- a/Gemfile +++ b/Gemfile @@ -4,7 +4,7 @@ gem 'compass-rails', '2.0.4' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' gem 'rails', '4.2.11.1' # Use Puma as web server -gem 'puma', '3.10.0' +gem 'puma', '3.12.4' # Use SCSS for stylesheets gem 'sass-rails', '5.0.1' @@ -61,7 +61,7 @@ group :test do gem 'webmock' end -group :production do +group :production, :staging do gem 'rails_12factor' end @@ -89,9 +89,6 @@ gem 'mini_magick' # upload files gem 'carrierwave' -gem 'twitter' -gem 'twitter-text' - # slug url gem 'friendly_id', '~> 5.1.0' @@ -140,8 +137,8 @@ gem 'apipie-rails' gem 'has_secure_token' # XLS files generation -gem 'axlsx_rails' gem 'caxlsx' +gem 'caxlsx_rails' gem 'rubyzip', '>= 1.3.0' gem 'rack-protection', '1.5.5' @@ -152,3 +149,5 @@ gem 'sys-filesystem' gem 'sha3' gem 'repost' + +gem 'icalendar' diff --git a/Gemfile.lock b/Gemfile.lock index 0c1c06a6b..2e47bddec 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -56,16 +56,12 @@ GEM descendants_tracker (~> 0.0.4) ice_nine (~> 0.11.0) thread_safe (~> 0.3, >= 0.3.1) - axlsx_rails (0.6.0) - actionpack (>= 3.1) - caxlsx (>= 3.0) bcrypt (3.1.13) binding_of_caller (0.7.3) debug_inspector (>= 0.0.1) bootstrap-sass (3.4.1) autoprefixer-rails (>= 5.2.1) sassc (>= 2.0.0) - buftok (0.2.0) builder (3.2.3) byebug (8.2.3) camertron-eprun (1.1.0) @@ -74,11 +70,14 @@ GEM activesupport (>= 3.2.0) json (>= 1.7) mime-types (>= 1.16) - caxlsx (3.0.0) + caxlsx (3.0.1) htmlentities (~> 4.3, >= 4.3.4) mimemagic (~> 0.3) - nokogiri (~> 1.8, >= 1.8.2) - rubyzip (~> 1.2, >= 1.2.1) + nokogiri (~> 1.10, >= 1.10.4) + rubyzip (>= 1.3.0, < 3) + caxlsx_rails (0.6.2) + actionpack (>= 3.1) + caxlsx (>= 3.0) chroma (0.0.1) chunky_png (1.3.4) cldr-plurals-runtime-rb (1.0.1) @@ -123,8 +122,6 @@ GEM responders warden (~> 1.2.3) docile (1.1.5) - domain_name (0.5.25) - unf (>= 0.0.5, < 1.0.0) elasticsearch (5.0.5) elasticsearch-api (= 5.0.5) elasticsearch-transport (= 5.0.5) @@ -153,7 +150,7 @@ GEM execjs (2.7.0) faker (1.4.3) i18n (~> 0.5) - faraday (0.17) + faraday (0.17.0) multipart-post (>= 1.2, < 3) ffi (1.9.24) figaro (1.1.0) @@ -177,20 +174,14 @@ GEM hashie (3.6.0) hike (1.2.3) htmlentities (4.3.4) - http (3.0.0) - addressable (~> 2.3) - http-cookie (~> 1.0) - http-form_data (>= 2.0.0.pre.pre2, < 3) - http_parser.rb (~> 0.6.0) - http-cookie (1.0.2) - domain_name (~> 0.5) - http-form_data (2.1.0) - http_parser.rb (0.6.0) httparty (0.13.7) json (~> 1.8) multi_xml (>= 0.5.2) i18n (0.9.5) concurrent-ruby (~> 1.0) + icalendar (2.5.3) + ice_cube (~> 0.16) + ice_cube (0.16.3) ice_nine (0.11.2) jaro_winkler (1.5.1) jbuilder (2.5.0) @@ -208,7 +199,7 @@ GEM actionpack (>= 3.0.0) activesupport (>= 3.0.0) libv8 (3.16.14.19) - loofah (2.3.0) + loofah (2.3.1) crass (~> 1.0.2) nokogiri (>= 1.5.9) mail (2.7.1) @@ -221,12 +212,10 @@ GEM 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.3) - mimemagic (0.3.3) + mimemagic (0.3.4) mini_magick (4.9.4) mini_mime (1.0.2) mini_portile2 (2.4.0) @@ -239,8 +228,7 @@ GEM multi_json (1.14.1) multi_xml (0.6.0) multipart-post (2.1.1) - naught (1.1.0) - nokogiri (1.10.4) + nokogiri (1.10.8) mini_portile2 (~> 2.4.0) notify_with (0.0.2) jbuilder (~> 2.0) @@ -284,11 +272,11 @@ GEM protected_attributes (1.1.3) activemodel (>= 4.0.1, < 5.0) public_suffix (3.0.2) - puma (3.10.0) + puma (3.12.4) pundit (1.0.0) activesupport (>= 3.0.0) raabro (1.1.6) - rack (1.6.11) + rack (1.6.13) rack-protection (1.5.5) rack rack-test (0.6.3) @@ -335,7 +323,7 @@ GEM recurrence (1.3.0) activesupport i18n - redis (4.1.2) + redis (4.1.3) redis-namespace (1.6.0) redis (>= 3.0.4) ref (2.0.0) @@ -372,15 +360,14 @@ GEM activerecord (~> 4) activesupport (~> 4) sha3 (1.0.1) - sidekiq (5.2.7) + sidekiq (5.2.8) connection_pool (~> 2.2, >= 2.2.2) - rack (>= 1.5.0) + rack (< 2.1.0) rack-protection (>= 1.5.0) redis (>= 3.3.5, < 5) sidekiq-cron (1.1.0) fugit (~> 1.1) sidekiq (>= 4.2.1) - simple_oauth (0.3.1) simplecov (0.12.0) docile (~> 1.1.0) json (>= 1.8, < 3) @@ -423,19 +410,6 @@ GEM tilt (1.4.1) tins (1.13.0) ttfunk (1.4.0) - twitter (6.2.0) - addressable (~> 2.3) - buftok (~> 0.2.0) - equalizer (~> 0.0.11) - http (~> 3.0) - http-form_data (~> 2.0) - http_parser.rb (~> 0.6.0) - memoizable (~> 0.4.0) - multipart-post (~> 2.0) - naught (~> 1.0) - simple_oauth (~> 0.3.0) - twitter-text (1.11.0) - unf (~> 0.1.0) twitter_cldr (3.2.1) camertron-eprun cldr-plurals-runtime-rb (~> 1.0.0) @@ -445,9 +419,6 @@ GEM thread_safe (~> 0.1) uglifier (4.1.20) execjs (>= 0.3.0, < 3) - unf (0.1.4) - unf_ext - unf_ext (0.0.6) unicode-display_width (1.4.0) vcr (3.0.1) virtus (1.0.5) @@ -477,11 +448,11 @@ DEPENDENCIES api-pagination apipie-rails awesome_print - axlsx_rails bootstrap-sass (>= 3.4.1) byebug carrierwave caxlsx + caxlsx_rails chroma compass-rails (= 2.0.4) coveralls @@ -497,6 +468,7 @@ DEPENDENCIES forgery friendly_id (~> 5.1.0) has_secure_token + icalendar jbuilder (~> 2.5) jbuilder_cache_multi jquery-rails @@ -516,7 +488,7 @@ DEPENDENCIES prawn prawn-table protected_attributes - puma (= 3.10.0) + puma (= 3.12.4) pundit rack-protection (= 1.5.5) railroady @@ -543,8 +515,6 @@ DEPENDENCIES sys-filesystem test_after_commit therubyracer (= 0.12.0) - twitter - twitter-text uglifier (>= 4.1.20) vcr web-console (~> 2.1.3) diff --git a/README.md b/README.md index d3fbc31cc..9e64b8952 100644 --- a/README.md +++ b/README.md @@ -1,42 +1,30 @@ -# FabManager +# Fab-manager -FabManager is the Fab Lab management solution. It provides a comprehensive, web-based, open-source tool to simplify your administrative tasks and your marker's projects. +Fab-manager is the Fab Lab management solution. It provides a comprehensive, web-based, open-source tool to simplify your administrative tasks and your marker's projects. [![Coverage Status](https://coveralls.io/repos/github/sleede/fab-manager/badge.svg)](https://coveralls.io/github/sleede/fab-manager) [![Docker pulls](https://img.shields.io/docker/pulls/sleede/fab-manager.svg)](https://hub.docker.com/r/sleede/fab-manager/) [![Docker Build Status](https://img.shields.io/docker/build/sleede/fab-manager.svg)](https://hub.docker.com/r/sleede/fab-manager/builds) +[![Crowdin](https://badges.crowdin.net/fab-manager/localized.svg)](https://crowdin.com/project/fab-manager) ##### Table of Contents 1. [Software stack](#software-stack) 2. [Contributing](#contributing) 3. [Setup a production environment](#setup-a-production-environment) -4. [Setup a development environment](#setup-a-development-environment)
-4.1. [General Guidelines](#general-guidelines)
-5. [PostgreSQL](#postgresql)
-5.1. [Install PostgreSQL 9.6](#setup-postgresql) -6. [ElasticSearch](#elasticsearch)
-6.1. [Install ElasticSearch](#setup-elasticsearch)
-6.2. [Rebuild statistics](#rebuild-stats)
-6.3. [Backup and Restore](#backup-and-restore-elasticsearch) -7. [Internationalization (i18n)](#i18n)
-7.1. [Translation](#i18n-translation)
-7.1.1. [Front-end translations](#i18n-translation-front)
-7.1.2. [Back-end translations](#i18n-translation-back)
-7.2. [Configuration](#i18n-configuration)
-7.2.1. [Settings](#i18n-settings)
-7.2.2. [Applying changes](#i18n-apply) -8. [Open Projects](#open-projects) -9. [Plugins](#plugins) -10. [Single Sign-On](#sso) -11. [Known issues](#known-issues) -12. [Related Documentation](#related-documentation) +4. [Setup a development environment](#setup-a-development-environment) +5. [Internationalization (i18n)](#i18n) +6. [Open Projects](#open-projects) +7. [Plugins](#plugins) +8. [Single Sign-On](#sso) +9. [Known issues](#known-issues) +10. [Related Documentation](#related-documentation) ## Software stack -FabManager is a Ruby on Rails / AngularJS web application that runs on the following software: +Fab-manager is a Ruby on Rails / AngularJS web application that runs on the following software: - Ubuntu LTS 14.04+ / Debian 8+ - Ruby 2.3 @@ -53,311 +41,40 @@ Contributions are welcome. Please read [the contribution guidelines](CONTRIBUTIN ## Setup a production environment -To run fab-manager as a production application, this is highly recommended to use [Docker-compose](https://docs.docker.com/compose/overview/). -The procedure to follow is described in the [docker-compose readme](docker/README.md). +To run Fab-manager as a production application, this is highly recommended to use [Docker-compose](https://docs.docker.com/compose/overview/). +The procedure to follow is described in the [docker-compose readme](doc/docker-compose_readme.md). ## Setup a development environment -In you intend to run fab-manager on your local machine to contribute to the project development, you can set it up with the following procedure. +In you intend to run Fab-manager on your local machine to contribute to the project development, you can set it up by following the [development readme](doc/development_readme.md). +This procedure relies on docker to set-up the dependencies. -This procedure is not easy to follow so if you don't need to write some code for Fab-manager, please prefer the [docker-compose installation method](docker/README.md). - -Optionally, you can use a virtual development environment that relies on Vagrant and Virtual Box by following the [virtual machine instructions](doc/virtual-machine.md). - - -### General Guidelines - -1. Install RVM, with the ruby version specified in the [.ruby-version file](.ruby-version). - For more details about the process, please read the [official RVM documentation](http://rvm.io/rvm/install). - If you're using ArchLinux, you may have to [read this](doc/archlinux_readme.md) before. - -2. Install NVM, with the node.js version specified in the [.nvmrc file](.nvmrc). - For instructions about installing NVM, please refer to [the NVM readme](https://github.com/creationix/nvm#installation). - -3. Install Yarn, the front-end package manager. - Depending on your system, the installation process may differ, please read the [official Yarn documentation](https://yarnpkg.com/en/docs/install#debian-stable). - -4. Install docker. - Your system may provide a pre-packaged version of docker in its repositories, but this version may be outdated. - Please refer to [ubuntu](https://docs.docker.com/install/linux/docker-ce/ubuntu/), [debian](https://docs.docker.com/install/linux/docker-ce/debian/) or [MacOS](https://docs.docker.com/docker-for-mac/install/) documentation to setup a recent version of docker. - -5. Add your current user to the docker group, to allow using docker without `sudo`. - ```bash - # add the docker group if it doesn't already exist - sudo groupadd docker - # add the current user to the docker group - sudo usermod -aG docker $(whoami) - # restart to validate changes - sudo reboot - ``` - -6. Create a docker network for fab-manager. - You may have to change the network address if it is already in use. - ```bash - docker network create --subnet=172.18.0.0/16 fabmanager - ``` - -7. Retrieve the project from Git - - ```bash - git clone https://github.com/sleede/fab-manager.git - ``` - -8. Install the software dependencies. - First install [PostgreSQL](#postgresql) and [ElasticSearch](#elasticsearch) as specified in their respective documentations. - Then install the other dependencies: - - For Ubuntu/Debian: - - ```bash - # on Ubuntu 18.04 server, you may have to enable the "universe" repository - sudo add-apt-repository universe - # then, install the dependencies - sudo apt-get install libpq-dev redis-server imagemagick - ``` - - For MacOS X: - - ```bash - brew install redis imagemagick - ``` - -9. Init the RVM and NVM instances and check they were correctly configured - - ```bash - cd fab-manager - rvm current | grep -q `cat .ruby-version`@fab-manager && echo "ok" - # Must print ok - nvm use - node --version | grep -q `cat .nvmrc` && echo "ok" - # Must print ok - ``` - -10. Install bundler in the current RVM gemset - - ```bash - gem install bundler --version=1.17.3 - ``` - -11. Install the required ruby gems and javascript plugins - - ```bash - bundle install - yarn install - ``` - -12. Create the default configuration files **and configure them!** (see the [environment configuration documentation](doc/environment.md)) - - ```bash - cp config/database.yml.default config/database.yml - cp config/application.yml.default config/application.yml - vi config/application.yml - # or use your favorite text editor instead of vi (nano, ne...) - ``` - -13. Build the databases. - - **Warning**: **DO NOT** run `rake db:setup` instead of these commands, as this will not run some required raw SQL instructions. - - **Please note**: Your password length must be between 8 and 128 characters, otherwise db:seed will be rejected. This is configured in [config/initializers/devise.rb](config/initializers/devise.rb) - - ```bash - # for dev - rake db:create - rake db:migrate - ADMIN_EMAIL='youradminemail' ADMIN_PASSWORD='youradminpassword' rake db:seed - rake fablab:es:build_stats - # for tests - RAILS_ENV=test rake db:create - RAILS_ENV=test rake db:migrate - ``` - -14. Create the pids folder used by Sidekiq. If you want to use a different location, you can configure it in `config/sidekiq.yml` - - ```bash - mkdir -p tmp/pids - ``` - -15. Start the development web server - - ```bash - foreman s -p 3000 - ``` - -16. You should now be able to access your local development FabManager instance by accessing `http://localhost:3000` in your web browser. - -17. You can login as the default administrator using the credentials defined previously. - -18. 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. - - - -## PostgreSQL - - -### Install PostgreSQL 9.6 - -We will use docker to easily install the required version of PostgreSQL. - -1. Create the docker binding folder - ```bash - mkdir -p .docker/postgresql - ``` - -2. Start the PostgreSQL container. - ```bash - docker run --restart=always -d --name fabmanager-postgres \ - -v $(pwd)/.docker/postgresql:/var/lib/postgresql/data \ - --network fabmanager --ip 172.18.0.2 \ - -p 5432:5432 \ - postgres:9.6 - ``` - -3. Configure fab-manager to use it. - On linux systems, PostgreSQL will be available at 172.18.0.2. - On MacOS, you'll have to set the host to 127.0.0.1 (or localhost). - See [environment.md](doc/environment.md) for more details. - -4 . Finally, you may want to have a look at detailed informations about PostgreSQL usage in fab-manager. - Some information about that is available in the [PostgreSQL Readme](doc/postgresql_readme.md). - - -## ElasticSearch - -ElasticSearch is a powerful search engine based on Apache Lucene combined with a NoSQL database used as a cache to index data and quickly process complex requests on it. - -In FabManager, it is used for the admin's statistics module and to perform searches in projects. - - -### Install ElasticSearch - -1. Create the docker binding folders - ```bash - mkdir -p .docker/elasticsearch/config - mkdir -p .docker/elasticsearch/plugins - mkdir -p .docker/elasticsearch/backups - ``` - -2. Copy the default configuration files - ```bash - cp docker/elasticsearch.yml .docker/elasticsearch/config - cp docker/log4j2.properties .docker/elasticsearch/config - ``` - -3. Start the ElasticSearch container. - ```bash - docker run --restart=always -d --name fabmanager-elastic \ - -v $(pwd)/.docker/elasticsearch/config:/usr/share/elasticsearch/config \ - -v $(pwd)/.docker/elasticsearch:/usr/share/elasticsearch/data \ - -v $(pwd)/.docker/elasticsearch/plugins:/usr/share/elasticsearch/plugins \ - -v $(pwd)/.docker/elasticsearch/backups:/usr/share/elasticsearch/backups \ - --network fabmanager --ip 172.18.0.3 \ - -p 9200:9200 -p 9300:9300 \ - elasticsearch:5.6 - ``` - -4. Configure fab-manager to use it. - On linux systems, ElasticSearch will be available at 172.18.0.3. - On MacOS, you'll have to set the host to 127.0.0.1 (or localhost). - See [environment.md](doc/environment.md) for more details. - - -### Rebuild statistics - -Every nights, the statistics for the day that just ended are built automatically at 01:00 (AM) and stored in ElastricSearch. -See [schedule.yml](config/schedule.yml) to modify this behavior. -If the scheduled task wasn't executed for any reason (eg. you are in a dev environment and your computer was turned off at 1 AM), you can force the statistics data generation in ElasticSearch, running the following command. - -```bash -# Here for the 50 last days -rake fablab:es:generate_stats[50] -``` - - -### Backup and Restore - -To backup and restore the ElasticSearch database, use the [elasticsearch-dump](https://github.com/taskrabbit/elasticsearch-dump) tool. - -Dump the database with: `elasticdump --input=http://localhost:9200/stats --output=fablab_stats.json`. -Restore it with: `elasticdump --input=fablab_stats.json --output=http://localhost:9200/stats`. +Optionally, you can use a virtual development environment that relies on Vagrant and Virtual Box by following the [virtual machine instructions](virtual-machine.md). ## Internationalization (i18n) -The FabManager application can only run in a single language but this language can easily be changed. +The Fab-manager application can only run in a single language but this language can easily be changed. - -### Translation - -Check the files located in `config/locales`: - -- Front app translations (angular.js) are located in `config/locales/app.scope.XX.yml`. - Where scope has one the following meaning : - - admin: translations of the administrator views (manage and configure the FabLab). - - logged: translations of the end-user's views accessible only to connected users. - - public: translation of end-user's views publicly accessible to anyone. - - shared: translations shared by many views (like forms or buttons). -- Back app translations (Ruby on Rails) are located in `config/locales/XX.yml`. -- Emails translations are located in `config/locales/mails.XX.yml`. -- Messages related to the authentication system are located in `config/locales/devise.XX.yml`. - -If you plan to translate the application to a new locale, please consider that the reference translation is French. -Indeed, in some cases, the English texts/sentences can seems confuse or lack of context as they were originally translated from French. - -To prevent syntax mistakes while translating locale files, we **STRONGLY advise** you to use a text editor which support syntax coloration for YML and Ruby. - - -#### Front-end translations - -Front-end translations uses [angular-translate](http://angular-translate.github.io) with some interpolations interpreted by angular.js and other interpreted by [MessageFormat](https://github.com/SlexAxton/messageformat.js/). -**These two kinds of interpolation use a near but different syntax witch SHOULD NOT be confused.** -Please refer to the official [angular-translate documentation](http://angular-translate.github.io/docs/#/guide/14_pluralization) before translating. - - -#### Back-end translations - -Back-end translations uses the [Ruby on Rails syntax](http://guides.rubyonrails.org/i18n.html) but some complex interpolations are interpreted by [MessageFormat](https://github.com/format-message/message-format-rb) and are marked as it in comments. -**DO NOT confuse the syntaxes.** - -In each cases, some inline comments are included in the localisation files. -They can be recognized as they start with the sharp character (#). -These comments are not required to be translated, they are intended to help the translator to have some context information about the sentence to translate. - -You will also need to translate the invoice watermark, located in `app/pdfs/data/`. -You'll find there the [GIMP source of the image](app/pdfs/data/watermark.xcf), which is using [Rubik Mono One](https://fonts.google.com/specimen/Rubik+Mono+One) as font. -Use it to generate a similar localised PNG image which keep the default image size, as PDF are not responsive. - - - -### Configuration - -Locales configurations are made in `config/application.yml`. -If you are in a development environment, your can keep the default values, otherwise, in production, values must be configured carefully. - - -#### Settings - -Please refer to the [environment configuration documentation](doc/environment.md#internationalization-settings) - - -#### Applying changes - -After modifying any values concerning the localisation, restart the application (ie. web server) to apply these changes in the i18n configuration. +Please refer to the [translation readme](doc/translation_readme.md) for instructions about configuring the language or to contribute to the translation. ## Open Projects **This configuration is optional.** -You can configure your fab-manager to synchronize every project with the [Open Projects platform](https://github.com/sleede/openlab-projects). -It's very simple and straightforward and in return, your users will be able to search over projects from all fab-manager instances from within your platform. +You can configure your Fab-manager to synchronize every project with the [Open Projects platform](https://github.com/sleede/openlab-projects). +It's very simple and straightforward and in return, your users will be able to search over projects from all Fab-manager instances from within your platform. The deal is fair, you share your projects and as reward you benefits from projects of the whole community. -If you want to try it, you can visit [this fab-manager](https://fablab.lacasemate.fr/#!/projects) and see projects from different fab-managers. +If you want to try it, you can visit [this Fab-manager](https://fablab.lacasemate.fr/#!/projects) and see projects from different Fab-managers. To start using this awesome feature, there are a few steps: -- send a mail to **contact@fab-manager.com** asking for your Open Projects client's credentials and giving them the name of your fab-manager, they will give you an `OPENLAB_APP_ID` and an `OPENLAB_APP_SECRET` +- send a mail to **contact@fab-manager.com** asking for your Open Projects client's credentials and giving them the name of your Fab-manager, they will give you an `OPENLAB_APP_ID` and an `OPENLAB_APP_SECRET` - fill in the value of the keys in your environment file -- start your fab-manager app -- export your projects to open-projects (if you already have projects created on your fab-manager, unless you can skip that part) executing this command: `bundle exec rake fablab:openlab:bulk_export` +- start your Fab-manager app +- export your projects to open-projects (if you already have projects created on your Fab-manager, unless you can skip that part) executing this command: `bundle exec rake fablab:openlab:bulk_export` **IMPORTANT: please run your server in production mode.** @@ -390,51 +107,7 @@ Developers may find information on how to implement their own authentication pro ## Known issues -- When browsing a machine page, you may encounter an "InterceptError" in the console and the loading bar will stop loading before reaching its ending. - This may happen if the machine was created through a seed file without any image. - To solve this, simply add an image to the machine's profile and refresh the web page. - -- When starting the Ruby on Rails server (eg. `foreman s`) you may receive the following error: - - worker.1 | invalid url: redis::6379 - web.1 | Exiting - worker.1 | ...lib/redis/client.rb...:in `_parse_options' - - This may happen when the `application.yml` file is missing. - To solve this issue copy `config/application.yml.default` to `config/application.yml`. - This is required before the first start. - -- Due to a stripe limitation, you won't be 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 "...". - : ... - test_after_commit (1.0.0) lib/test_after_commit/database_statements.rb:11:in `block in transaction' - test_after_commit (1.0.0) lib/test_after_commit/database_statements.rb:5:in `transaction' - - This is due to an ActiveRecord behavior witch disable referential integrity in PostgreSQL to load the fixtures. - PostgreSQL will prevent any users to disable referential integrity on the fly if they doesn't have the `SUPERUSER` role. - To fix that, logon as the `postgres` user and run the PostgreSQL shell (see [the dedicated section](#run-postgresql-cli) for instructions). - Then, run the following command (replace `sleede` with your test database user, as specified in your database.yml): - - ALTER ROLE sleede WITH SUPERUSER; - - DO NOT do this in a production environment, 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. - 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 - ``` -- In some cases, the invoices won't be generated. This can be due to the image included in the invoice header not being supported. - To fix this issue, change the image in the administrator interface (manage the invoices / invoices settings). - See [this thread](https://forum.fab-manager.com/t/resolu-erreur-generation-facture/428) for more info. +Before reporting an issue, please check if your issue is not listed in the [know issues](doc/known-issues.md) with its solution. ## Related Documentation diff --git a/app/assets/javascripts/app.js b/app/assets/javascripts/app.js index 01b4265e9..f6f36e803 100644 --- a/app/assets/javascripts/app.js +++ b/app/assets/javascripts/app.js @@ -20,9 +20,10 @@ angular.module('application', ['ngCookies', 'ngResource', 'ngSanitize', 'ui.rout 'ui.select', 'ui.calendar', 'angularMoment', 'Devise', 'DeviseModal', 'angular-growl', 'xeditable', 'checklist-model', 'unsavedChanges', 'angular-loading-bar', 'ngTouch', 'angular-google-analytics', 'angularUtils.directives.dirDisqus', 'summernote', 'elasticsearch', 'angular-medium-editor', 'naif.base64', - 'minicolors', 'pascalprecht.translate', 'ngFitText', 'ngAside', 'ngCapsLock', 'vcRecaptcha']) - .config(['$httpProvider', 'AuthProvider', 'growlProvider', 'unsavedWarningsConfigProvider', 'AnalyticsProvider', 'uibDatepickerPopupConfig', '$provide', '$translateProvider', - function ($httpProvider, AuthProvider, growlProvider, unsavedWarningsConfigProvider, AnalyticsProvider, uibDatepickerPopupConfig, $provide, $translateProvider) { + 'minicolors', 'pascalprecht.translate', 'ngFitText', 'ngAside', 'ngCapsLock', 'vcRecaptcha', 'ui.codemirror', + 'bm.uiTour']) + .config(['$httpProvider', 'AuthProvider', 'growlProvider', 'unsavedWarningsConfigProvider', 'AnalyticsProvider', 'uibDatepickerPopupConfig', '$provide', '$translateProvider', 'TourConfigProvider', + function ($httpProvider, AuthProvider, growlProvider, unsavedWarningsConfigProvider, AnalyticsProvider, uibDatepickerPopupConfig, $provide, $translateProvider, TourConfigProvider) { // Google analytics // first we check the user acceptance const cookiesConsent = document.cookie.replace(/(?:(?:^|.*;\s*)fab-manager-cookies-consent\s*=\s*([^;]*).*$)|^.*$/, '$1'); @@ -59,10 +60,12 @@ angular.module('application', ['ngCookies', 'ngResource', 'ngSanitize', 'ui.rout $translateProvider.useLoaderCache(true); // Secure i18n module against XSS attacks by escaping the output $translateProvider.useSanitizeValueStrategy('escapeParameters'); - // Enable the MessageFormat interpolation (used for pluralization) - $translateProvider.addInterpolation('$translateMessageFormatInterpolation'); + // Use the MessageFormat interpolation by default (used for pluralization) + $translateProvider.useMessageFormatInterpolation(); // Set the langage of the instance (from ruby configuration) $translateProvider.preferredLanguage(Fablab.locale); + // End the tour when the user clicks the forward or back buttons of the browser + TourConfigProvider.enableNavigationInterceptors(); }]).run(['$rootScope', '$log', 'AuthService', 'Auth', 'amMoment', '$state', 'editableOptions', 'Analytics', function ($rootScope, $log, AuthService, Auth, amMoment, $state, editableOptions, Analytics) { // Angular-moment (date-time manipulations library) @@ -86,6 +89,16 @@ angular.module('application', ['ngCookies', 'ngResource', 'ngSanitize', 'ui.rout $rootScope.fablabWithoutOnlinePayment = Fablab.withoutOnlinePayment; // Global config: if true, no invoices will be generated $rootScope.fablabWithoutInvoices = Fablab.withoutInvoices; + // Global config: if true, the phone number is required to create an account + $rootScope.phoneRequired = Fablab.phoneRequired; + // Global config: if true, the events are shown in the admin calendar + $rootScope.eventsInCalendar = Fablab.eventsInCalendar; + // Global config: machine/space slot duration + $rootScope.slotDuration = Fablab.slotDuration; + // Global config: if true, user must confirm his email to sign in + $rootScope.userConfirmationNeededToSignIn = Fablab.userConfirmationNeededToSignIn; + // Global config: if true, wallet will be disable + $rootScope.fablabWithoutWallet = Fablab.fablabWithoutWallet; // 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 diff --git a/app/assets/javascripts/application.js.erb b/app/assets/javascripts/application.js.erb index eaf6f38a8..9a5b256e8 100644 --- a/app/assets/javascripts/application.js.erb +++ b/app/assets/javascripts/application.js.erb @@ -21,7 +21,7 @@ //= require angular-cookies //= require angular-touch //= require @uirouter/angularjs/release/angular-ui-router -//= require angular-ui-bootstrap/ui-bootstrap-tpls +//= require angular-ui-bootstrap/dist/ui-bootstrap-tpls //= require ui-select/dist/select //= require moment/moment //= require moment-timezone/builds/moment-timezone-with-data-2012-2022 @@ -46,6 +46,7 @@ //= require elasticsearch-browser/elasticsearch.angular //= require d3/d3 //= require nvd3/build/nv.d3.js +//= require twitter-fetcher //= require app //= require router //= require medium-editor/dist/js/medium-editor @@ -55,6 +56,7 @@ //= require angular-base64-upload/dist/angular-base64-upload.min //= require summernote/dist/summernote //= require angular-summernote/dist/angular-summernote +//= require summernote-ext-nugget //= require jquery-minicolors/jquery.minicolors.js //= require angular-minicolors/angular-minicolors.js //= require angular-translate/dist/angular-translate @@ -65,6 +67,16 @@ //= require angular-aside/dist/js/angular-aside //= require ng-caps-lock/ng-caps-lock //= require angular-recaptcha +//= require codemirror/lib/codemirror +//= require codemirror/addon/edit/matchbrackets +//= require codemirror/mode/css/css +//= require codemirror/mode/sass/sass +//= require angular-ui-codemirror/src/ui-codemirror +//= require angular-hotkeys/build/hotkeys +//= require hone/dist/hone +//= require tether/dist/js/tether +//= require angular-bind-html-compile/angular-bind-html-compile +//= require angular-ui-tour/dist/angular-ui-tour //= require_tree ./controllers //= require_tree ./services //= require_tree ./directives diff --git a/app/assets/javascripts/controllers/admin/abuses.js b/app/assets/javascripts/controllers/admin/abuses.js index 2d1741a00..21da2309b 100644 --- a/app/assets/javascripts/controllers/admin/abuses.js +++ b/app/assets/javascripts/controllers/admin/abuses.js @@ -17,21 +17,21 @@ Application.Controllers.controller('AbusesController', ['$scope', '$state', 'Abu resolve: { object () { return { - title: _t('manage_abuses.confirmation_required'), - msg: _t('manage_abuses.report_will_be_destroyed') + title: _t('app.admin.manage_abuses.confirmation_required'), + msg: _t('app.admin.manage_abuses.report_will_be_destroyed') }; } } }, function () { // cancel confirmed Abuse.remove({ id: abuseId }, function () { // successfully canceled - growl.success(_t('manage_abuses.report_removed')); + growl.success(_t('app.admin.manage_abuses.report_removed')); Abuse.query({}, function (abuses) { $scope.abuses = abuses.abuses.filter(a => a.signaled_type === 'Project'); }); } , function () { // error while canceling - growl.error(_t('manage_abuses.failed_to_remove')); + growl.error(_t('app.admin.manage_abuses.failed_to_remove')); }); } ); diff --git a/app/assets/javascripts/controllers/admin/authentications.js.erb b/app/assets/javascripts/controllers/admin/authentications.js.erb index 10852ea53..9736b265d 100644 --- a/app/assets/javascripts/controllers/admin/authentications.js.erb +++ b/app/assets/javascripts/controllers/admin/authentications.js.erb @@ -54,14 +54,16 @@ const check_oauth2_id_is_mapped = function (mappings) { * - $scope.authMethods * - $scope.mappingFields * - $scope.cancel() + * - $scope.methodName() * - $scope.defineDataMapping(mapping) * * Requires : * - mappingFieldsPromise: retrieved by AuthProvider.mapping_fields() * - $state (Ui-Router) [ 'app.admin.members' ] + * - _t : translation method */ class AuthenticationController { - constructor ($scope, $state, $uibModal, mappingFieldsPromise) { + constructor ($scope, $state, $uibModal, _t, mappingFieldsPromise) { // list of supported authentication methods $scope.authMethods = METHODS; @@ -73,6 +75,13 @@ class AuthenticationController { */ $scope.cancel = function () { $state.go('app.admin.members'); }; + /** + * Return a localized string for the provided method + */ + $scope.methodName = function(method) { + return _t('app.shared.authentication.' + METHODS[method]); + } + /** * Open a modal allowing to specify the data mapping for the given field */ @@ -137,10 +146,10 @@ class AuthenticationController { $scope.ok = function () { $uibModalInstance.close($scope.transformation.rules); }; // do not save the modifications - return $scope.cancel = function () { $uibModalInstance.dismiss(); }; - } - ] }) - .result['finally'](null).then(function (transfo_rules) { mapping.transformation = transfo_rules; }); + $scope.cancel = function () { $uibModalInstance.dismiss(); }; + }] + }) + .result['finally'](null).then(function (transfo_rules) { mapping.transformation = transfo_rules; }); }; } } @@ -163,9 +172,9 @@ Application.Controllers.controller('AuthentificationController', ['$scope', '$st $scope.getType = function (type) { const text = METHODS[type]; if (typeof text !== 'undefined') { - return _t(text); + return _t(`app.admin.members.authentication_form.${text}`); } else { - return _t('unknown') + type; + return _t('app.admin.members.authentication_form.unknown') + type; } }; @@ -176,10 +185,10 @@ Application.Controllers.controller('AuthentificationController', ['$scope', '$st */ $scope.getState = function (status) { switch (status) { - case 'active': return _t('active'); - case 'pending': return _t('pending'); - case 'previous': return _t('previous_provider'); - default: return _t('unknown') + status; + case 'active': return _t('app.admin.members.authentication_form.active'); + case 'pending': return _t('app.admin.members.authentication_form.pending'); + case 'previous': return _t('app.admin.members.authentication_form.previous_provider'); + default: return _t('app.admin.members.authentication_form.unknown') + status; } }; @@ -194,8 +203,8 @@ Application.Controllers.controller('AuthentificationController', ['$scope', '$st resolve: { object () { return { - title: _t('confirmation_required'), - msg: _t('do_you_really_want_to_delete_the_TYPE_authentication_provider_NAME', { TYPE: $scope.getType(provider.providable_type), NAME: provider.name }) + title: _t('app.admin.members.authentication_form.confirmation_required'), + msg: _t('app.admin.members.authentication_form.do_you_really_want_to_delete_the_TYPE_authentication_provider_NAME', { TYPE: $scope.getType(provider.providable_type), NAME: provider.name }) }; } } @@ -206,9 +215,9 @@ Application.Controllers.controller('AuthentificationController', ['$scope', '$st { id: provider.id }, function () { providers.splice(findIdxById(providers, provider.id), 1); - growl.success(_t('authentication_provider_successfully_deleted')); + growl.success(_t('app.admin.members.authentication_form.authentication_provider_successfully_deleted')); }, - function () { growl.error(_t('an_error_occurred_unable_to_delete_the_specified_provider')); } + function () { growl.error(_t('app.admin.members.authentication_form.an_error_occurred_unable_to_delete_the_specified_provider')); } ); } ); @@ -254,19 +263,19 @@ Application.Controllers.controller('NewAuthenticationController', ['$scope', '$s // prevent from adding mode than 1 for (provider of Array.from(authProvidersPromise)) { if (provider.providable_type === 'DatabaseProvider') { - growl.error(_t('a_local_database_provider_already_exists_unable_to_create_another')); + growl.error(_t('app.admin.authentication_new.a_local_database_provider_already_exists_unable_to_create_another')); return false; } } return AuthProvider.save({ auth_provider: $scope.provider }, function (provider) { - growl.success(_t('local_provider_successfully_saved')); + growl.success(_t('app.admin.authentication_new.local_provider_successfully_saved')); return $state.go('app.admin.members'); }); // === OAuth2Provider === } else if ($scope.provider.providable_type === 'OAuth2Provider') { // check the ID mapping if (!check_oauth2_id_is_mapped($scope.provider.providable_attributes.o_auth2_mappings_attributes)) { - growl.error(_t('it_is_required_to_set_the_matching_between_User.uid_and_the_API_to_add_this_provider')); + growl.error(_t('app.admin.authentication_new.it_is_required_to_set_the_matching_between_User.uid_and_the_API_to_add_this_provider')); return false; } // discourage the use of unsecure SSO @@ -277,24 +286,24 @@ Application.Controllers.controller('NewAuthenticationController', ['$scope', '$s resolve: { object () { return { - title: _t('security_issue_detected'), - msg: _t('beware_the_oauth2_authenticatoin_provider_you_are_about_to_add_isnt_using_HTTPS') + - _t('this_is_a_serious_security_issue_on_internet_and_should_never_be_used_except_for_testing_purposes') + - _t('do_you_really_want_to_continue') + title: _t('app.admin.authentication_new.security_issue_detected'), + msg: _t('app.admin.authentication_new.beware_the_oauth2_authenticatoin_provider_you_are_about_to_add_isnt_using_HTTPS') + + _t('app.admin.authentication_new.this_is_a_serious_security_issue_on_internet_and_should_never_be_used_except_for_testing_purposes') + + _t('app.admin.authentication_new.do_you_really_want_to_continue') }; } } }, function () { // unsecured http confirmed AuthProvider.save({ auth_provider: $scope.provider }, function (provider) { - growl.success(_t('unsecured_oauth2_provider_successfully_added')); + growl.success(_t('app.admin.authentication_new.unsecured_oauth2_provider_successfully_added')); return $state.go('app.admin.members'); }); } ); } else { AuthProvider.save({ auth_provider: $scope.provider }, function (provider) { - growl.success(_t('oauth2_provider_successfully_added')); + growl.success(_t('app.admin.authentication_new.oauth2_provider_successfully_added')); return $state.go('app.admin.members'); }); } @@ -302,7 +311,7 @@ Application.Controllers.controller('NewAuthenticationController', ['$scope', '$s }; // Using the AuthenticationController - return new AuthenticationController($scope, $state, $uibModal, mappingFieldsPromise); + return new AuthenticationController($scope, $state, $uibModal, _t, mappingFieldsPromise); } ]); @@ -322,21 +331,21 @@ Application.Controllers.controller('EditAuthenticationController', ['$scope', '$ $scope.updateProvider = function () { // check the ID mapping if (!check_oauth2_id_is_mapped($scope.provider.providable_attributes.o_auth2_mappings_attributes)) { - growl.error(_t('it_is_required_to_set_the_matching_between_User.uid_and_the_API_to_add_this_provider')); + growl.error(_t('app.admin.authentication_edit.it_is_required_to_set_the_matching_between_User.uid_and_the_API_to_add_this_provider')); return false; } return AuthProvider.update( { id: $scope.provider.id }, { auth_provider: $scope.provider }, function (provider) { - growl.success(_t('provider_successfully_updated')); + growl.success(_t('app.admin.authentication_edit.provider_successfully_updated')); $state.go('app.admin.members'); }, - function () { growl.error(_t('an_error_occurred_unable_to_update_the_provider')); } + function () { growl.error(_t('app.admin.authentication_edit.an_error_occurred_unable_to_update_the_provider')); } ); }; // Using the AuthenticationController - return new AuthenticationController($scope, $state, $uibModal, mappingFieldsPromise); + return new AuthenticationController($scope, $state, $uibModal, _t, mappingFieldsPromise); } ]); diff --git a/app/assets/javascripts/controllers/admin/calendar.js.erb b/app/assets/javascripts/controllers/admin/calendar.js.erb index 6e35d5a91..0a6fe92b5 100644 --- a/app/assets/javascripts/controllers/admin/calendar.js.erb +++ b/app/assets/javascripts/controllers/admin/calendar.js.erb @@ -18,19 +18,18 @@ * Controller used in the calendar management page */ -Application.Controllers.controller('AdminCalendarController', ['$scope', '$state', '$uibModal', 'moment', 'Availability', 'Slot', 'Setting', 'Export', 'growl', 'dialogs', 'bookingWindowStart', 'bookingWindowEnd', 'machinesPromise', '_t', 'uiCalendarConfig', 'CalendarConfig', - function ($scope, $state, $uibModal, moment, Availability, Slot, Setting, Export, growl, dialogs, bookingWindowStart, bookingWindowEnd, machinesPromise, _t, uiCalendarConfig, CalendarConfig) { - /* PRIVATE STATIC CONSTANTS */ +Application.Controllers.controller('AdminCalendarController', ['$scope', '$state', '$uibModal', 'moment', 'Availability', 'Slot', 'Setting', 'Export', 'growl', 'dialogs', 'bookingWindowStart', 'bookingWindowEnd', 'machinesPromise', 'plansPromise', 'groupsPromise', '_t', 'uiCalendarConfig', 'CalendarConfig', 'Member', 'uiTourService', + function ($scope, $state, $uibModal, moment, Availability, Slot, Setting, Export, growl, dialogs, bookingWindowStart, bookingWindowEnd, machinesPromise, plansPromise, groupsPromise, _t, uiCalendarConfig, CalendarConfig, Member, uiTourService) { + /* PRIVATE STATIC CONSTANTS */ // The calendar is divided in slots of 30 minutes - let loadingCb; const BASE_SLOT = '00:30:00'; // The bookings can be positioned every half hours const BOOKING_SNAP = '00:30:00'; // We do not allow the creation of slots that are not a multiple of 60 minutes - const SLOT_MULTIPLE = 60; + const SLOT_MULTIPLE = Fablab.slotDuration; /* PUBLIC SCOPE */ @@ -40,6 +39,9 @@ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state // currently selected availability $scope.availability = null; + // corresponding fullCalendar item in the DOM + $scope.availabilityDom = null; + // bind the availabilities slots with full-Calendar events $scope.eventSources = []; $scope.eventSources.push({ @@ -62,7 +64,10 @@ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state return calendarEventClickCb(event, jsEvent, view); }, eventRender (event, element, view) { - return eventRenderCb(event, element); + return eventRenderCb(event, element, view); + }, + viewRender(view, element) { + return viewRenderCb(view, element); }, loading (isLoading, view) { return loadingCb(isLoading, view); @@ -80,10 +85,9 @@ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state resolve: { object () { return { - title: _t('admin_calendar.confirmation_required'), - msg: _t('admin_calendar.do_you_really_want_to_cancel_the_USER_s_reservation_the_DATE_at_TIME_concerning_RESERVATION' - , { GENDER: getGender($scope.currentUser), USER: slot.user.name, DATE: moment(slot.start_at).format('L'), TIME: moment(slot.start_at).format('LT'), RESERVATION: slot.reservable.name } - , 'messageformat') + title: _t('app.admin.calendar.confirmation_required'), + msg: _t('app.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 }) }; } } @@ -101,10 +105,10 @@ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state } } // notify the admin - return growl.success(_t('admin_calendar.reservation_was_successfully_cancelled')); + return growl.success(_t('app.admin.calendar.reservation_was_successfully_cancelled')); }, function (data, status) { // failed - growl.error(_t('admin_calendar.reservation_cancellation_failed')); + growl.error(_t('app.admin.calendar.reservation_cancellation_failed')); } ); } @@ -118,17 +122,17 @@ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state */ $scope.removeMachine = function (machine) { if ($scope.availability.machine_ids.length === 1) { - return growl.error(_t('admin_calendar.unable_to_remove_the_last_machine_of_the_slot_delete_the_slot_rather')); + return growl.error(_t('app.admin.calendar.unable_to_remove_the_last_machine_of_the_slot_delete_the_slot_rather')); } else { // open a confirmation dialog return dialogs.confirm({ resolve: { object () { return { - title: _t('admin_calendar.confirmation_required'), - msg: _t('admin_calendar.do_you_really_want_to_remove_MACHINE_from_this_slot', { GENDER: getGender($scope.currentUser), MACHINE: machine.name }, 'messageformat') + ' ' + - _t('admin_calendar.this_will_prevent_any_new_reservation_on_this_slot_but_wont_cancel_those_existing') + ' ' + - _t('admin_calendar.beware_this_cannot_be_reverted') + title: _t('app.admin.calendar.confirmation_required'), + msg: _t('app.admin.calendar.do_you_really_want_to_remove_MACHINE_from_this_slot', { GENDER: getGender($scope.currentUser), MACHINE: machine.name }) + ' ' + + _t('app.admin.calendar.this_will_prevent_any_new_reservation_on_this_slot_but_wont_cancel_those_existing') + '
' + + _t('app.admin.calendar.beware_this_cannot_be_reverted') + '' }; } } @@ -150,16 +154,52 @@ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state $scope.availability.title = data.title; uiCalendarConfig.calendars.calendar.fullCalendar('rerenderEvents'); // notify the admin - return growl.success(_t('admin_calendar.the_machine_was_successfully_removed_from_the_slot')); + return growl.success(_t('app.admin.calendar.the_machine_was_successfully_removed_from_the_slot')); } , function (data, status) { // failed - growl.error(_t('admin_calendar.deletion_failed')); + growl.error(_t('app.admin.calendar.deletion_failed')); } ); }); } }; + /** + * Open a confirmation modal to remove a plan for the currently selected availability, + * @param plan {Object} must contain the machine ID and name + */ + $scope.removePlan = function (plan) { + // open a confirmation dialog + return dialogs.confirm({ + resolve: { + object () { + return { + title: _t('app.admin.calendar.confirmation_required'), + msg: _t('app.admin.calendar.do_you_really_want_to_remove_PLAN_from_this_slot', { GENDER: getGender($scope.currentUser), PLAN: plan.name }) + }; + } + } + }, + function () { + // the admin has confirmed, remove the plan + const plans = _.drop($scope.availability.plan_ids, plan.id); + + return Availability.update({ id: $scope.availability.id }, { availability: { plans_attributes: [{ id: plan.id, _destroy: true }] } } + , function (data, status) { // success + // update the plan_ids attribute + $scope.availability.plan_ids = data.plan_ids; + $scope.availability.plans = availabilityPlans(); + uiCalendarConfig.calendars.calendar.fullCalendar('rerenderEvents'); + // notify the admin + return growl.success(_t('app.admin.calendar.the_plan_was_successfully_removed_from_the_slot')); + } + , function (data, status) { // failed + growl.error(_t('app.admin.calendar.deletion_failed')); + } + ); + }); + }; + /** * Callback to alert the admin that the export request was acknowledged and is * processing right now. @@ -167,7 +207,7 @@ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state $scope.alertExport = function (type) { Export.status({ category: 'availabilities', type }).then(function (res) { if (!res.data.exists) { - return growl.success(_t('admin_calendar.export_is_running_you_ll_be_notified_when_its_ready')); + return growl.success(_t('app.admin.calendar.export_is_running_you_ll_be_notified_when_its_ready')); } }); }; @@ -195,8 +235,8 @@ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state resolve: { object () { return { - title: _t('admin_calendar.confirmation_required'), - msg: locked ? _t('admin_calendar.do_you_really_want_to_allow_reservations') : _t('admin_calendar.do_you_really_want_to_block_this_slot') + title: _t('app.admin.calendar.confirmation_required'), + msg: locked ? _t('app.admin.calendar.do_you_really_want_to_allow_reservations') : _t('app.admin.calendar.do_you_really_want_to_block_this_slot') }; } } @@ -208,18 +248,18 @@ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state { lock: !locked }, function (data) { // success $scope.availability = data; - growl.success(locked ? _t('admin_calendar.unlocking_success') : _t('admin_calendar.locking_success')); + growl.success(locked ? _t('app.admin.calendar.unlocking_success') : _t('app.admin.calendar.locking_success')); uiCalendarConfig.calendars.calendar.fullCalendar('refetchEvents'); }, function (error) { // failed - growl.error(locked ? _t('admin_calendar.unlocking_failed') : _t('admin_calendar.locking_failed')); + growl.error(locked ? _t('app.admin.calendar.unlocking_failed') : _t('app.admin.calendar.locking_failed')); console.error(error); } ); } ); } else { - return growl.error(_t('admin_calendar.unlockable_because_reservations')); + return growl.error(_t('app.admin.calendar.unlockable_because_reservations')); } }; @@ -228,93 +268,195 @@ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state */ $scope.removeSlot = function () { // open a confirmation dialog - dialogs.confirm( - { - resolve: { - object () { - return { - title: _t('admin_calendar.confirmation_required'), - msg: _t('admin_calendar.do_you_really_want_to_delete_this_slot') - }; - } - } - }, - function () { - // the admin has confirmed, delete the slot - Availability.delete( - { id: $scope.availability.id }, - function () { - uiCalendarConfig.calendars.calendar.fullCalendar('removeEvents', $scope.availability.id); - - growl.success(_t('admin_calendar.the_slot_START-END_has_been_successfully_deleted', { START: moment(event.start).format('LL LT'), END: moment(event.end).format('LT') })); - $scope.availability = null; - }, - function () { - growl.error(_t('admin_calendar.unable_to_delete_the_slot_START-END_because_it_s_already_reserved_by_a_member', { START: moment(event.start).format('LL LT'), END: moment(event.end).format('LT') })); - }); + const modalInstance = $uibModal.open({ + animation: true, + templateUrl: '<%= asset_path "admin/calendar/deleteRecurrent.html" %>', + size: 'md', + controller: 'DeleteRecurrentAvailabilityController', + resolve: { + availabilityPromise: ['Availability', function (Availability) { return Availability.get({ id: $scope.availability.id }).$promise; }] } - ); + }); + // once the dialog was closed, do things depending on the result + modalInstance.result.then(function (res) { + if (res.status == 'success') { + $scope.availability = null; + } + for (const availability of res.availabilities) { + uiCalendarConfig.calendars.calendar.fullCalendar('removeEvents', availability); + } + }); }; + /** + * Setup the feature-tour for the admin/calendar page. + * This is intended as a contextual help (when pressing F1) + */ + $scope.setupCalendarTour = function () { + // get the tour defined by the ui-tour directive + const uitour = uiTourService.getTourByName('calendar'); + uitour.createStep({ + selector: 'body', + stepId: 'welcome', + order: 0, + title: _t('app.admin.tour.calendar.welcome.title'), + content: _t('app.admin.tour.calendar.welcome.content'), + placement: 'bottom', + orphan: true + }); + uitour.createStep({ + selector: '.admin-calendar .fc-view-container', + stepId: 'agenda', + order: 1, + title: _t('app.admin.tour.calendar.agenda.title'), + content: _t('app.admin.tour.calendar.agenda.content'), + placement: 'right', + popupClass: 'width-350' + }); + uitour.createStep({ + selector: '.admin-calendar .export-xls-button', + stepId: 'export', + order: 2, + title: _t('app.admin.tour.calendar.export.title'), + content: _t('app.admin.tour.calendar.export.content'), + placement: 'left' + }); + uitour.createStep({ + selector: '.heading .import-ics-button', + stepId: 'import', + order: 3, + title: _t('app.admin.tour.calendar.import.title'), + content: _t('app.admin.tour.calendar.import.content'), + placement: 'left' + }); + uitour.createStep({ + selector: 'body', + stepId: 'conclusion', + order: 4, + title: _t('app.admin.tour.conclusion.title'), + content: _t('app.admin.tour.conclusion.content'), + placement: 'bottom', + orphan: true + }); + // on tour end, save the status in database + uitour.on('ended', function () { + if (uitour.getStatus() === uitour.Status.ON && $scope.currentUser.profile.tours.indexOf('calendar') < 0) { + Member.completeTour({ id: $scope.currentUser.id }, { tour: 'calendar' }, function (res) { + $scope.currentUser.profile.tours = res.tours; + }); + } + }); + // if the user has never seen the tour, show him now + if (Fablab.featureTourDisplay !== 'manual' && $scope.currentUser.profile.tours.indexOf('calendar') < 0) { + uitour.start(); + } + // start this tour when an user press F1 - this is contextual help + window.addEventListener('keydown', handleF1); + } + /* PRIVATE SCOPE */ + /** + * Kind of constructor: these actions will be realized first when the controller is loaded + */ + const initialize = function () { + // listen the $destroy event of the controller to remove the F1 key binding + $scope.$on('$destroy', function () { + window.removeEventListener('keydown', handleF1); + }); + }; + /** * Return an enumerable meaninful string for the gender of the provider user * @param user {Object} Database user record * @return {string} 'male' or 'female' */ - var getGender = function (user) { + const getGender = function (user) { if (user.statistic_profile) { if (user.statistic_profile.gender === 'true') { return 'male'; } else { return 'female'; } } else { return 'other'; } }; + /** + * Return a list of plans classified by group + * + * @returns {array} + */ + var availabilityPlans = function() { + const plansClassifiedByGroup = []; + const _plans = _.filter(plansPromise, function (p) { return _.include($scope.availability.plan_ids, p.id) }); + for (let group of Array.from(groupsPromise)) { + const groupObj = { id: group.id, name: group.name, plans: [] }; + for (let plan of Array.from(_plans)) { + if (plan.group_id === group.id) { groupObj.plans.push(plan); } + } + if (groupObj.plans.length > 0) { + plansClassifiedByGroup.push(groupObj); + } + } + return plansClassifiedByGroup; + }; + // Triggered when the admin drag on the agenda to create a new reservable slot. // @see http://fullcalendar.io/docs/selection/select_callback/ // - var calendarSelectCb = function (start, end, jsEvent, view) { + const calendarSelectCb = function (start, end, jsEvent, view) { start = moment.tz(start.toISOString(), Fablab.timezone); end = moment.tz(end.toISOString(), Fablab.timezone); - // first we check that the selected slot is an N-hours multiple (ie. not decimal) - if (Number.isInteger(parseInt((end.valueOf() - start.valueOf()) / (SLOT_MULTIPLE * 1000), 10) / SLOT_MULTIPLE)) { - const today = new Date(); - if (parseInt((start.valueOf() - today) / (60 * 1000), 10) >= 0) { - // then we open a modal window to let the admin specify the slot type - const modalInstance = $uibModal.open({ - templateUrl: '<%= asset_path "admin/calendar/eventModal.html" %>', - controller: 'CreateEventModalController', - resolve: { - start () { return start; }, - end () { return end; }, - machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }], - trainingsPromise: ['Training', function (Training) { return Training.query().$promise; }], - spacesPromise: ['Space', function (Space) { return Space.query().$promise; }] - } }); - // when the modal is closed, we send the slot to the server for saving - modalInstance.result.then( - function (availability) { - uiCalendarConfig.calendars.calendar.fullCalendar( - 'renderEvent', - { - id: availability.id, - title: availability.title, - start: availability.start_at, - end: availability.end_at, - textColor: 'black', - backgroundColor: availability.backgroundColor, - borderColor: availability.borderColor, - tag_ids: availability.tag_ids, - tags: availability.tags, - machine_ids: availability.machine_ids - }, - true - ); - }, - function () { uiCalendarConfig.calendars.calendar.fullCalendar('unselect'); } - ); - } + + // check if slot is not in the past + const today = new Date(); + if (Math.trunc((start.valueOf() - today) / (60 * 1000)) < 0) { + growl.warning(_t('app.admin.calendar.event_in_the_past')); + return uiCalendarConfig.calendars.calendar.fullCalendar('unselect'); } + // check that the selected slot is an multiple of SLOT_MULTIPLE (ie. not decimal) + const slots = Math.trunc((end.valueOf() - start.valueOf()) / (60 * 1000)) / SLOT_MULTIPLE; + if (!Number.isInteger(slots)) { + // otherwise, round it to upper decimal + const upper = Math.ceil(slots) * SLOT_MULTIPLE; + end = moment(start).add(upper, 'minutes'); + } + + // then we open a modal window to let the admin specify the slot type + const modalInstance = $uibModal.open({ + templateUrl: '<%= asset_path "admin/calendar/eventModal.html" %>', + controller: 'CreateEventModalController', + resolve: { + start () { return start; }, + end () { return end; }, + machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }], + trainingsPromise: ['Training', function (Training) { return Training.query().$promise; }], + spacesPromise: ['Space', function (Space) { return Space.query().$promise; }], + tagsPromise: ['Tag', function (Tag) { return Tag.query().$promise; }], + plansPromise: ['Plan', function (Plan) { return Plan.query().$promise; }], + groupsPromise: ['Group', function (Group) { return Group.query().$promise; }] + } }); + // when the modal is closed, we send the slot to the server for saving + modalInstance.result.then( + function (availability) { + uiCalendarConfig.calendars.calendar.fullCalendar( + 'renderEvent', + { + id: availability.id, + title: availability.title, + start: availability.start_at, + end: availability.end_at, + textColor: 'black', + backgroundColor: availability.backgroundColor, + borderColor: availability.borderColor, + tag_ids: availability.tag_ids, + tags: availability.tags, + machine_ids: availability.machine_ids, + plan_ids: availability.plan_ids + }, + true + ); + }, + function () { uiCalendarConfig.calendars.calendar.fullCalendar('unselect'); } + ); + return uiCalendarConfig.calendars.calendar.fullCalendar('unselect'); }; @@ -322,8 +464,15 @@ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state * Triggered when the admin clicks on a availability slot in the agenda. * @see http://fullcalendar.io/docs/mouse/eventClick/ */ - var calendarEventClickCb = function (event, jsEvent, view) { + const calendarEventClickCb = function (event, jsEvent, view) { $scope.availability = event; + $scope.availability.plans = availabilityPlans(); + + if ($scope.availabilityDom) { + $scope.availabilityDom.classList.remove("fc-selected") + } + $scope.availabilityDom = jsEvent.target.closest('.fc-event'); + $scope.availabilityDom.classList.add("fc-selected") // if the user has clicked on the delete event button, delete the event if ($(jsEvent.target).hasClass('remove-event')) { @@ -339,8 +488,10 @@ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state * Append the event tag into the block, just after the event title. * @see http://fullcalendar.io/docs/event_rendering/eventRender/ */ - var eventRenderCb = function (event, element) { - element.find('.fc-content').prepend(''); + const eventRenderCb = function (event, element) { + if (event.available_type !== 'event') { + element.find('.fc-content').prepend(''); + } if (event.tags.length > 0) { let html = ''; for (let tag of Array.from(event.tags)) { @@ -355,12 +506,38 @@ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state * Triggered when resource fetching starts/stops. * @see https://fullcalendar.io/docs/resource_data/loading/ */ - return loadingCb = function (isLoading, view) { + const loadingCb = function (isLoading, view) { if (isLoading) { - // we remove existing events when fetching starts to prevent duplicates - return uiCalendarConfig.calendars.calendar.fullCalendar('removeEvents'); + // we remove existing events when fetching starts to prevent duplicates + uiCalendarConfig.calendars.calendar.fullCalendar('removeEvents'); + } }; + + /** + * Triggered when the view is changed + * @see https://fullcalendar.io/docs/v3/viewRender#v2 + */ + const viewRenderCb = function(view, element) { + // we unselect the current event to keep consistency + $scope.availability = null; + $scope.availabilityDom = null; + }; + + /** + * Callback used to trigger the feature tour when the user press the F1 key. + * @param e {KeyboardEvent} + */ + const handleF1 = function (e) { + if (e.key === 'F1') { + e.preventDefault(); + const tour = uiTourService.getTourByName('calendar'); + if (tour) { tour.start(); } + } + }; + + // !!! MUST BE CALLED AT THE END of the controller + return initialize(); } ]); @@ -368,9 +545,9 @@ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state /** * Controller used in the slot creation modal window */ -Application.Controllers.controller('CreateEventModalController', ['$scope', '$uibModalInstance', 'moment', 'start', 'end', 'machinesPromise', 'Availability', 'trainingsPromise', 'spacesPromise', 'Tag', 'growl', '_t', - function ($scope, $uibModalInstance, moment, start, end, machinesPromise, Availability, trainingsPromise, spacesPromise, Tag, growl, _t) { - // $uibModal parameter +Application.Controllers.controller('CreateEventModalController', ['$scope', '$uibModalInstance', '$sce', 'moment', 'start', 'end', 'machinesPromise', 'Availability', 'trainingsPromise', 'spacesPromise', 'tagsPromise', 'plansPromise', 'groupsPromise', 'growl', '_t', + function ($scope, $uibModalInstance, $sce, moment, start, end, machinesPromise, Availability, trainingsPromise, spacesPromise, tagsPromise, plansPromise, groupsPromise, growl, _t) { + // $uibModal parameter $scope.start = start; // $uibModal parameter @@ -385,8 +562,27 @@ Application.Controllers.controller('CreateEventModalController', ['$scope', '$ui // spaces list $scope.spaces = spacesPromise.filter(function (s) { return !s.disabled; }); + // all tags list + $scope.tags = tagsPromise; + + $scope.isOnlySubscriptions = false; + $scope.selectedPlans = []; + $scope.selectedPlansBinding = {}; + // list of plans, classified by group + $scope.plansClassifiedByGroup = []; + for (let group of Array.from(groupsPromise)) { + const groupObj = { id: group.id, name: group.name, plans: [] }; + for (let plan of Array.from(plansPromise)) { + if (plan.group_id === group.id) { groupObj.plans.push(plan); } + } + if (groupObj.plans.length > 0) { + $scope.plansClassifiedByGroup.push(groupObj); + } + } + // machines associated with the created slot $scope.selectedMachines = []; + $scope.selectedMachinesBinding = {}; // training associated with the created slot $scope.selectedTraining = null; @@ -416,9 +612,29 @@ Application.Controllers.controller('CreateEventModalController', ['$scope', '$ui $scope.availability = { start_at: start, end_at: end, - available_type: 'machines' // default + available_type: 'machines', // default + tag_ids: [], + is_recurrent: false, + period: 'week', + nb_periods: 1, + end_date: undefined // recurrence end }; + // recurrent slots + $scope.occurrences = []; + + // localized name(s) of the reservable item(s) + $scope.reservableName = ''; + + // localized name(s) of the selected tag(s) + $scope.tagsName = ''; + + // localized name(s) of the selected plan(s) + $scope.plansName = ''; + + // make the duration available for display + $scope.slotDuration = Fablab.slotDuration; + /** * Adds or removes the provided machine from the current slot * @param machine {Object} @@ -432,6 +648,49 @@ Application.Controllers.controller('CreateEventModalController', ['$scope', '$ui } }; + /** + * Select/unselect all the machines + */ + $scope.toggleAll = function() { + const count = $scope.selectedMachines.length; + $scope.selectedMachines = []; + $scope.selectedMachinesBinding = {}; + if (count == 0) { + $scope.machines.forEach(function (machine) { + $scope.selectedMachines.push(machine); + $scope.selectedMachinesBinding[machine.id] = true; + }) + } + } + + /** + * Adds or removes the provided plan from the current slot + * @param plan {Object} + */ + $scope.toggleSelectPlan = function (plan) { + const index = $scope.selectedPlans.indexOf(plan); + if (index > -1) { + return $scope.selectedPlans.splice(index, 1); + } else { + return $scope.selectedPlans.push(plan); + } + }; + + /** + * Select/unselect all the plans + */ + $scope.toggleAllPlans = function() { + const count = $scope.selectedPlans.length; + $scope.selectedPlans = []; + $scope.selectedPlansBinding = {}; + if (count == 0) { + plansPromise.forEach(function (plan) { + $scope.selectedPlans.push(plan); + $scope.selectedPlansBinding[plan.id] = true; + }) + } + }; + /** * Callback for the modal window validation: save the slot and closes the modal */ @@ -440,7 +699,7 @@ Application.Controllers.controller('CreateEventModalController', ['$scope', '$ui if ($scope.selectedMachines.length > 0) { $scope.availability.machine_ids = $scope.selectedMachines.map(function (m) { return m.id; }); } else { - growl.error(_t('admin_calendar.you_should_select_at_least_a_machine')); + growl.error(_t('app.admin.calendar.you_should_select_at_least_a_machine')); return; } } else if ($scope.availability.available_type === 'training') { @@ -448,9 +707,16 @@ Application.Controllers.controller('CreateEventModalController', ['$scope', '$ui } else if ($scope.availability.available_type === 'space') { $scope.availability.space_ids = [$scope.selectedSpace.id]; } + if ($scope.availability.is_recurrent) { + $scope.availability.occurrences = $scope.occurrences; + } + if ($scope.isOnlySubscriptions && $scope.selectedPlans.length > 0) { + $scope.availability.plan_ids = $scope.selectedPlans.map(function (p) { return p.id; }); + } return Availability.save( - { availability: $scope.availability } - , function (availability) { $uibModalInstance.close(availability); }); + { availability: $scope.availability }, + function (availability) { $uibModalInstance.close(availability); } + ); }; /** @@ -458,6 +724,8 @@ Application.Controllers.controller('CreateEventModalController', ['$scope', '$ui */ $scope.next = function () { if ($scope.step === 1) { $scope.setNbTotalPlaces(); } + if ($scope.step === 2) { return validateSelection(); } + if ($scope.step === 5) { return validateRecurrence(); } return $scope.step++; }; @@ -495,23 +763,33 @@ Application.Controllers.controller('CreateEventModalController', ['$scope', '$ui $scope.selectedSpace = $scope.spaces[0]; } - Tag.query().$promise.then(function (data) { $scope.tags = data; }); + // when disable is only subscriptions option, reset all selected plans + $scope.$watch('isOnlySubscriptions', function(value) { + if (!value) { + $scope.selectedPlans = []; + $scope.selectedPlansBinding = {}; + } + }); - // When we configure a machine availability, do not let the user change the end time, as the total - // time must be dividable by 60 minutes (base slot duration). For training availabilities, the user + // When we configure a machine/space availability, do not let the user change the end time, as the total + // time must be dividable by Fablab.slotDuration minutes (base slot duration). For training availabilities, the user // can configure any duration as it does not matters. $scope.$watch('availability.available_type', function (newValue, oldValue, scope) { if ((newValue === 'machines') || (newValue === 'space')) { $scope.endDateReadOnly = true; - const diff = moment($scope.end).diff($scope.start, 'hours'); // the result is rounded down by moment.js - $scope.end = moment($scope.start).add(diff, 'hours').toDate(); + const slots = Math.trunc(($scope.end.valueOf() - $scope.start.valueOf()) / (60 * 1000)) / Fablab.slotDuration; + if (!Number.isInteger(slots)) { + // otherwise, round it to upper decimal + const upper = Math.ceil(slots) * Fablab.slotDuration; + $scope.end = moment($scope.start).add(upper, 'minutes').toDate(); + } return $scope.availability.end_at = $scope.end; } else { return $scope.endDateReadOnly = false; } }); - // When the start date is changed, if we are configuring a machine availability, + // When the start date is changed, if we are configuring a machine/space availability, // maintain the relative length of the slot (ie. change the end time accordingly) $scope.$watch('start', function (newValue, oldValue, scope) { // for machine or space availabilities, adjust the end time @@ -520,27 +798,293 @@ Application.Controllers.controller('CreateEventModalController', ['$scope', '$ui end.add(moment(newValue).diff(oldValue), 'milliseconds'); $scope.end = end.toDate(); } else { // for training availabilities - // prevent the admin from setting the begining after the and - if (moment(newValue).add(1, 'hour').isAfter($scope.end)) { + // prevent the admin from setting the beginning after the end + if (moment(newValue).add(Fablab.slotDuration, 'minutes').isAfter($scope.end)) { $scope.start = oldValue; } } // update availability object - return $scope.availability.start_at = $scope.start; + $scope.availability.start_at = $scope.start; }); // Maintain consistency between the end time and the date object in the availability object - return $scope.$watch('end', function (newValue, oldValue, scope) { - // we prevent the admin from setting the end of the availability before its begining - if (moment($scope.start).add(1, 'hour').isAfter(newValue)) { + $scope.$watch('end', function (newValue, oldValue, scope) { + // we prevent the admin from setting the end of the availability before its beginning + if (moment($scope.start).add(Fablab.slotDuration, 'minutes').isAfter(newValue)) { $scope.end = oldValue; } // update availability object - return $scope.availability.end_at = $scope.end; + $scope.availability.end_at = $scope.end; }); }; + /** + * Validates that a machine or more was/were selected before continuing to step 3 (adjust time + tags) + */ + const validateSelection = function () { + if ($scope.availability.available_type === 'machines') { + if ($scope.selectedMachines.length === 0) { + return growl.error(_t('app.admin.calendar.you_should_select_at_least_a_machine')); + } + } + $scope.step++; + }; + + /** + * Validates that the recurrence parameters were correctly set before continuing to step 5 (summary) + */ + const validateRecurrence = function () { + if ($scope.availability.is_recurrent) { + if (!$scope.availability.period) { + return growl.error(_t('app.admin.calendar.select_period')); + } + if (!$scope.availability.nb_periods || $scope.availability.nb_periods < 1) { + return growl.error(_t('app.admin.calendar.select_nb_period')); + } + if (!$scope.availability.end_date) { + return growl.error(_t('app.admin.calendar.select_end_date')); + } + } + // settings are ok + computeOccurrences(); + computeNames(); + $scope.step++; + }; + + /** + * Compute the various occurrences of the availability, according to the recurrence settings + */ + const computeOccurrences = function () { + $scope.occurrences = []; + + if ($scope.availability.is_recurrent) { + const date = moment($scope.availability.start_at); + const diff = moment($scope.availability.end_at).diff($scope.availability.start_at); + const end = moment($scope.availability.end_date).endOf('day'); + while (date.isBefore(end)) { + const occur_end = moment(date).add(diff, 'ms'); + $scope.occurrences.push({ + start_at: date.toDate(), + end_at: occur_end.toDate() + }); + date.add($scope.availability.nb_periods, $scope.availability.period); + } + } else { + $scope.occurrences.push({ + start_at: $scope.availability.start_at, + end_at: $scope.availability.end_at + }); + } + }; + + const computeNames = function () { + $scope.reservableName = ''; + switch ($scope.availability.available_type) { + case 'machines': + $scope.reservableName = localizedList($scope.selectedMachines) + break; + case 'training': + $scope.reservableName = `${$scope.selectedTraining.name}`; + break; + case 'space': + $scope.reservableName = `${$scope.selectedSpace.name}`; + break; + default: + $scope.reservableName = `${_t("app.admin.calendar.none")}`; + } + const tags = $scope.tags.filter(function (t) { + return $scope.availability.tag_ids.indexOf(t.id) > -1; + }) + $scope.tagsName = localizedList(tags); + if ($scope.isOnlySubscriptions && $scope.selectedPlans.length > 0) { + $scope.plansName = localizedList($scope.selectedPlans); + } + } + + const localizedList = function (items) { + if (items.length === 0) return `${_t("app.admin.calendar.none")}`; + + const names = items.map(function (i) { return $sce.trustAsHtml(`${i.name}`); }); + if (items.length > 1) return names.slice(0, -1).join(', ') + ` ${_t('app.admin.calendar.and')} ` + names[names.length - 1]; + + return names[0]; + } + // !!! MUST BE CALLED AT THE END of the controller return initialize(); } ]); + +/** + * Controller used in the slot deletion modal window + */ +Application.Controllers.controller('DeleteRecurrentAvailabilityController', ['$scope', '$uibModalInstance', 'Availability', 'availabilityPromise', 'growl', '_t', + function ($scope, $uibModalInstance, Availability, availabilityPromise, growl, _t) { + + // is the current slot (to be deleted) recurrent? + $scope.isRecurrent = availabilityPromise.is_recurrent; + + // with recurrent slots: how many slots should we delete? + $scope.deleteMode = 'single'; + + /** + * Confirmation callback + */ + $scope.ok = function () { + const { id, start_at, end_at } = availabilityPromise; + // the admin has confirmed, delete the slot + Availability.delete( + { id, mode: $scope.deleteMode }, + function (res) { + // delete success + if (res.deleted > 1) { + growl.success(_t( + 'app.admin.calendar.slots_deleted', + {START: moment(start_at).format('LL LT'), COUNT: res.deleted - 1} + )); + } else { + growl.success(_t( + 'app.admin.calendar.slot_successfully_deleted', + {START: moment(start_at).format('LL LT'), END: moment(end_at).format('LT')} + )); + } + $uibModalInstance.close({ + status: 'success', + availabilities: res.details.map(function (d) { return d.availability.id }) + }); + }, + function (res) { + // not everything was deleted + const { data } = res; + if (data.total > 1) { + growl.warning(_t( + 'app.admin.calendar.slots_not_deleted', + {TOTAL: data.total, COUNT: data.total - data.deleted} + )); + } else { + growl.error(_t( + 'app.admin.calendar.unable_to_delete_the_slot', + {START: moment(start_at).format('LL LT'), END: moment(end_at).format('LT')} + )); + } + $uibModalInstance.close({ + status: 'failed', + availabilities: data.details.filter(function (d) { return d.status }).map(function (d) { return d.availability.id }) + }); + }); + } + + /** + * Cancellation callback + */ + $scope.cancel = function () { + $uibModalInstance.dismiss('cancel'); + } + } +]); + + + +/** + * Controller used in the iCalendar (ICS) imports management page + */ + +Application.Controllers.controller('AdminICalendarController', ['$scope', 'iCalendars', 'ICalendar', 'dialogs', 'growl', '_t', + function ($scope, iCalendars, ICalendar, dialogs, growl, _t) { + // list of ICS sources + $scope.calendars = iCalendars; + + // configuration of a new ICS source + $scope.newCalendar = { + color: undefined, + text_color: undefined, + url: undefined, + name: undefined, + text_hidden: false + }; + + /** + * Save the new iCalendar in database + */ + $scope.save = function () { + ICalendar.save({}, { i_calendar: $scope.newCalendar }, function (data) { + // success + $scope.calendars.push(data); + $scope.newCalendar.url = undefined; + $scope.newCalendar.name = undefined; + $scope.newCalendar.color = null; + $scope.newCalendar.text_color = null; + $scope.newCalendar.text_hidden = false; + }, function (error) { + // failed + growl.error(_t('app.admin.icalendar.create_error')); + console.error(error); + }) + } + + /** + * Return a CSS-like style of the given calendar configuration + * @param calendar + */ + $scope.calendarStyle = function (calendar) { + return { + 'border-color': calendar.color, + 'color': calendar.text_color, + 'width': calendar.text_hidden ? '50px' : 'auto', + 'height': calendar.text_hidden ? '21px' : 'auto' + }; + } + + /** + * Delete the given calendar from the database + * @param calendar + */ + $scope.delete = function (calendar) { + dialogs.confirm( + { + resolve: { + object () { + return { + title: _t('app.admin.icalendar.confirmation_required'), + msg: _t('app.admin.icalendar.confirm_delete_import') + }; + } + } + }, + function () { + ICalendar.delete( + { id: calendar.id }, + function () { + // success + const idx = $scope.calendars.indexOf(calendar); + $scope.calendars.splice(idx, 1); + growl.info(_t('app.admin.icalendar.delete_success')); + }, function (error) { + // failed + growl.error(_t('app.admin.icalendar.delete_failed')); + console.error(error); + } + ); + } + ) + } + + /** + * Asynchronously re-fetches the events from the given calendar + * @param calendar + */ + $scope.sync = function (calendar) { + ICalendar.sync( + { id: calendar.id }, + function () { + // success + growl.info(_t('app.admin.icalendar.refresh')); + }, function (error) { + // failed + growl.error(_t('app.admin.icalendar.sync_failed')); + console.error(error); + } + ) + } + } +]); diff --git a/app/assets/javascripts/controllers/admin/coupons.js b/app/assets/javascripts/controllers/admin/coupons.js index be1490482..e4b27ef91 100644 --- a/app/assets/javascripts/controllers/admin/coupons.js +++ b/app/assets/javascripts/controllers/admin/coupons.js @@ -1,19 +1,8 @@ -/* eslint-disable - no-return-assign, - no-undef, -*/ -// TODO: This file was created by bulk-decaffeinate. -// Fix any style issues and re-enable lint. -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ /* COMMON CODE */ // The validity per user defines how many time a user may ba able to use the same coupon // Here are the various options for this parameter -const userValidities = ['once', 'forever']; +const VALIDITIES = ['once', 'forever']; /** * Controller used in the coupon creation page @@ -27,7 +16,7 @@ Application.Controllers.controller('NewCouponController', ['$scope', '$state', ' }; // Options for the validity per user - $scope.validities = userValidities; + $scope.validities = VALIDITIES; // Default parameters for AngularUI-Bootstrap datepicker (used for coupon validity limit selection) $scope.datePicker = { @@ -39,6 +28,13 @@ Application.Controllers.controller('NewCouponController', ['$scope', '$state', ' } }; + /** + * Return a localized human-readable name for the provided validity + */ + $scope.validityName = function (validity) { + return _t(`app.shared.coupon.${validity}`); + }; + /** * Shows/hides the validity limit datepicker * @param $event {Object} jQuery event object @@ -46,17 +42,17 @@ Application.Controllers.controller('NewCouponController', ['$scope', '$state', ' $scope.toggleDatePicker = function ($event) { $event.preventDefault(); $event.stopPropagation(); - return $scope.datePicker.opened = !$scope.datePicker.opened; + $scope.datePicker.opened = !$scope.datePicker.opened; }; /** * Callback to save the new coupon in $scope.coupon and redirect the user to the listing page */ - return $scope.saveCoupon = () => + $scope.saveCoupon = () => Coupon.save({ coupon: $scope.coupon }, coupon => $state.go('app.admin.pricing') , function (err) { - growl.error(_t('unable_to_create_the_coupon_check_code_already_used')); - return console.error(err); + growl.error(_t('app.admin.coupons_new.unable_to_create_the_coupon_check_code_already_used')); + console.error(err); }); } ]); @@ -75,7 +71,7 @@ Application.Controllers.controller('EditCouponController', ['$scope', '$state', $scope.coupon = couponPromise; // Options for the validity per user - $scope.validities = userValidities; + $scope.validities = VALIDITIES; // Mapping for validation errors $scope.errors = {}; @@ -90,6 +86,13 @@ Application.Controllers.controller('EditCouponController', ['$scope', '$state', } }; + /** + * Return a localized human-readable name for the provided validity + */ + $scope.validityName = function (validity) { + return _t(`app.shared.coupon.${validity}`); + }; + /** * Shows/hides the validity limit datepicker * @param $event {Object} jQuery event object @@ -97,7 +100,7 @@ Application.Controllers.controller('EditCouponController', ['$scope', '$state', $scope.toggleDatePicker = function ($event) { $event.preventDefault(); $event.stopPropagation(); - return $scope.datePicker.opened = !$scope.datePicker.opened; + $scope.datePicker.opened = !$scope.datePicker.opened; }; /** @@ -105,10 +108,10 @@ Application.Controllers.controller('EditCouponController', ['$scope', '$state', */ $scope.updateCoupon = function () { $scope.errors = {}; - return Coupon.update({ id: $scope.coupon.id }, { coupon: $scope.coupon }, coupon => $state.go('app.admin.pricing') + Coupon.update({ id: $scope.coupon.id }, { coupon: $scope.coupon }, coupon => $state.go('app.admin.pricing') , function (err) { - growl.error(_t('unable_to_update_the_coupon_an_error_occurred')); - return $scope.errors = err.data; + growl.error(_t('app.admin.coupons_edit.unable_to_update_the_coupon_an_error_occurred')); + $scope.errors = err.data; }); }; @@ -120,7 +123,7 @@ Application.Controllers.controller('EditCouponController', ['$scope', '$state', const initialize = function () { // parse the date if any if (couponPromise.valid_until) { - return $scope.coupon.valid_until = moment(couponPromise.valid_until).toDate(); + $scope.coupon.valid_until = moment(couponPromise.valid_until).toDate(); } }; diff --git a/app/assets/javascripts/controllers/admin/events.js.erb b/app/assets/javascripts/controllers/admin/events.js.erb index 774111ed7..425e7af6d 100644 --- a/app/assets/javascripts/controllers/admin/events.js.erb +++ b/app/assets/javascripts/controllers/admin/events.js.erb @@ -56,14 +56,7 @@ class EventsController { * @param content {Object} JSON - The upload's result */ $scope.submited = function (content) { - if ((content.id == null)) { - $scope.alerts = []; - angular.forEach(content, function (v, k) { - angular.forEach(v, function (err) { $scope.alerts.push({ msg: k + ': ' + err, type: 'danger' }); }); - }); - } else { - $state.go('app.public.events_list'); - } + $scope.onSubmited(content); }; /** @@ -160,8 +153,8 @@ class EventsController { /** * Controller used in the events listing page (admin view) */ -Application.Controllers.controller('AdminEventsController', ['$scope', '$state', 'dialogs', '$uibModal', 'growl', 'Event', 'Category', 'EventTheme', 'AgeRange', 'PriceCategory', 'eventsPromise', 'categoriesPromise', 'themesPromise', 'ageRangesPromise', 'priceCategoriesPromise', '_t', - function ($scope, $state, dialogs, $uibModal, growl, Event, Category, EventTheme, AgeRange, PriceCategory, eventsPromise, categoriesPromise, themesPromise, ageRangesPromise, priceCategoriesPromise, _t) { +Application.Controllers.controller('AdminEventsController', ['$scope', '$state', 'dialogs', '$uibModal', 'growl', 'Event', 'Category', 'EventTheme', 'AgeRange', 'PriceCategory', 'eventsPromise', 'categoriesPromise', 'themesPromise', 'ageRangesPromise', 'priceCategoriesPromise', '_t', 'Member', 'uiTourService', + function ($scope, $state, dialogs, $uibModal, growl, Event, Category, EventTheme, AgeRange, PriceCategory, eventsPromise, categoriesPromise, themesPromise, ageRangesPromise, priceCategoriesPromise, _t, Member, uiTourService) { /* PUBLIC SCOPE */ // By default, the pagination mode is activated to limit the page size @@ -196,6 +189,9 @@ Application.Controllers.controller('AdminEventsController', ['$scope', '$state', $scope.eventsScope = { selected: '' }; + // default tab: events list + $scope.tabs = { active: 0 }; + /** * Adds a bucket of events to the bottom of the page, grouped by month */ @@ -228,26 +224,26 @@ Application.Controllers.controller('AdminEventsController', ['$scope', '$state', */ $scope.removeElement = function (model, index) { if ((model === 'category') && (getModel(model)[1].length === 1)) { - growl.error(_t('at_least_one_category_is_required') + ' ' + _t('unable_to_delete_the_last_one')); + growl.error(_t('app.admin.events.at_least_one_category_is_required') + ' ' + _t('app.admin.events.unable_to_delete_the_last_one')); return false; } if (getModel(model)[1][index].related_to > 0) { - growl.error(_t('unable_to_delete_ELEMENT_already_in_use_NUMBER_times', { ELEMENT: model, NUMBER: getModel(model)[1][index].related_to }, 'messageformat')); + growl.error(_t('app.admin.events.unable_to_delete_ELEMENT_already_in_use_NUMBER_times', { ELEMENT: model, NUMBER: getModel(model)[1][index].related_to })); return false; } return dialogs.confirm({ resolve: { object () { return { - title: _t('confirmation_required'), - msg: _t('do_you_really_want_to_delete_this_ELEMENT', { ELEMENT: model }, 'messageformat') + title: _t('app.admin.events.confirmation_required'), + msg: _t('app.admin.events.do_you_really_want_to_delete_this_ELEMENT', { ELEMENT: model }) }; } } } , function () { // delete confirmed getModel(model)[0].delete(getModel(model)[1][index], null, function () { getModel(model)[1].splice(index, 1); } - , function () { growl.error(_t('unable_to_delete_an_error_occured')); }); + , function () { growl.error(_t('app.admin.events.unable_to_delete_an_error_occured')); }); }); }; @@ -292,10 +288,10 @@ Application.Controllers.controller('AdminEventsController', ['$scope', '$state', // save the price category to the API PriceCategory.save(p_cat, function (cat) { $scope.priceCategories.push(cat); - return growl.success(_t('price_category_successfully_created')); + return growl.success(_t('app.admin.events.price_category_successfully_created')); } , function (err) { - growl.error(_t('unable_to_add_the_price_category_check_name_already_used')); + growl.error(_t('app.admin.events.unable_to_add_the_price_category_check_name_already_used')); return console.error(err); }); }); @@ -308,7 +304,7 @@ Application.Controllers.controller('AdminEventsController', ['$scope', '$state', */ $scope.editPriceCategory = function (id, index) { if ($scope.priceCategories[index].id !== id) { - return growl.error(_t('unexpected_error_occurred_please_refresh')); + return growl.error(_t('app.admin.events.unexpected_error_occurred_please_refresh')); } else { return $uibModal.open({ templateUrl: '<%= asset_path "admin/events/price_form.html" %>', @@ -320,10 +316,10 @@ Application.Controllers.controller('AdminEventsController', ['$scope', '$state', // update the price category to the API PriceCategory.update({ id }, { price_category: p_cat }, function (cat) { $scope.priceCategories[index] = cat; - return growl.success(_t('price_category_successfully_updated')); + return growl.success(_t('app.admin.events.price_category_successfully_updated')); } , function (err) { - growl.error(_t('unable_to_update_the_price_category')); + growl.error(_t('app.admin.events.unable_to_update_the_price_category')); return console.error(err); }); }); @@ -337,17 +333,17 @@ Application.Controllers.controller('AdminEventsController', ['$scope', '$state', */ $scope.removePriceCategory = function (id, index) { if ($scope.priceCategories[index].id !== id) { - return growl.error(_t('unexpected_error_occurred_please_refresh')); + return growl.error(_t('app.admin.events.unexpected_error_occurred_please_refresh')); } else if ($scope.priceCategories[index].events > 0) { - return growl.error(_t('unable_to_delete_this_price_category_because_it_is_already_used')); + return growl.error(_t('app.admin.events.unable_to_delete_this_price_category_because_it_is_already_used')); } else { return dialogs.confirm( { resolve: { object () { return { - title: _t('confirmation_required'), - msg: _t('do_you_really_want_to_delete_this_price_category') + title: _t('app.admin.events.confirmation_required'), + msg: _t('app.admin.events.do_you_really_want_to_delete_this_price_category') }; } } @@ -356,10 +352,10 @@ Application.Controllers.controller('AdminEventsController', ['$scope', '$state', PriceCategory.remove( { id }, function () { // successfully deleted - growl.success(_t('price_category_successfully_deleted')); + growl.success(_t('app.admin.events.price_category_successfully_deleted')); $scope.priceCategories.splice(index, 1); }, - function () { growl.error(_t('price_category_deletion_failed')); } + function () { growl.error(_t('app.admin.events.price_category_deletion_failed')); } ); } ); @@ -378,12 +374,127 @@ Application.Controllers.controller('AdminEventsController', ['$scope', '$state', return $scope.page = 1; }; + + /** + * Setup the feature-tour for the admin/events page. + * This is intended as a contextual help (when pressing F1) + */ + $scope.setupEventsTour = function () { + // get the tour defined by the ui-tour directive + const uitour = uiTourService.getTourByName('events'); + uitour.createStep({ + selector: 'body', + stepId: 'welcome', + order: 0, + title: _t('app.admin.tour.events.welcome.title'), + content: _t('app.admin.tour.events.welcome.content'), + placement: 'bottom', + orphan: true + }); + uitour.createStep({ + selector: '.events-management .events-list', + stepId: 'list', + order: 1, + title: _t('app.admin.tour.events.list.title'), + content: _t('app.admin.tour.events.list.content'), + placement: 'top' + }); + uitour.createStep({ + selector: '.events-management .events-list-filter', + stepId: 'filter', + order: 2, + title: _t('app.admin.tour.events.filter.title'), + content: _t('app.admin.tour.events.filter.content'), + placement: 'bottom' + }); + uitour.createStep({ + selector: '.events-management .events-categories', + stepId: 'categories', + order: 3, + title: _t('app.admin.tour.events.categories.title'), + content: _t('app.admin.tour.events.categories.content'), + placement: 'bottom' + }); + uitour.createStep({ + selector: '.events-management .events-themes', + stepId: 'themes', + order: 4, + title: _t('app.admin.tour.events.themes.title'), + content: _t('app.admin.tour.events.themes.content'), + placement: 'top' + }); + uitour.createStep({ + selector: '.events-management .events-age-ranges', + stepId: 'ages', + order: 5, + title: _t('app.admin.tour.events.ages.title'), + content: _t('app.admin.tour.events.ages.content'), + placement: 'top' + }); + uitour.createStep({ + selector: '.events-management .prices-tab', + stepId: 'prices', + order: 6, + title: _t('app.admin.tour.events.prices.title'), + content: _t('app.admin.tour.events.prices.content'), + placement: 'bottom' + }); + uitour.createStep({ + selector: 'body', + stepId: 'conclusion', + order: 7, + title: _t('app.admin.tour.conclusion.title'), + content: _t('app.admin.tour.conclusion.content'), + placement: 'bottom', + orphan: true + }); + // on step change, change the active tab if needed + uitour.on('stepChanged', function (nextStep) { + if (nextStep.stepId === 'list' || nextStep.stepId === 'filter') { $scope.tabs.active = 0; } + if (nextStep.stepId === 'categories' || nextStep.stepId === 'ages') { $scope.tabs.active = 1; } + if (nextStep.stepId === 'prices') { $scope.tabs.active = 2; } + }); + // on tour end, save the status in database + uitour.on('ended', function () { + if (uitour.getStatus() === uitour.Status.ON && $scope.currentUser.profile.tours.indexOf('events') < 0) { + Member.completeTour({ id: $scope.currentUser.id }, { tour: 'events' }, function (res) { + $scope.currentUser.profile.tours = res.tours; + }); + } + }); + // if the user has never seen the tour, show him now + if (Fablab.featureTourDisplay !== 'manual' && $scope.currentUser.profile.tours.indexOf('events') < 0) { + uitour.start(); + } + // start this tour when an user press F1 - this is contextual help + window.addEventListener('keydown', handleF1); + } + /* PRIVATE SCOPE */ /** * Kind of constructor: these actions will be realized first when the controller is loaded */ - const initialize = function () { paginationCheck(eventsPromise, $scope.events); }; + const initialize = function () { + paginationCheck(eventsPromise, $scope.events); + + // listen the $destroy event of the controller to remove the F1 key binding + $scope.$on('$destroy', function () { + window.removeEventListener('keydown', handleF1); + }); + }; + + /** + * Callback used to trigger the feature tour when the user press the F1 key. + * @param e {KeyboardEvent} + */ + const handleF1 = function (e) { + if (e.key === 'F1') { + e.preventDefault(); + const tour = uiTourService.getTourByName('events'); + if (tour) { tour.start(); } + } + }; /** * Check if all events are already displayed OR if the button 'load more events' @@ -391,7 +502,7 @@ Application.Controllers.controller('AdminEventsController', ['$scope', '$state', * @param lastEvents {Array} last events loaded onto the diplay (ie. last "page") * @param events {Array} full list of events displayed on the page (not only the last retrieved) */ - var paginationCheck = function (lastEvents, events) { + const paginationCheck = function (lastEvents, events) { if (lastEvents.length > 0) { if (events.length >= lastEvents[0].nb_total_events) { return $scope.paginateActive = false; @@ -408,7 +519,7 @@ Application.Controllers.controller('AdminEventsController', ['$scope', '$state', * @param name {string} 'category', 'theme' or 'age_range' * @return {[Object, Array]} model and datastore */ - var getModel = function (name) { + const getModel = function (name) { switch (name) { case 'category': return [Category, $scope.categories]; case 'theme': return [EventTheme, $scope.themes]; @@ -431,7 +542,16 @@ Application.Controllers.controller('ShowEventReservationsController', ['$scope', $scope.event = eventPromise; // list of reservations for the current event - return $scope.reservations = reservationsPromise; + $scope.reservations = reservationsPromise; + + /** + * Test if the provided reservation has been cancelled + * @param reservation {Reservation} + * @returns {boolean} + */ + $scope.isCancelled = function(reservation) { + return !!(reservation.slots[0].canceled_at); + } }]); /** @@ -474,13 +594,25 @@ Application.Controllers.controller('NewEventController', ['$scope', '$state', 'C // Possible types of recurrences for an event $scope.recurrenceTypes = [ - { label: _t('none'), value: 'none' }, - { label: _t('every_days'), value: 'day' }, - { label: _t('every_week'), value: 'week' }, - { label: _t('every_month'), value: 'month' }, - { label: _t('every_year'), value: 'year' } + { label: _t('app.admin.events_new.none'), value: 'none' }, + { label: _t('app.admin.events_new.every_days'), value: 'day' }, + { label: _t('app.admin.events_new.every_week'), value: 'week' }, + { label: _t('app.admin.events_new.every_month'), value: 'month' }, + { label: _t('app.admin.events_new.every_year'), value: 'year' } ]; + // triggered when the new event form was submitted to the API and have received an answer + $scope.onSubmited = function(content) { + if ((content.id == null)) { + $scope.alerts = []; + angular.forEach(content, function (v, k) { + angular.forEach(v, function (err) { $scope.alerts.push({ msg: k + ': ' + err, type: 'danger' }); }); + }); + } else { + $state.go('app.public.events_list'); + } + }; + // Using the EventsController return new EventsController($scope, $state); } @@ -489,9 +621,9 @@ Application.Controllers.controller('NewEventController', ['$scope', '$state', 'C /** * Controller used in the events edition page */ -Application.Controllers.controller('EditEventController', ['$scope', '$state', '$stateParams', 'CSRF', 'eventPromise', 'categoriesPromise', 'themesPromise', 'ageRangesPromise', 'priceCategoriesPromise', - function ($scope, $state, $stateParams, CSRF, eventPromise, categoriesPromise, themesPromise, ageRangesPromise, priceCategoriesPromise) { - /* PUBLIC SCOPE */ +Application.Controllers.controller('EditEventController', ['$scope', '$state', '$stateParams', 'CSRF', 'eventPromise', 'categoriesPromise', 'themesPromise', 'ageRangesPromise', 'priceCategoriesPromise', '$uibModal', 'growl', '_t', + function ($scope, $state, $stateParams, CSRF, eventPromise, categoriesPromise, themesPromise, ageRangesPromise, priceCategoriesPromise, $uibModal, growl, _t) { + /* PUBLIC SCOPE */ // API URL where the form will be posted $scope.actionUrl = `/api/events/${$stateParams.id}`; @@ -502,6 +634,9 @@ 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; + // We'll keep track of the initial dates here, for later comparison + $scope.initialDates = {}; + // List of categories for the events $scope.categories = categoriesPromise; @@ -514,6 +649,83 @@ Application.Controllers.controller('EditEventController', ['$scope', '$state', ' // List of age ranges $scope.ageRanges = ageRangesPromise; + // Default edit-mode for periodic event + $scope.editMode = 'single'; + + // show edit-mode modal if event is recurrent + $scope.isShowEditModeModal = $scope.event.recurrence_events.length > 0; + + $scope.editRecurrent = function (e) { + if ($scope.isShowEditModeModal && $scope.event.recurrence_events.length > 0) { + e.preventDefault(); + + // open a choice edit-mode dialog + const modalInstance = $uibModal.open({ + animation: true, + templateUrl: '<%= asset_path "events/editRecurrent.html" %>', + size: 'md', + controller: 'EditRecurrentEventController', + resolve: { + editMode: function () { return $scope.editMode; }, + initialDates: function () { return $scope.initialDates; }, + currentEvent: function () { return $scope.event; } + } + }); + // submit form event by edit-mode + modalInstance.result.then(function(res) { + $scope.isShowEditModeModal = false; + $scope.editMode = res.editMode; + e.target.click(); + }); + } + }; + + // triggered when the edit event form was submitted to the API and have received an answer + $scope.onSubmited = function(data) { + if (data.total === data.updated) { + if (data.updated > 1) { + growl.success(_t( + 'app.admin.events_edit.events_updated', + {COUNT: data.updated - 1} + )); + } else { + growl.success(_t( + 'app.admin.events_edit.event_successfully_updated' + )); + } + } else { + if (data.total > 1) { + growl.warning(_t( + 'app.admin.events_edit.events_not_updated', + {TOTAL: data.total, COUNT: data.total - data.updated} + )); + if (_.find(data.details, { error: 'EventPriceCategory' })) { + growl.error(_t( + 'app.admin.events_edit.error_deleting_reserved_price' + )); + } else { + growl.error(_t( + 'app.admin.events_edit.other_error' + )); + } + } else { + growl.error(_t( + 'app.admin.events_edit.unable_to_update_the_event' + )); + if (data.details[0].error === 'EventPriceCategory') { + growl.error(_t( + 'app.admin.events_edit.error_deleting_reserved_price' + )); + } else { + growl.error(_t( + 'app.admin.events_edit.other_error' + )); + } + } + } + $state.go('app.public.events_list'); + }; + /* PRIVATE SCOPE */ /** @@ -526,6 +738,11 @@ Application.Controllers.controller('EditEventController', ['$scope', '$state', ' $scope.event.start_date = moment($scope.event.start_date).toDate(); $scope.event.end_date = moment($scope.event.end_date).toDate(); + $scope.initialDates = { + start: new Date($scope.event.start_date.valueOf()), + end: new Date($scope.event.end_date.valueOf()) + }; + // Using the EventsController return new EventsController($scope, $state); }; @@ -534,3 +751,36 @@ Application.Controllers.controller('EditEventController', ['$scope', '$state', ' return initialize(); } ]); + +/** + * Controller used in the event edit-mode modal window + */ +Application.Controllers.controller('EditRecurrentEventController', ['$scope', '$uibModalInstance', 'editMode', 'growl', 'initialDates', 'currentEvent', '_t', + function ($scope, $uibModalInstance, editMode, growl, initialDates, currentEvent, _t) { + // with recurrent slots: how many slots should we update? + $scope.editMode = editMode; + + /** + * Confirmation callback + */ + $scope.ok = function () { + $uibModalInstance.close({ + editMode: $scope.editMode + }); + } + + /** + * Test if any of the dates of the event has changed + */ + $scope.hasDateChanged = function() { + return (!moment(initialDates.start).isSame(currentEvent.start_date, 'day') || !moment(initialDates.end).isSame(currentEvent.end_date, 'day')); + } + + /** + * Cancellation callback + */ + $scope.cancel = function () { + $uibModalInstance.dismiss('cancel'); + } + } +]); diff --git a/app/assets/javascripts/controllers/admin/graphs.js b/app/assets/javascripts/controllers/admin/graphs.js index e0448f35f..3883405e0 100644 --- a/app/assets/javascripts/controllers/admin/graphs.js +++ b/app/assets/javascripts/controllers/admin/graphs.js @@ -25,10 +25,10 @@ Application.Controllers.controller('GraphsController', ['$scope', '$state', '$ro const CHART_HEIGHT = 500; // Label of the charts' horizontal axes - const X_AXIS_LABEL = _t('date'); + const X_AXIS_LABEL = _t('app.admin.stats_graphs.date'); // Label of the charts' vertical axes - const Y_AXIS_LABEL = _t('number'); + const Y_AXIS_LABEL = _t('app.admin.stats_graphs.number'); // Colors for the line charts. Each new line uses the next color in this array const CHART_COLORS = ['#b35a94', '#1c5794', '#00b49e', '#6fac48', '#ebcf4a', '#fd7e33', '#ca3436', '#a26e3a']; @@ -193,9 +193,9 @@ Application.Controllers.controller('GraphsController', ['$scope', '$state', '$ro } } else if ($scope.display.interval === 'week') { if ((typeof x === 'number') || d instanceof Date) { - return d3.time.format(_t('week_short') + ' %U')(moment(d).toDate()); + return d3.time.format(_t('app.admin.stats_graphs.week_short') + ' %U')(moment(d).toDate()); } else if (typeof d === 'number') { - return _t('week_of_START_to_END', { START: moment(d).format('L'), END: moment(d).add(6, 'days').format('L') }); + return _t('app.admin.stats_graphs.week_of_START_to_END', { START: moment(d).format('L'), END: moment(d).add(6, 'days').format('L') }); } else { // typeof d == 'string' return d; } @@ -653,7 +653,7 @@ Application.Controllers.controller('GraphsController', ['$scope', '$state', '$ro // common for each charts chart.margin({ left: 100, right: 100 }); - chart.noData(_t('no_data_for_this_period')); + chart.noData(_t('app.admin.stats_graphs.no_data_for_this_period')); chart.height(CHART_HEIGHT); // add new chart to the page diff --git a/app/assets/javascripts/controllers/admin/groups.js b/app/assets/javascripts/controllers/admin/groups.js index 1f174ba5b..e102117c6 100644 --- a/app/assets/javascripts/controllers/admin/groups.js +++ b/app/assets/javascripts/controllers/admin/groups.js @@ -54,15 +54,15 @@ Application.Controllers.controller('GroupsController', ['$scope', 'groupsPromise */ $scope.saveGroup = function (data, id) { if (id != null) { - return Group.update({ id }, { group: data }, response => growl.success(_t('group_form.changes_successfully_saved')) - , error => growl.error(_t('group_form.an_error_occurred_while_saving_changes'))); + return Group.update({ id }, { group: data }, response => growl.success(_t('app.admin.members.group_form.changes_successfully_saved')) + , error => growl.error(_t('app.admin.members.group_form.an_error_occurred_while_saving_changes'))); } else { return Group.save({ group: data }, function (resp) { - growl.success(_t('group_form.new_group_successfully_saved')); + growl.success(_t('app.admin.members.group_form.new_group_successfully_saved')); return $scope.groups[$scope.groups.length - 1].id = resp.id; } , function (error) { - growl.error(_t('.group_forman_error_occurred_when_saving_the_new_group')); + growl.error(_t('app.admin.members.group_form.an_error_occurred_when_saving_the_new_group')); return $scope.groups.splice($scope.groups.length - 1, 1); }); } @@ -74,10 +74,10 @@ Application.Controllers.controller('GroupsController', ['$scope', 'groupsPromise */ $scope.removeGroup = index => Group.delete({ id: $scope.groups[index].id }, function (resp) { - growl.success(_t('group_form.group_successfully_deleted')); + growl.success(_t('app.admin.members.group_form.group_successfully_deleted')); return $scope.groups.splice(index, 1); } - , error => growl.error(_t('group_form.unable_to_delete_group_because_some_users_and_or_groups_are_still_linked_to_it'))); + , error => growl.error(_t('app.admin.members.group_form.unable_to_delete_group_because_some_users_and_or_groups_are_still_linked_to_it'))); /** * Enable/disable the group at the specified index @@ -86,13 +86,13 @@ Application.Controllers.controller('GroupsController', ['$scope', 'groupsPromise return $scope.toggleDisableGroup = function (index) { const group = $scope.groups[index]; if (!group.disabled && (group.users > 0)) { - return growl.error(_t('group_form.unable_to_disable_group_with_users', { USERS: group.users }, 'messageformat')); + return growl.error(_t('app.admin.members.group_form.unable_to_disable_group_with_users', { USERS: group.users })); } else { return Group.update({ id: group.id }, { group: { disabled: !group.disabled } }, function (response) { $scope.groups[index] = response; - return growl.success(_t('group_form.group_successfully_enabled_disabled', { STATUS: response.disabled }, 'messageformat')); + return growl.success(_t('app.admin.members.group_form.group_successfully_enabled_disabled', { STATUS: response.disabled })); } - , error => growl.error(_t('group_form.unable_to_enable_disable_group', { STATUS: !group.disabled }, 'messageformat'))); + , error => growl.error(_t('app.admin.members.group_form.unable_to_enable_disable_group', { STATUS: !group.disabled }))); } }; } diff --git a/app/assets/javascripts/controllers/admin/invoices.js.erb b/app/assets/javascripts/controllers/admin/invoices.js.erb index 1fa010392..9c7a72356 100644 --- a/app/assets/javascripts/controllers/admin/invoices.js.erb +++ b/app/assets/javascripts/controllers/admin/invoices.js.erb @@ -17,8 +17,8 @@ /** * Controller used in the admin invoices listing page */ -Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'Invoice', 'AccountingPeriod', 'invoices', 'closedPeriods', '$uibModal', 'growl', '$filter', 'Setting', 'settings', '_t', - function ($scope, $state, Invoice, AccountingPeriod, invoices, closedPeriods, $uibModal, growl, $filter, Setting, settings, _t) { +Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'Invoice', 'AccountingPeriod', 'invoices', 'closedPeriods', '$uibModal', 'growl', '$filter', 'Setting', 'settings', '_t', 'Member', 'uiTourService', + function ($scope, $state, Invoice, AccountingPeriod, invoices, closedPeriods, $uibModal, growl, $filter, Setting, settings, _t, Member, uiTourService) { /* PRIVATE STATIC CONSTANTS */ // number of invoices loaded each time we click on 'load more...' @@ -28,11 +28,10 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I // default active tab $scope.tabs = { - listing: { active: !Fablab.withoutInvoices }, - settings: { active: Fablab.withoutInvoices } + active: Fablab.withoutInvoices ? 1 : 0 }; - // List of all users invoices + // List of all invoices $scope.invoices = invoices; // Invoices filters @@ -215,7 +214,7 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I $scope.invoices.unshift(res.avoir); return Invoice.get({ id: invoice.id }, function (data) { invoice.has_avoir = data.has_avoir; - return growl.success(_t('invoices.refund_invoice_successfully_created')); + return growl.success(_t('app.admin.invoices.refund_invoice_successfully_created')); }); }); }; @@ -246,7 +245,7 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I }; /** - * Generate an order nmuber sample from the parametrized model + * Generate an order number sample from the parametrized model * @returns {string} invoice reference sample */ $scope.mkNumber = function () { @@ -289,10 +288,10 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I modalInstance.result.then(function (model) { Setting.update({ name: 'invoice_reference' }, { value: model }, function (data) { $scope.invoice.reference.model = model; - growl.success(_t('invoices.invoice_reference_successfully_saved')); + growl.success(_t('app.admin.invoices.invoice_reference_successfully_saved')); } , function (error) { - growl.error(_t('invoices.an_error_occurred_while_saving_invoice_reference')); + growl.error(_t('app.admin.invoices.an_error_occurred_while_saving_invoice_reference')); console.error(error); }); }); @@ -327,24 +326,24 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I Setting.update({ name: 'invoice_code-value' }, { value: result.model }, function (data) { $scope.invoice.code.model = result.model; if (result.active) { - return growl.success(_t('invoices.invoicing_code_succesfully_saved')); + return growl.success(_t('app.admin.invoices.invoicing_code_succesfully_saved')); } } , function (error) { - growl.error(_t('invoices.an_error_occurred_while_saving_the_invoicing_code')); + growl.error(_t('app.admin.invoices.an_error_occurred_while_saving_the_invoicing_code')); return console.error(error); }); return Setting.update({ name: 'invoice_code-active' }, { value: result.active ? 'true' : 'false' }, function (data) { $scope.invoice.code.active = result.active; if (result.active) { - return growl.success(_t('invoices.code_successfully_activated')); + return growl.success(_t('app.admin.invoices.code_successfully_activated')); } else { - return growl.success(_t('invoices.code_successfully_disabled')); + return growl.success(_t('app.admin.invoices.code_successfully_disabled')); } } , function (error) { - growl.error(_t('invoices.an_error_occurred_while_activating_the_invoicing_code')); + growl.error(_t('app.admin.invoices.an_error_occurred_while_activating_the_invoicing_code')); return console.error(error); }); }); @@ -373,10 +372,10 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I return modalInstance.result.then(function (model) { Setting.update({ name: 'invoice_order-nb' }, { value: model }, function (data) { $scope.invoice.number.model = model; - return growl.success(_t('invoices.order_number_successfully_saved')); + return growl.success(_t('app.admin.invoices.order_number_successfully_saved')); } , function (error) { - growl.error(_t('invoices.an_error_occurred_while_saving_the_order_number')); + growl.error(_t('app.admin.invoices.an_error_occurred_while_saving_the_order_number')); return console.error(error); }); }); @@ -431,24 +430,24 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I Setting.update({ name: 'invoice_VAT-rate' }, { value: result.rate + '' }, function (data) { $scope.invoice.VAT.rate = result.rate; if (result.active) { - return growl.success(_t('invoices.VAT_rate_successfully_saved')); + return growl.success(_t('app.admin.invoices.VAT_rate_successfully_saved')); } } , function (error) { - growl.error(_t('invoices.an_error_occurred_while_saving_the_VAT_rate')); + growl.error(_t('app.admin.invoices.an_error_occurred_while_saving_the_VAT_rate')); return console.error(error); }); return Setting.update({ name: 'invoice_VAT-active' }, { value: result.active ? 'true' : 'false' }, function (data) { $scope.invoice.VAT.active = result.active; if (result.active) { - return growl.success(_t('invoices.VAT_successfully_activated')); + return growl.success(_t('app.admin.invoices.VAT_successfully_activated')); } else { - return growl.success(_t('invoices.VAT_successfully_disabled')); + return growl.success(_t('app.admin.invoices.VAT_successfully_disabled')); } } , function (error) { - growl.error(_t('invoices.an_error_occurred_while_activating_the_VAT')); + growl.error(_t('app.admin.invoices.an_error_occurred_while_activating_the_VAT')); return console.error(error); }); }); @@ -461,10 +460,10 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I const parsed = parseHtml($scope.invoice.text.content); return Setting.update({ name: 'invoice_text' }, { value: parsed }, function (data) { $scope.invoice.text.content = parsed; - return growl.success(_t('invoices.text_successfully_saved')); + return growl.success(_t('app.admin.invoices.text_successfully_saved')); } , function (error) { - growl.error(_t('invoices.an_error_occurred_while_saving_the_text')); + growl.error(_t('app.admin.invoices.an_error_occurred_while_saving_the_text')); return console.error(error); }); }; @@ -476,10 +475,10 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I const parsed = parseHtml($scope.invoice.legals.content); return Setting.update({ name: 'invoice_legals' }, { value: parsed }, function (data) { $scope.invoice.legals.content = parsed; - return growl.success(_t('invoices.address_and_legal_information_successfully_saved')); + return growl.success(_t('app.admin.invoices.address_and_legal_information_successfully_saved')); } , function (error) { - growl.error(_t('invoices.an_error_occurred_while_saving_the_address_and_the_legal_information')); + growl.error(_t('app.admin.invoices.an_error_occurred_while_saving_the_address_and_the_legal_information')); return console.error(error); }); }; @@ -552,7 +551,7 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I $scope.save = function() { Setting.bulkUpdate( { settings: Object.values($scope.settings) }, - function () { growl.success(_t('invoices.codes_customization_success')); }, + function () { growl.success(_t('app.admin.invoices.codes_customization_success')); }, function (error) { growl.error('unexpected_error_occurred'); console.error(error); @@ -560,6 +559,126 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I ); } + /** + * Setup the feature-tour for the admin/invoices page. + * This is intended as a contextual help (when pressing F1) + */ + $scope.setupInvoicesTour = function () { + // get the tour defined by the ui-tour directive + const uitour = uiTourService.getTourByName('invoices'); + uitour.createStep({ + selector: 'body', + stepId: 'welcome', + order: 0, + title: _t('app.admin.tour.invoices.welcome.title'), + content: _t('app.admin.tour.invoices.welcome.content'), + placement: 'bottom', + orphan: true + }); + if (!Fablab.withoutInvoices && $scope.invoices.length > 0) { + uitour.createStep({ + selector: '.invoices-management .invoices-list', + stepId: 'list', + order: 1, + title: _t('app.admin.tour.invoices.list.title'), + content: _t('app.admin.tour.invoices.list.content'), + placement: 'top' + }); + uitour.createStep({ + selector: '.invoices-management .invoices-list .chained-indicator', + stepId: 'chained', + order: 2, + title: _t('app.admin.tour.invoices.chained.title'), + content: _t('app.admin.tour.invoices.chained.content'), + placement: 'right' + }); + uitour.createStep({ + selector: '.invoices-management .invoices-list .download-button', + stepId: 'download', + order: 3, + title: _t('app.admin.tour.invoices.download.title'), + content: _t('app.admin.tour.invoices.download.content'), + placement: 'left' + }); + uitour.createStep({ + selector: '.invoices-management .invoices-list .refund-button', + stepId: 'refund', + order: 4, + title: _t('app.admin.tour.invoices.refund.title'), + content: _t('app.admin.tour.invoices.refund.content'), + placement: 'left' + }); + } + uitour.createStep({ + selector: '.invoices-management .invoices-settings', + stepId: 'settings', + order: 5, + title: _t('app.admin.tour.invoices.settings.title'), + content: _t('app.admin.tour.invoices.settings.content'), + placement: 'bottom' + }); + uitour.createStep({ + selector: '.invoices-management .accounting-codes-tab', + stepId: 'codes', + order: 6, + title: _t('app.admin.tour.invoices.codes.title'), + content: _t('app.admin.tour.invoices.codes.content'), + placement: 'bottom' + }); + uitour.createStep({ + selector: '.heading .export-accounting-button', + stepId: 'export', + order: 7, + title: _t('app.admin.tour.invoices.export.title'), + content: _t('app.admin.tour.invoices.export.content'), + placement: 'bottom' + }); + uitour.createStep({ + selector: '.heading .close-accounting-periods-button', + stepId: 'periods', + order: 8, + title: _t('app.admin.tour.invoices.periods.title'), + content: _t('app.admin.tour.invoices.periods.content'), + placement: 'bottom', + popupClass: 'shift-left-50' + }); + uitour.createStep({ + selector: 'body', + stepId: 'conclusion', + order: 9, + title: _t('app.admin.tour.conclusion.title'), + content: _t('app.admin.tour.conclusion.content'), + placement: 'bottom', + orphan: true + }); + // on step change, change the active tab if needed + uitour.on('stepChanged', function (nextStep) { + if (nextStep.stepId === 'list' || nextStep.stepId === 'settings') { + $scope.tabs.active = 0; + } + if (nextStep.stepId === 'settings') { + $scope.tabs.active = 1; + } + if (nextStep.stepId === 'codes' || nextStep.stepId === 'export') { + $scope.tabs.active = 2; + } + }); + // on tour end, save the status in database + uitour.on('ended', function () { + if (uitour.getStatus() === uitour.Status.ON && $scope.currentUser.profile.tours.indexOf('invoices') < 0) { + Member.completeTour({ id: $scope.currentUser.id }, { tour: 'invoices' }, function (res) { + $scope.currentUser.profile.tours = res.tours; + }); + } + }); + // if the user has never seen the tour, show him now + if (Fablab.featureTourDisplay !== 'manual' && $scope.currentUser.profile.tours.indexOf('invoices') < 0) { + uitour.start(); + } + // start this tour when an user press F1 - this is contextual help + window.addEventListener('keydown', handleF1); + } + /* PRIVATE SCOPE */ /** @@ -586,19 +705,24 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I }; // Watch the logo, when a change occurs, save it - return $scope.$watch('invoice.logo', function () { + $scope.$watch('invoice.logo', function () { if ($scope.invoice.logo && $scope.invoice.logo.filesize) { return Setting.update( { name: 'invoice_logo' }, { value: $scope.invoice.logo.base64 }, - function (data) { growl.success(_t('invoices.logo_successfully_saved')); }, + function (data) { growl.success(_t('app.admin.invoices.logo_successfully_saved')); }, function (error) { - growl.error(_t('invoices.an_error_occurred_while_saving_the_logo')); + growl.error(_t('app.admin.invoices.an_error_occurred_while_saving_the_logo')); return console.error(error); } ); } }); + + // listen the $destroy event of the controller to remove the F1 key binding + $scope.$on('$destroy', function () { + window.removeEventListener('keydown', handleF1); + }); }; /** @@ -666,6 +790,18 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I }); }; + /** + * Callback used to trigger the feature tour when the user press the F1 key. + * @param e {KeyboardEvent} + */ + const handleF1 = function (e) { + if (e.key === 'F1') { + e.preventDefault(); + const tour = uiTourService.getTourByName('invoices'); + if (tour) { tour.start(); } + } + }; + // !!! MUST BE CALLED AT THE END of the controller return initialize(); } @@ -696,17 +832,17 @@ Application.Controllers.controller('AvoirModalController', ['$scope', '$uibModal // Possible refunding methods $scope.avoirModes = [ - { name: _t('invoices.none'), value: 'none' }, - { name: _t('invoices.by_cash'), value: 'cash' }, - { name: _t('invoices.by_cheque'), value: 'cheque' }, - { name: _t('invoices.by_transfer'), value: 'transfer' }, - { name: _t('invoices.by_wallet'), value: 'wallet' } + { name: _t('app.admin.invoices.none'), value: 'none' }, + { name: _t('app.admin.invoices.by_cash'), value: 'cash' }, + { name: _t('app.admin.invoices.by_cheque'), value: 'cheque' }, + { name: _t('app.admin.invoices.by_transfer'), value: 'transfer' }, + { name: _t('app.admin.invoices.by_wallet'), value: 'wallet' } ]; // If a subscription was took with the current invoice, should it be canceled or not $scope.subscriptionExpireOptions = {}; - $scope.subscriptionExpireOptions[_t('yes')] = true; - $scope.subscriptionExpireOptions[_t('no')] = false; + $scope.subscriptionExpireOptions[_t('app.shared.buttons.yes')] = true; + $scope.subscriptionExpireOptions[_t('app.shared.buttons.no')] = false; // AngularUI-Bootstrap datepicker parameters to define when to refund $scope.datePicker = { @@ -742,7 +878,7 @@ Application.Controllers.controller('AvoirModalController', ['$scope', '$uibModal } if ($scope.avoir.invoice_items_ids.length === 0) { - return growl.error(_t('invoices.you_must_select_at_least_one_element_to_create_a_refund')); + return growl.error(_t('app.admin.invoices.you_must_select_at_least_one_element_to_create_a_refund')); } else { return Invoice.save( { avoir: $scope.avoir }, @@ -750,7 +886,7 @@ Application.Controllers.controller('AvoirModalController', ['$scope', '$uibModal $uibModalInstance.close({ avoir, invoice: $scope.invoice }); }, function (err) { // failed - growl.error(_t('invoices.unable_to_create_the_refund')); + growl.error(_t('app.admin.invoices.unable_to_create_the_refund')); } ); } @@ -791,7 +927,7 @@ Application.Controllers.controller('AvoirModalController', ['$scope', '$uibModal }); if (invoice.stripe) { - return $scope.avoirModes.push({ name: _t('invoices.online_payment'), value: 'stripe' }); + return $scope.avoirModes.push({ name: _t('app.admin.invoices.online_payment'), value: 'stripe' }); } }; @@ -861,16 +997,16 @@ Application.Controllers.controller('ClosePeriodModalController', ['$scope', '$ui resolve: { object () { return { - title: _t('invoices.confirmation_required'), + title: _t('app.admin.invoices.confirmation_required'), msg: $sce.trustAsHtml( _t( 'invoices.confirm_close_START_END', { START: moment.utc($scope.period.start_at).format('LL'), END: moment.utc($scope.period.end_at).format('LL') } ) + '

' - + _t('invoices.period_must_match_fiscal_year') + + _t('app.admin.invoices.period_must_match_fiscal_year') + '

' - + _t('invoices.this_may_take_a_while') + + _t('app.admin.invoices.this_may_take_a_while') ) }; } @@ -895,7 +1031,7 @@ Application.Controllers.controller('ClosePeriodModalController', ['$scope', '$ui }, function(error) { $scope.pendingCreation = false; - growl.error(_t('invoices.failed_to_close_period')); + growl.error(_t('app.admin.invoices.failed_to_close_period')); $scope.errors = error.data; } ); @@ -982,7 +1118,7 @@ Application.Controllers.controller('AccountingExportModalController', ['$scope', Export.status(statusQry).then(function (res) { if (!res.data.exists) { - growl.success(_t('invoices.export_is_running')); + growl.success(_t('app.admin.invoices.export_is_running')); } $uibModalInstance.close(res); }); diff --git a/app/assets/javascripts/controllers/admin/members.js.erb b/app/assets/javascripts/controllers/admin/members.js.erb index 023f53dfe..d1bfa3fb8 100644 --- a/app/assets/javascripts/controllers/admin/members.js.erb +++ b/app/assets/javascripts/controllers/admin/members.js.erb @@ -126,8 +126,8 @@ class MembersController { /** * Controller used in the members/groups management page */ -Application.Controllers.controller('AdminMembersController', ['$scope', '$sce', 'membersPromise', 'adminsPromise', 'growl', 'Admin', 'dialogs', '_t', 'Member', 'Export', - function ($scope, $sce, membersPromise, adminsPromise, growl, Admin, dialogs, _t, Member, Export) { +Application.Controllers.controller('AdminMembersController', ['$scope', '$sce', 'membersPromise', 'adminsPromise', 'growl', 'Admin', 'dialogs', '_t', 'Member', 'Export', 'uiTourService', + function ($scope, $sce, membersPromise, adminsPromise, growl, Admin, dialogs, _t, Member, Export, uiTourService) { /* PRIVATE STATIC CONSTANTS */ // number of users loaded each time we click on 'load more...' @@ -146,7 +146,15 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce', // currently displayed page of members page: 1, // true when all members where loaded - noMore: false + noMore: false, + // default filter for members + memberFilter: 'all', + // options for members filtering + memberFilters: [ + 'all', + 'not_confirmed', + 'inactive_for_3_years' + ] }; // admins list @@ -155,6 +163,9 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce', // Admins ordering/sorting. Default: not sorted $scope.orderAdmin = null; + // default tab: members list + $scope.tabs = { active: 0 }; + /** * Change the members ordering criterion to the one provided * @param orderBy {string} ordering criterion @@ -182,6 +193,35 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce', } }; + /** + * Ask for confirmation then delete the specified user + * @param memberId {number} identifier of the user to delete + */ + $scope.deleteMember = function(memberId) { + dialogs.confirm( + { + resolve: { + object () { + return { + title: _t('app.admin.members.confirmation_required'), + msg: $sce.trustAsHtml(_t('app.admin.members.confirm_delete_member') + '

' + _t('app.admin.members.this_may_take_a_while_please_wait')) + }; + } + } + }, + function () { // cancel confirmed + Member.delete( + { id: memberId }, + function () { + $scope.members.splice(findItemIdxById($scope.members, memberId), 1); + return growl.success(_t('app.admin.members.member_successfully_deleted')); + }, + function (error) { growl.error(_t('app.admin.members.unable_to_delete_the_member')); } + ); + } + ); + } + /** * Ask for confirmation then delete the specified administrator * @param admins {Array} full list of administrators @@ -193,8 +233,8 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce', resolve: { object () { return { - title: _t('confirmation_required'), - msg: $sce.trustAsHtml(_t('do_you_really_want_to_delete_this_administrator_this_cannot_be_undone') + '

' + _t('this_may_take_a_while_please_wait')) + title: _t('app.admin.members.confirmation_required'), + msg: $sce.trustAsHtml(_t('app.admin.members.do_you_really_want_to_delete_this_administrator_this_cannot_be_undone') + '

' + _t('app.admin.members.this_may_take_a_while_please_wait')) }; } } @@ -203,10 +243,10 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce', Admin.delete( { id: admin.id }, function () { - admins.splice(findAdminIdxById(admins, admin.id), 1); - return growl.success(_t('administrator_successfully_deleted')); + admins.splice(findItemIdxById(admins, admin.id), 1); + return growl.success(_t('app.admin.members.administrator_successfully_deleted')); }, - function (error) { growl.error(_t('unable_to_delete_the_administrator')); } + function (error) { growl.error(_t('app.admin.members.unable_to_delete_the_administrator')); } ); } ); @@ -232,6 +272,14 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce', }, 300); }; + /** + * Callback when the member filter changes: reload the search results + */ + $scope.updateMemberFilter = function () { + resetSearchMember(); + memberSearch(); + }; + /** * Callback to alert the admin that the export request was acknowledged and is * processing right now. @@ -239,11 +287,154 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce', $scope.alertExport = function (type) { Export.status({ category: 'users', type }).then(function (res) { if (!res.data.exists) { - return growl.success(_t('export_is_running_you_ll_be_notified_when_its_ready')); + return growl.success(_t('app.admin.members.export_is_running_you_ll_be_notified_when_its_ready')); } }); }; + + /** + * Setup the feature-tour for the admin/members page. + * This is intended as a contextual help (when pressing F1) + */ + $scope.setupMembersTour = function () { + // get the tour defined by the ui-tour directive + const uitour = uiTourService.getTourByName('members'); + uitour.createStep({ + selector: 'body', + stepId: 'welcome', + order: 0, + title: _t('app.admin.tour.members.welcome.title'), + content: _t('app.admin.tour.members.welcome.content'), + placement: 'bottom', + orphan: true + }); + uitour.createStep({ + selector: '.members-management .members-list', + stepId: 'list', + order: 1, + title: _t('app.admin.tour.members.list.title'), + content: _t('app.admin.tour.members.list.content'), + placement: 'top' + }); + uitour.createStep({ + selector: '.members-management .search-members', + stepId: 'search', + order: 2, + title: _t('app.admin.tour.members.search.title'), + content: _t('app.admin.tour.members.search.content'), + placement: 'bottom' + }); + uitour.createStep({ + selector: '.members-management .filter-members', + stepId: 'filter', + order: 3, + title: _t('app.admin.tour.members.filter.title'), + content: _t('app.admin.tour.members.filter.content'), + placement: 'bottom' + }); + if ($scope.members.length > 0) { + uitour.createStep({ + selector: '.members-management .members-list .buttons', + stepId: 'actions', + order: 4, + title: _t('app.admin.tour.members.actions.title'), + content: _t('app.admin.tour.members.actions.content'), + placement: 'left' + }); + } + uitour.createStep({ + selector: '.members-management .exports-buttons', + stepId: 'exports', + order: 5, + title: _t('app.admin.tour.members.exports.title'), + content: _t('app.admin.tour.members.exports.content'), + placement: 'bottom' + }); + uitour.createStep({ + selector: '.heading .import-members', + stepId: 'import', + order: 6, + title: _t('app.admin.tour.members.import.title'), + content: _t('app.admin.tour.members.import.content'), + placement: 'left' + }); + uitour.createStep({ + selector: '.members-management .admins-tab', + stepId: 'admins', + order: 7, + title: _t('app.admin.tour.members.admins.title'), + content: _t('app.admin.tour.members.admins.content'), + placement: 'bottom' + }); + uitour.createStep({ + selector: '.members-management .groups-tab', + stepId: 'groups', + order: 8, + title: _t('app.admin.tour.members.groups.title'), + content: _t('app.admin.tour.members.groups.content'), + placement: 'bottom' + }); + uitour.createStep({ + selector: '.members-management .labels-tab', + stepId: 'labels', + order: 9, + title: _t('app.admin.tour.members.labels.title'), + content: _t('app.admin.tour.members.labels.content'), + placement: 'bottom' + }); + uitour.createStep({ + selector: '.members-management .sso-tab', + stepId: 'sso', + order: 10, + title: _t('app.admin.tour.members.sso.title'), + content: _t('app.admin.tour.members.sso.content'), + placement: 'bottom', + popupClass: 'shift-left-50' + }); + uitour.createStep({ + selector: 'body', + stepId: 'conclusion', + order: 11, + title: _t('app.admin.tour.conclusion.title'), + content: _t('app.admin.tour.conclusion.content'), + placement: 'bottom', + orphan: true + }); + // on step change, change the active tab if needed + uitour.on('stepChanged', function (nextStep) { + if (nextStep.stepId === 'list' || nextStep.stepId === 'import') { + $scope.tabs.active = 0; + } + if (nextStep.stepId === 'admins') { + $scope.tabs.active = 1; + } + if (nextStep.stepId === 'groups') { + $scope.tabs.active = 2; + } + if (nextStep.stepId === 'labels') { + $scope.tabs.active = 3; + } + if (nextStep.stepId === 'sso') { + $scope.tabs.active = 4; + } + }); + // on tour end, save the status in database + uitour.on('ended', function () { + if (uitour.getStatus() === uitour.Status.ON && $scope.currentUser.profile.tours.indexOf('members') < 0) { + Member.completeTour({ id: $scope.currentUser.id }, { tour: 'members' }, function (res) { + $scope.currentUser.profile.tours = res.tours; + }); + } + }); + // if the user has never seen the tour, show him now + if (Fablab.featureTourDisplay !== 'manual' && $scope.currentUser.profile.tours.indexOf('members') < 0) { + uitour.start(); + } + // start this tour when an user press F1 - this is contextual help + window.addEventListener('keydown', handleF1); + } + /* PRIVATE SCOPE */ /** @@ -253,6 +444,9 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce', if (!membersPromise[0] || (membersPromise[0].maxMembers <= $scope.members.length)) { return $scope.member.noMore = true; } + $scope.$on('$destroy', function () { + window.removeEventListener('keydown', handleF1); + }); }; /** @@ -261,13 +455,13 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce', var searchTimeout = null; /** - * Iterate through the provided array and return the index of the requested admin - * @param admins {Array} full list of users with role 'admin' - * @param id {Number} user id of the admin to retrieve in the list - * @returns {Number} index of the requested admin, in the provided array + * Iterate through the provided array and return the index of the requested item + * @param items {Array} full list of users with role 'admin' + * @param id {Number} id of the item to retrieve in the list + * @returns {Number} index of the requested item, in the provided array */ - var findAdminIdxById = function (admins, id) { - return (admins.map(function (admin) { return admin.id; })).indexOf(id); + var findItemIdxById = function (items, id) { + return (items.map(function (item) { return item.id; })).indexOf(id); }; /** @@ -288,6 +482,7 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce', query: { search: $scope.member.searchText, order_by: $scope.member.order, + filter: $scope.member.memberFilter, page: $scope.member.page, size: USERS_PER_PAGE } @@ -304,6 +499,18 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce', }); }; + /** + * Callback used to trigger the feature tour when the user press the F1 key. + * @param e {KeyboardEvent} + */ + const handleF1 = function (e) { + if (e.key === 'F1') { + e.preventDefault(); + const tour = uiTourService.getTourByName('members'); + if (tour) { tour.start(); } + } + }; + // !!! MUST BE CALLED AT THE END of the controller return initialize(); } @@ -395,10 +602,10 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state', { id: subscription.id }, { subscription: { expired_at: $scope.new_expired_at, free } }, function (_subscription) { - growl.success(_t('you_successfully_changed_the_expiration_date_of_the_user_s_subscription')); + growl.success(_t('app.admin.members_edit.you_successfully_changed_the_expiration_date_of_the_user_s_subscription')); return $uibModalInstance.close(_subscription); }, - function (error) { growl.error(_t('a_problem_occurred_while_saving_the_date')); } + function (error) { growl.error(_t('app.admin.members_edit.a_problem_occurred_while_saving_the_date')); } ); }; @@ -442,12 +649,12 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state', $scope.ok = function () { $scope.subscription.user_id = user.id; return Subscription.save({ }, { subscription: $scope.subscription }, function (_subscription) { - growl.success(_t('subscription_successfully_purchased')); + growl.success(_t('app.admin.members_edit.subscription_successfully_purchased')); $uibModalInstance.close(_subscription); return $state.reload(); } , function (error) { - growl.error(_t('a_problem_occurred_while_taking_the_subscription')); + growl.error(_t('app.admin.members_edit.a_problem_occurred_while_taking_the_subscription')); console.error(error); }); }; @@ -458,7 +665,7 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state', $scope.cancel = function () { $uibModalInstance.dismiss('cancel'); }; }] }); - // once the form was validated succesfully ... + // once the form was validated successfully ... return modalInstance.result.then(function (subscription) { $scope.subscription = subscription; }); }; @@ -507,11 +714,11 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state', avoir_description: $scope.description }, function (_wallet) { - growl.success(_t('wallet_credit_successfully')); + growl.success(_t('app.shared.wallet.wallet_credit_successfully')); return $uibModalInstance.close(_wallet); }, function (error) { - growl.error(_t('a_problem_occurred_for_wallet_credit')); + growl.error(_t('app.shared.wallet.a_problem_occurred_for_wallet_credit')); console.error(error); } ); @@ -721,11 +928,12 @@ Application.Controllers.controller('NewAdminController', ['$state', '$scope', 'A {}, { admin: $scope.admin }, function () { - growl.success(_t('administrator_successfully_created_he_will_receive_his_connection_directives_by_email', { GENDER: getGender($scope.admin) }, 'messageformat')); + growl.success(_t('app.admin.admins_new.administrator_successfully_created_he_will_receive_his_connection_directives_by_email', { GENDER: getGender($scope.admin) })); return $state.go('app.admin.members'); } , function (error) { - console.log(error); + growl.error(_t('app.admin.admins_new.failed_to_create_admin') + JSON.stringify(error.data ? error.data : error)); + console.error(error); } ); }; diff --git a/app/assets/javascripts/controllers/admin/open_api_clients.js b/app/assets/javascripts/controllers/admin/open_api_clients.js index 3ded18d15..d30ba1a8e 100644 --- a/app/assets/javascripts/controllers/admin/open_api_clients.js +++ b/app/assets/javascripts/controllers/admin/open_api_clients.js @@ -10,9 +10,9 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -Application.Controllers.controller('OpenAPIClientsController', ['$scope', 'clientsPromise', 'growl', 'OpenAPIClient', 'dialogs', '_t', - function ($scope, clientsPromise, growl, OpenAPIClient, dialogs, _t) { - /* PUBLIC SCOPE */ +Application.Controllers.controller('OpenAPIClientsController', ['$scope', 'clientsPromise', 'growl', 'OpenAPIClient', 'dialogs', '_t', 'Member', 'uiTourService', + function ($scope, clientsPromise, growl, OpenAPIClient, dialogs, _t, Member, uiTourService) { + /* PUBLIC SCOPE */ // clients list $scope.clients = clientsPromise; @@ -37,12 +37,12 @@ Application.Controllers.controller('OpenAPIClientsController', ['$scope', 'clien if (client.id != null) { OpenAPIClient.update({ id: client.id }, { open_api_client: client }, function (clientResp) { client = clientResp; - return growl.success(_t('client_successfully_updated')); + return growl.success(_t('app.admin.open_api_clients.client_successfully_updated')); }); } else { OpenAPIClient.save({ open_api_client: client }, function (client) { $scope.clients.push(client); - return growl.success(_t('client_successfully_created')); + return growl.success(_t('app.admin.open_api_clients.client_successfully_created')); }); } @@ -61,8 +61,8 @@ Application.Controllers.controller('OpenAPIClientsController', ['$scope', 'clien resolve: { object () { return { - title: _t('confirmation_required'), - msg: _t('do_you_really_want_to_delete_this_open_api_client') + title: _t('app.admin.open_api_clients.confirmation_required'), + msg: _t('app.admin.open_api_clients.do_you_really_want_to_delete_this_open_api_client') }; } } @@ -70,17 +70,17 @@ Application.Controllers.controller('OpenAPIClientsController', ['$scope', 'clien , () => OpenAPIClient.delete({ id: $scope.clients[index].id }, function () { $scope.clients.splice(index, 1); - return growl.success(_t('client_successfully_deleted')); + return growl.success(_t('app.admin.open_api_clients.client_successfully_deleted')); }) ); - return $scope.resetToken = client => + $scope.resetToken = client => dialogs.confirm({ resolve: { object () { return { - title: _t('confirmation_required'), - msg: _t('do_you_really_want_to_revoke_this_open_api_access') + title: _t('app.admin.open_api_clients.confirmation_required'), + msg: _t('app.admin.open_api_clients.do_you_really_want_to_revoke_this_open_api_access') }; } } @@ -88,9 +88,85 @@ Application.Controllers.controller('OpenAPIClientsController', ['$scope', 'clien , () => OpenAPIClient.resetToken({ id: client.id }, {}, function (clientResp) { client.token = clientResp.token; - return growl.success(_t('access_successfully_revoked')); + return growl.success(_t('app.admin.open_api_clients.access_successfully_revoked')); }) ); + + /** + * Setup the feature-tour for the admin/open_api_clients page. + * This is intended as a contextual help (when pressing F1) + */ + $scope.setupOpenAPITour = function () { + // get the tour defined by the ui-tour directive + const uitour = uiTourService.getTourByName('open-api'); + uitour.createStep({ + selector: 'body', + stepId: 'welcome', + order: 0, + title: _t('app.admin.tour.open_api.welcome.title'), + content: _t('app.admin.tour.open_api.welcome.content'), + placement: 'bottom', + orphan: true + }); + uitour.createStep({ + selector: '.heading .documentation-button', + stepId: 'doc', + order: 1, + title: _t('app.admin.tour.open_api.doc.title'), + content: _t('app.admin.tour.open_api.doc.content'), + placement: 'bottom' + }); + uitour.createStep({ + selector: 'body', + stepId: 'conclusion', + order: 2, + title: _t('app.admin.tour.conclusion.title'), + content: _t('app.admin.tour.conclusion.content'), + placement: 'bottom', + orphan: true + }); + // on tour end, save the status in database + uitour.on('ended', function () { + if (uitour.getStatus() === uitour.Status.ON && $scope.currentUser.profile.tours.indexOf('open-api') < 0) { + Member.completeTour({ id: $scope.currentUser.id }, { tour: 'open-api' }, function (res) { + $scope.currentUser.profile.tours = res.tours; + }); + } + }); + // if the user has never seen the tour, and if the display behavior is not configured to manual triggering only, show the tour now + if (Fablab.featureTourDisplay !== 'manual' && $scope.currentUser.profile.tours.indexOf('open-api') < 0) { + uitour.start(); + } + // start this tour when an user press F1 - this is contextual help + window.addEventListener('keydown', handleF1); + }; + + /* PRIVATE SCOPE */ + + /** + * Kind of constructor: these actions will be realized first when the controller is loaded + */ + const initialize = function () { + // listen the $destroy event of the controller to remove the F1 key binding + $scope.$on('$destroy', function () { + window.removeEventListener('keydown', handleF1); + }); + }; + + /** + * Callback used to trigger the feature tour when the user press the F1 key. + * @param e {KeyboardEvent} + */ + const handleF1 = function (e) { + if (e.key === 'F1') { + e.preventDefault(); + const tour = uiTourService.getTourByName('open-api'); + if (tour) { tour.start(); } + } + }; + + // !!! MUST BE CALLED AT THE END of the controller + return initialize(); } ]); diff --git a/app/assets/javascripts/controllers/admin/plans.js.erb b/app/assets/javascripts/controllers/admin/plans.js.erb index 7cfd3e2e3..a86ab8da8 100644 --- a/app/assets/javascripts/controllers/admin/plans.js.erb +++ b/app/assets/javascripts/controllers/admin/plans.js.erb @@ -121,7 +121,7 @@ Application.Controllers.controller('NewPlanController', ['$scope', '$uibModal', $uibModalInstance.close($scope.partner); }, function (error) { - growl.error(_t('new_plan.unable_to_save_this_user_check_that_there_isnt_an_already_a_user_with_the_same_name')); + growl.error(_t('app.admin.plans.new.unable_to_save_this_user_check_that_there_isnt_an_already_a_user_with_the_same_name')); console.error(error); } ); @@ -143,9 +143,9 @@ Application.Controllers.controller('NewPlanController', ['$scope', '$uibModal', */ $scope.afterSubmit = function (content) { if ((content.id == null) && (content.plan_ids == null)) { - return growl.error(_t('new_plan.unable_to_create_the_subscription_please_try_again')); + return growl.error(_t('app.admin.plans.new.unable_to_create_the_subscription_please_try_again')); } else { - growl.success(_t('new_plan.successfully_created_subscriptions_dont_forget_to_redefine_prices')); + growl.success(_t('app.admin.plans.new.successfully_created_subscriptions_dont_forget_to_redefine_prices')); if (content.plan_ids != null) { return $state.go('app.admin.pricing'); } else { @@ -237,9 +237,9 @@ Application.Controllers.controller('EditPlanController', ['$scope', 'groups', 'p */ $scope.afterSubmit = function (content) { if ((content.id == null) && (content.plan_ids == null)) { - return growl.error(_t('edit_plan.unable_to_save_subscription_changes_please_try_again')); + return growl.error(_t('app.admin.plans.edit.unable_to_save_subscription_changes_please_try_again')); } else { - growl.success(_t('edit_plan.subscription_successfully_changed')); + growl.success(_t('app.admin.plans.edit.subscription_successfully_changed')); return $state.go('app.admin.pricing'); } }; diff --git a/app/assets/javascripts/controllers/admin/pricing.js.erb b/app/assets/javascripts/controllers/admin/pricing.js.erb index 3f94af05e..2e8eae011 100644 --- a/app/assets/javascripts/controllers/admin/pricing.js.erb +++ b/app/assets/javascripts/controllers/admin/pricing.js.erb @@ -18,9 +18,10 @@ /** * Controller used in the prices edition page */ -Application.Controllers.controller('EditPricingController', ['$scope', '$state', '$uibModal', '$filter', 'TrainingsPricing', 'Credit', 'Pricing', 'Plan', 'Coupon', 'plans', 'groups', 'growl', 'machinesPricesPromise', 'Price', 'dialogs', 'trainingsPricingsPromise', 'trainingsPromise', 'machineCreditsPromise', 'machinesPromise', 'trainingCreditsPromise', 'couponsPromise', 'spacesPromise', 'spacesPricesPromise', 'spacesCreditsPromise', '_t', - function ($scope, $state, $uibModal, $filter, TrainingsPricing, Credit, Pricing, Plan, Coupon, plans, groups, growl, machinesPricesPromise, Price, dialogs, trainingsPricingsPromise, trainingsPromise, machineCreditsPromise, machinesPromise, trainingCreditsPromise, couponsPromise, spacesPromise, spacesPricesPromise, spacesCreditsPromise, _t) { - /* PUBLIC SCOPE */ +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', 'Member', 'uiTourService', + function ($scope, $state, $uibModal, $filter, TrainingsPricing, Credit, Pricing, Plan, Coupon, plans, groups, growl, machinesPricesPromise, Price, dialogs, trainingsPricingsPromise, trainingsPromise, machineCreditsPromise, machinesPromise, trainingCreditsPromise, couponsPromise, spacesPromise, spacesPricesPromise, spacesCreditsPromise, _t, Member, uiTourService) { + /* PUBLIC SCOPE */ + // List of machines prices (not considering any plan) $scope.machinesPrices = machinesPricesPromise; @@ -84,7 +85,7 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state', // Default: we do not filter coupons $scope.filter = { - coupon: 'all', + coupon: 'all' }; // Available status for filtering coupons @@ -96,6 +97,16 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state', 'active' ]; + // default tab: plans list + $scope.tabs = { active: 0 }; + + /** + * Retrieve a training price from all the trainings prices + * @param trainingsPricings {Array} all trainings prices + * @param trainingId {number} + * @param groupId {number} + * @returns {float} + */ $scope.findTrainingsPricing = function (trainingsPricings, trainingId, groupId) { for (let trainingsPricing of Array.from(trainingsPricings)) { if ((trainingsPricing.training_id === trainingId) && (trainingsPricing.group_id === groupId)) { @@ -104,11 +115,17 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state', } }; + /** + * Update the price of a training for the given parameters + * @param data {float} the new price + * @param trainingsPricing {Object} the training pricing to update + * @returns {Promise|string} + */ $scope.updateTrainingsPricing = function (data, trainingsPricing) { if (data != null) { return TrainingsPricing.update({ id: trainingsPricing.id }, { trainings_pricing: { amount: data } }).$promise; } else { - return _t('pricing.please_specify_a_number'); + return _t('app.admin.pricing.please_specify_a_number'); } }; @@ -146,7 +163,7 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state', */ $scope.showTrainings = function (trainings) { if (!angular.isArray(trainings) || !(trainings.length > 0)) { - return _t('pricing.none'); + return _t('app.admin.pricing.none'); } const selected = []; @@ -155,7 +172,7 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state', return selected.push(t.name); } }); - if (selected.length) { return selected.join(' | '); } else { return _t('pricing.none'); } + if (selected.length) { return selected.join(' | '); } else { return _t('app.admin.pricing.none'); } }; /** @@ -170,7 +187,7 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state', { training_credit_nb: newdata.training_credits } , angular.noop() // do nothing in case of success , function (error) { - growl.error(_t('pricing.an_error_occurred_while_saving_the_number_of_credits')); + growl.error(_t('app.admin.pricing.an_error_occurred_while_saving_the_number_of_credits')); console.error(error); } ); @@ -190,11 +207,11 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state', return $scope.trainingCreditsGroups[planId].splice($scope.trainingCreditsGroups[planId].indexOf(tc.id), 1); } , function (error) { - growl.error(_t('pricing.an_error_occurred_while_deleting_credit_with_the_TRAINING', { TRAINING: tc.creditable.name })); + growl.error(_t('app.admin.pricing.an_error_occurred_while_deleting_credit_with_the_TRAINING', { TRAINING: tc.creditable.name })); console.error(error); }); } else { - return growl.error(_t('pricing.an_error_occurred_unable_to_find_the_credit_to_revoke')); + return growl.error(_t('app.admin.pricing.an_error_occurred_unable_to_find_the_credit_to_revoke')); } } }); @@ -215,7 +232,7 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state', } , function (error) { // failed const training = getTrainingFromId(newTrainingId); - growl.error(_t('pricing.an_error_occurred_while_creating_credit_with_the_TRAINING', { TRAINING: training.name })); + growl.error(_t('app.admin.pricing.an_error_occurred_while_creating_credit_with_the_TRAINING', { TRAINING: training.name })); return console.error(error); }); } @@ -250,7 +267,7 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state', * @returns {String} */ $scope.showCreditableName = function (credit) { - let selected = _t('pricing.not_set'); + let selected = _t('app.admin.pricing.not_set'); if (credit && credit.creditable_id) { const object = $scope.getCreditable(credit); selected = object.name; @@ -295,7 +312,7 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state', $scope.saveMachineCredit = function (data, id) { for (let mc of Array.from($scope.machineCredits)) { if ((mc.plan_id === data.plan_id) && (mc.creditable_id === data.creditable_id) && ((id === null) || (mc.id !== id))) { - growl.error(_t('pricing.error_a_credit_linking_this_machine_with_that_subscription_already_exists')); + growl.error(_t('app.admin.pricing.error_a_credit_linking_this_machine_with_that_subscription_already_exists')); if (!id) { $scope.machineCredits.pop(); } @@ -304,18 +321,18 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state', } if (id != null) { - return Credit.update({ id }, { credit: data }, function () { growl.success(_t('pricing.changes_have_been_successfully_saved')); }); + return Credit.update({ id }, { credit: data }, function () { growl.success(_t('app.admin.pricing.changes_have_been_successfully_saved')); }); } else { data.creditable_type = 'Machine'; return Credit.save( { credit: data } , function (resp) { $scope.machineCredits[$scope.machineCredits.length - 1].id = resp.id; - return growl.success(_t('pricing.credit_was_successfully_saved')); + return growl.success(_t('app.admin.pricing.credit_was_successfully_saved')); } , function (err) { $scope.machineCredits.pop(); - growl.error(_t('pricing.error_creating_credit')); + growl.error(_t('app.admin.pricing.error_creating_credit')); console.error(err); }); } @@ -365,7 +382,7 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state', $scope.saveSpaceCredit = function (data, id) { for (let sc of Array.from($scope.spaceCredits)) { if ((sc.plan_id === data.plan_id) && (sc.creditable_id === data.creditable_id) && ((id === null) || (sc.id !== id))) { - growl.error(_t('pricing.error_a_credit_linking_this_space_with_that_subscription_already_exists')); + growl.error(_t('app.admin.pricing.error_a_credit_linking_this_space_with_that_subscription_already_exists')); if (!id) { $scope.spaceCredits.pop(); } @@ -374,18 +391,18 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state', } if (id != null) { - return Credit.update({ id }, { credit: data }, function () { growl.success(_t('pricing.changes_have_been_successfully_saved')); }); + return Credit.update({ id }, { credit: data }, function () { growl.success(_t('app.admin.pricing.changes_have_been_successfully_saved')); }); } else { data.creditable_type = 'Space'; return Credit.save( { credit: data } , function (resp) { $scope.spaceCredits[$scope.spaceCredits.length - 1].id = resp.id; - return growl.success(_t('pricing.credit_was_successfully_saved')); + return growl.success(_t('app.admin.pricing.credit_was_successfully_saved')); } , function (err) { $scope.spaceCredits.pop(); - return growl.error(_t('pricing.error_creating_credit')); + return growl.error(_t('app.admin.pricing.error_creating_credit')); }); } }; @@ -419,8 +436,8 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state', */ $scope.getPlanType = function (type) { if (type === 'PartnerPlan') { - return _t('pricing.partner'); - } else { return _t('pricing.standard'); } + return _t('app.admin.pricing.partner'); + } else { return _t('app.admin.pricing.standard'); } }; /** @@ -453,7 +470,7 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state', if (data != null) { return Price.update({ id: price.id }, { price: { amount: data } }).$promise; } else { - return _t('pricing.please_specify_a_number'); + return _t('app.admin.pricing.please_specify_a_number'); } }; @@ -471,8 +488,8 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state', resolve: { object () { return { - title: _t('pricing.confirmation_required'), - msg: _t('pricing.do_you_really_want_to_delete_this_subscription_plan') + title: _t('app.admin.pricing.confirmation_required'), + msg: _t('app.admin.pricing.do_you_really_want_to_delete_this_subscription_plan') }; } } @@ -482,12 +499,12 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state', Plan.delete( { id }, function (res) { - growl.success(_t('pricing.subscription_plan_was_successfully_deleted')); + growl.success(_t('app.admin.pricing.subscription_plan_was_successfully_deleted')); return $scope.plans.splice(findItemIdxById(plans, id), 1); }, function (error) { if (error.statusText) { console.error(`[EditPricingController::deletePlan] Error: ${error.statusText}`); } - growl.error(_t('pricing.unable_to_delete_the_specified_subscription_an_error_occurred')); + growl.error(_t('app.admin.pricing.unable_to_delete_the_specified_subscription_an_error_occurred')); } ); } @@ -519,8 +536,8 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state', resolve: { object () { return { - title: _t('pricing.confirmation_required'), - msg: _t('pricing.do_you_really_want_to_delete_this_coupon') + title: _t('app.admin.pricing.confirmation_required'), + msg: _t('app.admin.pricing.do_you_really_want_to_delete_this_coupon') }; } } @@ -528,16 +545,16 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state', , function () { // the admin has confirmed, delete the coupon Coupon.delete({ id }, function (res) { - growl.success(_t('coupon_was_successfully_deleted')); + growl.success(_t('app.admin.pricing.coupon_was_successfully_deleted')); return $scope.coupons.splice(findItemIdxById(coupons, id), 1); } , function (error) { if (error.statusText) { console.error(`[EditPricingController::deleteCoupon] Error: ${error.statusText}`); } if (error.status === 422) { - return growl.error(_t('pricing.unable_to_delete_the_specified_coupon_already_in_use')); + return growl.error(_t('app.admin.pricing.unable_to_delete_the_specified_coupon_already_in_use')); } else { - return growl.error(_t('pricing.unable_to_delete_the_specified_coupon_an_unexpected_error_occurred')); + return growl.error(_t('app.admin.pricing.unable_to_delete_the_specified_coupon_an_unexpected_error_occurred')); } }); }); @@ -566,11 +583,11 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state', // Callback to validate sending of the coupon $scope.ok = function () { Coupon.send({ coupon_code: coupon.code, user_id: $scope.ctrl.member.id }, function (res) { - growl.success(_t('pricing.coupon_successfully_sent_to_USER', { USER: $scope.ctrl.member.name })); + growl.success(_t('app.admin.pricing.coupon_successfully_sent_to_USER', { USER: $scope.ctrl.member.name })); return $uibModalInstance.close({ user_id: $scope.ctrl.member.id }); } , function (err) { - growl.error(_t('pricing.an_error_occurred_unable_to_send_the_coupon')); + growl.error(_t('app.admin.pricing.an_error_occurred_unable_to_send_the_coupon')); console.error(err); }); }; @@ -600,6 +617,107 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state', }); } + /** + * Setup the feature-tour for the admin/pricing page. + * This is intended as a contextual help (when pressing F1) + */ + $scope.setupPricingTour = function () { + // get the tour defined by the ui-tour directive + const uitour = uiTourService.getTourByName('pricing'); + uitour.createStep({ + selector: 'body', + stepId: 'welcome', + order: 0, + title: _t('app.admin.tour.pricing.welcome.title'), + content: _t('app.admin.tour.pricing.welcome.content'), + placement: 'bottom', + orphan: true + }); + uitour.createStep({ + selector: '.plans-pricing .new-plan-button', + stepId: 'new_plan', + order: 1, + title: _t('app.admin.tour.pricing.new_plan.title'), + content: _t('app.admin.tour.pricing.new_plan.content'), + placement: 'bottom' + }); + uitour.createStep({ + selector: '.plans-pricing .trainings-tab', + stepId: 'trainings', + order: 2, + title: _t('app.admin.tour.pricing.trainings.title'), + content: _t('app.admin.tour.pricing.trainings.content'), + placement: 'bottom' + }); + uitour.createStep({ + selector: '.plans-pricing .machines-tab', + stepId: 'machines', + order: 3, + title: _t('app.admin.tour.pricing.machines.title'), + content: _t('app.admin.tour.pricing.machines.content'), + placement: 'bottom' + }); + if (!Fablab.withoutSpaces) { + uitour.createStep({ + selector: '.plans-pricing .spaces-tab', + stepId: 'spaces', + order: 4, + title: _t('app.admin.tour.pricing.spaces.title'), + content: _t('app.admin.tour.pricing.spaces.content'), + placement: 'bottom' + }); + } + uitour.createStep({ + selector: '.plans-pricing .credits-tab', + stepId: 'credits', + order: 5, + title: _t('app.admin.tour.pricing.credits.title'), + content: _t('app.admin.tour.pricing.credits.content'), + placement: 'bottom' + }); + uitour.createStep({ + selector: '.plans-pricing .coupons-tab', + stepId: 'coupons', + order: 6, + title: _t('app.admin.tour.pricing.coupons.title'), + content: _t('app.admin.tour.pricing.coupons.content'), + placement: 'bottom', + popupClass: 'shift-left-50' + }); + uitour.createStep({ + selector: 'body', + stepId: 'conclusion', + order: 7, + title: _t('app.admin.tour.conclusion.title'), + content: _t('app.admin.tour.conclusion.content'), + placement: 'bottom', + orphan: true + }); + // on step change, change the active tab if needed + uitour.on('stepChanged', function (nextStep) { + if (nextStep.stepId === 'new_plan') { $scope.tabs.active = 0; } + if (nextStep.stepId === 'trainings') { $scope.tabs.active = 1; } + if (nextStep.stepId === 'machines') { $scope.tabs.active = 2; } + if (nextStep.stepId === 'spaces') { $scope.tabs.active = 3; } + if (nextStep.stepId === 'credits') { $scope.tabs.active = 4; } + if (nextStep.stepId === 'coupons') { $scope.tabs.active = 5; } + }); + // on tour end, save the status in database + uitour.on('ended', function () { + if (uitour.getStatus() === uitour.Status.ON && $scope.currentUser.profile.tours.indexOf('pricing') < 0) { + Member.completeTour({ id: $scope.currentUser.id }, { tour: 'pricing' }, function (res) { + $scope.currentUser.profile.tours = res.tours; + }); + } + }); + // if the user has never seen the tour, show him now + if (Fablab.featureTourDisplay !== 'manual' && $scope.currentUser.profile.tours.indexOf('pricing') < 0) { + uitour.start(); + } + // start this tour when an user press F1 - this is contextual help + window.addEventListener('keydown', handleF1); + } + /* PRIVATE SCOPE */ /** @@ -608,6 +726,11 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state', const initialize = function () { $scope.trainingCreditsGroups = groupCreditsByPlan($scope.trainingCredits); + // listen the $destroy event of the controller to remove the F1 key binding + $scope.$on('$destroy', function () { + window.removeEventListener('keydown', handleF1); + }); + // adds empty array for plan which hasn't any credits yet return (function () { const result = []; @@ -622,6 +745,18 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state', })(); }; + /** + * Callback used to trigger the feature tour when the user press the F1 key. + * @param e {KeyboardEvent} + */ + const handleF1 = function (e) { + if (e.key === 'F1') { + e.preventDefault(); + const tour = uiTourService.getTourByName('pricing'); + if (tour) { tour.start(); } + } + }; + /** * Retrieve an item index by its ID from the given array of objects * @param items {Array<{id:number}>} diff --git a/app/assets/javascripts/controllers/admin/project_elements.js b/app/assets/javascripts/controllers/admin/project_elements.js index 60157f14c..557404615 100644 --- a/app/assets/javascripts/controllers/admin/project_elements.js +++ b/app/assets/javascripts/controllers/admin/project_elements.js @@ -12,9 +12,9 @@ */ 'use strict'; -Application.Controllers.controller('ProjectElementsController', ['$scope', '$state', 'Component', 'Licence', 'Theme', 'componentsPromise', 'licencesPromise', 'themesPromise', - function ($scope, $state, Component, Licence, Theme, componentsPromise, licencesPromise, themesPromise) { - // Materials list (plastic, wood ...) +Application.Controllers.controller('ProjectElementsController', ['$scope', '$state', 'Component', 'Licence', 'Theme', 'componentsPromise', 'licencesPromise', 'themesPromise', '_t', 'Member', 'uiTourService', + function ($scope, $state, Component, Licence, Theme, componentsPromise, licencesPromise, themesPromise, _t, Member, uiTourService) { + // Materials list (plastic, wood ...) $scope.components = componentsPromise; // Licences list (Creative Common ...) @@ -149,12 +149,89 @@ Application.Controllers.controller('ProjectElementsController', ['$scope', '$sta * @param rowform {Object} see http://vitalets.github.io/angular-xeditable/ * @param index {number} licence index in the $scope.licences array */ - return $scope.cancelLicence = function (rowform, index) { + $scope.cancelLicence = function (rowform, index) { if ($scope.licences[index].id != null) { return rowform.$cancel(); } else { return $scope.licences.splice(index, 1); } }; + + /** + * Setup the feature-tour for the admin/project_elements page. + * This is intended as a contextual help (when pressing F1) + */ + $scope.setupProjectElementsTour = function () { + // get the tour defined by the ui-tour directive + const uitour = uiTourService.getTourByName('project-elements'); + uitour.createStep({ + selector: 'body', + stepId: 'welcome', + order: 0, + title: _t('app.admin.tour.project_elements.welcome.title'), + content: _t('app.admin.tour.project_elements.welcome.content'), + placement: 'bottom', + orphan: true + }); + uitour.createStep({ + selector: '.heading .abuses-button', + stepId: 'abuses', + order: 1, + title: _t('app.admin.tour.project_elements.abuses.title'), + content: _t('app.admin.tour.project_elements.abuses.content'), + placement: 'bottom', + popupClass: 'shift-left-40' + }); + uitour.createStep({ + selector: 'body', + stepId: 'conclusion', + order: 2, + title: _t('app.admin.tour.conclusion.title'), + content: _t('app.admin.tour.conclusion.content'), + placement: 'bottom', + orphan: true + }); + // on tour end, save the status in database + uitour.on('ended', function () { + if (uitour.getStatus() === uitour.Status.ON && $scope.currentUser.profile.tours.indexOf('project-elements') < 0) { + Member.completeTour({ id: $scope.currentUser.id }, { tour: 'project-elements' }, function (res) { + $scope.currentUser.profile.tours = res.tours; + }); + } + }); + // if the user has never seen the tour, show him now + if (Fablab.featureTourDisplay !== 'manual' && $scope.currentUser.profile.tours.indexOf('project-elements') < 0) { + uitour.start(); + } + // start this tour when an user press F1 - this is contextual help + window.addEventListener('keydown', handleF1); + }; + + /* PRIVATE SCOPE */ + + /** + * Kind of constructor: these actions will be realized first when the controller is loaded + */ + const initialize = function () { + // listen the $destroy event of the controller to remove the F1 key binding + $scope.$on('$destroy', function () { + window.removeEventListener('keydown', handleF1); + }); + }; + + /** + * Callback used to trigger the feature tour when the user press the F1 key. + * @param e {KeyboardEvent} + */ + const handleF1 = function (e) { + if (e.key === 'F1') { + e.preventDefault(); + const tour = uiTourService.getTourByName('project-elements'); + if (tour) { tour.start(); } + } + }; + + // !!! MUST BE CALLED AT THE END of the controller + return initialize(); } ]); diff --git a/app/assets/javascripts/controllers/admin/settings.js.erb b/app/assets/javascripts/controllers/admin/settings.js.erb index 9be82acea..d7e5fc337 100644 --- a/app/assets/javascripts/controllers/admin/settings.js.erb +++ b/app/assets/javascripts/controllers/admin/settings.js.erb @@ -12,8 +12,8 @@ */ 'use strict'; -Application.Controllers.controller('SettingsController', ['$scope', '$filter', '$uibModal', 'Setting', 'growl', 'settingsPromise', 'privacyDraftsPromise', 'cgvFile', 'cguFile', 'logoFile', 'logoBlackFile', 'faviconFile', 'profileImageFile', 'CSRF', '_t', - function ($scope, $filter, $uibModal, Setting, growl, settingsPromise, privacyDraftsPromise, cgvFile, cguFile, logoFile, logoBlackFile, faviconFile, profileImageFile, CSRF, _t) { +Application.Controllers.controller('SettingsController', ['$scope', '$rootScope', '$filter', '$uibModal', 'dialogs', 'Setting', 'growl', 'settingsPromise', 'privacyDraftsPromise', 'cgvFile', 'cguFile', 'logoFile', 'logoBlackFile', 'faviconFile', 'profileImageFile', 'CSRF', '_t', 'Member', 'uiTourService', + function ($scope, $rootScope, $filter, $uibModal, dialogs, Setting, growl, settingsPromise, privacyDraftsPromise, cgvFile, cguFile, logoFile, logoBlackFile, faviconFile, profileImageFile, CSRF, _t, Member, uiTourService) { /* PUBLIC SCOPE */ // timepickers steps configuration @@ -48,16 +48,22 @@ Application.Controllers.controller('SettingsController', ['$scope', '$filter', ' cgv: false }; + // default tab: general + $scope.tabs = { active: 0 }; + // full history of privacy policy drafts $scope.privacyDraftsHistory = []; // various configurable settings $scope.twitterSetting = { name: 'twitter_name', value: settingsPromise.twitter_name }; + $scope.linkName = { name: 'link_name', value: settingsPromise.link_name }; $scope.aboutTitleSetting = { name: 'about_title', value: settingsPromise.about_title }; $scope.aboutBodySetting = { name: 'about_body', value: settingsPromise.about_body }; $scope.privacyDpoSetting = { name: 'privacy_dpo', value: settingsPromise.privacy_dpo }; $scope.aboutContactsSetting = { name: 'about_contacts', value: settingsPromise.about_contacts }; $scope.homeBlogpostSetting = { name: 'home_blogpost', value: settingsPromise.home_blogpost }; + $scope.homeContent = { name: 'home_content', value: settingsPromise.home_content }; + $scope.homeCss = { name: 'home_css', value: settingsPromise.home_css }; $scope.machineExplicationsAlert = { name: 'machine_explications_alert', value: settingsPromise.machine_explications_alert }; $scope.trainingExplicationsAlert = { name: 'training_explications_alert', value: settingsPromise.training_explications_alert }; $scope.trainingInformationMessage = { name: 'training_information_message', value: settingsPromise.training_information_message }; @@ -123,12 +129,48 @@ Application.Controllers.controller('SettingsController', ['$scope', '$filter', ' value: (settingsPromise.display_name_enable === 'true') }; + $scope.fabAnalytics = { + name: 'fab_analytics', + value: (settingsPromise.fab_analytics === 'true') + }; + // By default, we display the currently published privacy policy $scope.privacyPolicy = { version: null, bodyTemp: settingsPromise.privacy_body }; + // Extend the options for summernote editor, with special tools for home page + $scope.summernoteOptsHomePage = angular.copy($rootScope.summernoteOpts); + $scope.summernoteOptsHomePage.toolbar[5][1].push('nugget'); // toolbar -> insert -> nugget + $scope.summernoteOptsHomePage.nugget = { + label: '\uF12E', + tooltip: _t('app.admin.settings.home_items'), + list: [ + `
${_t('app.admin.settings.item_news')}
`, + `
${_t('app.admin.settings.item_projects')}
`, + `
${_t('app.admin.settings.item_twitter')}
`, + `
${_t('app.admin.settings.item_members')}
`, + `
${_t('app.admin.settings.item_events')}
` + ] + } + $scope.summernoteOptsHomePage.height = 400; + + // codemirror editor + $scope.codeMirrorEditor = null; + + // Options for codemirror editor, used for custom css + $scope.codemirrorOpts = { + matchBrackets : true, + lineNumbers: true, + mode: 'sass' + } + + // Show or hide advanced settings + $scope.advancedSettings = { + open: false + } + /** * 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. @@ -166,7 +208,7 @@ Application.Controllers.controller('SettingsController', ['$scope', '$filter', ' Setting.update( { name: setting.name }, { value }, - function () { growl.success(_t('settings.customization_of_SETTING_successfully_saved', { SETTING: _t(`settings.${setting.name}`) })); }, + function () { growl.success(_t('app.admin.settings.customization_of_SETTING_successfully_saved', { SETTING: _t(`app.admin.settings.${setting.name}`) })); }, function (error) { console.log(error); } ); }; @@ -191,7 +233,7 @@ Application.Controllers.controller('SettingsController', ['$scope', '$filter', ' // reset history $scope.privacyDraftsHistory = []; data.setting.history.forEach(function (draft) { - $scope.privacyDraftsHistory.push({ id: draft.id, name: _t('settings.privacy.draft_from_USER_DATE', { USER: draft.user.name, DATE: draft.created_at }), content: draft.value }); + $scope.privacyDraftsHistory.push({ id: draft.id, name: _t('app.admin.settings.privacy.draft_from_USER_DATE', { USER: draft.user.name, DATE: draft.created_at }), content: draft.value }); }); if (type === 'privacy_draft') { const orderedHistory = $filter('orderBy')(data.setting.history, 'created_at'); @@ -219,7 +261,7 @@ Application.Controllers.controller('SettingsController', ['$scope', '$filter', ' angular.forEach(v, function(err) { growl.error(err); }) }); } else { - growl.success(_t('settings.file_successfully_updated')); + growl.success(_t('app.admin.settings.file_successfully_updated')); if (content.custom_asset.name === 'cgu-file') { $scope.cguFile = content.custom_asset; $scope.methods.cgu = 'put'; @@ -273,6 +315,162 @@ Application.Controllers.controller('SettingsController', ['$scope', '$filter', ' } }; + /** + * Open a modal showing a sample of the collected data if FabAnalytics is enabled + */ + $scope.analyticsModal = function() { + $uibModal.open({ + templateUrl: '<%= asset_path "admin/settings/analyticsModal.html" %>', + controller: 'AnalyticsModalController', + size: 'lg', + resolve: { + analyticsData: ['FabAnalytics', function (FabAnalytics) { return FabAnalytics.data().$promise; }] + } + }); + } + + /** + * Reset the home page to its initial state (factory value) + */ + $scope.resetHomePage = function () { + dialogs.confirm({ + resolve: { + object () { + return { + title: _t('app.admin.settings.confirmation_required'), + msg: _t('app.admin.settings.confirm_reset_home_page') + }; + } + } + } + , function () { // confirmed + Setting.reset({ name: 'home_content' }, function (data) { + $scope.homeContent.value = data.value; + growl.success(_t('app.admin.settings.home_content_reset')); + }) + } + ) + } + + /** + * Callback triggered when the codemirror editor is loaded into the DOM + * @param editor codemirror instance + */ + $scope.codemirrorLoaded = function (editor) { + $scope.codeMirrorEditor = editor; + } + + /** + * Setup the feature-tour for the admin/settings page. + * This is intended as a contextual help (when pressing F1) + */ + $scope.setupSettingsTour = function () { + // get the tour defined by the ui-tour directive + const uitour = uiTourService.getTourByName('settings'); + uitour.createStep({ + selector: 'body', + stepId: 'welcome', + order: 0, + title: _t('app.admin.tour.settings.welcome.title'), + content: _t('app.admin.tour.settings.welcome.content'), + placement: 'bottom', + orphan: true + }); + uitour.createStep({ + selector: '.admin-settings .home-page-content h4', + stepId: 'home', + order: 1, + title: _t('app.admin.tour.settings.home.title'), + content: _t('app.admin.tour.settings.home.content'), + placement: 'bottom' + }); + uitour.createStep({ + selector: '.admin-settings .home-page-content .note-toolbar .note-insert div', + stepId: 'components', + order: 2, + title: _t('app.admin.tour.settings.components.title'), + content: _t('app.admin.tour.settings.components.content'), + placement: 'bottom' + }); + uitour.createStep({ + selector: '.admin-settings .home-page-content .note-toolbar .btn-codeview', + stepId: 'codeview', + order: 3, + title: _t('app.admin.tour.settings.codeview.title'), + content: _t('app.admin.tour.settings.codeview.content'), + placement: 'bottom' + }); + uitour.createStep({ + selector: '.admin-settings .reset-button', + stepId: 'reset', + order: 4, + title: _t('app.admin.tour.settings.reset.title'), + content: _t('app.admin.tour.settings.reset.content'), + placement: 'left' + }); + uitour.createStep({ + selector: '.admin-settings .home-page-style', + stepId: 'css', + order: 5, + title: _t('app.admin.tour.settings.css.title'), + content: _t('app.admin.tour.settings.css.content'), + placement: 'top' + }); + uitour.createStep({ + selector: '.admin-settings .about-page-tab', + stepId: 'about', + order: 6, + title: _t('app.admin.tour.settings.about.title'), + content: _t('app.admin.tour.settings.about.content'), + placement: 'bottom' + }); + uitour.createStep({ + selector: '.admin-settings .privacy-page-tab', + stepId: 'privacy', + order: 7, + title: _t('app.admin.tour.settings.privacy.title'), + content: _t('app.admin.tour.settings.privacy.content'), + placement: 'bottom' + }); + uitour.createStep({ + selector: '.admin-settings .history-select', + stepId: 'draft', + order: 8, + title: _t('app.admin.tour.settings.draft.title'), + content: _t('app.admin.tour.settings.draft.content'), + placement: 'bottom' + }); + uitour.createStep({ + selector: 'body', + stepId: 'conclusion', + order: 9, + title: _t('app.admin.tour.conclusion.title'), + content: _t('app.admin.tour.conclusion.content'), + placement: 'bottom', + orphan: true + }); + // on step change, change the active tab if needed + uitour.on('stepChanged', function (nextStep) { + if (nextStep.stepId === 'home' || nextStep.stepId === 'css') { $scope.tabs.active = 1; } + if (nextStep.stepId === 'about') { $scope.tabs.active = 2; } + if (nextStep.stepId === 'privacy' || nextStep.stepId === 'draft') { $scope.tabs.active = 3; } + }); + // on tour end, save the status in database + uitour.on('ended', function () { + if (uitour.getStatus() === uitour.Status.ON && $scope.currentUser.profile.tours.indexOf('settings') < 0) { + Member.completeTour({ id: $scope.currentUser.id }, { tour: 'settings' }, function (res) { + $scope.currentUser.profile.tours = res.tours; + }); + } + }); + // if the user has never seen the tour, show him now + if (Fablab.featureTourDisplay !== 'manual' && $scope.currentUser.profile.tours.indexOf('settings') < 0) { + uitour.start(); + } + // start this tour when an user press F1 - this is contextual help + window.addEventListener('keydown', handleF1); + } + /* PRIVATE SCOPE */ /** @@ -316,10 +514,30 @@ Application.Controllers.controller('SettingsController', ['$scope', '$filter', ' } privacyDraftsPromise.setting.history.forEach(function (draft) { - $scope.privacyDraftsHistory.push({ id: draft.id, name: _t('settings.privacy.draft_from_USER_DATE', { USER: draft.user.name, DATE: moment(draft.created_at).format('L LT') }), content: draft.value }); + $scope.privacyDraftsHistory.push({ id: draft.id, name: _t('app.admin.settings.privacy.draft_from_USER_DATE', { USER: draft.user.name, DATE: moment(draft.created_at).format('L LT') }), content: draft.value }); + }); + + // refresh codemirror to display the fetched setting + $scope.$watch('advancedSettings.open', function (newValue) { + if (newValue) $scope.codeMirrorEditor.refresh(); + }) + // listen the $destroy event of the controller to remove the F1 key binding + $scope.$on('$destroy', function () { + window.removeEventListener('keydown', handleF1); }); }; + /** + * Callback used to trigger the feature tour when the user press the F1 key. + * @param e {KeyboardEvent} + */ + const handleF1 = function (e) { + if (e.key === 'F1') { + e.preventDefault(); + const tour = uiTourService.getTourByName('settings'); + if (tour) { tour.start(); } + } + }; // init the controller (call at the end !) return initialize(); } @@ -347,7 +565,7 @@ Application.Controllers.controller('SavePolicyController', ['$scope', '$uibModal */ $scope.publish = function () { saveCb({ name: 'privacy_body', value: privacyPolicy.bodyTemp }); - growl.info(_t('settings.privacy.users_notified')); + growl.info(_t('app.admin.settings.privacy.users_notified')); $uibModalInstance.close('privacy_body'); }; /** @@ -358,3 +576,18 @@ Application.Controllers.controller('SavePolicyController', ['$scope', '$uibModal }; } ]); + +/** + * Controller used in the "what do we collect?" modal, about FabAnalytics + */ +Application.Controllers.controller('AnalyticsModalController', ['$scope', '$uibModalInstance', 'analyticsData', + function ($scope,$uibModalInstance, analyticsData) { + // analytics data sample + $scope.data = analyticsData; + + // callback to close the modal + $scope.close = function () { + $uibModalInstance.dismiss(); + } + } +]) diff --git a/app/assets/javascripts/controllers/admin/statistics.js.erb b/app/assets/javascripts/controllers/admin/statistics.js.erb index d72ff1fca..8f6e42491 100644 --- a/app/assets/javascripts/controllers/admin/statistics.js.erb +++ b/app/assets/javascripts/controllers/admin/statistics.js.erb @@ -15,8 +15,8 @@ */ 'use strict'; -Application.Controllers.controller('StatisticsController', ['$scope', '$state', '$rootScope', '$uibModal', 'es', 'Member', '_t', 'membersPromise', 'statisticsPromise', - function ($scope, $state, $rootScope, $uibModal, es, Member, _t, membersPromise, statisticsPromise) { +Application.Controllers.controller('StatisticsController', ['$scope', '$state', '$rootScope', '$uibModal', 'es', 'Member', '_t', 'membersPromise', 'statisticsPromise', 'uiTourService', + function ($scope, $state, $rootScope, $uibModal, es, Member, _t, membersPromise, statisticsPromise, uiTourService) { /* PRIVATE STATIC CONSTANTS */ // search window size @@ -130,6 +130,13 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state', } }; + /** + * Return a localized name for the given field + */ + $scope.customFieldName = function (field) { + return _t(`app.admin.statistics.${field}`); + } + /** * Callback to open the datepicker (interval start) * @param $event {Object} jQuery event object @@ -174,11 +181,7 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state', if (tab.table) { if ((tab.es_type_key === 'subscription') && $rootScope.fablabWithoutPlans) { return true; - } else if ((tab.es_type_key === 'space') && $rootScope.fablabWithoutSpaces) { - return true; - } else { - return false; - } + } else return (tab.es_type_key === 'space') && $rootScope.fablabWithoutSpaces; } else { return true; } @@ -215,10 +218,10 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state', */ $scope.formatSex = function (sex) { if (sex === 'male') { - return _t('man'); + return _t('app.admin.statistics.man'); } if (sex === 'female') { - return _t('woman'); + return _t('app.admin.statistics.woman'); } }; @@ -335,6 +338,63 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state', .result['finally'](null).then(function (info) { console.log(info); }); }; + /** + * Setup the feature-tour for the admin/statistics page. + * This is intended as a contextual help (when pressing F1) + */ + $scope.setupStatisticsTour = function () { + // get the tour defined by the ui-tour directive + const uitour = uiTourService.getTourByName('statistics'); + uitour.createStep({ + selector: 'body', + stepId: 'welcome', + order: 0, + title: _t('app.admin.tour.statistics.welcome.title'), + content: _t('app.admin.tour.statistics.welcome.content'), + placement: 'bottom', + orphan: true + }); + uitour.createStep({ + selector: '.heading .export-button', + stepId: 'export', + order: 1, + title: _t('app.admin.tour.statistics.export.title'), + content: _t('app.admin.tour.statistics.export.content'), + placement: 'bottom' + }); + uitour.createStep({ + selector: '.heading .charts-button', + stepId: 'trending', + order: 2, + title: _t('app.admin.tour.statistics.trending.title'), + content: _t('app.admin.tour.statistics.trending.content'), + placement: 'left' + }); + uitour.createStep({ + selector: 'body', + stepId: 'conclusion', + order: 3, + title: _t('app.admin.tour.conclusion.title'), + content: _t('app.admin.tour.conclusion.content'), + placement: 'bottom', + orphan: true + }); + // on tour end, save the status in database + uitour.on('ended', function () { + if (uitour.getStatus() === uitour.Status.ON && $scope.currentUser.profile.tours.indexOf('statistics') < 0) { + Member.completeTour({ id: $scope.currentUser.id }, { tour: 'statistics' }, function (res) { + $scope.currentUser.profile.tours = res.tours; + }); + } + }); + // if the user has never seen the tour, show him now + if (Fablab.featureTourDisplay !== 'manual' && $scope.currentUser.profile.tours.indexOf('statistics') < 0) { + uitour.start(); + } + // start this tour when an user press F1 - this is contextual help + window.addEventListener('keydown', handleF1); + } + /* PRIVATE SCOPE */ /** @@ -348,6 +408,22 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state', return $scope.preventRefresh = true; } }); + // listen the $destroy event of the controller to remove the F1 key binding + $scope.$on('$destroy', function () { + window.removeEventListener('keydown', handleF1); + }); + }; + + /** + * Callback used to trigger the feature tour when the user press the F1 key. + * @param e {KeyboardEvent} + */ + const handleF1 = function (e) { + if (e.key === 'F1') { + e.preventDefault(); + const tour = uiTourService.getTourByName('statistics'); + if (tour) { tour.start(); } + } }; /** @@ -355,7 +431,7 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state', * @param $event {Object} jQuery event object * @param datePicker {Object} settings object of the concerned datepicker. Must have an 'opened' property */ - var toggleDatePicker = function ($event, datePicker) { + const toggleDatePicker = function ($event, datePicker) { $event.preventDefault(); $event.stopPropagation(); return datePicker.opened = !datePicker.opened; @@ -364,8 +440,8 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state', /** * Force update the statistics table, querying elasticSearch according to the current config values */ - var refreshStats = function () { - if ($scope.selectedIndex && !$scope.preventRefresh) { + const refreshStats = function () { + if ($scope.selectedIndex && !$scope.preventRefresh && $scope.type.active) { $scope.data = []; $scope.sumCA = 0; $scope.averageAge = 0; @@ -397,14 +473,14 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state', }; /** - * Run the elasticSearch query to retreive the /stats/type aggregations + * Run the elasticSearch query to retrieve the /stats/type aggregations * @param index {String} elasticSearch document type (account|event|machine|project|subscription|training) * @param type {String} statistics type (month|year|booking|hour|user|project) * @param custom {{key:{string}, value:{string}}|null} custom filter property or null to disable this filter * @param callback {function} function be to run after results were retrieved, it will receive * two parameters : results {Object}, error {String} (if any) */ - var queryElasticStats = function (index, type, custom, callback) { + const queryElasticStats = function (index, type, custom, callback) { // handle invalid callback if (typeof (callback) !== 'function') { console.error('[statisticsController::queryElasticStats] Error: invalid callback provided'); @@ -443,7 +519,7 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state', * @param intervalEnd {moment} statitics interval ending (moment.js type) * @param sortings {Array|null} elasticSearch criteria for sorting the results */ - var buildElasticDataQuery = function (type, custom, ageMin, ageMax, intervalBegin, intervalEnd, sortings) { + const buildElasticDataQuery = function (type, custom, ageMin, ageMax, intervalBegin, intervalEnd, sortings) { const q = { 'query': { 'bool': { @@ -518,7 +594,7 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state', * @param custom {Object} if custom is empty or undefined, an empty string will be returned * @returns {{match:*}|string} */ - var buildElasticCustomCriterion = function (custom) { + const buildElasticCustomCriterion = function (custom) { if (custom) { const criterion = { 'match': {} @@ -539,7 +615,7 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state', * Parse the provided criteria array and return the corresponding elasticSearch syntax * @param criteria {Array} array of {key_to_sort:order} */ - var buildElasticSortCriteria = function (criteria) { + const buildElasticSortCriteria = function (criteria) { const crits = []; angular.forEach(criteria, function (value, key) { if ((typeof value !== 'undefined') && (value !== null) && (value !== 'none')) { @@ -552,22 +628,27 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state', }; /** - * Fullfil the list of available options in the custom filter panel. The list will be based on common + * Fulfill the list of available options in the custom filter panel. The list will be based on common * properties and on index-specific properties (additional_fields) */ - var buildCustomFiltersList = function () { + const buildCustomFiltersList = function () { $scope.filters = [ - { key: 'date', label: _t('date'), values: ['input_date'] }, - { key: 'userId', label: _t('user_id'), values: ['input_number'] }, - { key: 'gender', label: _t('gender'), values: [{ key: 'male', label: _t('man') }, { key: 'female', label: _t('woman') }] }, - { key: 'age', label: _t('age'), values: ['input_number'] }, - { key: 'subType', label: _t('type'), values: $scope.type.active.subtypes }, - { key: 'ca', label: _t('revenue'), values: ['input_number'] } + { key: 'date', label: _t('app.admin.statistics.date'), values: ['input_date'] }, + { key: 'userId', label: _t('app.admin.statistics.user_id'), values: ['input_number'] }, + { key: 'gender', label: _t('app.admin.statistics.gender'), values: [{ key: 'male', label: _t('app.admin.statistics.man') }, { key: 'female', label: _t('app.admin.statistics.woman') }] }, + { key: 'age', label: _t('app.admin.statistics.age'), values: ['input_number'] }, + { key: 'ca', label: _t('app.admin.statistics.revenue'), values: ['input_number'] } ]; - if (!$scope.type.active.simple) { - const f = { key: 'stat', label: $scope.type.active.label, values: ['input_number'] }; - $scope.filters.push(f); + // if no plans were created, there's no types for statisticIndex=subscriptions + if ($scope.type.active) { + $scope.filters.splice(4, 0, { key: 'subType', label: _t('app.admin.statistics.type'), values: $scope.type.active.subtypes }) + + + if (!$scope.type.active.simple) { + const f = { key: 'stat', label: $scope.type.active.label, values: ['input_number'] }; + $scope.filters.push(f); + } } return angular.forEach($scope.selectedIndex.additional_fields, function (field) { @@ -588,7 +669,7 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state', * Build and return an object according to the custom filter set by the user, used to request elasticsearch * @return {Object|null} */ - var buildCustomFilterQuery = function () { + const buildCustomFilterQuery = function () { let custom = null; if (!angular.isUndefinedOrNull($scope.customFilter.criterion) && !angular.isUndefinedOrNull($scope.customFilter.criterion.key) && @@ -709,7 +790,7 @@ Application.Controllers.controller('ExportStatisticsController', [ '$scope', '$u Export.status(statusQry).then(function (res) { if (!res.data.exists) { - return growl.success(_t('export_is_running_you_ll_be_notified_when_its_ready')); + return growl.success(_t('app.admin.statistics.export_is_running_you_ll_be_notified_when_its_ready')); } }); diff --git a/app/assets/javascripts/controllers/admin/tags.js b/app/assets/javascripts/controllers/admin/tags.js index d5491e5d0..4faa51851 100644 --- a/app/assets/javascripts/controllers/admin/tags.js +++ b/app/assets/javascripts/controllers/admin/tags.js @@ -11,7 +11,7 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -Application.Controllers.controller('TagsController', ['$scope', 'tagsPromise', 'Tag', 'growl', '_t', function ($scope, tagsPromise, Tag, growl, _t) { +Application.Controllers.controller('TagsController', ['$scope', 'tagsPromise', 'Tag', 'dialogs', 'growl', '_t', function ($scope, tagsPromise, Tag, dialogs, growl, _t) { // List of users's tags $scope.tags = tagsPromise; @@ -44,15 +44,15 @@ Application.Controllers.controller('TagsController', ['$scope', 'tagsPromise', ' */ $scope.saveTag = function (data, id) { if (id != null) { - return Tag.update({ id }, { tag: data }, response => growl.success(_t('changes_successfully_saved')) - , error => growl.error(_t('an_error_occurred_while_saving_changes'))); + return Tag.update({ id }, { tag: data }, response => growl.success(_t('app.admin.members.tag_form.changes_successfully_saved')) + , error => growl.error(_t('app.admin.members.tag_form.an_error_occurred_while_saving_changes'))); } else { return Tag.save({ tag: data }, function (resp) { - growl.success(_t('new_tag_successfully_saved')); + growl.success(_t('app.admin.members.tag_form.new_tag_successfully_saved')); return $scope.tags[$scope.tags.length - 1].id = resp.id; } , function (error) { - growl.error(_t('an_error_occurred_while_saving_the_new_tag')); + growl.error(_t('app.admin.members.tag_form.an_error_occurred_while_saving_the_new_tag')); return $scope.tags.splice($scope.tags.length - 1, 1); }); } @@ -62,13 +62,24 @@ Application.Controllers.controller('TagsController', ['$scope', 'tagsPromise', ' * Deletes the tag at the specified index * @param index {number} tag index in the $scope.tags array */ - return $scope.removeTag = index => - // TODO add confirmation : les utilisateurs seront déasociés - Tag.delete({ id: $scope.tags[index].id }, function (resp) { - growl.success(_t('tag_successfully_deleted')); - return $scope.tags.splice(index, 1); + $scope.removeTag = index => + dialogs.confirm({ + resolve: { + object () { + return { + title: _t('app.admin.members.tag_form.confirmation_required'), + msg: _t('app.admin.members.tag_form.confirm_delete_tag_html') + }; + } + } } - , error => growl.error(_t('an_error_occurred_and_the_tag_deletion_failed'))); + , () => { + Tag.delete({ id: $scope.tags[index].id }, function (resp) { + growl.success(_t('app.admin.members.tag_form.tag_successfully_deleted')); + return $scope.tags.splice(index, 1); + } + , error => growl.error(_t('app.admin.members.tag_form.an_error_occurred_and_the_tag_deletion_failed'))); + }); } ]); diff --git a/app/assets/javascripts/controllers/admin/trainings.js.erb b/app/assets/javascripts/controllers/admin/trainings.js.erb index 8a748335b..1710b5806 100644 --- a/app/assets/javascripts/controllers/admin/trainings.js.erb +++ b/app/assets/javascripts/controllers/admin/trainings.js.erb @@ -150,22 +150,19 @@ Application.Controllers.controller('EditTrainingController', [ '$scope', '$state /** * Controller used in the trainings management page, allowing admins users to see and manage the list of trainings and reservations. */ -Application.Controllers.controller('TrainingsAdminController', ['$scope', '$state', '$uibModal', 'Training', 'trainingsPromise', 'machinesPromise', '_t', 'growl', 'dialogs', - function ($scope, $state, $uibModal, Training, trainingsPromise, machinesPromise, _t, growl, dialogs) { - /* PUBLIC SCOPE */ +Application.Controllers.controller('TrainingsAdminController', ['$scope', '$state', '$uibModal', 'Training', 'trainingsPromise', 'machinesPromise', '_t', 'growl', 'dialogs', 'Member', 'uiTourService', + function ($scope, $state, $uibModal, Training, trainingsPromise, machinesPromise, _t, growl, dialogs, Member, uiTourService) { // list of trainings - let groupAvailabilities; $scope.trainings = trainingsPromise; // simplified list of machines $scope.machines = machinesPromise; - // Training to monitor, binded with drop-down selection - $scope.monitoring = - { training: null }; + // Training to monitor, bound with drop-down selection + $scope.monitoring = { training: null }; - // list of training availabilies, grouped by date + // list of training availabilities, grouped by date $scope.groupedAvailabilities = {}; // default: accordions are not open @@ -184,6 +181,9 @@ Application.Controllers.controller('TrainingsAdminController', ['$scope', '$stat 'all' ]; + // default tab: trainings list + $scope.tabs = { active: 0 }; + /** * In the trainings listing tab, return the stringified list of machines associated with the provided training * @param training {Object} Training object, inherited from $resource @@ -196,7 +196,7 @@ Application.Controllers.controller('TrainingsAdminController', ['$scope', '$stat return selected.push(m.name); } }); - if (selected.length) { return selected.join(', '); } else { return _t('none'); } + if (selected.length) { return selected.join(', '); } else { return _t('app.admin.trainings.none'); } }; /** @@ -276,8 +276,8 @@ Application.Controllers.controller('TrainingsAdminController', ['$scope', '$stat resolve: { object () { return { - title: _t('confirmation_required'), - msg: _t('do_you_really_want_to_delete_this_training') + title: _t('app.admin.trainings.confirmation_required'), + msg: _t('app.admin.trainings.do_you_really_want_to_delete_this_training') }; } } @@ -285,10 +285,10 @@ Application.Controllers.controller('TrainingsAdminController', ['$scope', '$stat function () { // deletion confirmed training.$delete(function () { $scope.trainings.splice(index, 1); - growl.info(_t('training_successfully_deleted')); + growl.info(_t('app.admin.trainings.training_successfully_deleted')); }, function (error) { - growl.warning(_t('unable_to_delete_the_training_because_some_users_alredy_booked_it')); + growl.warning(_t('app.admin.trainings.unable_to_delete_the_training_because_some_users_already_booked_it')); console.error(error); }); } @@ -335,14 +335,107 @@ Application.Controllers.controller('TrainingsAdminController', ['$scope', '$stat }); }; + /** + * Setup the feature-tour for the admin/trainings page. + * This is intended as a contextual help (when pressing F1) + */ + $scope.setupTrainingsTour = function () { + // get the tour defined by the ui-tour directive + const uitour = uiTourService.getTourByName('trainings'); + uitour.createStep({ + selector: 'body', + stepId: 'welcome', + order: 0, + title: _t('app.admin.tour.trainings.welcome.title'), + content: _t('app.admin.tour.trainings.welcome.content'), + placement: 'bottom', + orphan: true + }); + uitour.createStep({ + selector: '.trainings-monitoring .manage-trainings', + stepId: 'trainings', + order: 1, + title: _t('app.admin.tour.trainings.trainings.title'), + content: _t('app.admin.tour.trainings.trainings.content'), + placement: 'bottom' + }); + uitour.createStep({ + selector: '.trainings-monitoring .filter-trainings', + stepId: 'filter', + order: 2, + title: _t('app.admin.tour.trainings.filter.title'), + content: _t('app.admin.tour.trainings.filter.content'), + placement: 'left' + }); + uitour.createStep({ + selector: '.trainings-monitoring .post-tracking', + stepId: 'tracking', + order: 3, + title: _t('app.admin.tour.trainings.tracking.title'), + content: _t('app.admin.tour.trainings.tracking.content'), + placement: 'bottom' + }); + uitour.createStep({ + selector: 'body', + stepId: 'conclusion', + order: 4, + title: _t('app.admin.tour.conclusion.title'), + content: _t('app.admin.tour.conclusion.content'), + placement: 'bottom', + orphan: true + }); + // on step change, change the active tab if needed + uitour.on('stepChanged', function (nextStep) { + if (nextStep.stepId === 'filter' || nextStep.stepId === 'machines') { $scope.tabs.active = 0; } + if (nextStep.stepId === 'tracking') { $scope.tabs.active = 1; } + }); + // on tour end, save the status in database + uitour.on('ended', function () { + if (uitour.getStatus() === uitour.Status.ON && $scope.currentUser.profile.tours.indexOf('trainings') < 0) { + Member.completeTour({ id: $scope.currentUser.id }, { tour: 'trainings' }, function (res) { + $scope.currentUser.profile.tours = res.tours; + }); + } + }); + // if the user has never seen the tour, show him now + if (Fablab.featureTourDisplay !== 'manual' && $scope.currentUser.profile.tours.indexOf('trainings') < 0) { + uitour.start(); + } + // start this tour when an user press F1 - this is contextual help + window.addEventListener('keydown', handleF1); + } + + /* PRIVATE SCOPE */ + /** + * Kind of constructor: these actions will be realized first when the controller is loaded + */ + const initialize = function () { + // listen the $destroy event of the controller to remove the F1 key binding + $scope.$on('$destroy', function () { + window.removeEventListener('keydown', handleF1); + }); + }; + + /** + * Callback used to trigger the feature tour when the user press the F1 key. + * @param e {KeyboardEvent} + */ + const handleF1 = function (e) { + if (e.key === 'F1') { + e.preventDefault(); + const tour = uiTourService.getTourByName('trainings'); + if (tour) { tour.start(); } + } + }; + /** * Group the trainings availabilities by trainings and by dates and return the resulting tree * @param trainings {Array} $scope.trainings is expected here * @returns {Object} Tree constructed as /training_name/year/month/day/[availabilities] */ - return groupAvailabilities = function (trainings) { + const groupAvailabilities = function (trainings) { const tree = {}; for (let training of Array.from(trainings)) { tree[training.name] = {}; @@ -367,6 +460,9 @@ Application.Controllers.controller('TrainingsAdminController', ['$scope', '$stat } return tree; }; + + // !!! MUST BE CALLED AT THE END of the controller + return initialize(); } ]); diff --git a/app/assets/javascripts/controllers/application.js.erb b/app/assets/javascripts/controllers/application.js.erb index f048af8dd..b5d2019b7 100644 --- a/app/assets/javascripts/controllers/application.js.erb +++ b/app/assets/javascripts/controllers/application.js.erb @@ -12,8 +12,8 @@ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -Application.Controllers.controller('ApplicationController', ['$rootScope', '$scope', '$window', '$locale', 'Session', 'AuthService', 'Auth', '$uibModal', '$state', 'growl', 'Notification', '$interval', 'Setting', '_t', 'Version', - function ($rootScope, $scope, $window, $locale, Session, AuthService, Auth, $uibModal, $state, growl, Notification, $interval, Setting, _t, Version) { +Application.Controllers.controller('ApplicationController', ['$rootScope', '$scope', '$window', '$locale', '$timeout', 'Session', 'AuthService', 'Auth', '$uibModal', '$state', 'growl', 'Notification', '$interval', 'Setting', '_t', 'Version', + function ($rootScope, $scope, $window, $locale, $timeout, Session, AuthService, Auth, $uibModal, $state, growl, Notification, $interval, Setting, _t, Version) { /* PRIVATE STATIC CONSTANTS */ // User's notifications will get refreshed every 30s @@ -23,7 +23,7 @@ Application.Controllers.controller('ApplicationController', ['$rootScope', '$sco // Fab-manager's version $scope.version = - { version: '' }; + { current: '' }; // currency symbol for the current locale (cf. angular-i18n) $rootScope.currencySymbol = $locale.NUMBER_FORMATS.CURRENCY_SYM; @@ -37,11 +37,11 @@ Application.Controllers.controller('ApplicationController', ['$rootScope', '$sco $rootScope.currentUser = user; Session.create(user); getNotifications(); - // fab-manager's app-version + // Fab-manager's app-version if (user.role === 'admin') { return $scope.version = Version.get(); } else { - return $scope.version = { version: '' }; + return $scope.version = { current: '' }; } } }; @@ -149,7 +149,7 @@ Application.Controllers.controller('ApplicationController', ['$rootScope', '$sco $uibModalInstance.close(user); } else { // the user was not saved in database, something wrong occurred - growl.error(_t('unexpected_error_occurred')); + growl.error(_t('app.public.common.unexpected_error_occurred')); } }, function (error) { // creation failed... @@ -169,7 +169,12 @@ Application.Controllers.controller('ApplicationController', ['$rootScope', '$sco }] }).result['finally'](null).then(function (user) { // when the account was created successfully, set the session to the newly created account - $scope.setCurrentUser(user); + if(Fablab.userConfirmationNeededToSignIn) { + Auth._currentUser = null; + growl.info(_t('app.public.common.you_will_receive_confirmation_instructions_by_email_detailed')); + } else { + $scope.setCurrentUser(user); + } }); <% end %> }; @@ -204,7 +209,7 @@ Application.Controllers.controller('ApplicationController', ['$rootScope', '$sco }; }] }).result['finally'](null).then(function () { - growl.success(_t('your_password_was_successfully_changed')); + growl.success(_t('app.public.common.your_password_was_successfully_changed')); return Auth.login().then(function (user) { $scope.setCurrentUser(user); }, function (error) { @@ -262,6 +267,36 @@ Application.Controllers.controller('ApplicationController', ['$rootScope', '$sco toggler.toggleClass('active'); }; + /** + * Open the modal dialog showing that an upgrade is available + */ + $scope.versionModal = function() { + if ($scope.version.up_to_date) return; + if ($rootScope.currentUser.role !== 'admin') return; + + $uibModal.open({ + templateUrl: '<%= asset_path "admin/versions/upgradeModal.html" %>', + controller: 'VersionModalController', + resolve: { + version() { return $scope.version; } + } + }); + } + + /** + * Trigger the contextual help "feature tour". + * @param event {Object} see https://docs.angularjs.org/guide/expression#-event- + */ + $scope.help = function (event) { + event.preventDefault(); + + // we wrap the event triggering into a $timeout to prevent conflicting with current $apply + $timeout(function () { + var evt = new KeyboardEvent('keydown', { key: 'F1' }); + window.dispatchEvent(evt); + }); + } + /* PRIVATE SCOPE */ /** * Kind of constructor: these actions will be realized first when the controller is loaded @@ -299,8 +334,11 @@ Application.Controllers.controller('ApplicationController', ['$rootScope', '$sco // we stop polling notifications when the page is not in foreground onPageVisible(function (state) { $rootScope.toCheckNotifications = (state === 'visible'); }); - Setting.get({ name: 'fablab_name' }, function (data) { $scope.fablabName = data.setting.value; }); - Setting.get({ name: 'name_genre' }, function (data) { $scope.nameGenre = data.setting.value; }); + Setting.query({ names: "['fablab_name', 'name_genre', 'link_name']" }, function (settings) { + $scope.fablabName = settings.fablab_name; + $scope.nameGenre = settings.name_genre; + $scope.linkName = settings.link_name; + }); // shorthands $scope.isAuthenticated = Auth.isAuthenticated; @@ -309,10 +347,10 @@ Application.Controllers.controller('ApplicationController', ['$rootScope', '$sco }; /** - * Retreive once the notifications from the server and display a message popup for each new one. + * Retrieve once the notifications from the server and display a message popup for each new one. * Then, periodically check for new notifications. */ - var getNotifications = function () { + const getNotifications = function () { $rootScope.toCheckNotifications = true; if (!$rootScope.checkNotificationsIsInit && !!$rootScope.currentUser) { setTimeout(function () { @@ -325,7 +363,7 @@ Application.Controllers.controller('ApplicationController', ['$rootScope', '$sco angular.forEach(notifications.notifications, function (n) { toDisplay.push(n); }); if (toDisplay.length < notifications.totals.unread) { - toDisplay.push({ message: { description: _t('and_NUMBER_other_notifications', { NUMBER: notifications.totals.unread - toDisplay.length }, 'messageformat') } }); + toDisplay.push({ message: { description: _t('app.public.common.and_NUMBER_other_notifications', { NUMBER: notifications.totals.unread - toDisplay.length }) } }); } angular.forEach(toDisplay, function (notification) { growl.info(notification.message.description); }); @@ -353,7 +391,7 @@ Application.Controllers.controller('ApplicationController', ['$rootScope', '$sco /** * Open the modal window allowing the user to log in. */ - var openLoginModal = function (toState, toParams, callback) { + const openLoginModal = function (toState, toParams, callback) { <% active_provider = AuthProvider.active %> <% if active_provider.providable_type != DatabaseProvider.name %> $window.location.href = '/sso-redirect'; @@ -375,7 +413,7 @@ Application.Controllers.controller('ApplicationController', ['$rootScope', '$sco console.error(`Authentication failed: ${JSON.stringify(error)}`); $scope.alerts = []; return $scope.alerts.push({ - msg: _t('wrong_email_or_password'), + msg: error.data.error, type: 'danger' }); }); @@ -388,6 +426,11 @@ Application.Controllers.controller('ApplicationController', ['$rootScope', '$sco return $uibModalInstance.dismiss('signup'); }; + $scope.openConfirmationNewModal = function(e) { + e.preventDefault(); + return $uibModalInstance.dismiss('confirmationNew'); + }; + return $scope.openResetPassword = function (e) { e.preventDefault(); return $uibModalInstance.dismiss('resetPassword'); @@ -418,13 +461,31 @@ Application.Controllers.controller('ApplicationController', ['$rootScope', '$sco $scope.alerts = []; return $http.post('/users/password.json', { user: $scope.user }).then(function () { $uibModalInstance.close(); }).catch(function () { $scope.alerts.push({ - msg: _t('your_email_address_is_unknown'), + msg: _t('app.public.common.your_email_address_is_unknown'), type: 'danger' }); }); }; }] - }).result['finally'](null).then(function () { growl.info(_t('you_will_receive_in_a_moment_an_email_with_instructions_to_reset_your_password')); }); + }).result['finally'](null).then(function () { growl.info(_t('app.public.common.you_will_receive_in_a_moment_an_email_with_instructions_to_reset_your_password')); }); + } else if (reason === 'confirmationNew') { + // open the 'reset password' modal + return $uibModal.open({ + templateUrl: '<%= asset_path "shared/ConfirmationNewModal.html" %>', + size: 'sm', + controller: ['$scope', '$uibModalInstance', '$http', function ($scope, $uibModalInstance, $http) { + $scope.user = { email: '' }; + return $scope.submitConfirmationNewForm = function () { + $scope.alerts = []; + return $http.post('/users/confirmation.json', { user: $scope.user }).then(function () { $uibModalInstance.close(); }).catch(function (res) { + $scope.alerts.push({ + msg: res.data.errors.email[0], + type: 'danger' + }); + }); + }; + }] + }).result['finally'](null).then(function () { growl.info(_t('app.public.common.you_will_receive_confirmation_instructions_by_email_detailed')); }); } }); // otherwise the user just closed the modal @@ -437,7 +498,7 @@ Application.Controllers.controller('ApplicationController', ['$rootScope', '$sco * When the status changes, the callback is triggered with the new status as parameter * Inspired by http://stackoverflow.com/questions/1060008/is-there-a-way-to-detect-if-a-browser-window-is-not-currently-active#answer-1060034 */ - var onPageVisible = function (callback) { + const onPageVisible = function (callback) { let hidden = 'hidden'; const onchange = function (evt) { @@ -485,3 +546,16 @@ Application.Controllers.controller('ApplicationController', ['$rootScope', '$sco return initialize(); } ]); + +/** + * Controller used in the modal showing details about the version and the upgrades + */ +Application.Controllers.controller('VersionModalController', ['$scope', '$uibModalInstance', 'version', function ($scope, $uibModalInstance, version) { + // version infos (current version + upgrade infos from hub) + $scope.version = version; + + // callback to close the modal + $scope.close = function () { + $uibModalInstance.dismiss(); + } +}]); diff --git a/app/assets/javascripts/controllers/calendar.js b/app/assets/javascripts/controllers/calendar.js index 0af678931..4f60257c5 100644 --- a/app/assets/javascripts/controllers/calendar.js +++ b/app/assets/javascripts/controllers/calendar.js @@ -16,8 +16,8 @@ * Controller used in the public calendar global */ -Application.Controllers.controller('CalendarController', ['$scope', '$state', '$aside', 'moment', 'Availability', 'Slot', 'Setting', 'growl', 'dialogs', 'bookingWindowStart', 'bookingWindowEnd', '_t', 'uiCalendarConfig', 'CalendarConfig', 'trainingsPromise', 'machinesPromise', 'spacesPromise', - function ($scope, $state, $aside, moment, Availability, Slot, Setting, growl, dialogs, bookingWindowStart, bookingWindowEnd, _t, uiCalendarConfig, CalendarConfig, trainingsPromise, machinesPromise, spacesPromise) { +Application.Controllers.controller('CalendarController', ['$scope', '$state', '$aside', 'moment', 'Availability', 'Slot', 'Setting', 'growl', 'dialogs', 'bookingWindowStart', 'bookingWindowEnd', '_t', 'uiCalendarConfig', 'CalendarConfig', 'trainingsPromise', 'machinesPromise', 'spacesPromise', 'iCalendarPromise', + function ($scope, $state, $aside, moment, Availability, Slot, Setting, growl, dialogs, bookingWindowStart, bookingWindowEnd, _t, uiCalendarConfig, CalendarConfig, trainingsPromise, machinesPromise, spacesPromise, iCalendarPromise) { /* PRIVATE STATIC CONSTANTS */ let currentMachineEvent = null; machinesPromise.forEach(m => m.checked = true); @@ -38,6 +38,9 @@ Application.Controllers.controller('CalendarController', ['$scope', '$state', '$ // List of spaces $scope.spaces = spacesPromise.filter(t => !t.disabled); + // List of external iCalendar sources + $scope.externals = iCalendarPromise.map(e => Object.assign(e, { checked: true })); + // add availabilities source to event sources $scope.eventSources = []; @@ -48,10 +51,41 @@ Application.Controllers.controller('CalendarController', ['$scope', '$state', '$ trainings: isSelectAll('trainings', scope), machines: isSelectAll('machines', scope), spaces: isSelectAll('spaces', scope), + externals: isSelectAll('externals', scope), evt: filter.evt, dispo: filter.dispo }); - return $scope.calendarConfig.events = availabilitySourceUrl(); + $scope.calendarConfig.events = availabilitySourceUrl(); + // external iCalendar events sources + $scope.externals.forEach(e => { + if (e.checked) { + if (!$scope.eventSources.some(evt => evt.id === e.id)) { + $scope.eventSources.push({ + id: e.id, + url: `/api/i_calendar/${e.id}/events`, + textColor: e.text_color || '#000', + color: e.color + }); + } + } else { + if ($scope.eventSources.some(evt => evt.id === e.id)) { + const idx = $scope.eventSources.findIndex(evt => evt.id === e.id); + $scope.eventSources.splice(idx, 1); + } + } + }); + uiCalendarConfig.calendars.calendar.fullCalendar('refetchEventSources'); + }; + + /** + * Return a CSS-like style of the given calendar configuration + * @param calendar + */ + $scope.calendarStyle = function (calendar) { + return { + 'border-color': calendar.color, + 'color': calendar.text_color + }; }; // a variable for formation/machine/event/dispo checkbox is or not checked @@ -59,6 +93,7 @@ Application.Controllers.controller('CalendarController', ['$scope', '$state', '$ trainings: isSelectAll('trainings', $scope), machines: isSelectAll('machines', $scope), spaces: isSelectAll('spaces', $scope), + externals: isSelectAll('externals', $scope), evt: true, dispo: true }; @@ -85,6 +120,9 @@ Application.Controllers.controller('CalendarController', ['$scope', '$state', '$ spaces () { return $scope.spaces; }, + externals () { + return $scope.externals; + }, filter () { return $scope.filter; }, @@ -95,10 +133,11 @@ Application.Controllers.controller('CalendarController', ['$scope', '$state', '$ return $scope.filterAvailabilities; } }, - controller: ['$scope', '$uibModalInstance', 'trainings', 'machines', 'spaces', 'filter', 'toggleFilter', 'filterAvailabilities', function ($scope, $uibModalInstance, trainings, machines, spaces, filter, toggleFilter, filterAvailabilities) { + controller: ['$scope', '$uibModalInstance', 'trainings', 'machines', 'spaces', 'externals', 'filter', 'toggleFilter', 'filterAvailabilities', function ($scope, $uibModalInstance, trainings, machines, spaces, externals, filter, toggleFilter, filterAvailabilities) { $scope.trainings = trainings; $scope.machines = machines; $scope.spaces = spaces; + $scope.externals = externals; $scope.filter = filter; $scope.toggleFilter = (type, filter) => toggleFilter(type, filter); @@ -114,78 +153,11 @@ Application.Controllers.controller('CalendarController', ['$scope', '$state', '$ /* PRIVATE SCOPE */ - const calendarEventClickCb = function (event, jsEvent, view) { - // current calendar object - const { calendar } = uiCalendarConfig.calendars; - if (event.available_type === 'machines') { - currentMachineEvent = event; - calendar.fullCalendar('changeView', 'agendaDay'); - return calendar.fullCalendar('gotoDate', event.start); - } else if (event.available_type === 'space') { - calendar.fullCalendar('changeView', 'agendaDay'); - return calendar.fullCalendar('gotoDate', event.start); - } else if (event.available_type === 'event') { - return $state.go('app.public.events_show', { id: event.event_id }); - } else if (event.available_type === 'training') { - return $state.go('app.public.training_show', { id: event.training_id }); - } else { - if (event.machine_id) { - return $state.go('app.public.machines_show', { id: event.machine_id }); - } else if (event.space_id) { - return $state.go('app.public.space_show', { id: event.space_id }); - } - } - }; - - // agendaDay view: disable slotEventOverlap - // agendaWeek view: enable slotEventOverlap - const toggleSlotEventOverlap = function (view) { - // set defaultView, because when we change slotEventOverlap - // ui-calendar will trigger rerender calendar - $scope.calendarConfig.defaultView = view.type; - const today = currentMachineEvent ? currentMachineEvent.start : moment().utc().startOf('day'); - if ((today > view.intervalStart) && (today < view.intervalEnd) && (today !== view.intervalStart)) { - $scope.calendarConfig.defaultDate = today; - } else { - $scope.calendarConfig.defaultDate = view.intervalStart; - } - if (view.type === 'agendaDay') { - return $scope.calendarConfig.slotEventOverlap = false; - } else { - return $scope.calendarConfig.slotEventOverlap = true; - } - }; - - // function is called when calendar view is rendered or changed - const viewRenderCb = function (view, element) { - toggleSlotEventOverlap(view); - if (view.type === 'agendaDay') { - // get availabilties by 1 day for show machine slots - return uiCalendarConfig.calendars.calendar.fullCalendar('refetchEvents'); - } - }; - - const eventRenderCb = function (event, element) { - if (event.tags.length > 0) { - let html = ''; - for (let tag of Array.from(event.tags)) { - html += `${tag.name} `; - } - element.find('.fc-title').append(`
${html}`); - } - }; - - const getFilter = function () { - const t = $scope.trainings.filter(t => t.checked).map(t => t.id); - const m = $scope.machines.filter(m => m.checked).map(m => m.id); - const s = $scope.spaces.filter(s => s.checked).map(s => s.id); - return { t, m, s, evt: $scope.filter.evt, dispo: $scope.filter.dispo }; - }; - - var availabilitySourceUrl = () => `/api/availabilities/public?${$.param(getFilter())}`; - - const initialize = () => - // fullCalendar (v2) configuration + /** + * Kind of constructor: these actions will be realized first when the controller is loaded + */ + const initialize = () => { + // fullCalendar (v2) configuration $scope.calendarConfig = CalendarConfig({ events: availabilitySourceUrl(), slotEventOverlap: true, @@ -207,6 +179,97 @@ Application.Controllers.controller('CalendarController', ['$scope', '$state', '$ return eventRenderCb(event, element); } }); + $scope.externals.forEach(e => { + if (e.checked) { + $scope.eventSources.push({ + id: e.id, + url: `/api/i_calendar/${e.id}/events`, + textColor: e.text_color || '#000', + color: e.color + }); + } + }); + }; + + /** + * Callback triggered when an event object is clicked in the fullCalendar view + */ + const calendarEventClickCb = function (event, jsEvent, view) { + // current calendar object + const { calendar } = uiCalendarConfig.calendars; + if (event.available_type === 'machines') { + currentMachineEvent = event; + calendar.fullCalendar('changeView', 'agendaDay'); + calendar.fullCalendar('gotoDate', event.start); + } else if (event.available_type === 'space') { + calendar.fullCalendar('changeView', 'agendaDay'); + calendar.fullCalendar('gotoDate', event.start); + } else if (event.available_type === 'event') { + $state.go('app.public.events_show', { id: event.event_id }); + } else if (event.available_type === 'training') { + $state.go('app.public.training_show', { id: event.training_id }); + } else { + if (event.machine_id) { + $state.go('app.public.machines_show', { id: event.machine_id }); + } else if (event.space_id) { + $state.go('app.public.space_show', { id: event.space_id }); + } + } + }; + + // agendaDay view: disable slotEventOverlap + // agendaWeek view: enable slotEventOverlap + const toggleSlotEventOverlap = function (view) { + // set defaultView, because when we change slotEventOverlap + // ui-calendar will trigger rerender calendar + $scope.calendarConfig.defaultView = view.type; + const today = currentMachineEvent ? currentMachineEvent.start : moment().utc().startOf('day'); + if ((today > view.intervalStart) && (today < view.intervalEnd) && (today !== view.intervalStart)) { + $scope.calendarConfig.defaultDate = today; + } else { + $scope.calendarConfig.defaultDate = view.intervalStart; + } + if (view.type === 'agendaDay') { + return $scope.calendarConfig.slotEventOverlap = false; + } else { + return $scope.calendarConfig.slotEventOverlap = true; + } + }; + + /** + * This function is called when calendar view is rendered or changed + * @see https://fullcalendar.io/docs/v3/viewRender#v2 + */ + const viewRenderCb = function (view, element) { + toggleSlotEventOverlap(view); + if (view.type === 'agendaDay') { + // get availabilties by 1 day for show machine slots + return uiCalendarConfig.calendars.calendar.fullCalendar('refetchEvents'); + } + }; + + /** + * Callback triggered by fullCalendar when it is about to render an event. + * @see https://fullcalendar.io/docs/v3/eventRender#v2 + */ + const eventRenderCb = function (event, element) { + if (event.tags && event.tags.length > 0) { + let html = ''; + for (let tag of Array.from(event.tags)) { + html += `${tag.name} `; + } + element.find('.fc-title').append(`
${html}`); + } + }; + + const getFilter = function () { + const t = $scope.trainings.filter(t => t.checked).map(t => t.id); + const m = $scope.machines.filter(m => m.checked).map(m => m.id); + const s = $scope.spaces.filter(s => s.checked).map(s => s.id); + return { t, m, s, evt: $scope.filter.evt, dispo: $scope.filter.dispo }; + }; + + var availabilitySourceUrl = () => `/api/availabilities/public?${$.param(getFilter())}`; // !!! MUST BE CALLED AT THE END of the controller return initialize(); diff --git a/app/assets/javascripts/controllers/events.js.erb b/app/assets/javascripts/controllers/events.js.erb index abd4bef75..d81811991 100644 --- a/app/assets/javascripts/controllers/events.js.erb +++ b/app/assets/javascripts/controllers/events.js.erb @@ -126,13 +126,16 @@ Application.Controllers.controller('EventsController', ['$scope', '$state', 'Eve } ]); -Application.Controllers.controller('ShowEventController', ['$scope', '$state', '$stateParams', '$rootScope', 'Event', '$uibModal', 'Member', 'Reservation', 'Price', 'CustomAsset', 'eventPromise', 'growl', '_t', 'Wallet', 'helpers', 'dialogs', 'priceCategoriesPromise', 'settingsPromise', - function ($scope, $state, $stateParams, $rootScope, Event, $uibModal, Member, Reservation, Price, CustomAsset, eventPromise, growl, _t, Wallet, helpers, dialogs, priceCategoriesPromise, settingsPromise) { - /* PUBLIC SCOPE */ +Application.Controllers.controller('ShowEventController', ['$scope', '$state', '$stateParams', '$rootScope', 'Event', '$uibModal', 'Member', 'Reservation', 'Price', 'CustomAsset', 'Slot', 'eventPromise', 'growl', '_t', 'Wallet', 'helpers', 'dialogs', 'priceCategoriesPromise', 'settingsPromise', + function ($scope, $state, $stateParams, $rootScope, Event, $uibModal, Member, Reservation, Price, CustomAsset, Slot, eventPromise, growl, _t, Wallet, helpers, dialogs, priceCategoriesPromise, settingsPromise) { + /* PUBLIC SCOPE */ // reservations for the currently shown event $scope.reservations = []; + // current date & time + $scope.now = moment(); + // user to deal with $scope.ctrl = { member: {} }; @@ -166,6 +169,12 @@ 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); + // Global config: is the user authorized to cancel his booking slots? + $scope.enableBookingCancel = settingsPromise.booking_cancel_enable === 'true'; + + // Global config: delay in hours from now when restrictions occurs about cancelling reservations + $scope.cancelBookingDelay = parseInt(settingsPromise.booking_cancel_delay); + // Message displayed to the end user about rules that applies to events reservations $scope.eventExplicationsAlert = settingsPromise.event_explications_alert; @@ -174,26 +183,22 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' * @param event {$resource} angular's Event $resource */ $scope.deleteEvent = function (event) { - dialogs.confirm({ + // open a confirmation dialog + const modalInstance = $uibModal.open({ + animation: true, + templateUrl: '<%= asset_path "events/deleteRecurrent.html" %>', + size: 'md', + controller: 'DeleteRecurrentEventController', resolve: { - object () { - return { - title: _t('confirmation_required'), - msg: _t('do_you_really_want_to_delete_this_event') - }; - } + eventPromise: ['Event', function (Event) { return Event.get({ id: $scope.event.id }).$promise; }] } - }, function () { - // the admin has confirmed, delete - event.$delete(function () { + }); + // once the dialog was closed, do things depending on the result + modalInstance.result.then(function (res) { + if (res.status == 'success') { $state.go('app.public.events_list'); - return growl.info(_t('event_successfully_deleted')); - }, function (error) { - console.error(error); - growl.error(_t('unable_to_delete_the_event_because_some_users_alredy_booked_it')); - }); - } - ); + } + }); }; /** @@ -241,13 +246,34 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' $scope.reserveSuccess = false; if (!$scope.isAuthenticated()) { return $scope.login(null, function (user) { - $scope.reserve.toReserve = !$scope.reserve.toReserve; if (user.role !== 'admin') { return $scope.ctrl.member = user; } + const sameTimeReservations = findReservationsAtSameTime(); + if (sameTimeReservations.length > 0) { + showReserveSlotSameTimeModal(sameTimeReservations, function(res) { + return $scope.reserve.toReserve = !$scope.reserve.toReserve; + }); + } else { + return $scope.reserve.toReserve = !$scope.reserve.toReserve; + } }); } else { - return $scope.reserve.toReserve = !$scope.reserve.toReserve; + if ($scope.currentUser.role === 'admin') { + return $scope.reserve.toReserve = !$scope.reserve.toReserve; + } else { + Member.get({ id: $scope.currentUser.id }, function (member) { + $scope.ctrl.member = member; + const sameTimeReservations = findReservationsAtSameTime(); + if (sameTimeReservations.length > 0) { + showReserveSlotSameTimeModal(sameTimeReservations, function(res) { + return $scope.reserve.toReserve = !$scope.reserve.toReserve; + }); + } else { + return $scope.reserve.toReserve = !$scope.reserve.toReserve; + } + }); + } } } }; @@ -279,7 +305,7 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' const amountToPay = helpers.getAmountToPay($scope.reserve.amountTotal, wallet.amount); if (($scope.currentUser.role !== 'admin') && (amountToPay > 0)) { if ($rootScope.fablabWithoutOnlinePayment) { - growl.error(_t('online_payment_disabled')); + growl.error(_t('app.public.events_show.online_payment_disabled')); } else { return payByStripe(reservation); } @@ -291,7 +317,7 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' }); } else { // otherwise we alert, this error musn't occur when the current user is not admin - return growl.error(_t('please_select_a_member_first')); + return growl.error(_t('app.public.events_show.please_select_a_member_first')); } }; @@ -341,16 +367,50 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' }); }; + /** + * Callback to cancel a reservation + * @param reservation {{id:number, reservable_id:number, nb_reserve_places:number}} + */ + $scope.cancelReservation = function(reservation) { + dialogs.confirm({ + resolve: { + object: function() { + return { + title: _t('app.public.events_show.cancel_the_reservation'), + msg: _t('app.public.events_show.do_you_really_want_to_cancel_this_reservation_this_apply_to_all_booked_tickets') + }; + } + } + }, function() { // cancel confirmed + Slot.cancel({ + id: reservation.slots[0].id + }, function() { // successfully canceled + let index; + growl.success(_t('app.public.events_show.reservation_was_successfully_cancelled')); + index = $scope.reservations.indexOf(reservation); + $scope.event.nb_free_places = $scope.event.nb_free_places + reservation.total_booked_seats; + $scope.reservations[index].slots[0].canceled_at = new Date(); + }, function(error) { + growl.warning(_t('app.public.events_show.cancellation_failed')); + }); + }); + }; + + /** + * Test if the provided reservation has been cancelled + * @param reservation {Reservation} + * @returns {boolean} + */ + $scope.isCancelled = function(reservation) { + return !!(reservation.slots[0].canceled_at); + } + /** * Callback to alter an already booked reservation date. A modal window will be opened to allow the user to choose * a new date for his reservation (if any available) * @param reservation {{id:number, reservable_id:number, nb_reserve_places:number}} - * @param e {Object} see https://docs.angularjs.org/guide/expression#-event- */ - $scope.modifyReservation = function (reservation, e) { - e.preventDefault(); - e.stopPropagation(); - + $scope.modifyReservation = function (reservation) { const index = $scope.reservations.indexOf(reservation); return $uibModal.open({ templateUrl: '<%= asset_path "events/modify_event_reservation_modal.html" %>', @@ -364,9 +424,9 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' $scope.reservation = angular.copy(reservation); // set the reservable_id to the first available event - for (e of Array.from(event.recurrence_events)) { - if (e.nb_free_places > reservation.total_booked_seats) { - $scope.reservation.reservable_id = e.id; + for (evt of Array.from(event.recurrence_events)) { + if (evt.nb_free_places > reservation.total_booked_seats) { + $scope.reservation.reservable_id = evt.id; break; } } @@ -419,7 +479,7 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' /** * Checks if the provided reservation is able to be moved (date change) - * @param reservation {{total_booked_seats:number}} + * @param reservation {{slots:[], total_booked_seats:number}} */ $scope.reservationCanModify = function (reservation) { const slotStart = moment(reservation.slots[0].start_at); @@ -432,6 +492,17 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' return (isAble && $scope.enableBookingMove && (slotStart.diff(now, 'hours') >= $scope.moveBookingDelay)); }; + /** + * Checks if the provided reservation is able to be cancelled + * @param reservation {{slots:[]}} + */ + $scope.reservationCanCancel = function(reservation) { + var now, slotStart; + slotStart = moment(reservation.slots[0].start_at); + now = moment(); + return $scope.enableBookingCancel && slotStart.diff(now, "hours") >= $scope.cancelBookingDelay; + }; + /** * Compute the total amount for the current reservation according to the previously set parameters * and assign the result in $scope.reserve.amountTotal @@ -567,7 +638,7 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' }; /** - * Set the current reservation to the default values. This implies to reservation form to be hidden. + * Set the current reservation to the default values. This implies the reservation form to be hidden. */ var resetEventReserve = function () { if ($scope.event) { @@ -696,12 +767,12 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' // Button label if ($scope.amount > 0) { - $scope.validButtonName = _t('confirm_payment_of_html', { ROLE: $scope.currentUser.role, AMOUNT: $filter('currency')($scope.amount) }, 'messageformat'); + $scope.validButtonName = _t('app.public.events_show.confirm_payment_of_html', { ROLE: $scope.currentUser.role, AMOUNT: $filter('currency')($scope.amount) }); } else { if ((price.price > 0) && ($scope.walletAmount === 0)) { - $scope.validButtonName = _t('confirm_payment_of_html', { ROLE: $scope.currentUser.role, AMOUNT: $filter('currency')(price.price) }, 'messageformat'); + $scope.validButtonName = _t('app.public.events_show.confirm_payment_of_html', { ROLE: $scope.currentUser.role, AMOUNT: $filter('currency')(price.price) }); } else { - $scope.validButtonName = _t('confirm'); + $scope.validButtonName = _t('app.shared.buttons.confirm'); } } @@ -735,7 +806,7 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' /** * What to do after the payment was successful - * @param resveration {Object} booked reservation + * @param reservation {Object} booked reservation */ var afterPayment = function (reservation) { $scope.event.nb_free_places = $scope.event.nb_free_places - reservation.total_booked_seats; @@ -748,6 +819,48 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' } }; + /** + * Find user's reservations, the same date at the same time, with event + */ + var findReservationsAtSameTime = function () { + let sameTimeReservations = [ + 'training_reservations', + 'machine_reservations', + 'space_reservations', + 'events_reservations' + ].map(function(k) { + return _.filter($scope.ctrl.member[k], function(r) { + if (r.reservable_type === 'Event' && r.reservable.id === $scope.event.id) { + return false; + } + return moment($scope.event.start_time).isSame(r.start_at) || + (moment($scope.event.end_time).isAfter(r.start_at) && moment($scope.event.end_time).isBefore(r.end_at)) || + (moment($scope.event.start_time).isAfter(r.start_at) && moment($scope.event.start_time).isBefore(r.end_at)) || + (moment($scope.event.start_time).isBefore(r.start_at) && moment($scope.event.end_time).isAfter(r.end_at)); + }); + }); + return _.union.apply(null, sameTimeReservations); + }; + + /** + * A modal for show reservations the same date at the same time + * + * @param sameTimeReservations {Array} reservations the same date at the same time + * @param callback {function} callback will invoke when user confirm + */ + var showReserveSlotSameTimeModal = function(sameTimeReservations, callback) { + const modalInstance = $uibModal.open({ + animation: true, + templateUrl: '<%= asset_path "shared/_reserve_slot_same_time.html" %>', + size: 'md', + controller: 'ReserveSlotSameTimeController', + resolve: { + sameTimeReservations: function() { return sameTimeReservations; }, + } + }); + modalInstance.result.then(callback); + }; + // !!! MUST BE CALLED AT THE END of the controller return initialize(); } @@ -762,3 +875,70 @@ function __range__ (left, right, inclusive) { } return range; } + + +/** + * Controller used in the event deletion modal window + */ +Application.Controllers.controller('DeleteRecurrentEventController', ['$scope', '$uibModalInstance', 'Event', 'eventPromise', 'growl', '_t', + function ($scope, $uibModalInstance, Event, eventPromise, growl, _t) { + + // is the current event (to be deleted) recurrent? + $scope.isRecurrent = eventPromise.recurrence_events.length > 0; + + // with recurrent slots: how many slots should we delete? + $scope.deleteMode = 'single'; + + /** + * Confirmation callback + */ + $scope.ok = function () { + const { id, start_at, end_at } = eventPromise; + // the admin has confirmed, delete the slot + Event.delete( + { id, mode: $scope.deleteMode }, + function (res) { + // delete success + if (res.deleted > 1) { + growl.success(_t( + 'app.public.events_show.events_deleted', + {COUNT: res.deleted - 1} + )); + } else { + growl.success(_t( + 'app.public.events_show.event_successfully_deleted' + )); + } + $uibModalInstance.close({ + status: 'success', + events: res.details.map(function (d) { return d.event.id }) + }); + }, + function (res) { + // not everything was deleted + const { data } = res; + if (data.total > 1) { + growl.warning(_t( + 'app.public.events_show.events_not_deleted', + {TOTAL: data.total, COUNT: data.total - data.deleted} + )); + } else { + growl.error(_t( + 'app.public.events_show.unable_to_delete_the_event' + )); + } + $uibModalInstance.close({ + status: 'failed', + availabilities: data.details.filter(function (d) { return d.status }).map(function (d) { return d.event.id }) + }); + }); + } + + /** + * Cancellation callback + */ + $scope.cancel = function () { + $uibModalInstance.dismiss('cancel'); + } + } +]); diff --git a/app/assets/javascripts/controllers/home.js b/app/assets/javascripts/controllers/home.js index 48163226b..3975737e9 100644 --- a/app/assets/javascripts/controllers/home.js +++ b/app/assets/javascripts/controllers/home.js @@ -1,59 +1,324 @@ -/* eslint-disable - no-undef, -*/ -// TODO: This file was created by bulk-decaffeinate. -// Fix any style issues and re-enable lint. -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ 'use strict'; -Application.Controllers.controller('HomeController', ['$scope', '$stateParams', 'Twitter', 'lastMembersPromise', 'lastProjectsPromise', 'upcomingEventsPromise', 'homeBlogpostPromise', 'twitterNamePromise', - function ($scope, $stateParams, Twitter, lastMembersPromise, lastProjectsPromise, upcomingEventsPromise, homeBlogpostPromise, twitterNamePromise) { +Application.Controllers.controller('HomeController', ['$scope', '$stateParams', 'settingsPromise', 'Member', 'uiTourService', '_t', + function ($scope, $stateParams, settingsPromise, Member, uiTourService, _t) { /* PUBLIC SCOPE */ - // The last registered members who confirmed their addresses - $scope.lastMembers = lastMembersPromise; + // Home page HTML content + $scope.homeContent = null; - // The last tweets from the Fablab official twitter account - $scope.lastTweets = []; - - // The last projects published/documented on the plateform - $scope.lastProjects = lastProjectsPromise; - - // The closest upcoming events - $scope.upcomingEvents = upcomingEventsPromise; - - // The admin blogpost - $scope.homeBlogpost = homeBlogpostPromise.setting.value; - - // Twitter username - $scope.twitterName = twitterNamePromise.setting.value; + // Status of the components in the home page (exists or not?) + $scope.status = { + news: false, + projects: false, + twitter: false, + members: false, + events: false + }; /** - * Test if the provided event run on a single day or not - * @param event {Object} single event from the $scope.upcomingEvents array - * @returns {boolean} false if the event runs on more that 1 day - */ - $scope.isOneDayEvent = event => moment(event.start_date).isSame(event.end_date, 'day'); + * Setup the feature-tour for the home page. (admins only) + * This is intended as a contextual help (when pressing F1) + */ + $scope.setupHomeTour = function () { + if ($scope.currentUser && $scope.currentUser.role === 'admin') { + setupWelcomeTour(); + } + }; /* PRIVATE SCOPE */ /** - * Kind of constructor: these actions will be realized first when the controller is loaded - */ + * Kind of constructor: these actions will be realized first when the controller is loaded + */ const initialize = function () { - // we retrieve tweets from here instead of ui-router's promise because, if adblock stop the API request, - // this prevent the whole home page to be blocked - $scope.lastTweets = Twitter.query({ limit: 1 }); - - // if we recieve a token to reset the password as GET parameter, trigger the + // if we receive a token to reset the password as GET parameter, trigger the // changePassword modal from the parent controller if ($stateParams.reset_password_token) { return $scope.$parent.editPassword($stateParams.reset_password_token); } + + // We set the home page content, with the directives replacing the placeholders + $scope.homeContent = insertDirectives(settingsPromise.home_content); + + // listen the $destroy event of the controller to remove the F1 key binding + $scope.$on('$destroy', function () { + window.removeEventListener('keydown', handleF1); + }); + + // for admins, setup the tour on login + $scope.$watch('currentUser', function (newValue, oldValue) { + if (!oldValue && newValue && newValue.role === 'admin') { + const uitour = uiTourService.getTourByName('welcome'); + if (!uitour.hasStep()) { + setupWelcomeTour(); + } + } + }); + }; + + /** + * Parse the provided html and replace the elements with special IDs (#news, #projects, #twitter, #members, #events) + * by their respective angular directives + * @param html {String} a valid html string, as defined by the summernote editor in admin/settings/home_page + * @returns {string} a valid html string containing angular directives for the specified plugins + */ + const insertDirectives = function (html) { + const node = document.createElement('div'); + node.innerHTML = html.trim(); + + node.querySelectorAll('div#news').forEach((newsNode) => { + const news = document.createElement('news'); + newsNode.parentNode.replaceChild(news, newsNode); + $scope.status.news = true; + }); + + node.querySelectorAll('div#projects').forEach((projectsNode) => { + const projects = document.createElement('projects'); + projectsNode.parentNode.replaceChild(projects, projectsNode); + $scope.status.projects = true; + }); + + node.querySelectorAll('div#twitter').forEach((twitterNode) => { + const twitter = document.createElement('twitter'); + twitterNode.parentNode.replaceChild(twitter, twitterNode); + $scope.status.twitter = true; + }); + + node.querySelectorAll('div#members').forEach((membersNode) => { + const members = document.createElement('members'); + membersNode.parentNode.replaceChild(members, membersNode); + $scope.status.members = true; + }); + + node.querySelectorAll('div#events').forEach((eventsNode) => { + const events = document.createElement('events'); + eventsNode.parentNode.replaceChild(events, eventsNode); + $scope.status.events = true; + }); + + return node.outerHTML; + }; + + /** + * Setup the feature-tour for the home page that will present an overview of the whole app. + * This is intended as a contextual help. + */ + const setupWelcomeTour = function () { + // get the tour defined by the ui-tour directive + const uitour = uiTourService.getTourByName('welcome'); + // add the steps + uitour.createStep({ + selector: 'body', + stepId: 'welcome', + order: 0, + title: _t('app.public.tour.welcome.welcome.title'), + content: _t('app.public.tour.welcome.welcome.content'), + placement: 'bottom', + orphan: true + }); + uitour.createStep({ + selector: '.nav-primary li.home-link', + stepId: 'home', + order: 1, + title: _t('app.public.tour.welcome.home.title'), + content: _t('app.public.tour.welcome.home.content'), + placement: 'right' + }); + uitour.createStep({ + selector: '.nav-primary li.public-calendar-link', + stepId: 'calendar', + order: 2, + title: _t('app.public.tour.welcome.calendar.title'), + content: _t('app.public.tour.welcome.calendar.content'), + placement: 'right' + }); + uitour.createStep({ + selector: '.nav-primary li.reserve-machine-link', + stepId: 'machines', + order: 3, + title: _t('app.public.tour.welcome.machines.title'), + content: _t('app.public.tour.welcome.machines.content'), + placement: 'right' + }); + if (!Fablab.withoutSpaces) { + uitour.createStep({ + selector: '.nav-primary li.reserve-space-link', + stepId: 'spaces', + order: 4, + title: _t('app.public.tour.welcome.spaces.title'), + content: _t('app.public.tour.welcome.spaces.content'), + placement: 'right' + }); + } + uitour.createStep({ + selector: '.nav-primary li.reserve-training-link', + stepId: 'trainings', + order: 5, + title: _t('app.public.tour.welcome.trainings.title'), + content: _t('app.public.tour.welcome.trainings.content'), + placement: 'right' + }); + uitour.createStep({ + selector: '.nav-primary li.reserve-event-link', + stepId: 'events', + order: 6, + title: _t('app.public.tour.welcome.events.title'), + content: _t('app.public.tour.welcome.events.content'), + placement: 'right' + }); + uitour.createStep({ + selector: '.nav-primary li.projects-gallery-link', + stepId: 'projects', + order: 7, + title: _t('app.public.tour.welcome.projects.title'), + content: _t('app.public.tour.welcome.projects.content'), + placement: 'right' + }); + uitour.createStep({ + selector: '.nav-primary li.plans-link', + stepId: 'plans', + order: 8, + title: _t('app.public.tour.welcome.plans.title'), + content: _t('app.public.tour.welcome.plans.content'), + placement: 'right' + }); + uitour.createStep({ + selector: '.nav-primary .admin-section', + stepId: 'admin', + order: 9, + title: _t('app.public.tour.welcome.admin.title'), + content: _t('app.public.tour.welcome.admin.content'), + placement: 'right' + }); + uitour.createStep({ + selector: '.navbar.header li.about-page-link', + stepId: 'about', + order: 10, + title: _t('app.public.tour.welcome.about.title'), + content: _t('app.public.tour.welcome.about.content'), + placement: 'bottom', + popupClass: 'shift-right-40' + }); + uitour.createStep({ + selector: '.navbar.header li.notification-center-link', + stepId: 'notifications', + order: 11, + title: _t('app.public.tour.welcome.notifications.title'), + content: _t('app.public.tour.welcome.notifications.content'), + placement: 'bottom' + }); + uitour.createStep({ + selector: '.navbar.header li.user-menu-dropdown', + stepId: 'profile', + order: 12, + title: _t('app.public.tour.welcome.profile.title'), + content: _t('app.public.tour.welcome.profile.content'), + placement: 'bottom', + popupClass: 'shift-left-80' + }); + if ($scope.status.news && settingsPromise.home_blogpost) { + uitour.createStep({ + selector: 'news', + stepId: 'news', + order: 13, + title: _t('app.public.tour.welcome.news.title'), + content: _t('app.public.tour.welcome.news.content'), + placement: 'bottom' + }); + } + if ($scope.status.projects) { + uitour.createStep({ + selector: 'projects', + stepId: 'last_projects', + order: 14, + title: _t('app.public.tour.welcome.last_projects.title'), + content: _t('app.public.tour.welcome.last_projects.content'), + placement: 'right' + }); + } + if ($scope.status.twitter) { + uitour.createStep({ + selector: 'twitter', + stepId: 'last_tweet', + order: 15, + title: _t('app.public.tour.welcome.last_tweet.title'), + content: _t('app.public.tour.welcome.last_tweet.content'), + placement: 'left' + }); + } + if ($scope.status.members) { + uitour.createStep({ + selector: 'members', + stepId: 'last_members', + order: 16, + title: _t('app.public.tour.welcome.last_members.title'), + content: _t('app.public.tour.welcome.last_members.content'), + placement: 'left' + }); + } + if ($scope.status.events) { + uitour.createStep({ + selector: 'events', + stepId: 'next_events', + order: 17, + title: _t('app.public.tour.welcome.next_events.title'), + content: _t('app.public.tour.welcome.next_events.content'), + placement: 'top' + }); + } + uitour.createStep({ + selector: 'body', + stepId: 'customize', + order: 18, + title: _t('app.public.tour.welcome.customize.title'), + content: _t('app.public.tour.welcome.customize.content'), + placement: 'bottom', + orphan: 'true' + }); + uitour.createStep({ + selector: '.app-generator .app-version', + stepId: 'version', + order: 19, + title: _t('app.public.tour.welcome.version.title'), + content: _t('app.public.tour.welcome.version.content'), + placement: 'top' + }); + uitour.createStep({ + selector: 'body', + stepId: 'conclusion', + order: 20, + title: _t('app.public.tour.conclusion.title'), + content: _t('app.public.tour.conclusion.content'), + placement: 'bottom', + orphan: true + }); + // on tour end, save the status in database + uitour.on('ended', function () { + if (uitour.getStatus() === uitour.Status.ON && $scope.currentUser.profile.tours.indexOf('welcome') < 0) { + Member.completeTour({ id: $scope.currentUser.id }, { tour: 'welcome' }, function (res) { + $scope.currentUser.profile.tours = res.tours; + }); + } + }); + // if the user has never seen the tour, show him now + if (Fablab.featureTourDisplay !== 'manual' && $scope.currentUser.profile.tours.indexOf('welcome') < 0) { + uitour.start(); + } + // start this tour when an user press F1 - this is contextual help + window.addEventListener('keydown', handleF1); + }; + + /** + * Callback used to trigger the feature tour when the user press the F1 key. + * @param e {KeyboardEvent} + */ + const handleF1 = function (e) { + if (e.key === 'F1') { + e.preventDefault(); + const tour = uiTourService.getTourByName('welcome'); + if (tour) { tour.start(); } + } }; // !!! MUST BE CALLED AT THE END of the controller diff --git a/app/assets/javascripts/controllers/machines.js.erb b/app/assets/javascripts/controllers/machines.js.erb index d673ce6a8..f21f106ce 100644 --- a/app/assets/javascripts/controllers/machines.js.erb +++ b/app/assets/javascripts/controllers/machines.js.erb @@ -146,7 +146,7 @@ const _reserveMachine = function (machine, e) { let text = ''; angular.forEach($scope.machine.trainings, function (training) { if (text.length > 0) { - text += _this._t('machines_list._or_the_'); + text += _this._t('app.public.machines_list._or_the_'); } return text += training.name.substr(0, 1).toLowerCase() + training.name.substr(1); }); @@ -180,8 +180,10 @@ const _reserveMachine = function (machine, e) { /** * Controller used in the public listing page, allowing everyone to see the list of machines */ -Application.Controllers.controller('MachinesController', ['$scope', '$state', '_t', 'Machine', '$uibModal', 'machinesPromise', - function ($scope, $state, _t, Machine, $uibModal, machinesPromise) { +Application.Controllers.controller('MachinesController', ['$scope', '$state', '_t', 'Machine', '$uibModal', 'machinesPromise', 'Member', 'uiTourService', + function ($scope, $state, _t, Machine, $uibModal, machinesPromise, Member, uiTourService) { + /* PUBLIC SCOPE */ + // Retrieve the list of machines $scope.machines = machinesPromise; @@ -205,11 +207,92 @@ Application.Controllers.controller('MachinesController', ['$scope', '$state', '_ $scope.machineFiltering = 'enabled'; // Available options for filtering machines by status - return $scope.filterDisabled = [ + $scope.filterDisabled = [ 'enabled', 'disabled', 'all' ]; + + /** + * Setup the feature-tour for the machines page. (admins only) + * This is intended as a contextual help (when pressing F1) + */ + $scope.setupMachinesTour = function () { + // setup the tour for admins only + if ($scope.currentUser && $scope.currentUser.role === 'admin') { + // get the tour defined by the ui-tour directive + const uitour = uiTourService.getTourByName('machines'); + uitour.createStep({ + selector: 'body', + stepId: 'welcome', + order: 0, + title: _t('app.public.tour.machines.welcome.title'), + content: _t('app.public.tour.machines.welcome.content'), + placement: 'bottom', + orphan: true + }); + if ($scope.machines.length > 0) { + uitour.createStep({ + selector: '.machines-list .show-button', + stepId: 'view', + order: 1, + title: _t('app.public.tour.machines.view.title'), + content: _t('app.public.tour.machines.view.content'), + placement: 'top' + }); + } + uitour.createStep({ + selector: 'body', + stepId: 'conclusion', + order: 2, + title: _t('app.public.tour.conclusion.title'), + content: _t('app.public.tour.conclusion.content'), + placement: 'bottom', + orphan: true + }); + // on tour end, save the status in database + uitour.on('ended', function () { + if (uitour.getStatus() === uitour.Status.ON && $scope.currentUser.profile.tours.indexOf('machines') < 0) { + Member.completeTour({ id: $scope.currentUser.id }, { tour: 'machines' }, function (res) { + $scope.currentUser.profile.tours = res.tours; + }); + } + }); + // if the user has never seen the tour, show him now + if (Fablab.featureTourDisplay !== 'manual' && $scope.currentUser.profile.tours.indexOf('machines') < 0) { + uitour.start(); + } + // start this tour when an user press F1 - this is contextual help + window.addEventListener('keydown', handleF1); + } + } + + /* PRIVATE SCOPE */ + + /** + * Kind of constructor: these actions will be realized first when the controller is loaded + */ + const initialize = function () { + // listen the $destroy event of the controller to remove the F1 key binding + $scope.$on('$destroy', function () { + window.removeEventListener('keydown', handleF1); + }); + } + + /** + * Callback used to trigger the feature tour when the user press the F1 key. + * @param e {KeyboardEvent} + */ + const handleF1 = function (e) { + if (e.key === 'F1') { + e.preventDefault(); + const tour = uiTourService.getTourByName('machines'); + if (tour) { tour.start(); } + } + }; + + // !!! MUST BE CALLED AT THE END of the controller + return initialize(); } ]); @@ -281,14 +364,14 @@ Application.Controllers.controller('ShowMachineController', ['$scope', '$state', $scope.delete = function (machine) { // check the permissions if ($scope.currentUser.role !== 'admin') { - console.error(_t('unauthorized_operation')); + console.error(_t('app.public.machines_show.unauthorized_operation')); } else { dialogs.confirm({ resolve: { object () { return { - title: _t('confirmation_required'), - msg: _t('do_you_really_want_to_delete_this_machine') + title: _t('app.public.machines_show.confirmation_required'), + msg: _t('app.public.machines_show.do_you_really_want_to_delete_this_machine') }; } } @@ -297,7 +380,7 @@ Application.Controllers.controller('ShowMachineController', ['$scope', '$state', // delete the machine then redirect to the machines listing machine.$delete( function () { $state.go('app.public.machines_list'); }, - function (error) { growl.warning(_t('the_machine_cant_be_deleted_because_it_is_already_reserved_by_some_users')); console.error(error); } + function (error) { growl.warning(_t('app.public.machines_show.the_machine_cant_be_deleted_because_it_is_already_reserved_by_some_users')); console.error(error); } ); }); } @@ -367,6 +450,8 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$stat $scope.settings = settingsPromise; // list of plans, classified by group + $scope.groups = groupsPromise; + $scope.plans = plansPromise; $scope.plansClassifiedByGroup = []; for (let group of Array.from(groupsPromise)) { const groupObj = { id: group.id, name: group.name, plans: [] }; @@ -402,16 +487,16 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$stat $scope.machineExplicationsAlert = settingsPromise.machine_explications_alert; /** - * Change the last selected slot's appearence to looks like 'added to cart' + * Change the last selected slot's appearance to looks like 'added to cart' */ $scope.markSlotAsAdded = function () { $scope.selectedEvent.backgroundColor = FREE_SLOT_BORDER_COLOR; - $scope.selectedEvent.title = _t('i_reserve'); + $scope.selectedEvent.title = _t('app.logged.machines_reserve.i_reserve'); return updateCalendar(); }; /** - * Change the last selected slot's appearence to looks like 'never added to cart' + * Change the last selected slot's appearance to looks like 'never added to cart' */ $scope.markSlotAsRemoved = function (slot) { slot.backgroundColor = 'white'; @@ -431,16 +516,16 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$stat $scope.slotCancelled = function () { $scope.markSlotAsRemoved($scope.selectedEvent); }; /** - * Change the last selected slot's appearence to looks like 'currently looking for a new destination to exchange' + * Change the last selected slot's appearance to looks like 'currently looking for a new destination to exchange' */ $scope.markSlotAsModifying = function () { $scope.selectedEvent.backgroundColor = '#eee'; - $scope.selectedEvent.title = _t('i_change'); + $scope.selectedEvent.title = _t('app.logged.machines_reserve.i_change'); return updateCalendar(); }; /** - * Change the last selected slot's appearence to looks like 'the slot being exchanged will take this place' + * Change the last selected slot's appearance to looks like 'the slot being exchanged will take this place' */ $scope.changeModifyMachineSlot = function () { if ($scope.events.placable) { @@ -449,7 +534,7 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$stat } if (!$scope.events.placable || ($scope.events.placable._id !== $scope.selectedEvent._id)) { $scope.selectedEvent.backgroundColor = '#bbb'; - $scope.selectedEvent.title = _t('i_shift'); + $scope.selectedEvent.title = _t('app.logged.machines_reserve.i_shift'); } return updateCalendar(); }; @@ -458,7 +543,7 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$stat * When modifying an already booked reservation, callback when the modification was successfully done. */ $scope.modifyMachineSlot = function () { - $scope.events.placable.title = $scope.currentUser.role !== 'admin' ? _t('i_ve_reserved') : _t('not_available'); + $scope.events.placable.title = $scope.currentUser.role !== 'admin' ? _t('app.logged.machines_reserve.i_ve_reserved') : _t('app.logged.machines_reserve.not_available'); $scope.events.placable.backgroundColor = 'white'; $scope.events.placable.borderColor = $scope.events.modifiable.borderColor; $scope.events.placable.id = $scope.events.modifiable.id; @@ -476,14 +561,14 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$stat }; /** - * Cancel the current booking modification, reseting the whole process + * Cancel the current booking modification, resetting the whole process */ $scope.cancelModifyMachineSlot = function () { if ($scope.events.placable) { $scope.events.placable.backgroundColor = 'white'; $scope.events.placable.title = ''; } - $scope.events.modifiable.title = $scope.currentUser.role !== 'admin' ? _t('i_ve_reserved') : _t('not_available'); + $scope.events.modifiable.title = $scope.currentUser.role !== 'admin' ? _t('app.logged.machines_reserve.i_ve_reserved') : _t('app.logged.machines_reserve.not_available'); $scope.events.modifiable.backgroundColor = 'white'; return updateCalendar(); @@ -500,7 +585,7 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$stat }; /** - * Changes the user current view from the plan subsription screen to the machine reservation agenda + * Changes the user current view from the plan subscription screen to the machine reservation agenda * @param e {Object} see https://docs.angularjs.org/guide/expression#-event- */ $scope.doNotSubscribePlan = function (e) { @@ -539,11 +624,11 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$stat machineSlot.is_reserved = true; machineSlot.can_modify = true; if ($scope.currentUser.role !== 'admin') { - machineSlot.title = _t('i_ve_reserved'); + machineSlot.title = _t('app.logged.machines_reserve.i_ve_reserved'); machineSlot.borderColor = BOOKED_SLOT_BORDER_COLOR; updateMachineSlot(machineSlot, reservation, $scope.currentUser); } else { - machineSlot.title = _t('not_available'); + machineSlot.title = _t('app.logged.machines_reserve.not_available'); machineSlot.borderColor = UNAVAILABLE_SLOT_BORDER_COLOR; updateMachineSlot(machineSlot, reservation, $scope.ctrl.member); } @@ -579,7 +664,7 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$stat }); if ($scope.currentUser.role !== 'admin') { - return $scope.ctrl.member = $scope.currentUser; + return Member.get({ id: $scope.currentUser.id }, function (member) { $scope.ctrl.member = member; }); } }; @@ -595,7 +680,7 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$stat }; /** - * Triggered when fullCalendar tries to graphicaly render an event block. + * Triggered when fullCalendar tries to graphically render an event block. * Append the event tag into the block, just after the event title. * @see http://fullcalendar.io/docs/event_rendering/eventRender/ */ diff --git a/app/assets/javascripts/controllers/main_nav.js b/app/assets/javascripts/controllers/main_nav.js index 7f97e3550..8335a3634 100644 --- a/app/assets/javascripts/controllers/main_nav.js +++ b/app/assets/javascripts/controllers/main_nav.js @@ -18,109 +18,121 @@ Application.Controllers.controller('MainNavController', ['$scope', function ($sc $scope.navLinks = [ { state: 'app.public.home', - linkText: 'home', - linkIcon: 'home' + linkText: 'app.public.common.home', + linkIcon: 'home', + class: 'home-link' + }, + { class: 'menu-spacer' }, + { + state: 'app.public.calendar', + linkText: 'app.public.common.public_calendar', + linkIcon: 'calendar', + class: 'public-calendar-link' }, - { state: 'app.public.machines_list', - linkText: 'reserve_a_machine', - linkIcon: 'cogs' + linkText: 'app.public.common.reserve_a_machine', + linkIcon: 'cogs', + class: 'reserve-machine-link' }, { state: 'app.public.trainings_list', - linkText: 'trainings_registrations', - linkIcon: 'graduation-cap' + linkText: 'app.public.common.trainings_registrations', + linkIcon: 'graduation-cap', + class: 'reserve-training-link' }, { state: 'app.public.events_list', - linkText: 'events_registrations', - linkIcon: 'tags' - }, - { - state: 'app.public.calendar', - linkText: 'public_calendar', - linkIcon: 'calendar' + linkText: 'app.public.common.events_registrations', + linkIcon: 'tags', + class: 'reserve-event-link' }, + { class: 'menu-spacer' }, { state: 'app.public.projects_list', - linkText: 'projects_gallery', - linkIcon: 'th' - } + linkText: 'app.public.common.projects_gallery', + linkIcon: 'th', + class: 'projects-gallery-link' + }, + { class: 'menu-spacer' } ]; if (!Fablab.withoutPlans) { $scope.navLinks.push({ state: 'app.public.plans', - linkText: 'subscriptions', - linkIcon: 'credit-card' + linkText: 'app.public.common.subscriptions', + linkIcon: 'credit-card', + class: 'plans-link' }); } if (!Fablab.withoutSpaces) { - $scope.navLinks.splice(3, 0, { + $scope.navLinks.splice(4, 0, { state: 'app.public.spaces_list', - linkText: 'reserve_a_space', - linkIcon: 'rocket' + linkText: 'app.public.common.reserve_a_space', + linkIcon: 'rocket', + class: 'reserve-space-link' }); } Fablab.adminNavLinks = Fablab.adminNavLinks || []; const adminNavLinks = [ - { - state: 'app.admin.trainings', - linkText: 'trainings_monitoring', - linkIcon: 'graduation-cap' - }, { state: 'app.admin.calendar', - linkText: 'manage_the_calendar', + linkText: 'app.public.common.manage_the_calendar', linkIcon: 'calendar' }, - { - state: 'app.admin.members', - linkText: 'manage_the_users', - linkIcon: 'users' - }, - { - state: 'app.admin.invoices', - linkText: 'manage_the_invoices', - linkIcon: 'file-pdf-o' - }, - { - state: 'app.admin.pricing', - linkText: 'subscriptions_and_prices', - linkIcon: 'money' - }, - { - state: 'app.admin.events', - linkText: 'manage_the_events', - linkIcon: 'tags' - }, { state: 'app.public.machines_list', - linkText: 'manage_the_machines', + linkText: 'app.public.common.manage_the_machines', linkIcon: 'cogs' }, { - state: 'app.admin.project_elements', - linkText: 'manage_the_projects_elements', - linkIcon: 'tasks' + state: 'app.admin.trainings', + linkText: 'app.public.common.trainings_monitoring', + linkIcon: 'graduation-cap' + }, + { + state: 'app.admin.events', + linkText: 'app.public.common.manage_the_events', + linkIcon: 'tags' + }, + { class: 'menu-spacer' }, + { + state: 'app.admin.members', + linkText: 'app.public.common.manage_the_users', + linkIcon: 'users' + }, + { + state: 'app.admin.pricing', + linkText: 'app.public.common.subscriptions_and_prices', + linkIcon: 'money' + }, + { + state: 'app.admin.invoices', + linkText: 'app.public.common.manage_the_invoices', + linkIcon: 'file-pdf-o' }, { state: 'app.admin.statistics', - linkText: 'statistics', + linkText: 'app.public.common.statistics', linkIcon: 'bar-chart-o' }, + { class: 'menu-spacer' }, { state: 'app.admin.settings', - linkText: 'customization', + linkText: 'app.public.common.customization', linkIcon: 'gear' }, + { + state: 'app.admin.project_elements', + linkText: 'app.public.common.manage_the_projects_elements', + linkIcon: 'tasks' + }, { state: 'app.admin.open_api_clients', - linkText: 'open_api_clients', + linkText: 'app.public.common.open_api_clients', linkIcon: 'cloud' } ].concat(Fablab.adminNavLinks); @@ -128,9 +140,9 @@ Application.Controllers.controller('MainNavController', ['$scope', function ($sc $scope.adminNavLinks = adminNavLinks; if (!Fablab.withoutSpaces) { - return $scope.adminNavLinks.splice(7, 0, { + return $scope.adminNavLinks.splice(4, 0, { state: 'app.public.spaces_list', - linkText: 'manage_the_spaces', + linkText: 'app.public.common.manage_the_spaces', linkIcon: 'rocket' }); } diff --git a/app/assets/javascripts/controllers/members.js b/app/assets/javascripts/controllers/members.js index 23127f37f..5afb42eba 100644 --- a/app/assets/javascripts/controllers/members.js +++ b/app/assets/javascripts/controllers/members.js @@ -142,10 +142,10 @@ Application.Controllers.controller('EditProfileController', ['$scope', '$rootSco $rootScope.currentUser = user; Auth._currentUser.group_id = user.group_id; $scope.group.change = false; - return growl.success(_t('edit_profile.your_group_has_been_successfully_changed')); + return growl.success(_t('app.logged.dashboard.settings.your_group_has_been_successfully_changed')); } , function (err) { - growl.error(_t('edit_profile.an_unexpected_error_prevented_your_group_from_being_changed')); + growl.error(_t('app.logged.dashboard.settings.an_unexpected_error_prevented_your_group_from_being_changed')); return console.error(err); }); @@ -198,13 +198,13 @@ Application.Controllers.controller('EditProfileController', ['$scope', '$rootSco resolve: { object () { return { - title: _t('confirmation_required'), + title: _t('app.logged.dashboard.settings.confirmation_required'), msg: $sce.trustAsHtml( - _t('edit_profile.confirm_delete_your_account') + '
' + - '' + _t('edit_profile.all_data_will_be_lost') + '

' + - _t('edit_profile.invoicing_data_kept') + '
' + - _t('edit_profile.statistic_data_anonymized') + '
' + - _t('edit_profile.no_further_access_to_projects') + _t('app.logged.dashboard.settings.confirm_delete_your_account') + '
' + + '' + _t('app.logged.dashboard.settings.all_data_will_be_lost') + '

' + + _t('app.logged.dashboard.settings.invoicing_data_kept') + '
' + + _t('app.logged.dashboard.settings.statistic_data_anonymized') + '
' + + _t('app.logged.dashboard.settings.no_further_access_to_projects') ) }; } @@ -214,12 +214,12 @@ Application.Controllers.controller('EditProfileController', ['$scope', '$rootSco Member.remove({ id: user.id }, () => Auth.logout().then(function () { $state.go('app.public.home'); - return growl.success(_t('edit_profile.your_user_account_has_been_successfully_deleted_goodbye')); + return growl.success(_t('app.logged.dashboard.settings.your_user_account_has_been_successfully_deleted_goodbye')); }) , function (error) { console.log(error); - return growl.error(_t('edit_profile.an_error_occured_preventing_your_account_from_being_deleted')); + return growl.error(_t('app.logged.dashboard.settings.an_error_occured_preventing_your_account_from_being_deleted')); }) ); diff --git a/app/assets/javascripts/controllers/plans.js.erb b/app/assets/javascripts/controllers/plans.js.erb index be4a19e7e..bbdbd4cee 100644 --- a/app/assets/javascripts/controllers/plans.js.erb +++ b/app/assets/javascripts/controllers/plans.js.erb @@ -99,7 +99,7 @@ Application.Controllers.controller('PlansIndexController', ['$scope', '$rootScop const amountToPay = helpers.getAmountToPay($scope.cart.total, wallet.amount); if (($scope.currentUser.role !== 'admin') && (amountToPay > 0)) { if ($rootScope.fablabWithoutOnlinePayment) { - growl.error(_t('online_payment_disabled')); + growl.error(_t('app.public.plans.online_payment_disabled')); } else { return payByStripe(); } @@ -133,16 +133,16 @@ Application.Controllers.controller('PlansIndexController', ['$scope', '$rootScop if ($scope.currentUser.role !== 'admin') { $rootScope.currentUser = user; Auth._currentUser.group_id = user.group_id; - growl.success(_t('your_group_was_successfully_changed')); + growl.success(_t('app.public.plans.your_group_was_successfully_changed')); } else { - growl.success(_t('the_user_s_group_was_successfully_changed')); + growl.success(_t('app.public.plans.the_user_s_group_was_successfully_changed')); } } , function (err) { if ($scope.currentUser.role !== 'admin') { - growl.error(_t('an_error_prevented_your_group_from_being_changed')); + growl.error(_t('app.public.plans.an_error_prevented_your_group_from_being_changed')); } else { - growl.error(_t('an_error_prevented_to_change_the_user_s_group')); + growl.error(_t('app.public.plans.an_error_prevented_to_change_the_user_s_group')); } console.error(err); }); @@ -318,12 +318,12 @@ Application.Controllers.controller('PlansIndexController', ['$scope', '$rootScop // Button label if ($scope.amount > 0) { - $scope.validButtonName = _t('confirm_payment_of_html', { ROLE: $scope.currentUser.role, AMOUNT: $filter('currency')($scope.amount) }, 'messageformat'); + $scope.validButtonName = _t('app.public.plans.confirm_payment_of_html', { ROLE: $scope.currentUser.role, AMOUNT: $filter('currency')($scope.amount) }); } else { if ((price.price > 0) && ($scope.walletAmount === 0)) { - $scope.validButtonName = _t('confirm_payment_of_html', { ROLE: $scope.currentUser.role, AMOUNT: $filter('currency')(price.price) }, 'messageformat'); + $scope.validButtonName = _t('app.public.plans.confirm_payment_of_html', { ROLE: $scope.currentUser.role, AMOUNT: $filter('currency')(price.price) }); } else { - $scope.validButtonName = _t('confirm'); + $scope.validButtonName = _t('app.shared.buttons.confirm'); } } @@ -345,7 +345,7 @@ Application.Controllers.controller('PlansIndexController', ['$scope', '$rootScop } , function (data, status) { // failed $scope.alerts = []; - $scope.alerts.push({ msg: _t('an_error_occured_during_the_payment_process_please_try_again_later'), type: 'danger' }); + $scope.alerts.push({ msg: _t('app.public.plans.an_error_occured_during_the_payment_process_please_try_again_later'), type: 'danger' }); $scope.attempting = false; } ); diff --git a/app/assets/javascripts/controllers/profile.js.erb b/app/assets/javascripts/controllers/profile.js.erb index 4c7e11048..0ae5e79ce 100644 --- a/app/assets/javascripts/controllers/profile.js.erb +++ b/app/assets/javascripts/controllers/profile.js.erb @@ -123,7 +123,7 @@ Application.Controllers.controller('CompleteProfileController', ['$scope', '$roo if (err.data.error) { growl.error(err.data.error); } else { - growl.error(_t('an_unexpected_error_occurred_check_your_authentication_code')); + growl.error(_t('app.logged.profile_completion.an_unexpected_error_occurred_check_your_authentication_code')); console.error(err); } }); @@ -174,7 +174,7 @@ Application.Controllers.controller('CompleteProfileController', ['$scope', '$roo function (email) { // Request the server to send an auth-migration email to the current user AuthProvider.send_code({ email }, - function (res) { growl.info(_t('code_successfully_sent_again')); }, + function (res) { growl.info(_t('app.logged.profile_completion.code_successfully_sent_again')); }, function (err) { growl.error(err.data.error); } ); } @@ -206,7 +206,7 @@ Application.Controllers.controller('CompleteProfileController', ['$scope', '$roo CSRF.setMetaTags(); // init the birth date to JS object - $scope.user.statistic_profile.birthday = moment($scope.user.statistic_profile.birthday).toDate(); + $scope.user.statistic_profile.birthday = $scope.user.statistic_profile.birthday ? moment($scope.user.statistic_profile.birthday).toDate() : undefined; // bind fields protection with sso fields angular.forEach(activeProviderPromise.mapping, function (map) { $scope.preventField[map] = true; }); diff --git a/app/assets/javascripts/controllers/projects.js.erb b/app/assets/javascripts/controllers/projects.js.erb index f0f2a9d63..a8c2f5481 100644 --- a/app/assets/javascripts/controllers/projects.js.erb +++ b/app/assets/javascripts/controllers/projects.js.erb @@ -165,8 +165,8 @@ class ProjectsController { resolve: { object () { return { - title: _t('confirmation_required'), - msg: _t('do_you_really_want_to_delete_this_step') + title: _t('app.shared.project.confirmation_required'), + msg: _t('app.shared.project.do_you_really_want_to_delete_this_step') }; } } @@ -215,6 +215,10 @@ class ProjectsController { return false; }; + /** + * This function will query the API to autocomplete the typed user's name + * @param nameLookup {string} + */ $scope.autoCompleteName = function (nameLookup) { if (!nameLookup) { return; @@ -246,6 +250,16 @@ class ProjectsController { return step.project_step_images_attributes.splice(index, 1); } }; + + /** + * Returns the text to display on the save button, depending on the current state of the project + */ + $scope.saveButtonValue = function () { + if (!$scope.project.state || $scope.project.state === 'draft') { + return _t('app.shared.project.save_as_draft'); + } + return _t('app.shared.buttons.save'); + } } } @@ -324,7 +338,7 @@ Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'P $scope.projectsPagination = new paginationService.Instance(OpenlabProject, currentPage, PROJECTS_PER_PAGE, null, { }, loadMoreOpenlabCallback); return OpenlabProject.query({ q: $scope.search.q, page: currentPage, per_page: PROJECTS_PER_PAGE }, function (projectsPromise) { if (projectsPromise.errors != null) { - growl.error(_t('projects_list.openlab_search_not_available_at_the_moment')); + growl.error(_t('app.public.projects_list.openlab_search_not_available_at_the_moment')); $scope.openlab.searchOverWholeNetwork = false; return $scope.triggerSearch(); } else { @@ -533,8 +547,8 @@ Application.Controllers.controller('ShowProjectController', ['$scope', '$state', resolve: { object () { return { - title: _t('confirmation_required'), - msg: _t('do_you_really_want_to_delete_this_project') + title: _t('app.public.projects_show.confirmation_required'), + msg: _t('app.public.projects_show.do_you_really_want_to_delete_this_project') }; } } @@ -543,7 +557,7 @@ Application.Controllers.controller('ShowProjectController', ['$scope', '$state', $scope.project.$delete(function () { $state.go('app.public.projects_list', {}, { reload: true }); }); }); } else { - return console.error(_t('unauthorized_operation')); + return console.error(_t('app.public.projects_show.unauthorized_operation')); } }; @@ -577,12 +591,12 @@ Application.Controllers.controller('ShowProjectController', ['$scope', '$state', { abuse: $scope.signaler }, function (res) { // creation successful - growl.success(_t('your_report_was_successful_thanks')); + growl.success(_t('app.public.projects_show.your_report_was_successful_thanks')); return $uibModalInstance.close(res); } , function (error) { // creation failed... - growl.error(_t('an_error_occured_while_sending_your_report')); + growl.error(_t('app.public.projects_show.an_error_occured_while_sending_your_report')); } ); }; diff --git a/app/assets/javascripts/controllers/spaces.js.erb b/app/assets/javascripts/controllers/spaces.js.erb index 113144cb7..bc0fba8ab 100644 --- a/app/assets/javascripts/controllers/spaces.js.erb +++ b/app/assets/javascripts/controllers/spaces.js.erb @@ -98,30 +98,115 @@ class SpacesController { /** * Controller used in the public listing page, allowing everyone to see the list of spaces */ -Application.Controllers.controller('SpacesController', ['$scope', '$state', 'spacesPromise', function ($scope, $state, spacesPromise) { - // Retrieve the list of spaces - $scope.spaces = spacesPromise; +Application.Controllers.controller('SpacesController', ['$scope', '$state', 'spacesPromise', '_t', 'Member', 'uiTourService', + function ($scope, $state, spacesPromise, _t, Member, uiTourService) { + /* PUBLIC SCOPE */ - /** - * Redirect the user to the space details page - */ - $scope.showSpace = function (space) { $state.go('app.public.space_show', { id: space.slug }); }; + // Retrieve the list of spaces + $scope.spaces = spacesPromise; - /** - * Callback to book a reservation for the current space - */ - $scope.reserveSpace = function (space) { $state.go('app.logged.space_reserve', { id: space.slug }); }; + /** + * Redirect the user to the space details page + */ + $scope.showSpace = function (space) { $state.go('app.public.space_show', { id: space.slug }); }; - // Default: we show only enabled spaces - $scope.spaceFiltering = 'enabled'; + /** + * Callback to book a reservation for the current space + */ + $scope.reserveSpace = function (space) { $state.go('app.logged.space_reserve', { id: space.slug }); }; - // Available options for filtering spaces by status - $scope.filterDisabled = [ - 'enabled', - 'disabled', - 'all' - ]; -}]); + // Default: we show only enabled spaces + $scope.spaceFiltering = 'enabled'; + + // Available options for filtering spaces by status + $scope.filterDisabled = [ + 'enabled', + 'disabled', + 'all' + ]; + + /** + * Setup the feature-tour for the spaces page. (admins only) + * This is intended as a contextual help (when pressing F1) + */ + $scope.setupSpacesTour = function () { + // setup the tour for admins only + if ($scope.currentUser && $scope.currentUser.role === 'admin') { + // get the tour defined by the ui-tour directive + const uitour = uiTourService.getTourByName('spaces'); + uitour.createStep({ + selector: 'body', + stepId: 'welcome', + order: 0, + title: _t('app.public.tour.spaces.welcome.title'), + content: _t('app.public.tour.spaces.welcome.content'), + placement: 'bottom', + orphan: true + }); + if ($scope.spaces.length > 0) { + uitour.createStep({ + selector: '.spaces-list .show-button', + stepId: 'view', + order: 1, + title: _t('app.public.tour.spaces.view.title'), + content: _t('app.public.tour.spaces.view.content'), + placement: 'top' + }); + } + uitour.createStep({ + selector: 'body', + stepId: 'conclusion', + order: 2, + title: _t('app.public.tour.conclusion.title'), + content: _t('app.public.tour.conclusion.content'), + placement: 'bottom', + orphan: true + }); + // on tour end, save the status in database + uitour.on('ended', function () { + if (uitour.getStatus() === uitour.Status.ON && $scope.currentUser.profile.tours.indexOf('spaces') < 0) { + Member.completeTour({ id: $scope.currentUser.id }, { tour: 'spaces' }, function (res) { + $scope.currentUser.profile.tours = res.tours; + }); + } + }); + // if the user has never seen the tour, show him now + if (Fablab.featureTourDisplay !== 'manual' && $scope.currentUser.profile.tours.indexOf('spaces') < 0) { + uitour.start(); + } + // start this tour when an user press F1 - this is contextual help + window.addEventListener('keydown', handleF1); + } + } + + /* PRIVATE SCOPE */ + + /** + * Kind of constructor: these actions will be realized first when the controller is loaded + */ + const initialize = function () { + // listen the $destroy event of the controller to remove the F1 key binding + $scope.$on('$destroy', function () { + window.removeEventListener('keydown', handleF1); + }); + } + + /** + * Callback used to trigger the feature tour when the user press the F1 key. + * @param e {KeyboardEvent} + */ + const handleF1 = function (e) { + if (e.key === 'F1') { + e.preventDefault(); + const tour = uiTourService.getTourByName('spaces'); + if (tour) { tour.start(); } + } + }; + + // !!! MUST BE CALLED AT THE END of the controller + return initialize(); + } +]); /** * Controller used in the space creation page (admin) @@ -185,14 +270,14 @@ Application.Controllers.controller('ShowSpaceController', ['$scope', '$state', ' event.preventDefault(); // check the permissions if ($scope.currentUser.role !== 'admin') { - return console.error(_t('space_show.unauthorized_operation')); + return console.error(_t('app.public.space_show.unauthorized_operation')); } else { return dialogs.confirm({ resolve: { object () { return { - title: _t('space_show.confirmation_required'), - msg: _t('space_show.do_you_really_want_to_delete_this_space') + title: _t('app.public.space_show.confirmation_required'), + msg: _t('app.public.space_show.do_you_really_want_to_delete_this_space') }; } } @@ -204,7 +289,7 @@ Application.Controllers.controller('ShowSpaceController', ['$scope', '$state', ' $state.go('app.public.spaces_list'); }, function (error) { - growl.warning(_t('space_show.the_space_cant_be_deleted_because_it_is_already_reserved_by_some_users')); + growl.warning(_t('app.public.space_show.the_space_cant_be_deleted_because_it_is_already_reserved_by_some_users')); console.error(error); } ); @@ -243,6 +328,8 @@ Application.Controllers.controller('ReserveSpaceController', ['$scope', '$stateP // list of plans, classified by group $scope.plansClassifiedByGroup = []; + $scope.groups = groupsPromise; + $scope.plans = plansPromise; for (let group of Array.from(groupsPromise)) { const groupObj = { id: group.id, name: group.name, plans: [] }; for (let plan of Array.from(plansPromise)) { @@ -333,7 +420,7 @@ Application.Controllers.controller('ReserveSpaceController', ['$scope', '$stateP */ $scope.markSlotAsModifying = function () { $scope.selectedEvent.backgroundColor = '#eee'; - $scope.selectedEvent.title = _t('space_reserve.i_change'); + $scope.selectedEvent.title = _t('app.logged.space_reserve.i_change'); return updateCalendar(); }; @@ -347,7 +434,7 @@ Application.Controllers.controller('ReserveSpaceController', ['$scope', '$stateP } if (!$scope.events.placable || ($scope.events.placable._id !== $scope.selectedEvent._id)) { $scope.selectedEvent.backgroundColor = '#bbb'; - $scope.selectedEvent.title = _t('space_reserve.i_shift'); + $scope.selectedEvent.title = _t('app.logged.space_reserve.i_shift'); } return updateCalendar(); }; @@ -356,7 +443,7 @@ Application.Controllers.controller('ReserveSpaceController', ['$scope', '$stateP * When modifying an already booked reservation, callback when the modification was successfully done. */ $scope.modifyTrainingSlot = function () { - $scope.events.placable.title = _t('space_reserve.i_ve_reserved'); + $scope.events.placable.title = _t('app.logged.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; @@ -382,7 +469,7 @@ Application.Controllers.controller('ReserveSpaceController', ['$scope', '$stateP $scope.events.placable.backgroundColor = 'white'; $scope.events.placable.title = ''; } - $scope.events.modifiable.title = _t('space_reserve.i_ve_reserved'); + $scope.events.modifiable.title = _t('app.logged.space_reserve.i_ve_reserved'); $scope.events.modifiable.backgroundColor = 'white'; return updateCalendar(); @@ -451,7 +538,7 @@ Application.Controllers.controller('ReserveSpaceController', ['$scope', '$stateP angular.forEach($scope.events.paid, function (spaceSlot, key) { spaceSlot.is_reserved = true; spaceSlot.can_modify = true; - spaceSlot.title = _t('space_reserve.i_ve_reserved'); + spaceSlot.title = _t('app.logged.space_reserve.i_ve_reserved'); spaceSlot.backgroundColor = 'white'; spaceSlot.borderColor = RESERVED_SLOT_BORDER_COLOR; return updateSpaceSlotId(spaceSlot, reservation); diff --git a/app/assets/javascripts/controllers/trainings.js.erb b/app/assets/javascripts/controllers/trainings.js.erb index d29ac27b9..dfecc48af 100644 --- a/app/assets/javascripts/controllers/trainings.js.erb +++ b/app/assets/javascripts/controllers/trainings.js.erb @@ -47,15 +47,15 @@ Application.Controllers.controller('ShowTrainingController', ['$scope', '$state' $scope.delete = function (training) { // check the permissions if ($scope.currentUser.role !== 'admin') { - console.error(_t('unauthorized_operation')); + growl.error(_t('app.public.training_show.unauthorized_operation')); } else { dialogs.confirm( { resolve: { object () { return { - title: _t('confirmation_required'), - msg: _t('do_you_really_want_to_delete_this_training') + title: _t('app.public.training_show.confirmation_required'), + msg: _t('app.public.training_show.do_you_really_want_to_delete_this_training') }; } } @@ -65,7 +65,7 @@ Application.Controllers.controller('ShowTrainingController', ['$scope', '$state' training.$delete( function () { $state.go('app.public.trainings_list'); }, function (error) { - growl.warning(_t('the_training_cant_be_deleted_because_it_is_already_reserved_by_some_users')); + growl.warning(_t('app.public.training_show.the_training_cant_be_deleted_because_it_is_already_reserved_by_some_users')); console.error(error); } ); @@ -111,6 +111,8 @@ Application.Controllers.controller('ReserveTrainingController', ['$scope', '$sta { member: {} }; // list of plans, classified by group + $scope.groups = groupsPromise; + $scope.plans = plansPromise; $scope.plansClassifiedByGroup = []; for (let group of Array.from(groupsPromise)) { const groupObj = { id: group.id, name: group.name, plans: [] }; @@ -208,7 +210,7 @@ Application.Controllers.controller('ReserveTrainingController', ['$scope', '$sta */ $scope.markSlotAsModifying = function () { $scope.selectedEvent.backgroundColor = '#eee'; - $scope.selectedEvent.title = $scope.selectedEvent.training.name + ' - ' + _t('i_change'); + $scope.selectedEvent.title = $scope.selectedEvent.training.name + ' - ' + _t('app.logged.trainings_reserve.i_change'); return updateCalendar(); }; @@ -222,7 +224,7 @@ Application.Controllers.controller('ReserveTrainingController', ['$scope', '$sta } if (!$scope.events.placable || ($scope.events.placable._id !== $scope.selectedEvent._id)) { $scope.selectedEvent.backgroundColor = '#bbb'; - $scope.selectedEvent.title = $scope.selectedEvent.training.name + ' - ' + _t('i_shift'); + $scope.selectedEvent.title = $scope.selectedEvent.training.name + ' - ' + _t('app.logged.trainings_reserve.i_shift'); } return updateCalendar(); }; @@ -231,7 +233,7 @@ Application.Controllers.controller('ReserveTrainingController', ['$scope', '$sta * When modifying an already booked reservation, callback when the modification was successfully done. */ $scope.modifyTrainingSlot = function () { - $scope.events.placable.title = $scope.currentUser.role !== 'admin' ? $scope.events.placable.training.name + ' - ' + _t('i_ve_reserved') : $scope.events.placable.training.name; + $scope.events.placable.title = $scope.currentUser.role !== 'admin' ? $scope.events.placable.training.name + ' - ' + _t('app.logged.trainings_reserve.i_ve_reserved') : $scope.events.placable.training.name; $scope.events.placable.backgroundColor = 'white'; $scope.events.placable.borderColor = $scope.events.modifiable.borderColor; $scope.events.placable.id = $scope.events.modifiable.id; @@ -257,7 +259,7 @@ Application.Controllers.controller('ReserveTrainingController', ['$scope', '$sta $scope.events.placable.backgroundColor = 'white'; $scope.events.placable.title = $scope.events.placable.training.name; } - $scope.events.modifiable.title = $scope.currentUser.role !== 'admin' ? $scope.events.modifiable.training.name + ' - ' + _t('i_ve_reserved') : $scope.events.modifiable.training.name; + $scope.events.modifiable.title = $scope.currentUser.role !== 'admin' ? $scope.events.modifiable.training.name + ' - ' + _t('app.logged.trainings_reserve.i_ve_reserved') : $scope.events.modifiable.training.name; $scope.events.modifiable.backgroundColor = 'white'; return updateCalendar(); @@ -329,7 +331,7 @@ Application.Controllers.controller('ReserveTrainingController', ['$scope', '$sta $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'); + $scope.events.paid[0].title = $scope.events.paid[0].training.name + ' - ' + _t('app.logged.trainings_reserve.i_ve_reserved'); if ($scope.selectedPlan) { $scope.ctrl.member.subscribed_plan = angular.copy($scope.selectedPlan); diff --git a/app/assets/javascripts/directives/cart.js.erb b/app/assets/javascripts/directives/cart.js.erb index 75dcce2f0..35546e187 100644 --- a/app/assets/javascripts/directives/cart.js.erb +++ b/app/assets/javascripts/directives/cart.js.erb @@ -10,8 +10,8 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs', 'growl', 'Auth', 'Price', 'Wallet', 'CustomAsset', 'Slot', 'helpers', '_t', - function ($rootScope, $uibModal, dialogs, growl, Auth, Price, Wallet, CustomAsset, Slot, helpers, _t) { +Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs', 'growl', 'Auth', 'Price', 'Wallet', 'CustomAsset', 'Slot', 'helpers', '_t', '$uibModal', + function ($rootScope, $uibModal, dialogs, growl, Auth, Price, Wallet, CustomAsset, Slot, helpers, _t, $uibModal) { return ({ restrict: 'E', scope: { @@ -23,6 +23,8 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs', plan: '=', planSelectionTime: '=', settings: '=', + plans: '=', + groups: '=', onSlotAddedToCart: '=', onSlotRemovedFromCart: '=', onSlotStartToModify: '=', @@ -72,8 +74,38 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs', * @param slot {Object} fullCalendar event object */ $scope.validateSlot = function (slot) { - slot.isValid = true; - return updateCartPrice(); + let sameTimeReservations = [ + 'training_reservations', + 'machine_reservations', + 'space_reservations', + 'events_reservations' + ].map(function (k) { + return _.filter($scope.user[k], function(r) { + return slot.start.isSame(r.start_at) || + (slot.end.isAfter(r.start_at) && slot.end.isBefore(r.end_at)) || + (slot.start.isAfter(r.start_at) && slot.start.isBefore(r.end_at)) || + (slot.start.isBefore(r.start_at) && slot.end.isAfter(r.end_at)); + }) + }); + sameTimeReservations = _.union.apply(null, sameTimeReservations); + if (sameTimeReservations.length > 0) { + const modalInstance = $uibModal.open({ + animation: true, + templateUrl: '<%= asset_path "shared/_reserve_slot_same_time.html" %>', + size: 'md', + controller: 'ReserveSlotSameTimeController', + resolve: { + sameTimeReservations: function() { return sameTimeReservations; } + } + }); + modalInstance.result.then(function(res) { + slot.isValid = true; + return updateCartPrice(); + }); + } else { + slot.isValid = true; + return updateCartPrice(); + } }; /** @@ -112,12 +144,19 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs', * Switch the user's view from the reservation agenda to the plan subscription */ $scope.showPlans = function () { - // first, we ensure that a user was selected (admin) or logged (member) - if (Object.keys($scope.user).length > 0) { + // first, we ensure that a user was selected (admin) or logged (member) + const isSelectedUser = Object.keys($scope.user).length > 0; + // all slots are in future + const areFutureSlots = _.every($scope.events.reserved, function(s) { + return s.start.isAfter(); + }); + if (isSelectedUser && areFutureSlots) { return $scope.modePlans = true; - } else { + } else if (!isSelectedUser){ // otherwise we alert, this error musn't occur when the current user hasn't the admin role - return growl.error(_t('cart.please_select_a_member_first')); + return growl.error(_t('app.shared.cart.please_select_a_member_first')); + } else if (!areFutureSlots){ + return growl.error(_t('app.shared.cart.unable_to_select_plan_if_slots_in_the_past')); } }; @@ -127,25 +166,58 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs', $scope.payCart = function () { // first, we check that a user was selected if (Object.keys($scope.user).length > 0) { - const reservation = mkReservation($scope.user, $scope.events.reserved, $scope.selectedPlan); - return Wallet.getWalletByUser({ user_id: $scope.user.id }, function (wallet) { - const amountToPay = helpers.getAmountToPay($scope.amountTotal, wallet.amount); - if (!$scope.isAdmin() && (amountToPay > 0)) { - if ($rootScope.fablabWithoutOnlinePayment) { - growl.error(_t('cart.online_payment_disabled')); + // check user was selected a plan if slot is restricted for subscriptions + const slotValidations = []; + let slotNotValid; + let slotNotValidError; + $scope.events.reserved.forEach(function (slot) { + if (slot.plan_ids.length > 0) { + if ( + ($scope.selectedPlan && _.include(slot.plan_ids, $scope.selectedPlan.id)) || + ($scope.user.subscribed_plan && _.include(slot.plan_ids, $scope.user.subscribed_plan.id)) + ) { + slotValidations.push(true); } else { - return payByStripe(reservation); - } - } else { - if ($scope.isAdmin() || (amountToPay === 0)) { - return payOnSite(reservation); + slotNotValid = slot; + if ($scope.selectedPlan && !_.include(slot.plan_ids, $scope.selectedPlan.id)) { + slotNotValidError = 'selectedPlanError'; + } + if ($scope.user.subscribed_plan && !_.include(slot.plan_ids, $scope.user.subscribed_plan.id)) { + slotNotValidError = 'userPlanError'; + } + if (!$scope.selectedPlan || !$scope.user.subscribed_plan) { + slotNotValidError = 'noPlanError'; + } + slotValidations.push(false); } } }); + const hasPlanForSlot = slotValidations.every(function (a) { return a; }); + if (!hasPlanForSlot) { + if (!$scope.isAdmin()) { + return growl.error(_t('app.shared.cart.slot_restrict_subscriptions_must_select_plan')); + } else { + const modalInstance = $uibModal.open({ + animation: true, + templateUrl: '<%= asset_path "shared/_reserve_slot_without_plan.html" %>', + size: 'md', + controller: 'ReserveSlotWithoutPlanController', + resolve: { + slot: function() { return slotNotValid; }, + slotNotValidError: function() { return slotNotValidError; }, + } + }); + modalInstance.result.then(function(res) { + return paySlots(); + }); + } + } else { + return paySlots(); + } } else { // otherwise we alert, this error musn't occur when the current user is not admin - return growl.error(_t('cart.please_select_a_member_first')); + return growl.error(_t('app.shared.cart.please_select_a_member_first')); } }; @@ -173,7 +245,7 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs', return $scope.events.modifiable = null; } , function (err) { // failure - growl.error(_t('cart.unable_to_change_the_reservation')); + growl.error(_t('app.shared.cart.unable_to_change_the_reservation')); return console.error(err); }); }; @@ -255,6 +327,27 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs', */ var slotSelectionChanged = function () { if ($scope.slot) { + // build a list of plans if this slot is restricted for subscriptions + if ($scope.slot.plan_ids.length > 0) { + const _plans = _.filter($scope.plans, function (p) { return _.include($scope.slot.plan_ids, p.id) }); + $scope.slot.plansGrouped = []; + $scope.slot.group_ids = []; + for (let group of Array.from($scope.groups)) { + const groupObj = { id: group.id, name: group.name, plans: [] }; + for (let plan of Array.from(_plans)) { + if (plan.group_id === group.id) { groupObj.plans.push(plan); } + } + if (groupObj.plans.length > 0) { + if ($scope.isAdmin()) { + $scope.slot.plansGrouped.push(groupObj); + } else if ($scope.user.group_id === groupObj.id) { + $scope.slot.plansGrouped.push(groupObj); + } + } + } + $scope.slot.group_ids = $scope.slot.plansGrouped.map(function(g) { return g.id; }); + } + if (!$scope.slot.is_reserved && !$scope.events.modifiable && !$scope.slot.is_completed) { // slot is not reserved and we are not currently modifying a slot // -> can be added to cart or removed if already present @@ -315,19 +408,19 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs', resolve: { object () { return { - title: _t('cart.confirmation_required'), - msg: _t('cart.do_you_really_want_to_cancel_this_reservation') + title: _t('app.shared.cart.confirmation_required'), + msg: _t('app.shared.cart.do_you_really_want_to_cancel_this_reservation') }; } } }, function () { // cancel confirmed Slot.cancel({ id: $scope.slot.id }, function () { // successfully canceled - growl.success(_t('cart.reservation_was_cancelled_successfully')); + growl.success(_t('app.shared.cart.reservation_was_cancelled_successfully')); if (typeof $scope.onSlotCancelSuccess === 'function') { return $scope.onSlotCancelSuccess(); } } , function () { // error while canceling - growl.error(_t('cart.cancellation_failed')); + growl.error(_t('app.shared.cart.cancellation_failed')); }); } ); @@ -403,7 +496,7 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs', }); } 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')); + growl.warning(_t('app.shared.cart.please_select_a_member_first')); return $scope.amountTotal = null; } }; @@ -556,12 +649,12 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs', // Button label if ($scope.amount > 0) { - $scope.validButtonName = _t('cart.confirm_payment_of_html', { ROLE: $rootScope.currentUser.role, AMOUNT: $filter('currency')($scope.amount) }, 'messageformat'); + $scope.validButtonName = _t('app.shared.cart.confirm_payment_of_html', { ROLE: $rootScope.currentUser.role, AMOUNT: $filter('currency')($scope.amount) }); } else { if ((price.price > 0) && ($scope.walletAmount === 0)) { - $scope.validButtonName = _t('cart.confirm_payment_of_html', { ROLE: $rootScope.currentUser.role, AMOUNT: $filter('currency')(price.price) }, 'messageformat'); + $scope.validButtonName = _t('app.shared.cart.confirm_payment_of_html', { ROLE: $rootScope.currentUser.role, AMOUNT: $filter('currency')(price.price) }); } else { - $scope.validButtonName = _t('confirm'); + $scope.validButtonName = _t('app.shared.buttons.confirm'); } } @@ -576,7 +669,7 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs', } , function (response) { $scope.alerts = []; - $scope.alerts.push({ msg: _t('cart.a_problem_occurred_during_the_payment_process_please_try_again_later'), type: 'danger' }); + $scope.alerts.push({ msg: _t('app.shared.cart.a_problem_occurred_during_the_payment_process_please_try_again_later'), type: 'danger' }); return $scope.attempting = false; }); }; @@ -601,9 +694,75 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs', return $scope.selectedPlan = null; }; + /** + * Actions to pay slots + */ + var paySlots = function() { + const reservation = mkReservation($scope.user, $scope.events.reserved, $scope.selectedPlan); + + return Wallet.getWalletByUser({ user_id: $scope.user.id }, function (wallet) { + const amountToPay = helpers.getAmountToPay($scope.amountTotal, wallet.amount); + if (!$scope.isAdmin() && (amountToPay > 0)) { + if ($rootScope.fablabWithoutOnlinePayment) { + growl.error(_t('app.shared.cart.online_payment_disabled')); + } else { + return payByStripe(reservation); + } + } else { + if ($scope.isAdmin() || (amountToPay === 0)) { + return payOnSite(reservation); + } + } + }); + }; + // !!! MUST BE CALLED AT THE END of the directive return initialize(); } }); } ]); + +/** + * Controller of modal for show reservations the same date at the same time + */ +Application.Controllers.controller('ReserveSlotSameTimeController', ['$scope', '$uibModalInstance', 'sameTimeReservations', 'growl', '_t', + function ($scope, $uibModalInstance, sameTimeReservations, growl, _t) { + $scope.sameTimeReservations = sameTimeReservations; + $scope.bookSlotAtSameTime = Fablab.bookSlotAtSameTime; + /** + * Confirmation callback + */ + $scope.ok = function () { + $uibModalInstance.close({}); + } + /** + * Cancellation callback + */ + $scope.cancel = function () { + $uibModalInstance.dismiss('cancel'); + } + } +]); + +/** + * Controller used to alert admin reserve slot without plan + */ +Application.Controllers.controller('ReserveSlotWithoutPlanController', ['$scope', '$uibModalInstance', 'slot', 'slotNotValidError', 'growl', '_t', + function ($scope, $uibModalInstance, slot, slotNotValidError, growl, _t) { + $scope.slot = slot; + $scope.slotNotValidError = slotNotValidError; + /** + * Confirmation callback + */ + $scope.ok = function () { + $uibModalInstance.close({}); + } + /** + * Cancellation callback + */ + $scope.cancel = function () { + $uibModalInstance.dismiss('cancel'); + } + } +]); diff --git a/app/assets/javascripts/directives/compile.js b/app/assets/javascripts/directives/compile.js new file mode 100644 index 000000000..cafd00cb4 --- /dev/null +++ b/app/assets/javascripts/directives/compile.js @@ -0,0 +1,21 @@ +Application.Directives.directive('compile', ['$compile', function ($compile) { + return function (scope, element, attrs) { + scope.$watch( + function (scope) { + // watch the 'compile' expression for changes + return scope.$eval(attrs.compile); + }, + function (value) { + // when the 'compile' expression changes + // assign it into the current DOM + element.html(value); + + // compile the new DOM and link it to the current + // scope. + // NOTE: we only compile .childNodes so that + // we don't get into infinite loop compiling ourselves + $compile(element.contents())(scope); + } + ); + }; +}]); diff --git a/app/assets/javascripts/directives/coupon.js.erb b/app/assets/javascripts/directives/coupon.js.erb index fe0c0f861..0d8e6316a 100644 --- a/app/assets/javascripts/directives/coupon.js.erb +++ b/app/assets/javascripts/directives/coupon.js.erb @@ -53,15 +53,15 @@ Application.Directives.directive('coupon', [ '$rootScope', 'Coupon', '_t', funct $scope.status = 'valid'; $scope.coupon = res; if (res.type === 'percent_off') { - return $scope.messages.push({ type: 'success', message: _t('the_coupon_has_been_applied_you_get_PERCENT_discount', { PERCENT: res.percent_off }) }); + return $scope.messages.push({ type: 'success', message: _t('app.shared.coupon_input.the_coupon_has_been_applied_you_get_PERCENT_discount', { PERCENT: res.percent_off }) }); } else { - return $scope.messages.push({ type: 'success', message: _t('the_coupon_has_been_applied_you_get_AMOUNT_CURRENCY', { AMOUNT: res.amount_off, CURRENCY: $rootScope.currencySymbol }) }); + return $scope.messages.push({ type: 'success', message: _t('app.shared.coupon_input.the_coupon_has_been_applied_you_get_AMOUNT_CURRENCY', { AMOUNT: res.amount_off, CURRENCY: $rootScope.currencySymbol }) }); } } , function (err) { $scope.status = 'invalid'; $scope.coupon = null; - return $scope.messages.push({ type: 'danger', message: _t(`unable_to_apply_the_coupon_because_${err.data.status}`) }); + return $scope.messages.push({ type: 'danger', message: _t(`app.shared.coupon_input.unable_to_apply_the_coupon_because_${err.data.status}`) }); }); } }; diff --git a/app/assets/javascripts/directives/events.js.erb b/app/assets/javascripts/directives/events.js.erb new file mode 100644 index 000000000..aca43325a --- /dev/null +++ b/app/assets/javascripts/directives/events.js.erb @@ -0,0 +1,31 @@ +Application.Directives.directive('events', [ 'Event', + function (Event) { + return ({ + restrict: 'E', + templateUrl: '<%= asset_path "home/events.html" %>', + link ($scope, element, attributes) { + // The closest upcoming events + $scope.upcomingEvents = null; + + /** + * Test if the provided event run on a single day or not + * @param event {Object} single event from the $scope.upcomingEvents array + * @returns {boolean} false if the event runs on more that 1 day + */ + $scope.isOneDayEvent = function(event) { + return moment(event.start_date).isSame(event.end_date, 'day'); + } + + // constructor + const initialize = function () { + Event.upcoming({ limit: 3 }, function (data) { + $scope.upcomingEvents = data; + }) + }; + + // !!! MUST BE CALLED AT THE END of the directive + return initialize(); + } + }); + } +]); diff --git a/app/assets/javascripts/directives/members.js.erb b/app/assets/javascripts/directives/members.js.erb new file mode 100644 index 000000000..24b195efd --- /dev/null +++ b/app/assets/javascripts/directives/members.js.erb @@ -0,0 +1,22 @@ +Application.Directives.directive('members', [ 'Member', + function (Member) { + return ({ + restrict: 'E', + templateUrl: '<%= asset_path "home/members.html" %>', + link ($scope, element, attributes) { + // The last registered members who confirmed their addresses + $scope.lastMembers = null; + + // constructor + const initialize = function () { + Member.lastSubscribed({ limit: 4 }, function (data) { + $scope.lastMembers = data; + }) + }; + + // !!! MUST BE CALLED AT THE END of the directive + return initialize(); + } + }); + } +]); diff --git a/app/assets/javascripts/directives/news.js.erb b/app/assets/javascripts/directives/news.js.erb new file mode 100644 index 000000000..06b8f56d7 --- /dev/null +++ b/app/assets/javascripts/directives/news.js.erb @@ -0,0 +1,22 @@ +Application.Directives.directive('news', [ 'Setting', + function (Setting) { + return ({ + restrict: 'E', + templateUrl: '<%= asset_path "home/news.html" %>', + link ($scope, element, attributes) { + // The admin blogpost + $scope.homeBlogpost = null; + + // constructor + const initialize = function () { + Setting.get({ name: 'home_blogpost' }, function (data) { + $scope.homeBlogpost = data.setting.value; + }) + }; + + // !!! MUST BE CALLED AT THE END of the directive + return initialize(); + } + }); + } +]); diff --git a/app/assets/javascripts/directives/post_render.js b/app/assets/javascripts/directives/post_render.js new file mode 100644 index 000000000..7111d5efc --- /dev/null +++ b/app/assets/javascripts/directives/post_render.js @@ -0,0 +1,12 @@ +Application.Directives.directive('postRender', [ '$timeout', + function ($timeout) { + return ({ + restrict: 'A', + terminal: false, + transclude: false, + link: function (scope, element, attrs) { + $timeout(scope[attrs.postRender], 0); + } + }); + } +]); diff --git a/app/assets/javascripts/directives/projects.js.erb b/app/assets/javascripts/directives/projects.js.erb new file mode 100644 index 000000000..1b7812dc1 --- /dev/null +++ b/app/assets/javascripts/directives/projects.js.erb @@ -0,0 +1,25 @@ +Application.Directives.directive('projects', [ 'Project', + function (Project) { + return ({ + restrict: 'E', + templateUrl: '<%= asset_path "home/projects.html" %>', + link ($scope, element, attributes) { + // The last projects published/documented on the platform + $scope.lastProjects = null; + + // The default slide shown in the carousel + $scope.activeSlide = 0; + + // constructor + const initialize = function () { + Project.lastPublished(function (data) { + $scope.lastProjects = data; + }) + }; + + // !!! MUST BE CALLED AT THE END of the directive + return initialize(); + } + }); + } +]); diff --git a/app/assets/javascripts/directives/selectMember.js.erb b/app/assets/javascripts/directives/selectMember.js.erb index aeaaeb4bf..760b2874c 100644 --- a/app/assets/javascripts/directives/selectMember.js.erb +++ b/app/assets/javascripts/directives/selectMember.js.erb @@ -1,14 +1,3 @@ -/* eslint-disable - no-return-assign, - no-undef, -*/ -// TODO: This file was created by bulk-decaffeinate. -// Fix any style issues and re-enable lint. -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ 'use strict'; /** @@ -36,9 +25,9 @@ Application.Directives.directive('selectMember', [ 'Diacritics', 'Member', funct q['subscription'] = attributes.subscription; } - return Member.search(q, function (users) { + Member.search(q, function (users) { scope.matchingMembers = users; - return scope.isLoadingMembers = false; + scope.isLoadingMembers = false; } , function (error) { console.error(error); }); }; diff --git a/app/assets/javascripts/directives/stripe-form.js.erb b/app/assets/javascripts/directives/stripe-form.js.erb index 15835c91c..4f125e4b4 100644 --- a/app/assets/javascripts/directives/stripe-form.js.erb +++ b/app/assets/javascripts/directives/stripe-form.js.erb @@ -73,7 +73,7 @@ Application.Directives.directive('stripeForm', ['Payment', 'growl', '_t', if (response.error.statusText) { growl.error(response.error.statusText); } else { - growl.error(`${_t('payment_card_error')} ${response.error}`); + growl.error(`${_t('app.shared.messages.payment_card_error')} ${response.error}`); } confirmButton.prop('disabled', false); } else if (response.requires_action) { diff --git a/app/assets/javascripts/directives/twitter.js.erb b/app/assets/javascripts/directives/twitter.js.erb new file mode 100644 index 000000000..914852e7b --- /dev/null +++ b/app/assets/javascripts/directives/twitter.js.erb @@ -0,0 +1,43 @@ +/* global twitterFetcher */ + +/** + * This directive will show the last tweet. + * Usage: + */ +Application.Directives.directive('twitter', ['Setting', + function (Setting) { + return ({ + restrict: 'E', + templateUrl: '<%= asset_path "home/twitter.html" %>', + link ($scope, element, attributes) { + // Twitter username + $scope.twitterName = null; + + // constructor + const initialize = function () { + Setting.get({ name: 'twitter_name' }, function (data) { + $scope.twitterName = data.setting.value; + if ($scope.twitterName) { + const configProfile = { + 'profile': { 'screenName': $scope.twitterName }, + 'domId': 'twitter', + 'maxTweets': 1, + 'enableLinks': true, + 'showUser': false, + 'showTime': true, + 'showImages': false, + 'showRetweet': true, + 'showInteraction': false, + 'lang': Fablab.locale + }; + twitterFetcher.fetch(configProfile); + } + }) + }; + + // !!! MUST BE CALLED AT THE END of the directive + return initialize(); + } + }); + } +]); diff --git a/app/assets/javascripts/filters/filters.js b/app/assets/javascripts/filters/filters.js index 572c2bcbf..8c808901d 100644 --- a/app/assets/javascripts/filters/filters.js +++ b/app/assets/javascripts/filters/filters.js @@ -318,22 +318,12 @@ Application.Filters.filter('toIsoDate', [function () { }; }]); -Application.Filters.filter('booleanFormat', [ '_t', function (_t) { - return function (boolean) { - if (boolean || (boolean === 'true')) { - return _t('yes'); - } else { - return _t('no'); - } - }; -}]); - Application.Filters.filter('booleanFormat', [ '_t', function (_t) { return function (boolean) { if (((typeof boolean === 'boolean') && boolean) || ((typeof boolean === 'string') && (boolean === 'true'))) { - return _t('yes'); + return _t('app.shared.buttons.yes'); } else { - return _t('no'); + return _t('app.shared.buttons.no'); } }; }]); @@ -341,7 +331,7 @@ Application.Filters.filter('booleanFormat', [ '_t', function (_t) { Application.Filters.filter('maxCount', [ '_t', function (_t) { return function (max) { if ((typeof max === 'undefined') || (max === null) || ((typeof max === 'number') && (max === 0))) { - return _t('unlimited'); + return _t('app.admin.pricing.unlimited'); } else { return max; } diff --git a/app/assets/javascripts/router.js.erb b/app/assets/javascripts/router.js.erb index cfd27e274..cb3935ea2 100644 --- a/app/assets/javascripts/router.js.erb +++ b/app/assets/javascripts/router.js.erb @@ -36,18 +36,21 @@ angular.module('application.router', ['ui.router']) resolve: { logoFile: ['CustomAsset', function (CustomAsset) { return CustomAsset.get({ name: 'logo-file' }).$promise; }], logoBlackFile: ['CustomAsset', function (CustomAsset) { return CustomAsset.get({ name: 'logo-black-file' }).$promise; }], - commonTranslations: ['Translations', function (Translations) { return Translations.query(['app.public.common', 'app.shared.buttons', 'app.shared.elements']).$promise; }] + sharedTranslations: ['Translations', function (Translations) { return Translations.query(['app.shared', 'app.public.common']).$promise; }] }, onEnter: ['$rootScope', 'logoFile', 'logoBlackFile', 'CSRF', function ($rootScope, logoFile, logoBlackFile, CSRF) { // Retrieve Anti-CSRF tokens from cookies CSRF.setMetaTags(); // Application logo $rootScope.logo = logoFile.custom_asset; - return $rootScope.logoBlack = logoBlackFile.custom_asset; + $rootScope.logoBlack = logoBlackFile.custom_asset; }] }) .state('app.public', { - abstract: true + abstract: true, + resolve: { + publicTranslations: ['Translations', function (Translations) { return Translations.query(['app.public']).$promise; }] + } }) .state('app.logged', { abstract: true, @@ -55,7 +58,8 @@ angular.module('application.router', ['ui.router']) authorizedRoles: ['member', 'admin'] }, resolve: { - currentUser: ['Auth', function (Auth) { return Auth.currentUser(); }] + currentUser: ['Auth', function (Auth) { return Auth.currentUser(); }], + loggedTranslations: ['Translations', function (Translations) { return Translations.query(['app.logged']).$promise; }] }, onEnter: ['$state', '$timeout', 'currentUser', '$rootScope', function ($state, $timeout, currentUser, $rootScope) { $rootScope.currentUser = currentUser; @@ -67,7 +71,8 @@ angular.module('application.router', ['ui.router']) authorizedRoles: ['admin'] }, resolve: { - currentUser: ['Auth', function (Auth) { return Auth.currentUser(); }] + currentUser: ['Auth', function (Auth) { return Auth.currentUser(); }], + adminTranslations: ['Translations', function (Translations) { return Translations.query(['app.admin']).$promise; }] }, onEnter: ['$state', '$timeout', 'currentUser', '$rootScope', function ($state, $timeout, currentUser, $rootScope) { $rootScope.currentUser = currentUser; @@ -82,9 +87,6 @@ angular.module('application.router', ['ui.router']) templateUrl: '<%= asset_path "shared/about.html" %>', controller: 'AboutController' } - }, - resolve: { - translations: ['Translations', function (Translations) { return Translations.query('app.public.about').$promise; }] } }) .state('app.public.home', { @@ -96,12 +98,7 @@ angular.module('application.router', ['ui.router']) } }, resolve: { - lastMembersPromise: ['Member', function (Member) { return Member.lastSubscribed({ limit: 4 }).$promise; }], - lastProjectsPromise: ['Project', function (Project) { return Project.lastPublished().$promise; }], - upcomingEventsPromise: ['Event', function (Event) { return Event.upcoming({ limit: 3 }).$promise; }], - homeBlogpostPromise: ['Setting', function (Setting) { return Setting.get({ name: 'home_blogpost' }).$promise; }], - twitterNamePromise: ['Setting', function (Setting) { return Setting.get({ name: 'twitter_name' }).$promise; }], - translations: ['Translations', function (Translations) { return Translations.query('app.public.home').$promise; }] + settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['home_content', 'home_blogpost']" }).$promise; }] } }) .state('app.public.privacy', { @@ -111,9 +108,6 @@ angular.module('application.router', ['ui.router']) templateUrl: '<%= asset_path "shared/privacy.html" %>', controller: 'PrivacyController' } - }, - resolve: { - translations: ['Translations', function (Translations) { return Translations.query('app.public.privacy').$promise; }] } }) @@ -132,7 +126,6 @@ angular.module('application.router', ['ui.router']) groupsPromise: ['Group', function (Group) { return Group.query().$promise; }], cguFile: ['CustomAsset', function (CustomAsset) { return CustomAsset.get({ name: 'cgu-file' }).$promise; }], memberPromise: ['Member', 'currentUser', function (Member, currentUser) { return Member.get({ id: currentUser.id }).$promise; }], - translations: ['Translations', function (Translations) { return Translations.query(['app.logged.profileCompletion', 'app.shared.user']).$promise; }] } }) @@ -151,9 +144,6 @@ angular.module('application.router', ['ui.router']) templateUrl: '<%= asset_path "dashboard/profile.html" %>', controller: 'DashboardController' } - }, - resolve: { - translations: ['Translations', function (Translations) { return Translations.query(['app.logged.dashboard.profile', 'app.shared.public_profile']).$promise; }] } }) .state('app.logged.dashboard.settings', { @@ -167,7 +157,6 @@ angular.module('application.router', ['ui.router']) resolve: { groups: ['Group', function (Group) { return Group.query().$promise; }], activeProviderPromise: ['AuthProvider', function (AuthProvider) { return AuthProvider.active().$promise; }], - translations: ['Translations', function (Translations) { return Translations.query(['app.logged.dashboard.settings', 'app.shared.user']).$promise; }] } }) .state('app.logged.dashboard.projects', { @@ -177,9 +166,6 @@ angular.module('application.router', ['ui.router']) templateUrl: '<%= asset_path "dashboard/projects.html" %>', controller: 'DashboardController' } - }, - resolve: { - translations: ['Translations', function (Translations) { return Translations.query('app.logged.dashboard.projects').$promise; }] } }) .state('app.logged.dashboard.trainings', { @@ -189,9 +175,6 @@ angular.module('application.router', ['ui.router']) templateUrl: '<%= asset_path "dashboard/trainings.html" %>', controller: 'DashboardController' } - }, - resolve: { - translations: ['Translations', function (Translations) { return Translations.query('app.logged.dashboard.trainings').$promise; }] } }) .state('app.logged.dashboard.events', { @@ -201,9 +184,6 @@ angular.module('application.router', ['ui.router']) templateUrl: '<%= asset_path "dashboard/events.html" %>', controller: 'DashboardController' } - }, - resolve: { - translations: ['Translations', function (Translations) { return Translations.query('app.logged.dashboard.events').$promise; }] } }) .state('app.logged.dashboard.invoices', { @@ -213,9 +193,6 @@ angular.module('application.router', ['ui.router']) templateUrl: '<%= asset_path "dashboard/invoices.html" %>', controller: 'DashboardController' } - }, - resolve: { - translations: ['Translations', function (Translations) { return Translations.query('app.logged.dashboard.invoices').$promise; }] } }) .state('app.logged.dashboard.wallet', { @@ -228,8 +205,7 @@ angular.module('application.router', ['ui.router']) }, resolve: { walletPromise: ['Wallet', 'currentUser', function (Wallet, currentUser) { return Wallet.getWalletByUser({ user_id: currentUser.id }).$promise; }], - transactionsPromise: ['Wallet', 'walletPromise', function (Wallet, walletPromise) { return Wallet.transactions({ id: walletPromise.id }).$promise; }], - translations: ['Translations', function (Translations) { return Translations.query(['app.shared.wallet']).$promise; }] + transactionsPromise: ['Wallet', 'walletPromise', function (Wallet, walletPromise) { return Wallet.transactions({ id: walletPromise.id }).$promise; }] } }) @@ -243,8 +219,7 @@ angular.module('application.router', ['ui.router']) } }, resolve: { - memberPromise: ['$stateParams', 'Member', function ($stateParams, Member) { return Member.get({ id: $stateParams.id }).$promise; }], - translations: ['Translations', function (Translations) { return Translations.query(['app.logged.members_show', 'app.shared.public_profile']).$promise; }] + memberPromise: ['$stateParams', 'Member', function ($stateParams, Member) { return Member.get({ id: $stateParams.id }).$promise; }] } }) .state('app.logged.members', { @@ -256,8 +231,7 @@ angular.module('application.router', ['ui.router']) } }, resolve: { - membersPromise: ['Member', function (Member) { return Member.query({ requested_attributes: '[profile]', page: 1, size: 10 }).$promise; }], - translations: ['Translations', function (Translations) { return Translations.query('app.logged.members').$promise; }] + membersPromise: ['Member', function (Member) { return Member.query({ requested_attributes: '[profile]', page: 1, size: 10 }).$promise; }] } }) @@ -273,8 +247,7 @@ angular.module('application.router', ['ui.router']) resolve: { themesPromise: ['Theme', function (Theme) { return Theme.query().$promise; }], componentsPromise: ['Component', function (Component) { return Component.query().$promise; }], - machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }], - translations: ['Translations', function (Translations) { return Translations.query('app.public.projects_list').$promise; }] + machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }] } }) .state('app.logged.projects_new', { @@ -286,8 +259,7 @@ angular.module('application.router', ['ui.router']) } }, resolve: { - allowedExtensions: ['Project', function (Project) { return Project.allowedExtensions().$promise; }], - translations: ['Translations', function (Translations) { return Translations.query(['app.logged.projects_new', 'app.shared.project']).$promise; }] + allowedExtensions: ['Project', function (Project) { return Project.allowedExtensions().$promise; }] } }) .state('app.public.projects_show', { @@ -299,8 +271,7 @@ angular.module('application.router', ['ui.router']) } }, resolve: { - projectPromise: ['$stateParams', 'Project', function ($stateParams, Project) { return Project.get({ id: $stateParams.id }).$promise; }], - translations: ['Translations', function (Translations) { return Translations.query('app.public.projects_show').$promise; }] + projectPromise: ['$stateParams', 'Project', function ($stateParams, Project) { return Project.get({ id: $stateParams.id }).$promise; }] } }) .state('app.logged.projects_edit', { @@ -313,8 +284,7 @@ angular.module('application.router', ['ui.router']) }, resolve: { projectPromise: ['$stateParams', 'Project', function ($stateParams, Project) { return Project.get({ id: $stateParams.id }).$promise; }], - allowedExtensions: ['Project', function (Project) { return Project.allowedExtensions().$promise; }], - translations: ['Translations', function (Translations) { return Translations.query(['app.logged.projects_edit', 'app.shared.project']).$promise; }] + allowedExtensions: ['Project', function (Project) { return Project.allowedExtensions().$promise; }] } }) @@ -328,8 +298,7 @@ angular.module('application.router', ['ui.router']) } }, resolve: { - machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }], - translations: ['Translations', function (Translations) { return Translations.query(['app.public.machines_list', 'app.shared.training_reservation_modal', 'app.shared.request_training_modal']).$promise; }] + machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }] } }) .state('app.admin.machines_new', { @@ -339,9 +308,6 @@ angular.module('application.router', ['ui.router']) templateUrl: '<%= asset_path "machines/new.html" %>', controller: 'NewMachineController' } - }, - resolve: { - translations: ['Translations', function (Translations) { return Translations.query(['app.admin.machines_new', 'app.shared.machine']).$promise; }] } }) .state('app.public.machines_show', { @@ -353,8 +319,7 @@ angular.module('application.router', ['ui.router']) } }, resolve: { - machinePromise: ['Machine', '$stateParams', function (Machine, $stateParams) { return Machine.get({ id: $stateParams.id }).$promise; }], - translations: ['Translations', function (Translations) { return Translations.query(['app.public.machines_show', 'app.shared.training_reservation_modal', 'app.shared.request_training_modal']).$promise; }] + machinePromise: ['Machine', '$stateParams', function (Machine, $stateParams) { return Machine.get({ id: $stateParams.id }).$promise; }] } }) .state('app.logged.machines_reserve', { @@ -380,11 +345,6 @@ angular.module('application.router', ['ui.router']) 'booking_cancel_delay', \ 'subscription_explications_alert']` }).$promise; - }], - translations: ['Translations', function (Translations) { - return Translations.query(['app.logged.machines_reserve', 'app.shared.plan_subscribe', 'app.shared.member_select', - 'app.shared.stripe', 'app.shared.valid_reservation_modal', 'app.shared.confirm_modify_slot_modal', - 'app.shared.wallet', 'app.shared.coupon_input', 'app.shared.cart']).$promise; }] } }) @@ -397,8 +357,7 @@ angular.module('application.router', ['ui.router']) } }, resolve: { - machinePromise: ['Machine', '$stateParams', function (Machine, $stateParams) { return Machine.get({ id: $stateParams.id }).$promise; }], - translations: ['Translations', function (Translations) { return Translations.query(['app.admin.machines_edit', 'app.shared.machine']).$promise; }] + machinePromise: ['Machine', '$stateParams', function (Machine, $stateParams) { return Machine.get({ id: $stateParams.id }).$promise; }] } }) @@ -413,8 +372,7 @@ angular.module('application.router', ['ui.router']) } }, resolve: { - spacesPromise: ['Space', function (Space) { return Space.query().$promise; }], - translations: ['Translations', function (Translations) { return Translations.query(['app.public.spaces_list']).$promise; }] + spacesPromise: ['Space', function (Space) { return Space.query().$promise; }] } }) .state('app.admin.space_new', { @@ -425,9 +383,6 @@ angular.module('application.router', ['ui.router']) templateUrl: '<%= asset_path "spaces/new.html" %>', controller: 'NewSpaceController' } - }, - resolve: { - translations: ['Translations', function (Translations) { return Translations.query(['app.admin.space_new', 'app.shared.space']).$promise; }] } }) .state('app.public.space_show', { @@ -440,8 +395,7 @@ angular.module('application.router', ['ui.router']) } }, resolve: { - spacePromise: ['Space', '$stateParams', function (Space, $stateParams) { return Space.get({ id: $stateParams.id }).$promise; }], - translations: ['Translations', function (Translations) { return Translations.query(['app.public.space_show']).$promise; }] + spacePromise: ['Space', '$stateParams', function (Space, $stateParams) { return Space.get({ id: $stateParams.id }).$promise; }] } }) .state('app.admin.space_edit', { @@ -454,8 +408,7 @@ angular.module('application.router', ['ui.router']) } }, resolve: { - spacePromise: ['Space', '$stateParams', function (Space, $stateParams) { return Space.get({ id: $stateParams.id }).$promise; }], - translations: ['Translations', function (Translations) { return Translations.query(['app.admin.space_edit', 'app.shared.space']).$promise; }] + spacePromise: ['Space', '$stateParams', function (Space, $stateParams) { return Space.get({ id: $stateParams.id }).$promise; }] } }) .state('app.logged.space_reserve', { @@ -482,11 +435,6 @@ angular.module('application.router', ['ui.router']) 'booking_cancel_delay', \ 'subscription_explications_alert', \ 'space_explications_alert']` }).$promise; - }], - translations: ['Translations', function (Translations) { - return Translations.query(['app.logged.space_reserve', 'app.shared.plan_subscribe', 'app.shared.member_select', - 'app.shared.stripe', 'app.shared.valid_reservation_modal', 'app.shared.confirm_modify_slot_modal', - 'app.shared.wallet', 'app.shared.coupon_input', 'app.shared.cart']).$promise; }] } }) @@ -501,8 +449,7 @@ angular.module('application.router', ['ui.router']) } }, resolve: { - trainingsPromise: ['Training', function (Training) { return Training.query({ public_page: true }).$promise; }], - translations: ['Translations', function (Translations) { return Translations.query(['app.public.trainings_list']).$promise; }] + trainingsPromise: ['Training', function (Training) { return Training.query({ public_page: true }).$promise; }] } }) .state('app.public.training_show', { @@ -514,8 +461,7 @@ angular.module('application.router', ['ui.router']) } }, resolve: { - trainingPromise: ['Training', '$stateParams', function (Training, $stateParams) { return Training.get({ id: $stateParams.id }).$promise; }], - translations: ['Translations', function (Translations) { return Translations.query(['app.public.training_show']).$promise; }] + trainingPromise: ['Training', '$stateParams', function (Training, $stateParams) { return Training.get({ id: $stateParams.id }).$promise; }] } }) .state('app.logged.trainings_reserve', { @@ -545,11 +491,6 @@ angular.module('application.router', ['ui.router']) 'subscription_explications_alert', \ 'training_explications_alert', \ 'training_information_message']` }).$promise; - }], - translations: ['Translations', function (Translations) { - return Translations.query(['app.logged.trainings_reserve', 'app.shared.plan_subscribe', 'app.shared.member_select', - 'app.shared.stripe', 'app.shared.valid_reservation_modal', 'app.shared.confirm_modify_slot_modal', - 'app.shared.wallet', 'app.shared.coupon_input', 'app.shared.cart']).$promise; }] } }) @@ -561,9 +502,6 @@ angular.module('application.router', ['ui.router']) templateUrl: '<%= asset_path "notifications/index.html.erb" %>', controller: 'NotificationsController' } - }, - resolve: { - translations: ['Translations', function (Translations) { return Translations.query('app.logged.notifications').$promise; }] } }) @@ -580,11 +518,7 @@ angular.module('application.router', ['ui.router']) resolve: { subscriptionExplicationsPromise: ['Setting', function (Setting) { return Setting.get({ name: 'subscription_explications_alert' }).$promise; }], plansPromise: ['Plan', function (Plan) { return Plan.query().$promise; }], - groupsPromise: ['Group', function (Group) { return Group.query().$promise; }], - translations: ['Translations', function (Translations) { - return Translations.query(['app.public.plans', 'app.shared.member_select', 'app.shared.stripe', 'app.shared.wallet', - 'app.shared.coupon_input']).$promise; - }] + groupsPromise: ['Group', function (Group) { return Group.query().$promise; }] } }) @@ -600,8 +534,7 @@ angular.module('application.router', ['ui.router']) resolve: { categoriesPromise: ['Category', function (Category) { return Category.query().$promise; }], themesPromise: ['EventTheme', function (EventTheme) { return EventTheme.query().$promise; }], - ageRangesPromise: ['AgeRange', function (AgeRange) { return AgeRange.query().$promise; }], - translations: ['Translations', function (Translations) { return Translations.query('app.public.events_list').$promise; }] + ageRangesPromise: ['AgeRange', function (AgeRange) { return AgeRange.query().$promise; }] } }) .state('app.public.events_show', { @@ -615,11 +548,7 @@ angular.module('application.router', ['ui.router']) resolve: { eventPromise: ['Event', '$stateParams', function (Event, $stateParams) { return Event.get({ id: $stateParams.id }).$promise; }], priceCategoriesPromise: ['PriceCategory', function (PriceCategory) { return PriceCategory.query().$promise; }], - settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['booking_move_enable', 'booking_move_delay', 'event_explications_alert']" }).$promise; }], - translations: ['Translations', function (Translations) { - return Translations.query(['app.public.events_show', 'app.shared.member_select', 'app.shared.stripe', - 'app.shared.valid_reservation_modal', 'app.shared.wallet', 'app.shared.coupon_input']).$promise; - }] + settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['booking_move_enable', 'booking_move_delay', 'booking_cancel_enable', 'booking_cancel_delay', 'event_explications_alert']" }).$promise; }] } }) @@ -638,7 +567,7 @@ angular.module('application.router', ['ui.router']) trainingsPromise: ['Training', function (Training) { return Training.query().$promise; }], machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }], spacesPromise: ['Space', function (Space) { return Space.query().$promise; }], - translations: ['Translations', function (Translations) { return Translations.query(['app.public.calendar']).$promise; }] + iCalendarPromise: ['ICalendar', function (ICalendar) { return ICalendar.query().$promise; }] } }) @@ -656,7 +585,20 @@ angular.module('application.router', ['ui.router']) bookingWindowStart: ['Setting', function (Setting) { return Setting.get({ name: 'booking_window_start' }).$promise; }], bookingWindowEnd: ['Setting', function (Setting) { return Setting.get({ name: 'booking_window_end' }).$promise; }], machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }], - translations: ['Translations', function (Translations) { return Translations.query('app.admin.calendar').$promise; }] + plansPromise: ['Plan', function (Plan) { return Plan.query().$promise; }], + groupsPromise: ['Group', function (Group) { return Group.query().$promise; }] + } + }) + .state('app.admin.calendar.icalendar', { + url: '/admin/calendar/icalendar', + views: { + 'main@': { + templateUrl: '<%= asset_path "admin/calendar/icalendar.html" %>', + controller: 'AdminICalendarController' + } + }, + resolve: { + iCalendars: ['ICalendar', function (ICalendar) { return ICalendar.query().$promise; }] } }) @@ -672,8 +614,7 @@ angular.module('application.router', ['ui.router']) resolve: { componentsPromise: ['Component', function (Component) { return Component.query().$promise; }], licencesPromise: ['Licence', function (Licence) { return Licence.query().$promise; }], - themesPromise: ['Theme', function (Theme) { return Theme.query().$promise; }], - translations: ['Translations', function (Translations) { return Translations.query('app.admin.project_elements').$promise; }] + themesPromise: ['Theme', function (Theme) { return Theme.query().$promise; }] } }) .state('app.admin.manage_abuses', { @@ -685,8 +626,7 @@ angular.module('application.router', ['ui.router']) } }, resolve: { - abusesPromise: ['Abuse', function(Abuse) { return Abuse.query().$promise; }], - translations: ['Translations', function(Translations) { return Translations.query('app.admin.manage_abuses').$promise; }] + abusesPromise: ['Abuse', function(Abuse) { return Abuse.query().$promise; }] } }) @@ -701,8 +641,7 @@ angular.module('application.router', ['ui.router']) }, resolve: { trainingsPromise: ['Training', function (Training) { return Training.query().$promise; }], - machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }], - translations: ['Translations', function (Translations) { return Translations.query(['app.admin.trainings', 'app.shared.trainings']).$promise; }] + machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }] } }) .state('app.admin.trainings_new', { @@ -714,8 +653,7 @@ angular.module('application.router', ['ui.router']) } }, resolve: { - machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }], - translations: ['Translations', function (Translations) { return Translations.query(['app.admin.trainings_new', 'app.shared.trainings']).$promise; }] + machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }] } }) .state('app.admin.trainings_edit', { @@ -728,8 +666,7 @@ angular.module('application.router', ['ui.router']) }, resolve: { trainingPromise: ['Training', '$stateParams', function (Training, $stateParams) { return Training.get({ id: $stateParams.id }).$promise; }], - machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }], - translations: ['Translations', function (Translations) { return Translations.query('app.shared.trainings').$promise; }] + machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }] } }) // events @@ -746,8 +683,7 @@ angular.module('application.router', ['ui.router']) categoriesPromise: ['Category', function (Category) { return Category.query().$promise; }], themesPromise: ['EventTheme', function (EventTheme) { return EventTheme.query().$promise; }], ageRangesPromise: ['AgeRange', function (AgeRange) { return AgeRange.query().$promise; }], - priceCategoriesPromise: ['PriceCategory', function (PriceCategory) { return PriceCategory.query().$promise; }], - translations: ['Translations', function (Translations) { return Translations.query('app.admin.events').$promise; }] + priceCategoriesPromise: ['PriceCategory', function (PriceCategory) { return PriceCategory.query().$promise; }] } }) .state('app.admin.events_new', { @@ -762,8 +698,7 @@ angular.module('application.router', ['ui.router']) categoriesPromise: ['Category', function (Category) { return Category.query().$promise; }], themesPromise: ['EventTheme', function (EventTheme) { return EventTheme.query().$promise; }], ageRangesPromise: ['AgeRange', function (AgeRange) { return AgeRange.query().$promise; }], - priceCategoriesPromise: ['PriceCategory', function (PriceCategory) { return PriceCategory.query().$promise; }], - translations: ['Translations', function (Translations) { return Translations.query(['app.admin.events_new', 'app.shared.event']).$promise; }] + priceCategoriesPromise: ['PriceCategory', function (PriceCategory) { return PriceCategory.query().$promise; }] } }) .state('app.admin.events_edit', { @@ -779,8 +714,7 @@ angular.module('application.router', ['ui.router']) categoriesPromise: ['Category', function (Category) { return Category.query().$promise; }], themesPromise: ['EventTheme', function (EventTheme) { return EventTheme.query().$promise; }], ageRangesPromise: ['AgeRange', function (AgeRange) { return AgeRange.query().$promise; }], - priceCategoriesPromise: ['PriceCategory', function (PriceCategory) { return PriceCategory.query().$promise; }], - translations: ['Translations', function (Translations) { return Translations.query(['app.admin.events_edit', 'app.shared.event']).$promise; }] + priceCategoriesPromise: ['PriceCategory', function (PriceCategory) { return PriceCategory.query().$promise; }] } }) .state('app.admin.event_reservations', { @@ -793,8 +727,7 @@ angular.module('application.router', ['ui.router']) }, resolve: { eventPromise: ['Event', '$stateParams', function (Event, $stateParams) { return Event.get({ id: $stateParams.id }).$promise; }], - reservationsPromise: ['Reservation', '$stateParams', function (Reservation, $stateParams) { return Reservation.query({ reservable_id: $stateParams.id, reservable_type: 'Event' }).$promise; }], - translations: ['Translations', function (Translations) { return Translations.query('app.admin.event_reservations').$promise; }] + reservationsPromise: ['Reservation', '$stateParams', function (Reservation, $stateParams) { return Reservation.query({ reservable_id: $stateParams.id, reservable_type: 'Event' }).$promise; }] } }) @@ -812,7 +745,6 @@ angular.module('application.router', ['ui.router']) groups: ['Group', function (Group) { return Group.query().$promise; }], machinesPricesPromise: ['Price', function (Price) { return Price.query({ priceable_type: 'Machine', plan_id: 'null' }).$promise; }], trainingsPricingsPromise: ['TrainingsPricing', function (TrainingsPricing) { return TrainingsPricing.query().$promise; }], - translations: ['Translations', function (Translations) { return Translations.query(['app.admin.pricing', 'app.shared.member_select', 'app.shared.coupon']).$promise; }], trainingsPromise: ['Training', function (Training) { return Training.query().$promise; }], machineCreditsPromise: ['Credit', function (Credit) { return Credit.query({ creditable_type: 'Machine' }).$promise; }], machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }], @@ -840,9 +772,6 @@ angular.module('application.router', ['ui.router']) templateUrl: '<%= asset_path "admin/plans/new.html" %>', controller: 'NewPlanController' } - }, - resolve: { - translations: ['Translations', function (Translations) { return Translations.query(['app.admin.plans.new', 'app.shared.plan']).$promise; }] } }) .state('app.admin.plans.edit', { @@ -857,8 +786,7 @@ angular.module('application.router', ['ui.router']) spaces: ['Space', function (Space) { return Space.query().$promise; }], machines: ['Machine', function (Machine) { return Machine.query().$promise; }], plans: ['Plan', function (Plan) { return Plan.query().$promise; }], - planPromise: ['Plan', '$stateParams', function (Plan, $stateParams) { return Plan.get({ id: $stateParams.id }).$promise; }], - translations: ['Translations', function (Translations) { return Translations.query(['app.admin.plans.edit', 'app.shared.plan']).$promise; }] + planPromise: ['Plan', '$stateParams', function (Plan, $stateParams) { return Plan.get({ id: $stateParams.id }).$promise; }] } }) @@ -870,9 +798,6 @@ angular.module('application.router', ['ui.router']) templateUrl: '<%= asset_path "admin/coupons/new.html" %>', controller: 'NewCouponController' } - }, - resolve: { - translations: ['Translations', function (Translations) { return Translations.query(['app.admin.coupons_new', 'app.shared.coupon']).$promise; }] } }) .state('app.admin.coupons_edit', { @@ -884,8 +809,7 @@ angular.module('application.router', ['ui.router']) } }, resolve: { - couponPromise: ['Coupon', '$stateParams', function (Coupon, $stateParams) { return Coupon.get({ id: $stateParams.id }).$promise; }], - translations: ['Translations', function (Translations) { return Translations.query(['app.admin.coupons_edit', 'app.shared.coupon']).$promise; }] + couponPromise: ['Coupon', '$stateParams', function (Coupon, $stateParams) { return Coupon.get({ id: $stateParams.id }).$promise; }] } }) @@ -914,8 +838,7 @@ angular.module('application.router', ['ui.router']) query: { number: '', customer: '', date: null, order_by: '-reference', page: 1, size: 20 } }).$promise; }], - closedPeriods: [ 'AccountingPeriod', function(AccountingPeriod) { return AccountingPeriod.query().$promise; }], - translations: ['Translations', function (Translations) { return Translations.query('app.admin.invoices').$promise; }] + closedPeriods: [ 'AccountingPeriod', function(AccountingPeriod) { return AccountingPeriod.query().$promise; }] } }) @@ -945,8 +868,7 @@ angular.module('application.router', ['ui.router']) adminsPromise: ['Admin', function (Admin) { return Admin.query().$promise; }], groupsPromise: ['Group', function (Group) { return Group.query().$promise; }], tagsPromise: ['Tag', function (Tag) { return Tag.query().$promise; }], - authProvidersPromise: ['AuthProvider', function (AuthProvider) { return AuthProvider.query().$promise; }], - translations: ['Translations', function (Translations) { return Translations.query('app.admin.members').$promise; }] + authProvidersPromise: ['AuthProvider', function (AuthProvider) { return AuthProvider.query().$promise; }] } }) .state('app.admin.members_new', { @@ -956,9 +878,6 @@ angular.module('application.router', ['ui.router']) templateUrl: '<%= asset_path "admin/members/new.html" %>', controller: 'NewMemberController' } - }, - resolve: { - translations: ['Translations', function (Translations) { return Translations.query(['app.admin.members_new', 'app.shared.user', 'app.shared.user_admin']).$promise; }] } }) .state('app.admin.members_import', { @@ -970,7 +889,6 @@ angular.module('application.router', ['ui.router']) } }, resolve: { - translations: ['Translations', function (Translations) { return Translations.query(['app.admin.members_import', 'app.shared.user', 'app.shared.user_admin']).$promise; }], tags: ['Tag', function(Tag) { return Tag.query().$promise }] } }) @@ -983,7 +901,6 @@ angular.module('application.router', ['ui.router']) } }, resolve: { - translations: ['Translations', function (Translations) { return Translations.query(['app.admin.members_import_result', 'app.shared.user', 'app.shared.user_admin']).$promise; }], importItem: ['Import', '$stateParams', function(Import, $stateParams) { return Import.get({ id: $stateParams.id }).$promise }] } }) @@ -1000,8 +917,7 @@ angular.module('application.router', ['ui.router']) activeProviderPromise: ['AuthProvider', function (AuthProvider) { return AuthProvider.active().$promise; }], walletPromise: ['Wallet', '$stateParams', function (Wallet, $stateParams) { return Wallet.getWalletByUser({ user_id: $stateParams.id }).$promise; }], transactionsPromise: ['Wallet', 'walletPromise', function (Wallet, walletPromise) { return Wallet.transactions({ id: walletPromise.id }).$promise; }], - tagsPromise: ['Tag', function (Tag) { return Tag.query().$promise; }], - translations: ['Translations', function (Translations) { return Translations.query(['app.admin.members_edit', 'app.shared.user', 'app.shared.user_admin', 'app.shared.wallet']).$promise; }] + tagsPromise: ['Tag', function (Tag) { return Tag.query().$promise; }] } }) .state('app.admin.admins_new', { @@ -1011,9 +927,6 @@ angular.module('application.router', ['ui.router']) templateUrl: '<%= asset_path "admin/admins/new.html" %>', controller: 'NewAdminController' } - }, - resolve: { - translations: ['Translations', function (Translations) { return Translations.query('app.admin.admins_new').$promise; }] } }) @@ -1028,8 +941,7 @@ angular.module('application.router', ['ui.router']) }, resolve: { mappingFieldsPromise: ['AuthProvider', function (AuthProvider) { return AuthProvider.mapping_fields().$promise; }], - authProvidersPromise: ['AuthProvider', function (AuthProvider) { return AuthProvider.query().$promise; }], - translations: ['Translations', function (Translations) { return Translations.query(['app.admin.authentication_new', 'app.shared.authentication', 'app.shared.oauth2']).$promise; }] + authProvidersPromise: ['AuthProvider', function (AuthProvider) { return AuthProvider.query().$promise; }] } }) .state('app.admin.authentication_edit', { @@ -1042,8 +954,7 @@ angular.module('application.router', ['ui.router']) }, resolve: { providerPromise: ['AuthProvider', '$stateParams', function (AuthProvider, $stateParams) { return AuthProvider.get({ id: $stateParams.id }).$promise; }], - mappingFieldsPromise: ['AuthProvider', function (AuthProvider) { return AuthProvider.mapping_fields().$promise; }], - translations: ['Translations', function (Translations) { return Translations.query(['app.admin.authentication_edit', 'app.shared.authentication', 'app.shared.oauth2']).$promise; }] + mappingFieldsPromise: ['AuthProvider', function (AuthProvider) { return AuthProvider.mapping_fields().$promise; }] } }) @@ -1058,8 +969,7 @@ angular.module('application.router', ['ui.router']) }, resolve: { membersPromise: ['Member', function (Member) { return Member.mapping().$promise; }], - statisticsPromise: ['Statistics', function (Statistics) { return Statistics.query().$promise; }], - translations: ['Translations', function (Translations) { return Translations.query('app.admin.statistics').$promise; }] + statisticsPromise: ['Statistics', function (Statistics) { return Statistics.query().$promise; }] } }) .state('app.admin.stats_graphs', { @@ -1069,9 +979,6 @@ angular.module('application.router', ['ui.router']) templateUrl: '<%= asset_path "admin/statistics/graphs.html" %>', controller: 'GraphsController' } - }, - resolve: { - translations: ['Translations', function (Translations) { return Translations.query('app.admin.stats_graphs').$promise; }] } }) @@ -1087,42 +994,23 @@ angular.module('application.router', ['ui.router']) resolve: { settingsPromise: ['Setting', function (Setting) { return Setting.query({ - names: `['twitter_name', \ - 'about_title', \ - 'about_body', \ - 'privacy_body', \ - 'privacy_dpo', \ - 'about_contacts', \ - 'home_blogpost', \ - 'machine_explications_alert', \ - 'training_explications_alert', \ - 'training_information_message', \ - 'subscription_explications_alert', \ - 'event_explications_alert', \ - 'space_explications_alert', \ - 'booking_window_start', \ - 'booking_window_end', \ - 'booking_move_enable', \ - 'booking_move_delay', \ - 'booking_cancel_enable', \ - 'booking_cancel_delay', \ - 'main_color', \ - 'secondary_color', \ - 'fablab_name', \ - 'name_genre', \ - 'reminder_enable', \ - 'reminder_delay', \ - 'visibility_yearly', \ - 'visibility_others', \ - 'display_name_enable', \ - 'machines_sort_by']` }).$promise; + names: `['twitter_name', 'about_title', 'about_body', \ + 'privacy_body', 'privacy_dpo', 'about_contacts', \ + 'home_blogpost', 'machine_explications_alert', 'training_explications_alert', \ + 'training_information_message', 'subscription_explications_alert', 'event_explications_alert', \ + 'space_explications_alert', 'booking_window_start', 'booking_window_end', \ + 'booking_move_enable', 'booking_move_delay', 'booking_cancel_enable', \ + 'booking_cancel_delay', 'main_color', 'secondary_color', \ + 'fablab_name', 'name_genre', 'reminder_enable', \ + 'reminder_delay', 'visibility_yearly', 'visibility_others', \ + 'display_name_enable', 'machines_sort_by', 'fab_analytics', \ + 'link_name', 'home_content', 'home_css']` }).$promise; }], privacyDraftsPromise: ['Setting', function (Setting) { return Setting.get({ name: 'privacy_draft', history: true }).$promise; }], cguFile: ['CustomAsset', function (CustomAsset) { return CustomAsset.get({ name: 'cgu-file' }).$promise; }], cgvFile: ['CustomAsset', function (CustomAsset) { return CustomAsset.get({ name: 'cgv-file' }).$promise; }], faviconFile: ['CustomAsset', function (CustomAsset) { return CustomAsset.get({ name: 'favicon-file' }).$promise; }], - profileImageFile: ['CustomAsset', function (CustomAsset) { return CustomAsset.get({ name: 'profile-image-file' }).$promise; }], - translations: ['Translations', function (Translations) { return Translations.query('app.admin.settings').$promise; }] + profileImageFile: ['CustomAsset', function (CustomAsset) { return CustomAsset.get({ name: 'profile-image-file' }).$promise; }] } }) @@ -1136,8 +1024,7 @@ angular.module('application.router', ['ui.router']) } }, resolve: { - clientsPromise: ['OpenAPIClient', function (OpenAPIClient) { return OpenAPIClient.query().$promise; }], - translations: ['Translations', function (Translations) { return Translations.query('app.admin.open_api_clients').$promise; }] + clientsPromise: ['OpenAPIClient', function (OpenAPIClient) { return OpenAPIClient.query().$promise; }] } }); } diff --git a/app/assets/javascripts/services/auth.js.erb b/app/assets/javascripts/services/auth.js.erb index 041082753..c871465cd 100644 --- a/app/assets/javascripts/services/auth.js.erb +++ b/app/assets/javascripts/services/auth.js.erb @@ -1,17 +1,18 @@ 'use strict'; Application.Services.factory('AuthService', ['Session', 'CSRF', function (Session, CSRF) { - return { - isAuthenticated () { - return (Session.currentUser != null) && (Session.currentUser.id != null); - }, + let service = {}; - isAuthorized (authorizedRoles) { - if (!angular.isArray(authorizedRoles)) { - authorizedRoles = [authorizedRoles]; - } - - return this.isAuthenticated() && (authorizedRoles.indexOf(Session.currentUser.role) !== -1); - } + service.isAuthenticated = function() { + return (Session.currentUser != null) && (Session.currentUser.id != null); }; + + service.isAuthorized = function(authorizedRoles) { + if (!angular.isArray(authorizedRoles)) { + authorizedRoles = [authorizedRoles]; + } + return service.isAuthenticated() && (authorizedRoles.indexOf(Session.currentUser.role) !== -1); + }; + + return service; }]); diff --git a/app/assets/javascripts/services/calendar.js b/app/assets/javascripts/services/calendar.js index c977264c5..25855ff1d 100644 --- a/app/assets/javascripts/services/calendar.js +++ b/app/assets/javascripts/services/calendar.js @@ -17,7 +17,7 @@ Application.Services.factory('CalendarConfig', [() => center: 'title', right: 'today prev,next' }, - firstDay: 1, // Week start on monday (France) + firstDay: Fablab.weekStartingDay, scrollTime: DEFAULT_CALENDAR_POSITION, slotDuration: BASE_SLOT, allDayDefault: false, diff --git a/app/assets/javascripts/services/fab_analytics.js b/app/assets/javascripts/services/fab_analytics.js new file mode 100644 index 000000000..a7254f672 --- /dev/null +++ b/app/assets/javascripts/services/fab_analytics.js @@ -0,0 +1,10 @@ +Application.Services.factory('FabAnalytics', ['$resource', function ($resource) { + return $resource('/api/analytics', + {}, { + data: { + method: 'GET', + url: '/api/analytics/data' + } + } + ); +}]); diff --git a/app/assets/javascripts/services/ical.js b/app/assets/javascripts/services/ical.js new file mode 100644 index 000000000..dec1ed635 --- /dev/null +++ b/app/assets/javascripts/services/ical.js @@ -0,0 +1,5 @@ +'use strict'; + +Application.Services.factory('Ical', ['$resource', function ($resource) { + return $resource('/api/ical/externals'); +}]); diff --git a/app/assets/javascripts/services/icalendar.js b/app/assets/javascripts/services/icalendar.js new file mode 100644 index 000000000..93c790dab --- /dev/null +++ b/app/assets/javascripts/services/icalendar.js @@ -0,0 +1,17 @@ +'use strict'; + +Application.Services.factory('ICalendar', ['$resource', function ($resource) { + return $resource('/api/i_calendar/:id', + { id: '@id' }, { + events: { + method: 'GET', + url: '/api/i_calendar/:id/events' + }, + sync: { + method: 'POST', + url: '/api/i_calendar/:id/sync', + params: { id: '@id' } + } + } + ); +}]); diff --git a/app/assets/javascripts/services/member.js b/app/assets/javascripts/services/member.js index 9d4cd01a9..52eab1157 100644 --- a/app/assets/javascripts/services/member.js +++ b/app/assets/javascripts/services/member.js @@ -1,6 +1,6 @@ 'use strict'; -Application.Services.factory('Member', ['$resource', function ($resource) { +Application.Services.factory('Member', ['$resource', '$q', function ($resource, $q) { return $resource('/api/members/:id', { id: '@id' }, { update: { @@ -30,6 +30,20 @@ Application.Services.factory('Member', ['$resource', function ($resource) { mapping: { method: 'GET', url: '/api/members/mapping' + }, + completeTour: { + method: 'PATCH', + url: '/api/members/:id/complete_tour', + params: { id: '@id' }, + interceptor: { + response: function (response) { + if (Fablab.featureTourDisplay === 'session') { + Fablab.sessionTours.push(response.data.tours[0]); + return { tours: Fablab.sessionTours }; + } + return response.data; + } + } } } ); diff --git a/app/assets/javascripts/services/setting.js b/app/assets/javascripts/services/setting.js index c0b43b645..f97ef9af6 100644 --- a/app/assets/javascripts/services/setting.js +++ b/app/assets/javascripts/services/setting.js @@ -15,6 +15,11 @@ Application.Services.factory('Setting', ['$resource', function ($resource) { }, query: { isArray: false + }, + reset: { + url: '/api/settings/reset/:name', + params: { name: '@name' }, + method: 'PUT' } } ); diff --git a/app/assets/javascripts/services/twitter.js b/app/assets/javascripts/services/twitter.js deleted file mode 100644 index 45b2ae3d4..000000000 --- a/app/assets/javascripts/services/twitter.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -Application.Services.factory('Twitter', ['$resource', function ($resource) { - return $resource('/api/feeds/twitter_timelines'); -}]); diff --git a/app/assets/stylesheets/app.colors.scss b/app/assets/stylesheets/app.colors.scss index c7a66e439..e011b84b7 100644 --- a/app/assets/stylesheets/app.colors.scss +++ b/app/assets/stylesheets/app.colors.scss @@ -12,6 +12,10 @@ .bg-stage { background-color: $violet; } .bg-success { background-color: $brand-success; } .bg-info { background-color: $brand-info; } +.border-machine { border-color: $beige !important; } +.border-space { border-color: $cyan !important; } +.border-formation { border-color: $violet !important; } +.border-event { border-color: $japonica !important; } .bg-black-light { background-color: #424242 !important; } diff --git a/app/assets/stylesheets/app.components.scss b/app/assets/stylesheets/app.components.scss index f477fd3dc..e58909ed6 100644 --- a/app/assets/stylesheets/app.components.scss +++ b/app/assets/stylesheets/app.components.scss @@ -290,6 +290,11 @@ @include border-radius(3px); padding: 5px 10px; } + &.well-disabled { + border-color: $gray-lighter; + background-color: $gray-lighter; + color: $gray-light; + } } .read { @@ -629,3 +634,46 @@ padding: 10px; .help-block.error { color: #ff565d; } + +.disabled { + background-color: $gray-lighter; + color: $gray-light; + + & a { + color: $gray; + } + + .canceled-marker { + float: right; + top: -13px; + position: relative; + color: red; + text-transform: uppercase; + } +} +.calendar-legend-block { + text-align: right; + padding-right: 2em; + + h4 { + font-size: 12px; + font-style: italic; + } + .legends { + display: flex; + flex-direction: row-reverse; + } + .calendar-legend { + border: 1px solid; + border-left: 3px solid; + border-radius: 3px; + font-size: 10px; + padding: 2px; + margin-left: 10px; + display: inline-block; + } +} + +input[type=date].form-control { + line-height: 25px; +} diff --git a/app/assets/stylesheets/app.layout.scss b/app/assets/stylesheets/app.layout.scss index 538fd9833..1c257746b 100644 --- a/app/assets/stylesheets/app.layout.scss +++ b/app/assets/stylesheets/app.layout.scss @@ -101,7 +101,6 @@ } } - body.container{ padding: 0; } @@ -636,4 +635,4 @@ body.container{ position: absolute; left: -4px; } -} \ No newline at end of file +} diff --git a/app/assets/stylesheets/app.nav.scss b/app/assets/stylesheets/app.nav.scss index 89a505b0e..aa831b9ab 100644 --- a/app/assets/stylesheets/app.nav.scss +++ b/app/assets/stylesheets/app.nav.scss @@ -89,6 +89,11 @@ } } } + > li.menu-spacer { + height: 1px; + margin: 6px 80% 6px 5px; + background: linear-gradient(45deg, black, transparent); + } ul{ display: none; } diff --git a/app/assets/stylesheets/app.plugins.scss b/app/assets/stylesheets/app.plugins.scss index af46e9830..8145371a0 100644 --- a/app/assets/stylesheets/app.plugins.scss +++ b/app/assets/stylesheets/app.plugins.scss @@ -1,4 +1,4 @@ -// medium editor placeholder +// medium editor placeholder .medium-editor-placeholder { min-height: 30px; // fix for firefox } @@ -126,6 +126,10 @@ } } +.fc-selected { + box-shadow: 0 6px 10px 0 rgba(0,0,0,0.14),0 1px 18px 0 rgba(0,0,0,0.12),0 3px 5px -1px rgba(0,0,0,0.2); +} + @@ -415,3 +419,19 @@ .slider-nav__item--current { background: #ccc; } + + +// Twitter +#twitter { + ul { + margin: 0px 15px; + padding-left: 0; + li { + display: block; + } + } + + .timePosted { + margin-top: 0; + } +} diff --git a/app/assets/stylesheets/app.utilities.scss b/app/assets/stylesheets/app.utilities.scss index 274545752..ad1d4241c 100644 --- a/app/assets/stylesheets/app.utilities.scss +++ b/app/assets/stylesheets/app.utilities.scss @@ -102,6 +102,7 @@ p, .widget p { .text-italic { font-style: italic; } +.text-left { text-align: left !important; } .text-center { text-align: center; } .text-right { text-align: right; } @@ -379,6 +380,10 @@ p, .widget p { justify-content: center; } +.pointer { + cursor: pointer; +} + @media screen and (min-width: $screen-lg-min) { .b-r-lg {border-right: 1px solid $border-color; } .hide-b-r-lg { border: none !important; } diff --git a/app/assets/stylesheets/application.scss.erb b/app/assets/stylesheets/application.scss.erb index ada57aaa4..96b307415 100644 --- a/app/assets/stylesheets/application.scss.erb +++ b/app/assets/stylesheets/application.scss.erb @@ -14,6 +14,7 @@ *= require summernote/dist/summernote *= require jquery-minicolors/jquery.minicolors.css *= require angular-aside/dist/css/angular-aside + *= require codemirror/lib/codemirror */ @import "app.functions"; @@ -32,11 +33,7 @@ @import "app.buttons"; @import "app.components"; @import "app.plugins"; -@import "modules/invoice"; -@import "modules/signup"; -@import "modules/abuses"; -@import "modules/cookies"; -@import "modules/stripe"; +@import "modules/*"; @import "app.responsive"; diff --git a/app/assets/stylesheets/modules/abuses.scss b/app/assets/stylesheets/modules/abuses.scss index 300ba1922..fbd61f222 100644 --- a/app/assets/stylesheets/modules/abuses.scss +++ b/app/assets/stylesheets/modules/abuses.scss @@ -28,4 +28,4 @@ li.abuse { margin-top: 1em; } } -} \ No newline at end of file +} diff --git a/app/assets/stylesheets/modules/icalendar.scss b/app/assets/stylesheets/modules/icalendar.scss new file mode 100644 index 000000000..d97ed60f7 --- /dev/null +++ b/app/assets/stylesheets/modules/icalendar.scss @@ -0,0 +1,24 @@ +.calendar-form { + margin : 2em; + border: 1px solid #ddd; + border-radius: 3px; + padding: 1em; + + & > .input-group, & > .minicolors { + margin-top: 1em; + } +} + +.calendar-name { + font-weight: 600; + font-style: italic; +} + +.calendar-url { + overflow: hidden; +} + +.external-calendar-legend { + border-left: 3px solid; + border-radius: 3px; +} \ No newline at end of file diff --git a/app/assets/stylesheets/modules/settings.scss b/app/assets/stylesheets/modules/settings.scss new file mode 100644 index 000000000..2536f61d2 --- /dev/null +++ b/app/assets/stylesheets/modules/settings.scss @@ -0,0 +1,63 @@ +.admin-settings { + .home-page-settings { + .home-page-content { + .note-editor { + .note-toolbar .note-btn-group .note-btn .nugget { + font-family: "FontAwesome"; + } + .note-editing-area .note-editable { + #news { + width: 100%; + background-color: #b1b1b1; + color: white; + border: 1px dashed #8f9091; + border-radius: 5px; + text-align: center; + vertical-align: middle; + line-height: 10rem; + font-size: 2em; + } + + #projects { + @extend #news; + line-height: 523px; + } + + #twitter { + @extend #news; + line-height: 162px; + } + + #members { + @extend #news; + line-height: 320px; + } + + #events { + @extend #news; + line-height: 621px; + } + } + } + } + .home-page-style { + .panel { + border: 0; + .panel-heading { + background: none; + + .panel-title { + font-size: 12px; + font-style: italic; + } + } + + .CodeMirror { + border: 1px solid #ddd; + font-size: 12px; + height: 400px; + } + } + } + } +} diff --git a/app/assets/stylesheets/modules/tour.scss b/app/assets/stylesheets/modules/tour.scss new file mode 100644 index 000000000..8e0fdb8fe --- /dev/null +++ b/app/assets/stylesheets/modules/tour.scss @@ -0,0 +1,31 @@ +.ui-tour-backdrop { + background-color: rgba(0, 0, 0, 0.5); + fill: rgba(0, 0, 0, 0.5); + z-index: 10000; +} + + +.ui-tour-popup.popover { + max-width: 414px; +} + +.tour-step-navigation.popover-navigation .btn-group { + display: flex; + flex-direction: row; + justify-content: space-between; + padding: 0 14px; + + .button-placekeeper { + width: 90px; + height: 30px; + background-color: transparent; + } +} + +.shift-right-40 { margin-left: 40px; } + +.shift-left-40 { margin-left: -40px; } +.shift-left-50 { margin-left: -50px; } +.shift-left-80 { margin-left: -80px; } + +.width-350 { width: 350px; } diff --git a/app/assets/templates/admin/abuses/index.html b/app/assets/templates/admin/abuses/index.html index 05be2c867..7335971c3 100644 --- a/app/assets/templates/admin/abuses/index.html +++ b/app/assets/templates/admin/abuses/index.html @@ -7,7 +7,7 @@
-

{{ 'manage_abuses.abuses_list' }}

+

{{ 'app.admin.manage_abuses.abuses_list' }}

@@ -15,27 +15,27 @@
- {{ 'manage_abuses.no_reports' }} + {{ 'app.admin.manage_abuses.no_reports' }}
  • {{abuse.signaled.name}}, - {{ 'manage_abuses.published_by' }} + {{ 'app.admin.manage_abuses.published_by' }} {{abuse.signaled.author.full_name}}, - {{ 'manage_abuses.at_date' }} + {{ 'app.admin.manage_abuses.at_date' }} {{abuse.signaled.published_at | amDateFormat:'L' }}
    - {{ 'manage_abuses.at_date' }} + {{ 'app.admin.manage_abuses.at_date' }} {{abuse.created_at | amDateFormat:'L' }}, {{abuse.first_name}} {{abuse.last_name}} - {{ 'manage_abuses.has_reported' }} + {{ 'app.admin.manage_abuses.has_reported' }} {{ abuse.message }}
-
\ No newline at end of file + diff --git a/app/assets/templates/admin/admins/new.html.erb b/app/assets/templates/admin/admins/new.html.erb index a53506394..4164626ab 100644 --- a/app/assets/templates/admin/admins/new.html.erb +++ b/app/assets/templates/admin/admins/new.html.erb @@ -7,7 +7,7 @@
-

{{ 'add_an_administrator' }}

+

{{ 'app.admin.admins_new.add_an_administrator' }}

@@ -20,12 +20,12 @@
- + '">
diff --git a/app/assets/templates/admin/authentications/_data_mapping.html.erb b/app/assets/templates/admin/authentications/_data_mapping.html.erb index 6fd8376ea..ce9264a01 100644 --- a/app/assets/templates/admin/authentications/_data_mapping.html.erb +++ b/app/assets/templates/admin/authentications/_data_mapping.html.erb @@ -1,16 +1,16 @@ diff --git a/app/assets/templates/admin/authentications/_oauth2.html.erb b/app/assets/templates/admin/authentications/_oauth2.html.erb index 50d0300e8..af830d6a8 100644 --- a/app/assets/templates/admin/authentications/_oauth2.html.erb +++ b/app/assets/templates/admin/authentications/_oauth2.html.erb @@ -1,7 +1,7 @@
- +
- {{ 'common_url_is_required' }} - {{ 'provided_url_is_not_a_valid_url' }} + {{ 'app.shared.oauth2.common_url_is_required' }} + {{ 'app.shared.oauth2.provided_url_is_not_a_valid_url' }}
- +
- {{ 'oauth2_authorization_endpoint_is_required' }} - {{ 'provided_endpoint_is_not_valid' }} + {{ 'app.shared.oauth2.oauth2_authorization_endpoint_is_required' }} + {{ 'app.shared.oauth2.provided_endpoint_is_not_valid' }}
- +
- {{ 'oauth2_token_acquisition_endpoint_is_required' }} - {{ 'provided_endpoint_is_not_valid' }} + {{ 'app.shared.oauth2.oauth2_token_acquisition_endpoint_is_required' }} + {{ 'app.shared.oauth2.provided_endpoint_is_not_valid' }}
- +
- {{ 'profile_edition_url_is_required' }} - {{ 'provided_url_is_not_a_valid_url' }} + {{ 'app.shared.oauth2.profile_edition_url_is_required' }} + {{ 'app.shared.oauth2.provided_url_is_not_a_valid_url' }}
- +
- {{ 'oauth2_client_identifier_is_required' }} {{ 'obtain_it_when_registering_with_your_provider' }} + {{ 'app.shared.oauth2.oauth2_client_identifier_is_required' }} {{ 'obtain_it_when_registering_with_your_provider' }}
- +
- {{ 'oauth2_client_secret_is_required' }} {{ 'obtain_it_when_registering_with_your_provider' }} + {{ 'app.shared.oauth2.oauth2_client_secret_is_required' }} {{ 'obtain_it_when_registering_with_your_provider' }}
- \ No newline at end of file +'"> diff --git a/app/assets/templates/admin/authentications/_oauth2_mapping.html.erb b/app/assets/templates/admin/authentications/_oauth2_mapping.html.erb index 7756608d0..a810ca5c7 100644 --- a/app/assets/templates/admin/authentications/_oauth2_mapping.html.erb +++ b/app/assets/templates/admin/authentications/_oauth2_mapping.html.erb @@ -1,15 +1,15 @@ -

{{ 'define_the_fields_mapping' }}

+

{{ 'app.shared.oauth2.define_the_fields_mapping' }}

- + - - - - - + + + + + @@ -76,4 +76,4 @@ -
{{ 'model' }}{{ 'field' }}{{ 'api_endpoint_url' }}{{ 'api_type' }}{{ 'api_fields' }}{{ 'app.shared.oauth2.model' }}{{ 'app.shared.oauth2.field' }}{{ 'app.shared.oauth2.api_endpoint_url' }}{{ 'app.shared.oauth2.api_type' }}{{ 'app.shared.oauth2.api_fields' }}
\ No newline at end of file + diff --git a/app/assets/templates/admin/authentications/edit.html.erb b/app/assets/templates/admin/authentications/edit.html.erb index 43e282f9d..b57d6a020 100644 --- a/app/assets/templates/admin/authentications/edit.html.erb +++ b/app/assets/templates/admin/authentications/edit.html.erb @@ -9,7 +9,7 @@
-

{{ 'provider' | translate }} {{provider.name}}

+

{{ 'app.admin.authentication_edit.provider' | translate }} {{provider.name}}

@@ -17,7 +17,7 @@
- {{ 'cancel' }} + {{ 'app.shared.buttons.cancel' }}
@@ -35,13 +35,13 @@
- - + '"> + '" ng-if="provider.providable_type == 'OAuth2Provider'">
diff --git a/app/assets/templates/admin/authentications/index.html.erb b/app/assets/templates/admin/authentications/index.html.erb index 4b0a75bc5..d9f542266 100644 --- a/app/assets/templates/admin/authentications/index.html.erb +++ b/app/assets/templates/admin/authentications/index.html.erb @@ -2,23 +2,23 @@
- +
- + - + - + - + - + @@ -31,7 +31,7 @@
{{ 'name' }}{{ 'app.admin.members.authentication_form.name' }}{{ 'strategy_name' }}{{ 'app.admin.members.authentication_form.strategy_name' }}{{ 'type' }}{{ 'app.admin.members.authentication_form.type' }}{{ 'state' }}{{ 'app.admin.members.authentication_form.state' }}
{{ getState(provider.status) }}
-
\ No newline at end of file + diff --git a/app/assets/templates/admin/authentications/new.html.erb b/app/assets/templates/admin/authentications/new.html.erb index bdc47246b..635474d24 100644 --- a/app/assets/templates/admin/authentications/new.html.erb +++ b/app/assets/templates/admin/authentications/new.html.erb @@ -9,7 +9,7 @@
-

{{ 'add_a_new_authentication_provider' }}

+

{{ 'app.admin.authentication_new.add_a_new_authentication_provider' }}

@@ -17,7 +17,7 @@
- {{ 'cancel' }} + {{ 'app.shared.buttons.cancel' }}
@@ -36,13 +36,13 @@
- - + '"> + '" ng-if="provider.providable_type == 'OAuth2Provider'">
diff --git a/app/assets/templates/admin/calendar/calendar.html.erb b/app/assets/templates/admin/calendar/calendar.html.erb index 3b78432b2..fe97354e8 100644 --- a/app/assets/templates/admin/calendar/calendar.html.erb +++ b/app/assets/templates/admin/calendar/calendar.html.erb @@ -5,17 +5,17 @@
-
+
-

{{ 'admin_calendar.calendar_management' }}

+

{{ 'app.admin.calendar.calendar_management' }}

-
-
- {{ 'admin_calendar.trainings' }}
- {{ 'admin_calendar.machines' }}
- {{ 'admin_calendar.spaces' }} + @@ -23,29 +23,44 @@
-
+
'" + ui-tour-use-hotkeys="true" + ui-tour-scroll-parent-id="content-main" + post-render="setupCalendarTour">
+
+

{{ 'app.admin.calendar.legend' }}

+
+ {{ 'app.admin.calendar.trainings' }}
+ {{ 'app.admin.calendar.machines' }}
+ {{ 'app.admin.calendar.spaces' }} + {{ 'app.admin.calendar.events' }} +
+
-
+
-

{{ 'admin_calendar.ongoing_reservations' }}

+

{{ 'app.admin.calendar.ongoing_reservations' }}

    @@ -56,14 +71,14 @@
-
{{ 'admin_calendar.no_reservations' }}
-
{{ 'admin_calendar.reservations_locked' }}
+
{{ 'app.admin.calendar.no_reservations' }}
+
{{ 'app.admin.calendar.reservations_locked' }}
-

{{ 'admin_calendar.machines' }}

+

{{ 'app.admin.calendar.machines' }}

    @@ -75,29 +90,62 @@
-
+
-

{{ 'admin_calendar.actions' }}

+

{{ 'app.admin.calendar.plans' }}

+
    +
  • +
    {{::g.name}}
    +
      +
    • + {{::plan.name}} + +
    • +
    +
  • +
+
+
+ +
+
+

{{ 'app.admin.calendar.actions' }}

+
+
+
-
\ No newline at end of file +
diff --git a/app/assets/templates/admin/calendar/deleteRecurrent.html b/app/assets/templates/admin/calendar/deleteRecurrent.html new file mode 100644 index 000000000..6d26fe67a --- /dev/null +++ b/app/assets/templates/admin/calendar/deleteRecurrent.html @@ -0,0 +1,26 @@ + + + diff --git a/app/assets/templates/admin/calendar/eventModal.html.erb b/app/assets/templates/admin/calendar/eventModal.html.erb index 938b91668..10c3cdfc5 100644 --- a/app/assets/templates/admin/calendar/eventModal.html.erb +++ b/app/assets/templates/admin/calendar/eventModal.html.erb @@ -1,27 +1,27 @@
diff --git a/app/assets/templates/admin/groups/index.html.erb b/app/assets/templates/admin/groups/index.html.erb index 3aaf62f54..2a2ec1cf4 100644 --- a/app/assets/templates/admin/groups/index.html.erb +++ b/app/assets/templates/admin/groups/index.html.erb @@ -1,13 +1,13 @@
@@ -16,7 +16,7 @@ - + @@ -39,11 +39,11 @@
{{ 'group_form.group_name' }}{{ 'app.admin.members.group_form.group_name' }}
- + @@ -91,7 +91,7 @@ - + - + diff --git a/app/assets/templates/admin/invoices/avoirModal.html.erb b/app/assets/templates/admin/invoices/avoirModal.html.erb index 08f9cd531..4e0192886 100644 --- a/app/assets/templates/admin/invoices/avoirModal.html.erb +++ b/app/assets/templates/admin/invoices/avoirModal.html.erb @@ -1,10 +1,10 @@
{{ 'invoices.exportColumns.' + column }}{{ 'app.admin.invoices.exportColumns.' + column }}
- - + + @@ -49,13 +49,13 @@
{{ 'invoices.description' }}{{ 'invoices.price' }}{{ 'app.admin.invoices.description' }}{{ 'app.admin.invoices.price' }}
- -

{{ 'invoices.will_appear_on_the_refund_invoice' }}

+ +

{{ 'app.admin.invoices.will_appear_on_the_refund_invoice' }}

diff --git a/app/assets/templates/admin/invoices/closePeriodModal.html.erb b/app/assets/templates/admin/invoices/closePeriodModal.html.erb index f2ada5808..7c909bd2c 100644 --- a/app/assets/templates/admin/invoices/closePeriodModal.html.erb +++ b/app/assets/templates/admin/invoices/closePeriodModal.html.erb @@ -1,10 +1,10 @@
-

{{ 'invoices.previous_closings' }}

+

{{ 'app.admin.invoices.previous_closings' }}

- - + + @@ -72,10 +72,10 @@
{{ 'invoices.start_date' }}{{ 'invoices.end_date' }}{{ 'app.admin.invoices.start_date' }}{{ 'app.admin.invoices.end_date' }}
-
{{ 'invoices.no_periods'}}
+
{{ 'app.admin.invoices.no_periods'}}
diff --git a/app/assets/templates/admin/invoices/index.html.erb b/app/assets/templates/admin/invoices/index.html.erb index 9ca28e005..58c52f8d5 100644 --- a/app/assets/templates/admin/invoices/index.html.erb +++ b/app/assets/templates/admin/invoices/index.html.erb @@ -5,33 +5,39 @@ -
+
-

{{ 'invoices.invoices' }}

+

{{ 'app.admin.invoices.invoices' }}

- -
+
'" + ui-tour-use-hotkeys="true" + ui-tour-scroll-parent-id="content-main" + post-render="setupInvoicesTour">
- - -

{{ 'invoices.filter_invoices' | translate }}

+ + +

{{ 'app.admin.invoices.filter_invoices' | translate }}

- {{ 'invoices.invoice_num_' }} + {{ 'app.admin.invoices.invoice_num_' }}
@@ -40,7 +46,7 @@
- {{ 'invoices.customer_' }} + {{ 'app.admin.invoices.customer_' }}
@@ -49,7 +55,7 @@
- {{ "invoices.date_" | translate }} + {{ "app.admin.invoices.date_" | translate }}
@@ -60,24 +66,24 @@ @@ -116,211 +122,211 @@ - - -
- -
- {{ 'invoices.john_smith' }} -
{{ 'invoices.john_smith_at_example_com' }}
+ + -
{{ 'invoices.invoice_reference_' | translate }} {{mkReference()}}
-
{{ 'invoices.code_' | translate }} {{invoice.code.model}}
-
{{ 'invoices.code_disabled' }}
-
{{ 'invoices.order_num' | translate }} {{mkNumber()}}
-
{{ 'invoices.invoice_issued_on_DATE_at_TIME' | translate:{DATE:(today | amDateFormat:'L'), TIME:(today | amDateFormat:'LT')} }}
-
- {{ 'invoices.object_reservation_of_john_smith_on_DATE_at_TIME' | translate:{DATE:(inOneWeek | amDateFormat:'L'), TIME:(inOneWeek | amDateFormat:'LT')} }} -
-
- {{ 'invoices.order_summary' | translate }} - - - - - - - - - - - - + + +
+ {{ 'app.admin.invoices.john_smith' }} +
{{ 'app.admin.invoices.john_smith_at_example_com' }}
+
+
{{ 'app.admin.invoices.invoice_reference_' | translate }} {{mkReference()}}
+
{{ 'app.admin.invoices.code_' | translate }} {{invoice.code.model}}
+
{{ 'app.admin.invoices.code_disabled' }}
+
{{ 'app.admin.invoices.order_num' | translate }} {{mkNumber()}}
+
{{ 'app.admin.invoices.invoice_issued_on_DATE_at_TIME' | translate:{DATE:(today | amDateFormat:'L'), TIME:(today | amDateFormat:'LT')} }}
+
+ {{ 'app.admin.invoices.object_reservation_of_john_smith_on_DATE_at_TIME' | translate:{DATE:(inOneWeek | amDateFormat:'L'), TIME:(inOneWeek | amDateFormat:'LT')} }} +
+
+ {{ 'app.admin.invoices.order_summary' | translate }} +
{{ 'invoices.details' }}{{ 'invoices.amount' }}
{{ 'invoices.machine_booking-3D_printer' | translate }} {{inOneWeek | amDateFormat:'LLL'}} - {{inOneWeekAndOneHour | amDateFormat:'LT'}}{{30.0 | currency}}
+ + + + + + + + + + + - - - - - + + + + + - - - - + + + + - - - - - - - - - - - - + + + + + + + + + + + + - -
{{ 'app.admin.invoices.details' }}{{ 'app.admin.invoices.amount' }}
{{ 'app.admin.invoices.machine_booking-3D_printer' | translate }} {{inOneWeek | amDateFormat:'LLL'}} - {{inOneWeekAndOneHour | amDateFormat:'LT'}}{{30.0 | currency}}
{{ 'invoices.total_amount' }}{{ 'invoices.total_including_all_taxes' }}{{30.0 | currency}}
{{ 'app.admin.invoices.total_amount' }}{{ 'app.admin.invoices.total_including_all_taxes' }}{{30.0 | currency}}
{{ 'invoices.VAT_disabled' }}
{{ 'app.admin.invoices.VAT_disabled' }}
{{ 'invoices.including_VAT' | translate }} {{invoice.VAT.rate}} %{{30-(30/(invoice.VAT.rate/100+1)) | currency}}
{{ 'invoices.including_total_excluding_taxes' }}{{30/(invoice.VAT.rate/100+1) | currency}}
{{ 'invoices.including_amount_payed_on_ordering' }}{{30.0 | currency}}
{{ 'app.admin.invoices.including_VAT' | translate }} {{invoice.VAT.rate}} %{{30-(30/(invoice.VAT.rate/100+1)) | currency}}
{{ 'app.admin.invoices.including_total_excluding_taxes' }}{{30/(invoice.VAT.rate/100+1) | currency}}
{{ 'app.admin.invoices.including_amount_payed_on_ordering' }}{{30.0 | currency}}
-

- {{ 'invoices.settlement_by_debit_card_on_DATE_at_TIME_for_an_amount_of_AMOUNT' }} -

-
-
-
-
-
- + + +

+ {{ 'app.admin.invoices.settlement_by_debit_card_on_DATE_at_TIME_for_an_amount_of_AMOUNT' }} +

+
+
+
+
+
+
- +
- - + +
- - + +
- - + +
- - + +
- - + +
- - + +
- - + +
- - + +
- - + +
- - + +
- - + +
- - + +
- - + +
- - + +
- - + +
- - + +
- - + +
- - + +
- - + +
- - + +
- - + +
- +
@@ -333,125 +339,125 @@ @@ -461,33 +467,33 @@ @@ -496,23 +502,23 @@ diff --git a/app/assets/templates/admin/members/_form.html.erb b/app/assets/templates/admin/members/_form.html.erb index a6d9fc7d6..7c311aff5 100644 --- a/app/assets/templates/admin/members/_form.html.erb +++ b/app/assets/templates/admin/members/_form.html.erb @@ -1,18 +1,18 @@
- {{ 'group_is_required' }} + {{ 'app.shared.user_admin.group_is_required' }}
- +
@@ -28,7 +28,7 @@
- +
diff --git a/app/assets/templates/admin/members/administrators.html.erb b/app/assets/templates/admin/members/administrators.html.erb index fa2cc4c79..31ce9c3b0 100644 --- a/app/assets/templates/admin/members/administrators.html.erb +++ b/app/assets/templates/admin/members/administrators.html.erb @@ -2,23 +2,23 @@
- +
\ No newline at end of file +
diff --git a/app/assets/templates/admin/members/edit.html.erb b/app/assets/templates/admin/members/edit.html.erb index 90d8de79a..3897f6f43 100644 --- a/app/assets/templates/admin/members/edit.html.erb +++ b/app/assets/templates/admin/members/edit.html.erb @@ -9,8 +9,8 @@
-

{{ 'user' | translate }} {{ user.name }}

- {{ 'incomplete_profile' }} +

{{ 'app.shared.user_admin.user' | translate }} {{ user.name }}

+ {{ 'app.shared.user_admin.incomplete_profile' }}
@@ -18,7 +18,7 @@
- {{ 'cancel' }} + {{ 'app.shared.buttons.cancel' }}
@@ -34,11 +34,11 @@ - +
- {{ 'warning_incomplete_user_profile_probably_imported_from_sso' }} + {{ 'app.shared.user_admin.warning_incomplete_user_profile_probably_imported_from_sso' }}
@@ -46,21 +46,21 @@
- + '"> - + '">
- +
@@ -68,24 +68,24 @@

{{ subscription.plan | humanReadablePlanName }}

- {{ 'duration' | translate }} {{ subscription.plan.interval | planIntervalFilter: subscription.plan.interval_count }} + {{ 'app.admin.members_edit.duration' | translate }} {{ subscription.plan.interval | planIntervalFilter: subscription.plan.interval_count }}

- {{ 'expires_at' | translate }} {{ subscription.expired_at | amDateFormat: 'L' }} + {{ 'app.admin.members_edit.expires_at' | translate }} {{ subscription.expired_at | amDateFormat: 'L' }}

- {{ 'price_' | translate }} {{ subscription.plan.amount | currency}} + {{ 'app.admin.members_edit.price_' | translate }} {{ subscription.plan.amount | currency}}

- - + +

- {{ 'user_has_no_current_subscription' }} + {{ 'app.admin.members_edit.user_has_no_current_subscription' }}

- +
@@ -93,11 +93,11 @@
- +
-

{{ 'next_trainings' | translate }}

+

{{ 'app.admin.members_edit.next_trainings' | translate }}

    @@ -105,14 +105,14 @@ {{r.reservable.name}} - {{ r.start_at | amDateFormat:'LLL' }} - {{ r.end_at | amDateFormat:'LT' }}
-
{{ 'no_trainings' }}
+
{{ 'app.admin.members_edit.no_trainings' }}
-

{{ 'passed_trainings' | translate }}

+

{{ 'app.admin.members_edit.passed_trainings' | translate }}

    @@ -125,14 +125,14 @@
--> -
{{ 'no_trainings' }}
+
{{ 'app.admin.members_edit.no_trainings' }}
-

{{ 'validated_trainings' | translate }}

+

{{ 'app.admin.members_edit.validated_trainings' | translate }}

    @@ -140,17 +140,17 @@ {{t.name}}
-
{{ 'no_trainings' }}
+
{{ 'app.admin.members_edit.no_trainings' }}
- +
-

{{ 'next_events' | translate }}

+

{{ 'app.admin.members_edit.next_events' | translate }}

    @@ -158,22 +158,22 @@ {{r.reservable.title}} - {{ r.start_at | amDateFormat:'LLL' }} - {{ r.end_at | amDateFormat:'LT' }}
    - {{ 'NUMBER_full_price_tickets_reserved' }} + {{ 'app.admin.members_edit.NUMBER_full_price_tickets_reserved' }}

    - {{ 'NUMBER_NAME_tickets_reserved' }} + {{ 'app.admin.members_edit.NUMBER_NAME_tickets_reserved' }}
-
{{ 'no_upcoming_events' }}
+
{{ 'app.admin.members_edit.no_upcoming_events' }}
-

{{ 'passed_events' | translate }}

+

{{ 'app.admin.members_edit.passed_events' | translate }}

    @@ -181,22 +181,22 @@ {{r.reservable.title}} - {{ r.start_at | amDateFormat:'LLL' }} - {{ r.end_at | amDateFormat:'LT' }}
-
{{ 'no_passed_events' }}
+
{{ 'app.admin.members_edit.no_passed_events' }}
- +
- - - + + + @@ -209,34 +209,34 @@
{{ 'invoice_num' }}{{ 'date' }}{{ 'price' }}{{ 'app.admin.members_edit.invoice_num' }}{{ 'app.admin.members_edit.date' }}{{ 'app.admin.members_edit.price' }}
-

{{ 'no_invoices_for_now' }}

+

{{ 'app.admin.members_edit.no_invoices_for_now' }}

- +
- + '">
- +
- + '">
diff --git a/app/assets/templates/admin/members/import.html.erb b/app/assets/templates/admin/members/import.html.erb index 8ce03827e..d7c46e128 100644 --- a/app/assets/templates/admin/members/import.html.erb +++ b/app/assets/templates/admin/members/import.html.erb @@ -11,14 +11,14 @@
-

{{ 'members_import.import_members' }}

+

{{ 'app.admin.members_import.import_members' }}

@@ -29,7 +29,7 @@

- {{ 'members_import.info' }} + {{ 'app.admin.members_import.info' }}

@@ -37,12 +37,12 @@
-

{{ 'members_import.groups' }}

+

{{ 'app.admin.members_import.groups' }}

- - + + @@ -59,12 +59,12 @@
-

{{ 'members_import.trainings' }}

+

{{ 'app.admin.members_import.trainings' }}

{{ 'members_import.group_name' }}{{ 'members_import.group_identifier' }}{{ 'app.admin.members_import.group_name' }}{{ 'app.admin.members_import.group_identifier' }}
- - + + @@ -86,12 +86,12 @@
-

{{ 'members_import.tags' }}

+

{{ 'app.admin.members_import.tags' }}

{{ 'members_import.training_name' }}{{ 'members_import.training_identifier' }}{{ 'app.admin.members_import.training_name' }}{{ 'app.admin.members_import.training_identifier' }}
- - + + @@ -119,10 +119,10 @@

- {{ 'members_import.required_fields' }} + {{ 'app.admin.members_import.required_fields' }}

- {{ 'members_import.about_example' }} + {{ 'app.admin.members_import.about_example' }}

@@ -130,8 +130,8 @@
{{file.attachment}}
- {{ 'members_import.select_file' }} - {{ 'change' }} + {{ 'app.admin.members_import.select_file' }} + {{ 'app.shared.buttons.change' }}
- {{ 'members_import.update_field' }} + {{ 'app.admin.members_import.update_field' }}
@@ -165,7 +165,7 @@ diff --git a/app/assets/templates/admin/members/import_result.html b/app/assets/templates/admin/members/import_result.html index 16e7ae9e3..231db53c2 100644 --- a/app/assets/templates/admin/members/import_result.html +++ b/app/assets/templates/admin/members/import_result.html @@ -11,7 +11,7 @@
-

{{ 'members_import_result.import_results' }}

+

{{ 'app.admin.members_import_result.import_results' }}

@@ -22,11 +22,11 @@
-

{{ 'members_import_result.import_details' | translate:{DATE:(import.created_at | amDateFormat:'L'), USER:import.user.full_name, ID:import.id} }}

+

{{ 'app.admin.members_import_result.import_details' | translate:{DATE:(import.created_at | amDateFormat:'L'), USER:import.user.full_name, ID:import.id} }}

-

{{ 'members_import_result.pending' }}

+

{{ 'app.admin.members_import_result.pending' }}

-

{{ 'members_import_result.results' }}

+

{{ 'app.admin.members_import_result.results' }}

@@ -41,21 +41,21 @@
- {{ 'members_import_result.status_' + resultRow.status | translate:{ID:resultRow.user} }} + {{ 'app.admin.members_import_result.status_' + resultRow.status | translate:{ID:resultRow.user} }} - {{ 'members_import_result.success' }} + {{ 'app.admin.members_import_result.success' }} - {{ 'members_import_result.failed' }} + {{ 'app.admin.members_import_result.failed' }}
- {{ 'members_import_result.error_details' }}{{resultRow}} + {{ 'app.admin.members_import_result.error_details' }}{{resultRow}}
diff --git a/app/assets/templates/admin/members/index.html.erb b/app/assets/templates/admin/members/index.html.erb index a2bd27f2c..0540e0b58 100644 --- a/app/assets/templates/admin/members/index.html.erb +++ b/app/assets/templates/admin/members/index.html.erb @@ -7,12 +7,12 @@
-

{{ 'users_management' }}

+

{{ 'app.admin.members.users_management' }}

- +
@@ -21,28 +21,34 @@
-
+
'" + ui-tour-use-hotkeys="true" + ui-tour-scroll-parent-id="content-main" + post-render="setupMembersTour">
- + - - + + '"> - - + + '"> - +
- +
- +
diff --git a/app/assets/templates/admin/members/members.html.erb b/app/assets/templates/admin/members/members.html.erb index 8da4bf427..404268bfb 100644 --- a/app/assets/templates/admin/members/members.html.erb +++ b/app/assets/templates/admin/members/members.html.erb @@ -1,37 +1,47 @@
-
-
- - +
+
+
+
+ + +
+
+
+
+
+ +
{{ 'members_import.tag_name' }}{{ 'members_import.tag_identifier' }}{{ 'app.admin.members_import.tag_name' }}{{ 'app.admin.members_import.tag_identifier' }}
+
- - - - - - - + + + + + + + @@ -44,16 +54,19 @@
{{ 'surname' | translate }} {{ 'first_name' | translate }} {{ 'app.admin.members.surname' | translate }} {{ 'app.admin.members.first_name' | translate }}
- - {{ 'incomplete_profile' }} + + {{ 'app.shared.user_admin.incomplete_profile' }}
- +
-
\ No newline at end of file +
diff --git a/app/assets/templates/admin/members/new.html.erb b/app/assets/templates/admin/members/new.html.erb index a4b233c19..4c01fae74 100644 --- a/app/assets/templates/admin/members/new.html.erb +++ b/app/assets/templates/admin/members/new.html.erb @@ -9,7 +9,7 @@
-

{{ 'members_new.add_a_member' }}

+

{{ 'app.admin.members_new.add_a_member' }}

@@ -17,7 +17,7 @@
- {{ 'cancel' }} + {{ 'app.shared.buttons.cancel' }}
@@ -45,19 +45,19 @@ ng-model="user.organization" ng-change="toggleOrganization()" value="false"/> - +
- + '"> - + '"> diff --git a/app/assets/templates/admin/open_api_clients/index.html.erb b/app/assets/templates/admin/open_api_clients/index.html.erb index 8f4e3bf70..07e9764f7 100644 --- a/app/assets/templates/admin/open_api_clients/index.html.erb +++ b/app/assets/templates/admin/open_api_clients/index.html.erb @@ -7,15 +7,15 @@
-

{{ 'open_api_clients' }}

+

{{ 'app.admin.open_api_clients.open_api_clients' }}

-
+
'" + ui-tour-use-hotkeys="true" + ui-tour-scroll-parent-id="content-main" + post-render="setupOpenAPITour">
- +
- +
- - + +
- + - + - + - + @@ -62,11 +68,11 @@
{{ 'name' | translate }} {{ 'app.admin.open_api_clients.name' | translate }} {{ 'calls_count' | translate }} {{ 'app.admin.open_api_clients.calls_count' | translate }} {{ 'token' | translate }}{{ 'app.admin.open_api_clients.token' | translate }}{{ 'created_at' | translate }} {{ 'app.admin.open_api_clients.created_at' | translate }}
+
- {{ 'plan_form.as_part_of_a_partner_subscription_some_notifications_may_be_sent_to_this_user' }} + {{ 'app.shared.plan.as_part_of_a_partner_subscription_some_notifications_may_be_sent_to_this_user' }}
diff --git a/app/assets/templates/admin/plans/edit.html.erb b/app/assets/templates/admin/plans/edit.html.erb index 509b4c626..f06b7a7e6 100644 --- a/app/assets/templates/admin/plans/edit.html.erb +++ b/app/assets/templates/admin/plans/edit.html.erb @@ -7,13 +7,13 @@
-

{{ 'edit_plan.subscription_plan' | translate }} {{ plan.base_name }}

+

{{ 'app.admin.plans.edit.subscription_plan' | translate }} {{ plan.base_name }}

@@ -28,37 +28,37 @@
- + '">
- + - {{ 'plan_form.disable_plan_will_not_unsubscribe_users' }} + {{ 'app.shared.plan.disable_plan_will_not_unsubscribe_users' }}
-

{{ 'edit_plan.prices' }}

+

{{ 'app.admin.plans.edit.prices' }}

- +
-

{{ 'edit_plan.machines' }}

+

{{ 'app.admin.plans.edit.machines' }}

- - + + @@ -75,11 +75,11 @@
{{ 'edit_plan.machine' }}{{ 'edit_plan.hourly_rate' }}{{ 'app.admin.plans.edit.machine' }}{{ 'app.admin.plans.edit.hourly_rate' }}
-

{{ 'edit_plan.spaces' }}

+

{{ 'app.admin.plans.edit.spaces' }}

- - + + @@ -97,7 +97,7 @@
{{ 'edit_plan.space' }}{{ 'edit_plan.hourly_rate' }}{{ 'app.admin.plans.edit.space' }}{{ 'app.admin.plans.edit.hourly_rate' }}
diff --git a/app/assets/templates/admin/plans/new.html.erb b/app/assets/templates/admin/plans/new.html.erb index d1acc9cc6..12ad2d12f 100644 --- a/app/assets/templates/admin/plans/new.html.erb +++ b/app/assets/templates/admin/plans/new.html.erb @@ -7,7 +7,7 @@
-

{{ 'new_plan.add_a_subscription_plan' }}

+

{{ 'app.admin.plans.new.add_a_subscription_plan' }}

@@ -20,10 +20,10 @@
- + '">
diff --git a/app/assets/templates/admin/pricing/coupons.html.erb b/app/assets/templates/admin/pricing/coupons.html.erb index 556a00cbb..b1118e4cb 100644 --- a/app/assets/templates/admin/pricing/coupons.html.erb +++ b/app/assets/templates/admin/pricing/coupons.html.erb @@ -1,15 +1,15 @@ -

{{ 'pricing.list_of_the_coupons' }}

+

{{ 'app.admin.pricing.list_of_the_coupons' }}

@@ -18,10 +18,10 @@ - - - - + + + + @@ -33,7 +33,7 @@ {{coupon.amount_off}} {{currencySymbol}} - +
{{ 'pricing.name' }}{{ 'pricing.discount' }}{{ 'pricing.nb_of_usages' }}{{ 'pricing.status' }}{{ 'app.admin.pricing.name' }}{{ 'app.admin.pricing.discount' }}{{ 'app.admin.pricing.nb_of_usages' }}{{ 'app.admin.pricing.status' }}
{{coupon.usages}}{{'pricing.'+coupon.status}}{{'app.admin.pricing.'+coupon.status}} @@ -44,5 +44,5 @@
- +
diff --git a/app/assets/templates/admin/pricing/credits.html.erb b/app/assets/templates/admin/pricing/credits.html.erb index 07c23d359..0832e9a7a 100644 --- a/app/assets/templates/admin/pricing/credits.html.erb +++ b/app/assets/templates/admin/pricing/credits.html.erb @@ -1,10 +1,10 @@ -

{{ 'pricing.trainings' }}

+

{{ 'app.admin.pricing.trainings' }}

- - - + + + @@ -35,7 +35,7 @@
@@ -43,17 +43,17 @@
{{ 'pricing.subscription' }}{{ 'pricing.credits' }}{{ 'pricing.related_trainings' }}{{ 'app.admin.pricing.subscription' }}{{ 'app.admin.pricing.credits' }}{{ 'app.admin.pricing.related_trainings' }}
-

{{ 'pricing.machines' }}

+

{{ 'app.admin.pricing.machines' }}

- +
- - - + + + @@ -85,10 +85,10 @@
@@ -96,16 +96,16 @@
{{ 'pricing.machine' }}{{ 'pricing.hours' }}{{ 'pricing.related_subscriptions' }}{{ 'app.admin.pricing.machine' }}{{ 'app.admin.pricing.hours' | translate:{DURATION:slotDuration} }}{{ 'app.admin.pricing.related_subscriptions' }}
-

{{ 'pricing.spaces' }}

+

{{ 'app.admin.pricing.spaces' }}

- +
- - - + + + @@ -137,13 +137,13 @@
-
{{ 'pricing.space' }}{{ 'pricing.hours' }}{{ 'pricing.related_subscriptions' }}{{ 'app.admin.pricing.space' }}{{ 'app.admin.pricing.hours' | translate:{DURATION:slotDuration} }}{{ 'app.admin.pricing.related_subscriptions' }}
\ No newline at end of file +
diff --git a/app/assets/templates/admin/pricing/index.html.erb b/app/assets/templates/admin/pricing/index.html.erb index c1aff1cb7..6a84225a4 100644 --- a/app/assets/templates/admin/pricing/index.html.erb +++ b/app/assets/templates/admin/pricing/index.html.erb @@ -7,7 +7,7 @@
-

{{ 'pricing.pricing_management' }}

+

{{ 'app.admin.pricing.pricing_management' }}

@@ -15,34 +15,40 @@
-
+
'" + ui-tour-use-hotkeys="true" + ui-tour-scroll-parent-id="content-main" + post-render="setupPricingTour">
- + - - + + '"> - - + + '"> - - + + '"> - - + + '"> - - + + '"> - - + + '">
diff --git a/app/assets/templates/admin/pricing/machine_hours.html.erb b/app/assets/templates/admin/pricing/machine_hours.html.erb index 636219c63..d8ef5780c 100644 --- a/app/assets/templates/admin/pricing/machine_hours.html.erb +++ b/app/assets/templates/admin/pricing/machine_hours.html.erb @@ -1,10 +1,10 @@
- {{ 'pricing.these_prices_match_machine_hours_rates_' | translate }} {{ 'pricing._without_subscriptions' }}. + {{ 'app.admin.pricing.these_prices_match_machine_hours_rates_' | translate:{DURATION:slotDuration} }} {{ 'app.admin.pricing._without_subscriptions' }}.
- + @@ -24,4 +24,4 @@ -
{{ 'pricing.machines' }}{{ 'app.admin.pricing.machines' }} {{group.name}}
\ No newline at end of file + diff --git a/app/assets/templates/admin/pricing/sendCoupon.html.erb b/app/assets/templates/admin/pricing/sendCoupon.html.erb index fad186cd7..83e63e536 100644 --- a/app/assets/templates/admin/pricing/sendCoupon.html.erb +++ b/app/assets/templates/admin/pricing/sendCoupon.html.erb @@ -1,11 +1,11 @@ \ No newline at end of file + + +
diff --git a/app/assets/templates/admin/pricing/spaces.html.erb b/app/assets/templates/admin/pricing/spaces.html.erb index 25417cb6e..03675b2e3 100644 --- a/app/assets/templates/admin/pricing/spaces.html.erb +++ b/app/assets/templates/admin/pricing/spaces.html.erb @@ -1,10 +1,10 @@
- {{ 'pricing.these_prices_match_space_hours_rates_' | translate }} {{ 'pricing._without_subscriptions' }}. + {{ 'app.admin.pricing.these_prices_match_space_hours_rates_' | translate:{DURATION:slotDuration} }} {{ 'app.admin.pricing._without_subscriptions' }}.
- + @@ -24,4 +24,4 @@ -
{{ 'pricing.spaces' }}{{ 'app.admin.pricing.spaces' }} {{group.name}}
\ No newline at end of file + diff --git a/app/assets/templates/admin/pricing/subscriptions.html.erb b/app/assets/templates/admin/pricing/subscriptions.html.erb index bcb98f4bc..59dd3dc27 100644 --- a/app/assets/templates/admin/pricing/subscriptions.html.erb +++ b/app/assets/templates/admin/pricing/subscriptions.html.erb @@ -1,21 +1,21 @@ -

{{ 'pricing.list_of_the_subscription_plans' }}

+

{{ 'app.admin.pricing.list_of_the_subscription_plans' }}

- {{ '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 }} -
{{ 'pricing.for_safety_reasons_please_dont_create_subscriptions_if_you_dont_want_intend_to_use_them_later' | translate }} + {{ 'app.admin.pricing.beware_the_subscriptions_are_disabled_on_this_application' | translate }} + {{ 'app.admin.pricing.you_can_create_some_but_they_wont_be_available_until_the_project_is_redeployed_by_the_server_manager' | translate }} +
{{ 'app.admin.pricing.for_safety_reasons_please_dont_create_subscriptions_if_you_dont_want_intend_to_use_them_later' | translate }}
-
@@ -24,12 +24,12 @@ - - - - - - + + + + + + diff --git a/app/assets/templates/admin/pricing/trainings.html.erb b/app/assets/templates/admin/pricing/trainings.html.erb index 2dc0544b6..6c0bde16f 100644 --- a/app/assets/templates/admin/pricing/trainings.html.erb +++ b/app/assets/templates/admin/pricing/trainings.html.erb @@ -1,7 +1,7 @@
{{ 'pricing.type' | translate }} {{ 'pricing.name' | translate }} {{ 'pricing.duration' | translate }} {{ 'pricing.group' | translate }} {{ 'pricing.price' | translate }} {{ 'app.admin.pricing.type' | translate }} {{ 'app.admin.pricing.name' | translate }} {{ 'app.admin.pricing.duration' | translate }} {{ 'app.admin.pricing.group' | translate }} {{ 'app.admin.pricing.price' | translate }}
- + @@ -21,4 +21,4 @@ -
{{ 'pricing.trainings' }}{{ 'app.admin.pricing.trainings' }} {{group.name}}
\ No newline at end of file + diff --git a/app/assets/templates/admin/project_elements/index.html.erb b/app/assets/templates/admin/project_elements/index.html.erb index 4b9ce21e5..cb510fe01 100644 --- a/app/assets/templates/admin/project_elements/index.html.erb +++ b/app/assets/templates/admin/project_elements/index.html.erb @@ -7,34 +7,40 @@
-

{{ 'project_elements.projects_elements_management' }}

+

{{ 'app.admin.project_elements.projects_elements_management' }}

-
+
'" + ui-tour-use-hotkeys="true" + ui-tour-scroll-parent-id="content-main" + post-render="setupProjectElementsTour">
- - + + '"> - - + + '"> - - + + '">
-
\ No newline at end of file +
diff --git a/app/assets/templates/admin/project_elements/licences.html.erb b/app/assets/templates/admin/project_elements/licences.html.erb index ac0352d92..afa0f94cf 100644 --- a/app/assets/templates/admin/project_elements/licences.html.erb +++ b/app/assets/templates/admin/project_elements/licences.html.erb @@ -1,10 +1,10 @@ - + - - + + @@ -32,7 +32,7 @@
{{ 'name' }}{{ 'app.admin.project_elements.name' }}
\ No newline at end of file + diff --git a/app/assets/templates/admin/project_elements/materials.html.erb b/app/assets/templates/admin/project_elements/materials.html.erb index 6ea552964..fb96dc881 100644 --- a/app/assets/templates/admin/project_elements/materials.html.erb +++ b/app/assets/templates/admin/project_elements/materials.html.erb @@ -1,9 +1,9 @@ - + - + @@ -26,7 +26,7 @@
{{ 'name' }}{{ 'app.admin.project_elements.name' }}
\ No newline at end of file + diff --git a/app/assets/templates/admin/project_elements/themes.html.erb b/app/assets/templates/admin/project_elements/themes.html.erb index 0650bb470..0583accb1 100644 --- a/app/assets/templates/admin/project_elements/themes.html.erb +++ b/app/assets/templates/admin/project_elements/themes.html.erb @@ -1,9 +1,9 @@ - + - + @@ -26,7 +26,7 @@
{{ 'name' }}{{ 'app.admin.project_elements.name' }}
\ No newline at end of file + diff --git a/app/assets/templates/admin/settings/about.html b/app/assets/templates/admin/settings/about.html index c58a5c440..817db1c11 100644 --- a/app/assets/templates/admin/settings/about.html +++ b/app/assets/templates/admin/settings/about.html @@ -1,36 +1,59 @@
+
+ {{ 'app.admin.settings.link' }} +
+
+
+
+ +
+
+
+ +
+
+ +
+
+
+
+ +
+
+ {{ 'app.admin.settings.content' }} +
-

- {{ 'settings.shift_enter_to_force_carriage_return' | translate }} - +

+ {{ 'app.admin.settings.shift_enter_to_force_carriage_return' | translate }} +
-
- {{ 'settings.drag_and_drop_to_insert_images' | translate }} - + {{ 'app.admin.settings.drag_and_drop_to_insert_images' | translate }} +
-
- {{ 'settings.shift_enter_to_force_carriage_return' | translate }} - + {{ 'app.admin.settings.shift_enter_to_force_carriage_return' | translate }} +
-
\ No newline at end of file + diff --git a/app/assets/templates/admin/settings/analyticsModal.html b/app/assets/templates/admin/settings/analyticsModal.html new file mode 100644 index 000000000..f8f988d5a --- /dev/null +++ b/app/assets/templates/admin/settings/analyticsModal.html @@ -0,0 +1,25 @@ + + + diff --git a/app/assets/templates/admin/settings/general.html b/app/assets/templates/admin/settings/general.html index 2822ad1ba..208dc2b04 100644 --- a/app/assets/templates/admin/settings/general.html +++ b/app/assets/templates/admin/settings/general.html @@ -1,42 +1,42 @@
- {{ 'settings.title' }} + {{ 'app.admin.settings.title' }}
- +
- +
- +
-

{{ 'settings.title_concordance' }}

+

{{ 'app.admin.settings.title_concordance' }}



- +
@@ -45,62 +45,62 @@
- {{ 'settings.customize_information_messages' }} + {{ 'app.admin.settings.customize_information_messages' }}
-

{{ 'settings.message_of_the_machine_booking_page' }}

-
+
- +
-

{{ 'settings.warning_message_of_the_training_booking_page'}}

-
+
- +
-

{{ 'settings.information_message_of_the_training_reservation_page'}}

-
+
- +
-

{{ 'settings.message_of_the_subscriptions_page' }}

-
+
- +
-

{{ 'settings.message_of_the_events_page' }}

-
+
- +
-

{{ 'settings.message_of_the_spaces_page' }}

-
+
- +
@@ -108,25 +108,25 @@
- {{ 'settings.legal_documents'}} + {{ 'app.admin.settings.legal_documents'}}
- {{ 'settings.if_these_documents_are_not_filled_no_consent_about_them_will_be_asked_to_the_user' }} + {{ 'app.admin.settings.if_these_documents_are_not_filled_no_consent_about_them_will_be_asked_to_the_user' }}
- +
{{cgvFile.custom_asset_file_attributes.attachment}}
- {{ 'browse' }} - {{ 'change' }} + {{ 'app.shared.buttons.browse' }} + {{ 'app.shared.buttons.change' }}
- +
- +
{{cguFile.custom_asset_file_attributes.attachment}}
- {{ 'browse' }} - {{ 'change' }} + {{ 'app.shared.buttons.browse' }} + {{ 'app.shared.buttons.change' }}
- +
@@ -167,21 +167,21 @@
- {{ 'settings.customize_the_graphics' }} + {{ 'app.admin.settings.customize_the_graphics' }}
- {{ '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' }}
- {{ 'settings.concerning_the_favicon_it_must_be_at_ICO_format_with_a_size_of_16x16_pixels' }}
+ {{ 'app.admin.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' }}
+ {{ 'app.admin.settings.concerning_the_favicon_it_must_be_at_ICO_format_with_a_size_of_16x16_pixels' }}

- {{ 'settings.remember_to_refresh_the_page_for_the_changes_to_take_effect' }} + {{ 'app.admin.settings.remember_to_refresh_the_page_for_the_changes_to_take_effect' }}
-

{{ 'settings.logo_white_background' }}

+

{{ 'app.admin.settings.logo_white_background' }}

-
+
'" + ui-tour-use-hotkeys="true" + ui-tour-scroll-parent-id="content-main" + post-render="setupSettingsTour">
- + - - + + '"> - - + + '"> - - + + '"> - - + + '"> - - + + '">
diff --git a/app/assets/templates/admin/settings/privacy.html b/app/assets/templates/admin/settings/privacy.html index 0392f86cd..b57df6b55 100644 --- a/app/assets/templates/admin/settings/privacy.html +++ b/app/assets/templates/admin/settings/privacy.html @@ -1,30 +1,59 @@
+
+ {{ 'app.admin.settings.privacy.title' }} +
- + -
- {{ 'settings.drag_and_drop_to_insert_images' | translate }} - + {{ 'app.admin.settings.drag_and_drop_to_insert_images' | translate }} +
-
- {{ 'settings.shift_enter_to_force_carriage_return' | translate }} - + {{ 'app.admin.settings.shift_enter_to_force_carriage_return' | translate }} +
+
+
+ +
+
+ {{ 'app.admin.settings.privacy.analytics.title' }} +
+
+
+
+ + +

+ {{ 'app.admin.settings.privacy.about_analytics' }} + {{ 'app.admin.settings.privacy.read_more' }} +

+ +
+
diff --git a/app/assets/templates/admin/settings/reservations.html b/app/assets/templates/admin/settings/reservations.html index 532931a8b..643d51930 100644 --- a/app/assets/templates/admin/settings/reservations.html +++ b/app/assets/templates/admin/settings/reservations.html @@ -1,30 +1,30 @@
- {{ 'settings.reservations_parameters' }} + {{ 'app.admin.settings.reservations_parameters' }}
-

{{ 'settings.confine_the_booking_agenda' }}

+

{{ 'app.admin.settings.confine_the_booking_agenda' }}

-

{{ 'settings.opening_time' }}

+

{{ 'app.admin.settings.opening_time' }}

- +
-

{{ 'settings.closing_time' }}

+

{{ 'app.admin.settings.closing_time' }}

- +
-

{{ 'settings.max_visibility' }}

+

{{ 'app.admin.settings.max_visibility' }}

- +
@@ -33,10 +33,10 @@
- +
- +
@@ -45,27 +45,27 @@
- +
-

{{ 'settings.ability_for_the_users_to_move_their_reservations' }}

+

{{ 'app.admin.settings.ability_for_the_users_to_move_their_reservations' }}

- + - +
- +
@@ -74,27 +74,27 @@
- +
-

{{ 'settings.ability_for_the_users_to_cancel_their_reservations' }}

+

{{ 'app.admin.settings.ability_for_the_users_to_cancel_their_reservations' }}

- + - +
- +
@@ -103,7 +103,7 @@
- +
@@ -112,27 +112,27 @@
- {{ 'settings.reservations_reminders' }} + {{ 'app.admin.settings.reservations_reminders' }}
-

{{ 'settings.notification_sending_before_the_reservation_occurs' }}

+

{{ 'app.admin.settings.notification_sending_before_the_reservation_occurs' }}

- + - +
- +
@@ -141,10 +141,10 @@
- {{ 'settings.default_value_is_24_hours' | translate }} + {{ 'app.admin.settings.default_value_is_24_hours' | translate }}
- +
@@ -153,22 +153,22 @@
- {{ 'settings.confidentiality' }} + {{ 'app.admin.settings.confidentiality' }}
-

{{ 'settings.display_machine_reservation_user_name' }}

+

{{ 'app.admin.settings.display_machine_reservation_user_name' }}

- + - +
diff --git a/app/assets/templates/admin/settings/save_policy.html b/app/assets/templates/admin/settings/save_policy.html index 16fa3c214..5d608f5c7 100644 --- a/app/assets/templates/admin/settings/save_policy.html +++ b/app/assets/templates/admin/settings/save_policy.html @@ -1,12 +1,12 @@ diff --git a/app/assets/templates/admin/statistics/export.html.erb b/app/assets/templates/admin/statistics/export.html.erb index 93ab32932..3cfc56a9a 100644 --- a/app/assets/templates/admin/statistics/export.html.erb +++ b/app/assets/templates/admin/statistics/export.html.erb @@ -1,16 +1,16 @@ @@ -70,7 +70,7 @@ - + - +
diff --git a/app/assets/templates/admin/statistics/graphs.html.erb b/app/assets/templates/admin/statistics/graphs.html.erb index 6095fab90..3b59cd433 100644 --- a/app/assets/templates/admin/statistics/graphs.html.erb +++ b/app/assets/templates/admin/statistics/graphs.html.erb @@ -7,12 +7,12 @@
-

{{ 'statistics' }}

+

{{ 'app.admin.stats_graphs.statistics' }}

@@ -26,9 +26,9 @@
@@ -36,9 +36,9 @@ - {{ 'from_date' }} + {{ 'app.admin.stats_graphs.from_date' }} {{datePickerStart.selected | amDateFormat:'L'}} - {{ 'to_date' }} + {{ 'app.admin.stats_graphs.to_date' }} {{datePickerEnd.selected | amDateFormat:'L'}} @@ -46,7 +46,7 @@
  • - {{ 'start' }} + {{ 'app.admin.stats_graphs.start' }}
    @@ -69,7 +69,7 @@
  • - {{ 'end' }} + {{ 'app.admin.stats_graphs.end' }}
    @@ -93,7 +93,7 @@
@@ -110,21 +110,21 @@
-

{{ 'top_list_of' | translate}} {{stat.label}}

+

{{ 'app.admin.stats_graphs.top_list_of' | translate}} {{stat.label}}

@@ -144,4 +144,4 @@
-
\ No newline at end of file +
diff --git a/app/assets/templates/admin/statistics/index.html.erb b/app/assets/templates/admin/statistics/index.html.erb index 8246d478c..852b59bce 100644 --- a/app/assets/templates/admin/statistics/index.html.erb +++ b/app/assets/templates/admin/statistics/index.html.erb @@ -7,21 +7,27 @@
-

{{ 'statistics' }}

+

{{ 'app.admin.statistics.statistics' }}

-
+
'" + ui-tour-use-hotkeys="true" + ui-tour-scroll-parent-id="content-main" + post-render="setupStatisticsTour">
@@ -34,27 +40,27 @@ - {{ 'from_age' }} + {{ 'app.admin.statistics.from_age' }} {{agePicker.start}} - {{ 'to_age' }} + {{ 'app.admin.statistics.to_age' }} {{agePicker.end}} - {{ '_years_old' }} + {{ 'app.admin.statistics._years_old' }} - {{ 'age_filter' }} + {{ 'app.admin.statistics.age_filter' }}
    -
  • {{ 'start' }} +
  • {{ 'app.admin.statistics.start' }}
  • -
  • {{ 'end' }} +
  • {{ 'app.admin.statistics.end' }}
    @@ -72,9 +78,9 @@
      -
    • {{ 'criterion' }} +
    • {{ 'app.admin.statistics.criterion' }}
      {{ 'exclude' | translate }} + {{ 'app.admin.statistics.exclude' | translate }}
    • @@ -155,7 +161,7 @@
      @@ -164,9 +170,9 @@ - {{ 'from_date' }} + {{ 'app.admin.statistics.from_date' }} {{datePickerStart.selected | amDateFormat:'L'}} - {{ 'to_date' }} + {{ 'app.admin.statistics.to_date' }} {{datePickerEnd.selected | amDateFormat:'L'}} @@ -174,7 +180,7 @@
      • - {{ 'start' }} + {{ 'app.admin.statistics.start' }}
        @@ -197,7 +203,7 @@
      • - {{ 'end' }} + {{ 'app.admin.statistics.end' }}
        @@ -221,7 +227,7 @@
      @@ -231,25 +237,31 @@
        -
      • {{ 'entries' | translate }} {{totalHits}}
      • -
      • {{ 'revenue_' | translate }} {{sumCA | currency}}
      • -
      • {{ 'average_age' | translate }} {{averageAge}} {{ 'years_old' | translate }}
      • -
      • {{ 'total' | translate }} {{type.active.label}} : {{sumStat}}
      • -
      • {{ custom.field | translate }} {{customAggs[custom.field]}}
      • +
      • {{ 'app.admin.statistics.entries' | translate }} {{totalHits}}
      • +
      • {{ 'app.admin.statistics.revenue_' | translate }} {{sumCA | currency}}
      • +
      • {{ 'app.admin.statistics.average_age' | translate }} {{averageAge}} {{ 'app.admin.statistics.years_old' | translate }}
      • +
      • {{ 'app.admin.statistics.total' | translate }} {{type.active.label}} : {{sumStat}}
      • +
      • {{ customFieldName(custom.field) }} {{customAggs[custom.field]}}
      +
      + + {{ 'app.admin.statistics.create_plans_to_start' }} + {{ 'app.admin.statistics.click_here' }} +
      + - - - - - + + + + + - @@ -283,12 +295,12 @@ {{datum._source[field.key]}} - +
      {{ 'date' }}{{ 'user' }}{{ 'gender' }}{{ 'age' }}{{ 'type' }}{{ 'app.admin.statistics.date' }}{{ 'app.admin.statistics.user' }}{{ 'app.admin.statistics.gender' }}{{ 'app.admin.statistics.age' }}{{ 'app.admin.statistics.type' }} {{type.active.label}} {{field.label}}{{ 'revenue' | translate }} + {{ 'app.admin.statistics.revenue' | translate }} @@ -265,12 +277,12 @@ {{formatDate(datum._source.date)}} {{getUserNameFromId(datum._source.userId)}} - {{ 'deleted_user' }} + {{ 'app.admin.statistics.deleted_user' }} {{formatSex(datum._source.gender)}} - {{datum._source.age}} {{ 'years_old' | translate }} - {{ 'unknown' }} + {{datum._source.age}} {{ 'app.admin.statistics.years_old' | translate }} + {{ 'app.admin.statistics.unknown' }} {{formatSubtype(datum._source.subType)}} {{datum._source.stat}} {{datum._source.ca | currency}}{{ 'unknown' }}{{datum._source.ca | currency}}{{ 'app.admin.statistics.unknown' }}
      - +
      diff --git a/app/assets/templates/admin/subscriptions/create_modal.html.erb b/app/assets/templates/admin/subscriptions/create_modal.html.erb index 7ff44ecdc..3554747eb 100644 --- a/app/assets/templates/admin/subscriptions/create_modal.html.erb +++ b/app/assets/templates/admin/subscriptions/create_modal.html.erb @@ -1,10 +1,10 @@ diff --git a/app/assets/templates/admin/subscriptions/expired_at_modal.html.erb b/app/assets/templates/admin/subscriptions/expired_at_modal.html.erb index fb320d582..6d02cd49f 100644 --- a/app/assets/templates/admin/subscriptions/expired_at_modal.html.erb +++ b/app/assets/templates/admin/subscriptions/expired_at_modal.html.erb @@ -1,20 +1,20 @@
      -

      {{ 'trainings_new.add_a_new_training' }}

      +

      {{ 'app.admin.trainings_new.add_a_new_training' }}

      @@ -22,11 +22,11 @@
      - + '">
      diff --git a/app/assets/templates/admin/trainings/validTrainingModal.html.erb b/app/assets/templates/admin/trainings/validTrainingModal.html.erb index 9cb819f6b..868fd45e9 100644 --- a/app/assets/templates/admin/trainings/validTrainingModal.html.erb +++ b/app/assets/templates/admin/trainings/validTrainingModal.html.erb @@ -1,19 +1,21 @@ diff --git a/app/assets/templates/admin/versions/upgradeModal.html b/app/assets/templates/admin/versions/upgradeModal.html new file mode 100644 index 000000000..7bb6cd973 --- /dev/null +++ b/app/assets/templates/admin/versions/upgradeModal.html @@ -0,0 +1,19 @@ + + + diff --git a/app/assets/templates/calendar/calendar.html.erb b/app/assets/templates/calendar/calendar.html.erb index bc3c16530..b918bbc70 100644 --- a/app/assets/templates/calendar/calendar.html.erb +++ b/app/assets/templates/calendar/calendar.html.erb @@ -7,14 +7,14 @@
      -

      {{ 'calendar.calendar' }}

      +

      {{ 'app.public.calendar.calendar' }}

      @@ -38,10 +38,10 @@ @@ -53,10 +53,10 @@
      diff --git a/app/assets/templates/calendar/filter.html.erb b/app/assets/templates/calendar/filter.html.erb index c275fbeb9..3983289c0 100644 --- a/app/assets/templates/calendar/filter.html.erb +++ b/app/assets/templates/calendar/filter.html.erb @@ -1,6 +1,6 @@
      -

      {{ 'calendar.trainings' }}

      +

      {{ 'app.public.calendar.trainings' }}

      @@ -10,7 +10,7 @@
      -

      {{ 'calendar.machines' }}

      +

      {{ 'app.public.calendar.machines' }}

      @@ -20,7 +20,7 @@
      -

      {{ 'calendar.spaces' }}

      +

      {{ 'app.public.calendar.spaces' }}

      @@ -29,10 +29,21 @@
      -

      {{ 'calendar.events' }}

      +

      {{ 'app.public.calendar.events' }}

      -

      {{ 'calendar.show_unavailables' }}

      +

      {{ 'app.public.calendar.show_unavailables' }}

      +
      +
      +

      {{ 'app.public.calendar.externals' }}

      + +
      + +
      + {{::e.name}} + +
      +
      diff --git a/app/assets/templates/dashboard/events.html.erb b/app/assets/templates/dashboard/events.html.erb index 2c199becf..4880e632c 100644 --- a/app/assets/templates/dashboard/events.html.erb +++ b/app/assets/templates/dashboard/events.html.erb @@ -2,7 +2,7 @@
      - + '">
      @@ -13,7 +13,7 @@
      -

      {{ 'your_next_events' | translate }}

      +

      {{ 'app.logged.dashboard.events.your_next_events' | translate }}

        @@ -23,28 +23,26 @@ {{ r.start_at | amDateFormat:'LLL' }} - {{ r.end_at | amDateFormat:'LT' }}
        - {{ 'NUMBER_normal_places_reserved' }} + translate-values="{NUMBER: r.nb_reserve_places}"> + {{ 'app.logged.dashboard.events.NUMBER_normal_places_reserved' }}
        - {{ 'NUMBER_of_NAME_places_reserved' }} + translate-values="{NUMBER: ticket.booked, NAME: ticket.price_category.name}"> + {{ 'app.logged.dashboard.events.NUMBER_of_NAME_places_reserved' }}
      -
      {{ 'no_events_to_come' }}
      +
      {{ 'app.logged.dashboard.events.no_events_to_come' }}
      -

      {{ 'your_previous_events' | translate }}

      +

      {{ 'app.logged.dashboard.events.your_previous_events' | translate }}

        @@ -52,7 +50,7 @@ {{r.reservable.title}} - {{ r.start_at | amDateFormat:'LLL' }} - {{ r.end_at | amDateFormat:'LT' }}
      -
      {{ 'no_passed_events' }}
      +
      {{ 'app.logged.dashboard.events.no_passed_events' }}
      diff --git a/app/assets/templates/dashboard/invoices.html.erb b/app/assets/templates/dashboard/invoices.html.erb index 5d103fc15..7f81dc5ea 100644 --- a/app/assets/templates/dashboard/invoices.html.erb +++ b/app/assets/templates/dashboard/invoices.html.erb @@ -2,7 +2,7 @@
      - + '">
      @@ -15,9 +15,9 @@ - - - + + + @@ -30,17 +30,17 @@
      {{ 'reference_number' }}{{ 'date' }}{{ 'price' }}{{ 'app.logged.dashboard.invoices.reference_number' }}{{ 'app.logged.dashboard.invoices.date' }}{{ 'app.logged.dashboard.invoices.price' }}
      -

      {{ 'no_invoices_for_now' }}

      +

      {{ 'app.logged.dashboard.invoices.no_invoices_for_now' }}

      diff --git a/app/assets/templates/dashboard/nav.html.erb b/app/assets/templates/dashboard/nav.html.erb index ee1fd3dfb..b839c47f4 100644 --- a/app/assets/templates/dashboard/nav.html.erb +++ b/app/assets/templates/dashboard/nav.html.erb @@ -8,15 +8,15 @@
      diff --git a/app/assets/templates/dashboard/profile.html.erb b/app/assets/templates/dashboard/profile.html.erb index db6c65f32..6610ff6d4 100644 --- a/app/assets/templates/dashboard/profile.html.erb +++ b/app/assets/templates/dashboard/profile.html.erb @@ -2,7 +2,7 @@
      - + '">
      @@ -10,7 +10,7 @@
      - + '">
      -
    \ No newline at end of file +
    diff --git a/app/assets/templates/dashboard/projects.html.erb b/app/assets/templates/dashboard/projects.html.erb index a91df72c8..d6ec103a4 100644 --- a/app/assets/templates/dashboard/projects.html.erb +++ b/app/assets/templates/dashboard/projects.html.erb @@ -2,7 +2,7 @@
    - + '">
    @@ -10,17 +10,17 @@
    -
    {{ 'you_dont_have_any_projects' }}
    +
    {{ 'app.logged.dashboard.projects.you_dont_have_any_projects' }}

    {{project.name}}

    - {{project.author_id == currentUser.id ? 'author' : 'collaborator' | translate}} - {{ 'rough_draft' }} + {{project.author_id == currentUser.id ? 'app.logged.dashboard.projects.author' : 'app.logged.dashboard.projects.collaborator' | translate}} + {{ 'app.logged.dashboard.projects.rough_draft' }}
    @@ -28,7 +28,7 @@
    -

    {{ 'description' }}

    +

    {{ 'app.logged.dashboard.projects.description' }}

    @@ -39,14 +39,14 @@
    -

    {{ 'machines_and_materials' }}

    +

    {{ 'app.logged.dashboard.projects.machines_and_materials' }}

    -

    {{ 'machines' | translate }} :

    +

    {{ 'app.logged.dashboard.projects.machines' | translate }} :

    • {{m.name}}
    -

    {{ 'materials' | translate }} :

    +

    {{ 'app.logged.dashboard.projects.materials' | translate }} :

    • {{c.name}}
    @@ -57,7 +57,7 @@
    -

    {{ 'collaborators' }}

    +

    {{ 'app.logged.dashboard.projects.collaborators' }}

  • diff --git a/app/assets/templates/dashboard/settings.html.erb b/app/assets/templates/dashboard/settings.html.erb index 86bf6593f..7225a723a 100644 --- a/app/assets/templates/dashboard/settings.html.erb +++ b/app/assets/templates/dashboard/settings.html.erb @@ -1,6 +1,6 @@
    - + '"> @@ -13,11 +13,11 @@
    {{user.name}}
    {{user.email}}
    -
    {{ 'edit_profile.last_activity_on_' | translate }} {{user.last_sign_in_at | amDateFormat: 'LL'}}
    +
    {{ 'app.logged.dashboard.settings.last_activity_on_' | translate:{DATE:(user.last_sign_in_at | amDateFormat: 'LL')} }}
    -

    {{ 'edit_profile.group' }}

    +

    {{ 'app.logged.dashboard.settings.group' }}

    {{getUserGroup().name}} @@ -26,7 +26,7 @@ ng-click="group.change = !group.change" ng-hide="user.subscribed_plan.name || user.role === 'admin'" translate> - {{ 'edit_profile.i_want_to_change_group' }} + {{ 'app.logged.dashboard.settings.i_want_to_change_group' }}
    @@ -35,57 +35,57 @@
    -

    {{ 'edit_profile.subscription' }}

    +

    {{ 'app.logged.dashboard.settings.subscription' }}

    {{ user.subscribed_plan | humanReadablePlanName }} -
    {{ 'edit_profile.your_subscription_expires_on_' | translate }} {{user.subscription.expired_at | amDateFormat: 'LL'}}
    +
    {{ 'app.logged.dashboard.settings.your_subscription_expires_on_' | translate }} {{user.subscription.expired_at | amDateFormat: 'LL'}}
    -
    {{ 'edit_profile.no_subscriptions' | translate }}
    {{ 'edit_profile.i_want_to_subscribe' }}
    +
    {{ 'app.logged.dashboard.settings.no_subscriptions' | translate }}
    {{ 'app.logged.dashboard.settings.i_want_to_subscribe' }}
    -

    {{ 'edit_profile.trainings' }}

    +

    {{ 'app.logged.dashboard.settings.trainings' }}

    • - {{r.reservable.name}} - {{ 'edit_profile.to_come' | translate }} + {{r.reservable.name}} - {{ 'app.logged.dashboard.settings.to_come' | translate }}
    • - {{t.name}} - {{ 'edit_profile.approved' | translate }} + {{t.name}} - {{ 'app.logged.dashboard.settings.approved' | translate }}
    -
    {{ 'edit_profile.no_trainings' }}
    +
    {{ 'app.logged.dashboard.settings.no_trainings' }}
    -

    {{ 'edit_profile.projects' }}

    +

    {{ 'app.logged.dashboard.settings.projects' }}

    • {{p.name}}
    -
    {{ 'edit_profile.no_projects' }}
    +
    {{ 'app.logged.dashboard.settings.no_projects' }}
    -

    {{ 'edit_profile.labels' }}

    +

    {{ 'app.logged.dashboard.settings.labels' }}

    {{t.name}} -
    {{ 'edit_profile.no_labels' }}
    +
    {{ 'app.logged.dashboard.settings.no_labels' }}
    -

    {{ 'edit_profile.cookies' }}

    -
    {{ 'edit_profile.cookies_accepted' }}
    -
    {{ 'edit_profile.cookies_declined' }}
    -
    {{ 'edit_profile.cookies_unset' }}
    - +

    {{ 'app.logged.dashboard.settings.cookies' }}

    +
    {{ 'app.logged.dashboard.settings.cookies_accepted' }}
    +
    {{ 'app.logged.dashboard.settings.cookies_declined' }}
    +
    {{ 'app.logged.dashboard.settings.cookies_unset' }}
    +
    - +
@@ -94,7 +94,7 @@
-

{{ 'edit_profile.edit_my_profile' }}

+

{{ 'app.logged.dashboard.settings.edit_my_profile' }}

@@ -108,25 +108,25 @@
- {{ 'edit_profile.change_my_data' | translate }} + {{ 'app.logged.dashboard.settings.change_my_data' | translate }} -

{{ 'edit_profile.once_your_data_are_up_to_date_' | translate }} {{ 'edit_profile._click_on_the_synchronization_button_opposite_' }} {{ 'edit_profile.or' | translate}} {{ 'edit_profile._disconnect_then_reconnect_' }} {{ 'edit_profile._for_your_changes_to_take_effect' | translate }}

+

{{ 'app.logged.dashboard.settings.once_your_data_are_up_to_date_' | translate }} {{ 'app.logged.dashboard.settings._click_on_the_synchronization_button_opposite_' }} {{ 'app.logged.dashboard.settings.or' | translate}} {{ 'app.logged.dashboard.settings._disconnect_then_reconnect_' }} {{ 'app.logged.dashboard.settings._for_your_changes_to_take_effect' | translate }}

- + '">
diff --git a/app/assets/templates/dashboard/trainings.html.erb b/app/assets/templates/dashboard/trainings.html.erb index e7282172d..de757fff1 100644 --- a/app/assets/templates/dashboard/trainings.html.erb +++ b/app/assets/templates/dashboard/trainings.html.erb @@ -2,7 +2,7 @@
- + '">
@@ -13,7 +13,7 @@
-

{{ 'your_next_trainings' | translate }}

+

{{ 'app.logged.dashboard.trainings.your_next_trainings' | translate }}

    @@ -21,14 +21,14 @@ {{r.reservable.name}} - {{ r.start_at | amDateFormat:'LLL' }} - {{ r.end_at | amDateFormat:'LT' }}
-
{{ 'no_trainings' }}
+
{{ 'app.logged.dashboard.trainings.no_trainings' }}
-

{{ 'your_previous_trainings' | translate }}

+

{{ 'app.logged.dashboard.trainings.your_previous_trainings' | translate }}

    @@ -36,14 +36,14 @@ {{r.reservable.name}} - {{ r.start_at | amDateFormat:'LLL' }} - {{ r.end_at | amDateFormat:'LT' }}
-
{{ 'no_trainings' }}
+
{{ 'app.logged.dashboard.trainings.no_trainings' }}
-

{{ 'your_approved_trainings' | translate }}

+

{{ 'app.logged.dashboard.trainings.your_approved_trainings' | translate }}

    @@ -51,7 +51,7 @@ {{t.name}}
-
{{ 'no_trainings' }}
+
{{ 'app.logged.dashboard.trainings.no_trainings' }}
diff --git a/app/assets/templates/dashboard/wallet.html.erb b/app/assets/templates/dashboard/wallet.html.erb index 175504c64..3eb89eb71 100644 --- a/app/assets/templates/dashboard/wallet.html.erb +++ b/app/assets/templates/dashboard/wallet.html.erb @@ -2,7 +2,7 @@
- + '">
@@ -10,11 +10,11 @@
- + '">
- + '">
diff --git a/app/assets/templates/events/_form.html.erb b/app/assets/templates/events/_form.html.erb index 37f8b5aee..049163ed7 100644 --- a/app/assets/templates/events/_form.html.erb +++ b/app/assets/templates/events/_form.html.erb @@ -10,16 +10,16 @@
- +
- {{ 'title_is_required' }} + {{ 'app.shared.event.title_is_required' }}
- +
@@ -29,7 +29,7 @@
- {{ 'choose_a_picture' | translate }} {{ 'change' }} + {{ 'app.shared.event.choose_a_picture' | translate }} {{ 'app.shared.buttons.change' }}
@@ -38,7 +38,7 @@
- +
- {{ 'description_is_required' }} + {{ 'app.shared.event.description_is_required' }}
- +
@@ -65,23 +65,25 @@
{{file.attachment}}
- {{ 'browse' }} - {{ 'change' }} + {{ 'app.shared.buttons.browse' }} + {{ 'app.shared.buttons.change' }}
- {{ 'add_a_new_file' | translate }} + {{ 'app.shared.event.add_a_new_file' | translate }}
+
@@ -92,7 +94,7 @@
-

{{ 'event_type' }} *

+

{{ 'app.shared.event.event_type' }} *

@@ -109,7 +111,7 @@
-

{{ 'event_theme' }}

+

{{ 'app.shared.event.event_theme' }}

@@ -127,7 +129,7 @@
-

{{ 'age_range' }}

+

{{ 'app.shared.event.age_range' }}

@@ -143,23 +145,23 @@
-

{{ 'dates_and_opening_hours' }}

+

{{ 'app.shared.event.dates_and_opening_hours' }}

- +
- +
- +
- +
- +
@@ -210,12 +212,12 @@
- +
- {{ '_and_ends_on' | translate }} + {{ 'app.shared.event._and_ends_on' | translate }}
-

{{ 'prices_and_availabilities' }}

+

{{ 'app.shared.event.prices_and_availabilities' }}

- +
{{currencySymbol}}
- {{ '0_equal_free' }} + {{ 'app.shared.event.0_equal_free' }}
@@ -278,7 +280,7 @@
- +
diff --git a/app/assets/templates/events/deleteRecurrent.html b/app/assets/templates/events/deleteRecurrent.html new file mode 100644 index 000000000..9ed1a1847 --- /dev/null +++ b/app/assets/templates/events/deleteRecurrent.html @@ -0,0 +1,26 @@ + + + diff --git a/app/assets/templates/events/edit.html.erb b/app/assets/templates/events/edit.html.erb index 85b9b162e..9e902c0fc 100644 --- a/app/assets/templates/events/edit.html.erb +++ b/app/assets/templates/events/edit.html.erb @@ -9,16 +9,16 @@
-

{{ 'edit_the_event' }}

+

{{ 'app.admin.events_edit.edit_the_event' }}

-
+ - + '">
diff --git a/app/assets/templates/events/editRecurrent.html b/app/assets/templates/events/editRecurrent.html new file mode 100644 index 000000000..12a67707d --- /dev/null +++ b/app/assets/templates/events/editRecurrent.html @@ -0,0 +1,26 @@ + + + diff --git a/app/assets/templates/events/index.html.erb b/app/assets/templates/events/index.html.erb index 49b85f4a3..84fc67b1f 100644 --- a/app/assets/templates/events/index.html.erb +++ b/app/assets/templates/events/index.html.erb @@ -7,13 +7,13 @@
-

{{ 'the_fablab_s_events' }}

+

{{ 'app.public.events_list.the_fablab_s_events' }}

@@ -23,19 +23,19 @@
@@ -53,10 +53,10 @@
{{event.category.name}}

{{event.title}}

{{event.start_date | amDateFormat:'L'}}

-

{{event.start_date | amDateFormat:'L'}} {{ 'to_date' }} {{event.end_date | amDateFormat:'L'}}

+

{{event.start_date | amDateFormat:'L'}} {{ 'app.public.events_list.to_date' }} {{event.end_date | amDateFormat:'L'}}

-
{{ 'free_admission' }}
-
{{ 'full_price_' | translate }} {{event.amount | currency}} / {{ price.category.name }} {{price.amount | currency}}
+
{{ 'app.public.events_list.free_admission' }}
+
{{ 'app.public.events_list.full_price_' | translate }} {{event.amount | currency}} / {{ price.category.name }} {{price.amount | currency}}
{{event.event_themes[0].name}} @@ -64,10 +64,10 @@
- {{event.nb_free_places}} {{ 'still_available' | translate }} - {{ 'sold_out' }} - {{ 'cancelled' }} - {{ 'free_entry' }} + {{event.nb_free_places}} {{ 'app.public.events_list.still_available' | translate }} + {{ 'app.public.events_list.sold_out' }} + {{ 'app.public.events_list.cancelled' }} + {{ 'app.public.events_list.free_entry' }}
@@ -86,7 +86,7 @@ diff --git a/app/assets/templates/events/modify_event_reservation_modal.html.erb b/app/assets/templates/events/modify_event_reservation_modal.html.erb index a8adc6fbc..f7adf7770 100644 --- a/app/assets/templates/events/modify_event_reservation_modal.html.erb +++ b/app/assets/templates/events/modify_event_reservation_modal.html.erb @@ -1,14 +1,14 @@ diff --git a/app/assets/templates/events/new.html.erb b/app/assets/templates/events/new.html.erb index 4bf8351e4..7b1d0ac18 100644 --- a/app/assets/templates/events/new.html.erb +++ b/app/assets/templates/events/new.html.erb @@ -9,7 +9,7 @@
-

{{ 'add_an_event' }}

+

{{ 'app.admin.events_new.add_an_event' }}

@@ -18,7 +18,7 @@
- + '">
diff --git a/app/assets/templates/events/show.html.erb b/app/assets/templates/events/show.html.erb index b0351a0aa..b8174107a 100644 --- a/app/assets/templates/events/show.html.erb +++ b/app/assets/templates/events/show.html.erb @@ -17,7 +17,7 @@ -

{{ 'event_description' }}

+

{{ 'app.public.events_show.event_description' }}

@@ -52,7 +52,7 @@
{{event.event_files_attributes.length}} -

{{ 'downloadable_documents' }}

+

{{ 'app.public.events_show.downloadable_documents' }}

    @@ -68,7 +68,7 @@
    -

    {{ 'information_and_booking' }}

    +

    {{ 'app.public.events_show.information_and_booking' }}

    @@ -79,15 +79,15 @@
    {{event.age_range.name}}
    -
    {{ 'dates' | translate }}
    -
    {{ 'beginning' | translate }} {{event.start_date | amDateFormat:'L'}}
    {{ 'ending' | translate }} {{event.end_date | amDateFormat:'L'}}
    -
    {{ 'opening_hours' | translate }}
    -
    {{ 'all_day' }}
    -
    {{ 'from_time' | translate }} {{event.start_date | amDateFormat:'LT'}} {{ 'to_time' | translate }} {{event.end_date | amDateFormat:'LT'}}
    +
    {{ 'app.public.events_show.dates' | translate }}
    +
    {{ 'app.public.events_show.beginning' | translate }} {{event.start_date | amDateFormat:'L'}}
    {{ 'app.public.events_show.ending' | translate }} {{event.end_date | amDateFormat:'L'}}
    +
    {{ 'app.public.events_show.opening_hours' | translate }}
    +
    {{ 'app.public.events_show.all_day' }}
    +
    {{ 'app.public.events_show.from_time' | translate }} {{event.start_date | amDateFormat:'LT'}} {{ 'app.public.events_show.to_time' | translate }} {{event.end_date | amDateFormat:'LT'}}
    -
    {{ 'full_price_' | translate }} {{ event.amount | currency}}
    +
    {{ 'app.public.events_show.full_price_' | translate }} {{ event.amount | currency}}
    {{price.category.name}} : @@ -97,12 +97,12 @@
    -
    {{ 'tickets_still_availables' | translate }} {{event.nb_free_places}}
    -
    {{ 'sold_out' }}
    -
    {{ 'cancelled' }}
    +
    {{ 'app.public.events_show.tickets_still_availables' | translate }} {{event.nb_free_places}}
    +
    {{ 'app.public.events_show.sold_out' }}
    +
    {{ 'app.public.events_show.cancelled' }}
    -
    {{ 'free_entry' }}
    +
    {{ 'app.public.events_show.free_entry' }}
    @@ -112,29 +112,29 @@
    - +
    {{ 'ticket' | translate:{NUMBER:reserve.nbReservePlaces}:"messageformat" }} + {{ 'app.public.events_show.ticket' | translate:{NUMBER:reserve.nbReservePlaces} }}
    {{ 'ticket' | translate:{NUMBER:reserve.tickets[price.id]}:"messageformat" }} + {{ 'app.public.events_show.ticket' | translate:{NUMBER:reserve.tickets[price.id]} }}
    - + @@ -145,36 +145,49 @@
    -
    {{ 'thank_you_your_payment_has_been_successfully_registered' | translate }}
    - {{ 'you_can_find_your_reservation_s_details_on_your_' | translate }} {{ 'dashboard' }} +
    {{ 'app.public.events_show.thank_you_your_payment_has_been_successfully_registered' | translate }}
    + {{ 'app.public.events_show.you_can_find_your_reservation_s_details_on_your_' | translate }} {{ 'app.public.events_show.dashboard' }}
    -
    -
    {{ 'you_booked_DATE' | translate:{DATE:(reservation.created_at | amDateFormat:'L LT')} }}
    -
    {{ 'full_price_' | translate }} {{reservation.nb_reserve_places}} {{ 'ticket' | translate:{NUMBER:reservation.nb_reserve_places}:"messageformat" }}
    -
    - {{ticket.event_price_category.price_category.name}} : {{ticket.booked}} {{ 'ticket' | translate:{NUMBER:ticket.booked}:"messageformat" }} +
    +
    +
    {{ 'app.public.events_show.you_booked_DATE' | translate:{DATE:(reservation.created_at | amDateFormat:'L LT')} }}
    +
    {{ 'app.public.events_show.full_price_' | translate }} {{reservation.nb_reserve_places}} {{ 'app.public.events_show.ticket' | translate:{NUMBER:reservation.nb_reserve_places} }}
    +
    + {{ticket.event_price_category.price_category.name}} : {{ticket.booked}} {{ 'app.public.events_show.ticket' | translate:{NUMBER:ticket.booked} }} +
    + +
    -
    - {{ 'change' }} +
    +
    {{ 'app.public.events_show.canceled_reservation_SEATS' | translate:{SEATS:reservation.total_booked_seats} }}
    - +
    + {{ 'app.public.events_show.event_is_over' }} + {{ 'app.public.events_show.thanks_for_coming' }} + {{ 'app.public.events_show.view_event_list' }} +
    +
    diff --git a/app/assets/templates/home.html.erb b/app/assets/templates/home.html.erb index f11b15937..6e758acf7 100644 --- a/app/assets/templates/home.html.erb +++ b/app/assets/templates/home.html.erb @@ -1,148 +1,9 @@ - -
    - -
    - -
    -
    -

    {{ 'latest_documented_projects' }}

    - - - - - - - -
    - - -
    - -
    -
    - -

    {{ 'latest_tweets' }}

    -
    - -
      -
    • -
    • -
    - -
    - -
    -
    - -

    {{ 'latest_registered_members' }}

    -
    - -
    - - - -
    -
    - -
    - -
    - -
    - - - -
    - - - -
    - -
    -

    {{ 'fablab_s_next_events' | translate }} {{ 'every_events' | translate }}

    - -
    - -
    - - -
    -
    - -
    -
    -
    -
    -

    {{event.title}}

    -
    -
    - {{event.category.name}} -
    -
    -

    - -
    -
    -
    - -
    {{ 'from_date_to_date' | translate:{START:(event.start_date | amDateFormat:'L'), END:(event.end_date | amDateFormat:'L')} }}
    -
    {{ 'on_the_date' | translate:{DATE:(event.start_date | amDateFormat:'L')} }}
    -
    -
    - -
    - {{ 'all_day' }} - {{ 'from_time_to_time' | translate:{START:(event.start_date | amDateFormat:'LT'), END:(event.end_date | amDateFormat:'LT')} }} -
    -
    -
    - -
    -
    - -
    - {{ 'still_available' | translate }} {{event.nb_free_places}} - {{ 'free_entry' }} - {{ 'event_full' }} -
    -
    -
    - -
    - {{ 'free_admission' }} - {{ 'full_price' | translate }} {{event.amount | currency}} -
    -
    -
    - -
    -
    {{ 'consult' }}
    -
    -
    -
    -
    -
    -
    - +
    '" + ui-tour-use-hotkeys="true" + ui-tour-scroll-parent-id="content-main" + post-render="setupHomeTour"> +
    +
    diff --git a/app/assets/templates/home/events.html b/app/assets/templates/home/events.html new file mode 100644 index 000000000..4f315be44 --- /dev/null +++ b/app/assets/templates/home/events.html @@ -0,0 +1,65 @@ +
    +

    {{ 'app.public.home.fablab_s_next_events' | translate }} {{ 'app.public.home.every_events' | translate }}

    + +
    + +
    + + +
    +
    + +
    +
    +
    +
    +

    {{event.title}}

    +
    +
    + {{event.category.name}} +
    +
    +

    + +
    +
    +
    + +
    {{ 'app.public.home.from_date_to_date' | translate:{START:(event.start_date | amDateFormat:'L'), END:(event.end_date | amDateFormat:'L')} }}
    +
    {{ 'app.public.home.on_the_date' | translate:{DATE:(event.start_date | amDateFormat:'L')} }}
    +
    +
    + +
    + {{ 'app.public.home.all_day' }} + {{ 'app.public.home.from_time_to_time' | translate:{START:(event.start_date | amDateFormat:'LT'), END:(event.end_date | amDateFormat:'LT')} }} +
    +
    +
    + +
    +
    + +
    + {{ 'app.public.home.still_available' | translate }} {{event.nb_free_places}} + {{ 'app.public.home.free_entry' }} + {{ 'app.public.home.event_full' }} +
    +
    +
    + +
    + {{ 'app.public.home.free_admission' }} + {{ 'app.public.home.full_price' | translate }} {{event.amount | currency}} +
    +
    +
    + +
    +
    {{ 'app.shared.buttons.consult' }}
    +
    +
    +
    +
    +
    +
    diff --git a/app/assets/templates/home/members.html b/app/assets/templates/home/members.html new file mode 100644 index 000000000..de16e1254 --- /dev/null +++ b/app/assets/templates/home/members.html @@ -0,0 +1,27 @@ +
    +
    +

    {{ 'app.public.home.latest_registered_members' }}

    +
    + +
    + + +
    +
    + +
    + +
    + +
    +
    diff --git a/app/assets/templates/home/news.html b/app/assets/templates/home/news.html new file mode 100644 index 000000000..ba5c3fb4e --- /dev/null +++ b/app/assets/templates/home/news.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/app/assets/templates/home/projects.html b/app/assets/templates/home/projects.html new file mode 100644 index 000000000..1dce03281 --- /dev/null +++ b/app/assets/templates/home/projects.html @@ -0,0 +1,11 @@ +
    +

    {{ 'app.public.home.latest_documented_projects' }}

    + + + + + + +
    diff --git a/app/assets/templates/home/twitter.html b/app/assets/templates/home/twitter.html new file mode 100644 index 000000000..0fead3f86 --- /dev/null +++ b/app/assets/templates/home/twitter.html @@ -0,0 +1,14 @@ +
    +
    + +

    {{ 'app.public.home.latest_tweets' }}

    +
    +
    +
    diff --git a/app/assets/templates/machines/_form.html.erb b/app/assets/templates/machines/_form.html.erb index 02cb5a1f1..ebe7d18f8 100644 --- a/app/assets/templates/machines/_form.html.erb +++ b/app/assets/templates/machines/_form.html.erb @@ -15,21 +15,21 @@ {{alert.msg}}
    - +
    - {{ 'machine_form.name_is_required' }} + {{ 'app.shared.machine.name_is_required' }}
    - +
    @@ -40,8 +40,8 @@
    - {{ 'machine_form.add_an_illustration' | translate }} - {{ 'change' }} + {{ 'app.shared.machine.add_an_illustration' | translate }} + {{ 'app.shared.buttons.change' }} - {{ 'delete' }} + {{ 'app.shared.buttons.delete' }}
    -
    - +
    - {{ 'machine_form.description_is_required' }} + {{ 'app.shared.machine.description_is_required' }}
    - +
    - {{ 'machine_form.technical_specifications_are_required' }} + {{ 'app.shared.machine.technical_specifications_are_required' }}
    - +
    @@ -101,19 +100,19 @@
    {{file.attachment}}
    - {{ 'machine_form.attach_a_file' }} - {{ 'change' }} + {{ 'app.shared.machine.attach_a_file' }} + {{ 'app.shared.buttons.change' }}
    - {{ 'machine_form.add_an_attachment' | translate }} + {{ 'app.shared.machine.add_an_attachment' | translate }}
    @@ -132,7 +131,7 @@ diff --git a/app/assets/templates/machines/edit.html.erb b/app/assets/templates/machines/edit.html.erb index 1f374a689..66ef5339a 100644 --- a/app/assets/templates/machines/edit.html.erb +++ b/app/assets/templates/machines/edit.html.erb @@ -15,7 +15,7 @@
    -
    {{ 'cancel' }}
    +
    {{ 'app.shared.buttons.cancel' }}
    @@ -24,7 +24,7 @@
    - + '">
diff --git a/app/assets/templates/machines/index.html.erb b/app/assets/templates/machines/index.html.erb index 112f4dbdb..52c0c5d11 100644 --- a/app/assets/templates/machines/index.html.erb +++ b/app/assets/templates/machines/index.html.erb @@ -7,29 +7,42 @@
-

{{ 'machines_list.the_fablab_s_machines' }}

+

{{ 'app.public.machines_list.the_fablab_s_machines' }}

-
+
'" + ui-tour-use-hotkeys="true" + ui-tour-scroll-parent-id="content-main" + post-render="setupMachinesTour"> -
-
+
+
+
+ +
@@ -48,13 +61,13 @@
- +
-
+
- +
diff --git a/app/assets/templates/machines/new.html.erb b/app/assets/templates/machines/new.html.erb index a4ce973c8..911668277 100644 --- a/app/assets/templates/machines/new.html.erb +++ b/app/assets/templates/machines/new.html.erb @@ -7,7 +7,7 @@
-

{{ 'declare_a_new_machine' }}

+

{{ 'app.admin.machines_new.declare_a_new_machine' }}

@@ -22,14 +22,10 @@
- -
- -
- + '">
diff --git a/app/assets/templates/machines/request_training_modal.html.erb b/app/assets/templates/machines/request_training_modal.html.erb index 660f2e9e8..48a9b4812 100644 --- a/app/assets/templates/machines/request_training_modal.html.erb +++ b/app/assets/templates/machines/request_training_modal.html.erb @@ -10,9 +10,9 @@
diff --git a/app/assets/templates/machines/reserve.html.erb b/app/assets/templates/machines/reserve.html.erb index 330fa7026..222cbfabd 100644 --- a/app/assets/templates/machines/reserve.html.erb +++ b/app/assets/templates/machines/reserve.html.erb @@ -9,7 +9,7 @@
-

{{ 'machine_planning' | translate }} : {{machine.name}}

+

{{ 'app.logged.machines_reserve.machine_planning' | translate }} : {{machine.name}}

@@ -19,7 +19,7 @@
- + '">
@@ -37,6 +37,8 @@ plan="selectedPlan" plan-selection-time="planSelectionTime" settings="settings" + plans="plans" + groups="groups" on-slot-added-to-cart="markSlotAsAdded" on-slot-removed-from-cart="markSlotAsRemoved" on-slot-start-to-modify="markSlotAsModifying" diff --git a/app/assets/templates/machines/show.html.erb b/app/assets/templates/machines/show.html.erb index 0e332c35b..2e4849842 100644 --- a/app/assets/templates/machines/show.html.erb +++ b/app/assets/templates/machines/show.html.erb @@ -15,15 +15,15 @@
- {{ 'book_this_machine' }} + translate>{{ 'app.public.machines_show.book_this_machine' }} - {{ 'edit' | translate }} + {{ 'app.shared.buttons.edit' | translate }} - +
@@ -48,7 +48,7 @@
-

{{ 'technical_specifications' }}

+

{{ 'app.public.machines_show.technical_specifications' }}

@@ -59,19 +59,19 @@
{{machine.machine_files_attributes.length}} -

{{ 'files_to_download' }}

+

{{ 'app.public.machines_show.files_to_download' }}

- -
-

{{ 'projects_using_the_machine' }}

+

{{ 'app.public.machines_show.projects_using_the_machine' }}

    diff --git a/app/assets/templates/machines/training_reservation_modal.html.erb b/app/assets/templates/machines/training_reservation_modal.html.erb index 7e77250e1..6b986a511 100644 --- a/app/assets/templates/machines/training_reservation_modal.html.erb +++ b/app/assets/templates/machines/training_reservation_modal.html.erb @@ -1,10 +1,10 @@ diff --git a/app/assets/templates/members/index.html.erb b/app/assets/templates/members/index.html.erb index e9f5e2124..4a68a1514 100644 --- a/app/assets/templates/members/index.html.erb +++ b/app/assets/templates/members/index.html.erb @@ -7,7 +7,7 @@
-

{{ 'the_fablab_members' }}

+

{{ 'app.logged.members.the_fablab_members' }}

@@ -21,10 +21,10 @@ - - - - + + + + @@ -41,7 +41,7 @@ @@ -49,9 +49,9 @@
{{ 'avatar' }}{{ 'user' }}{{ 'pseudonym' }}{{ 'email_address' }}{{ 'app.logged.members.avatar' }}{{ 'app.logged.members.user' }}{{ 'app.logged.members.pseudonym' }}{{ 'app.logged.members.email_address' }}
- +
-

{{ 'no_members_for_now' }}

+

{{ 'app.logged.members.no_members_for_now' }}

diff --git a/app/assets/templates/members/show.html.erb b/app/assets/templates/members/show.html.erb index 355c83924..7205b8424 100644 --- a/app/assets/templates/members/show.html.erb +++ b/app/assets/templates/members/show.html.erb @@ -15,11 +15,11 @@
- + '">
diff --git a/app/assets/templates/notifications/index.html.erb b/app/assets/templates/notifications/index.html.erb index 1d244d4cd..18a1505aa 100644 --- a/app/assets/templates/notifications/index.html.erb +++ b/app/assets/templates/notifications/index.html.erb @@ -7,7 +7,7 @@
-

{{ 'notifications_center' }}

+

{{ 'app.logged.notifications.notifications_center' }}

@@ -19,14 +19,14 @@
- + - - + + @@ -42,13 +42,13 @@ - +
{{ 'date' }}{{ 'notif_title' }}{{ 'app.logged.notifications.date' }}{{ 'app.logged.notifications.notif_title' }}
{{ 'no_new_notifications' }}{{ 'app.logged.notifications.no_new_notifications' }}
-
{{ 'archives' }}
+
{{ 'app.logged.notifications.archives' }}
@@ -73,7 +73,7 @@ - + @@ -81,7 +81,7 @@
{{ 'no_archived_notifications' }}{{ 'app.logged.notifications.no_archived_notifications' }}
- {{ 'load_the_next_notifications' }} + {{ 'app.logged.notifications.load_the_next_notifications' }}
diff --git a/app/assets/templates/plans/_plan.html.erb b/app/assets/templates/plans/_plan.html.erb index c7eb3296c..287c3fc9e 100644 --- a/app/assets/templates/plans/_plan.html.erb +++ b/app/assets/templates/plans/_plan.html.erb @@ -23,12 +23,12 @@
- +
- {{ 'do_not_subscribe' | translate }} + {{ 'app.shared.plan_subscribe.do_not_subscribe' | translate }} diff --git a/app/assets/templates/plans/index.html.erb b/app/assets/templates/plans/index.html.erb index 174a71d62..090eb9822 100644 --- a/app/assets/templates/plans/index.html.erb +++ b/app/assets/templates/plans/index.html.erb @@ -7,7 +7,7 @@
-

{{ 'subcriptions' }}

+

{{ 'app.public.plans.subscriptions' }}

@@ -44,24 +44,24 @@
- +

- {{ 'more_information' }} + {{ 'app.public.plans.more_information' }} @@ -69,7 +69,7 @@
- {{ 'your_subscription_expires_on_the_DATE' | translate:{DATE:(currentUser.subscription.expired_at | amDateFormat:'L' )} }} + {{ 'app.public.plans.your_subscription_expires_on_the_DATE' | translate:{DATE:(currentUser.subscription.expired_at | amDateFormat:'L' )} }}
@@ -87,8 +87,8 @@
-

{{ 'my_group' }}

-

{{ 'his_group' }}

+

{{ 'app.public.plans.my_group' }}

+

{{ 'app.public.plans.his_group' }}

@@ -99,72 +99,70 @@ ng-click="group.change = !group.change" ng-show="(!selectedPlan && ctrl.member && !ctrl.member.subscribed_plan && ctrl.member.subscription) || (!paid.plan)" translate - translate-values="{ROLE:currentUser.role}" - translate-interpolation="messageformat">{{ 'he_wants_to_change_group' }} + translate-values="{ROLE:currentUser.role}">{{ 'app.public.plans.he_wants_to_change_group' }}
+ translate-values="{ROLE:currentUser.role, GENDER:getGender(currentUser)}">{{ 'app.public.plans.change_my_group' }}
-

{{ 'summary' }}

+

{{ 'app.public.plans.summary' }}

- {{ 'your_subscription_has_expired_on_the_DATE' | translate:{DATE:(ctrl.member.subscription.expired_at | amDateFormat:'LL')} }} + {{ 'app.public.plans.your_subscription_has_expired_on_the_DATE' | translate:{DATE:(ctrl.member.subscription.expired_at | amDateFormat:'LL')} }}
{{ctrl.member.subscription.plan | humanReadablePlanName }} -
{{ 'subscription_price' | translate }} {{ctrl.member.subscription.plan.amount | currency}}
+
{{ 'app.public.plans.subscription_price' | translate }} {{ctrl.member.subscription.plan.amount | currency}}
-

{{ 'summary' }}

+

{{ 'app.public.plans.summary' }}

- {{ 'you_ve_just_selected_a_' | translate }} {{ '_subscription' }} : + {{ 'app.public.plans.you_ve_just_selected_a_subscription_html' }}
{{ selectedPlan | humanReadablePlanName }} -
{{ 'subscription_price' | translate }} {{selectedPlan.amount | currency}}
+
{{ 'app.public.plans.subscription_price' | translate }} {{selectedPlan.amount | currency}}
-

{{ 'summary' }}

+

{{ 'app.public.plans.summary' }}

- {{ 'you_ve_just_payed_the_' | translate }} {{ '_subscription' }} : + {{ 'app.public.plans.you_ve_just_payed_the_subscription_html' }}
{{ paid.plan | humanReadablePlanName }} -
{{ 'subscription_price' | translate }} {{paid.plan.amount | currency}}
+
{{ 'app.public.plans.subscription_price' | translate }} {{paid.plan.amount | currency}}
-
{{ 'thank_you_your_subscription_is_successful' | translate }}
- {{ 'your_invoice_will_be_available_soon_from_your_' | translate }} {{ 'dashboard' }}
+
{{ 'app.public.plans.thank_you_your_subscription_is_successful' | translate }}
+ {{ 'app.public.plans.your_invoice_will_be_available_soon_from_your_dashboard' }}
diff --git a/app/assets/templates/plans/payment_modal.html.erb b/app/assets/templates/plans/payment_modal.html.erb index 7b1f97185..db6145684 100644 --- a/app/assets/templates/plans/payment_modal.html.erb +++ b/app/assets/templates/plans/payment_modal.html.erb @@ -1,15 +1,15 @@ diff --git a/app/assets/templates/profile/_token.html.erb b/app/assets/templates/profile/_token.html.erb index 3017b18ed..ca4e99d26 100644 --- a/app/assets/templates/profile/_token.html.erb +++ b/app/assets/templates/profile/_token.html.erb @@ -1,9 +1,9 @@
-

{{ 'do_you_already_have_an_account' }}

-

{{ 'do_not_fill_the_form_beside_but_specify_here_the_code_you_ve_received_by_email_to_recover_your_access' }}

-

{{ 'just_specify_code_here_to_recover_access' }}

-

{{ 'i_did_not_receive_the_code' }}

+

{{ 'app.logged.profile_completion.do_you_already_have_an_account' }}

+

{{ 'app.logged.profile_completion.do_not_fill_the_form_beside_but_specify_here_the_code_you_ve_received_by_email_to_recover_your_access' }}

+

{{ 'app.logged.profile_completion.just_specify_code_here_to_recover_access' }}

+

{{ 'app.logged.profile_completion.i_did_not_receive_the_code' }}

@@ -14,7 +14,7 @@ ng-model="user.auth_token" class="form-control" name="user[auth_token]" - placeholder="{{ 'authentification_code' | translate }}"/> + placeholder="{{ 'app.logged.profile_completion.authentification_code' | translate }}"/>
@@ -22,9 +22,9 @@ -
\ No newline at end of file + diff --git a/app/assets/templates/profile/complete.html.erb b/app/assets/templates/profile/complete.html.erb index a024ce6ef..978ff03bc 100644 --- a/app/assets/templates/profile/complete.html.erb +++ b/app/assets/templates/profile/complete.html.erb @@ -9,7 +9,7 @@
-

{{ 'confirm_your_new_account' }}

+

{{ 'app.logged.profile_completion.confirm_your_new_account' }}

@@ -25,11 +25,11 @@
- {{ 'you_ve_just_created_a_new_account_on_the_fablab_by_logging_from' | translate:{ GENDER: nameGenre, NAME: fablabName }:"messageformat" }}
+ {{ 'app.logged.profile_completion.you_ve_just_created_a_new_account_on_the_fablab_by_logging_from' | translate:{ GENDER: nameGenre, NAME: fablabName } }}
{{activeProvider.name}} ({{ssoEmail()}})
-

{{ 'we_need_some_more_details' }}.

-

{{ 'your_email_is_already_used_by_another_account_on_the_platform' }}

+

{{ 'app.logged.profile_completion.we_need_some_more_details' }}.

+

{{ 'app.logged.profile_completion.your_email_is_already_used_by_another_account_on_the_platform' }}

@@ -37,7 +37,7 @@ @@ -46,10 +46,10 @@
-

{{ 'new_on_this_platform' }}

-

{{ 'please_fill_the_following_form'}}.

-

{{ 'some_data_may_have_already_been_provided_by_provider_and_cannot_be_modified' | translate:{NAME:activeProvider.name} }}.
- {{ 'then_click_on_' | translate }} {{ 'confirm_changes' }} {{ '_to_start_using_the_application' | translate }}.

+

{{ 'app.logged.profile_completion.new_on_this_platform' }}

+

{{ 'app.logged.profile_completion.please_fill_the_following_form'}}.

+

{{ 'app.logged.profile_completion.some_data_may_have_already_been_provided_by_provider_and_cannot_be_modified' | translate:{NAME:activeProvider.name} }}.
+ {{ 'app.logged.profile_completion.then_click_on_' | translate }} {{ 'app.shared.buttons.confirm_changes' }} {{ 'app.logged.profile_completion._to_start_using_the_application' | translate }}.

- + '">
@@ -73,16 +73,17 @@
- + - + +
- {{ 'user_s_profile_is_required' }} + {{ 'app.logged.profile_completion.user_s_profile_is_required' }}
@@ -91,8 +92,8 @@ name="cgu" ng-model="user.cgu" value="true" - ng-required="cgu != null"/> {{ 'i_ve_read_and_i_accept_' | translate }} - {{ '_the_fablab_policy' }} + ng-required="cgu != null"/> {{ 'app.logged.profile_completion.i_ve_read_and_i_accept_' | translate }} + {{ 'app.logged.profile_completion._the_fablab_policy' }}
@@ -103,7 +104,7 @@ @@ -114,21 +115,29 @@
-

{{ 'new_on_this_platform' }}

+

{{ 'app.logged.profile_completion.new_on_this_platform' }}

- {{ 'your_email_' | translate }} ({{ssoEmail()}}) {{ '_is_currently_associated_with_another_account_on_this_platform' | translate }} - {{ 'please_click_to_change_email_associated_with_your_PROVIDER_account' | translate:{PROVDER: activeProvider.name} }} + {{ 'app.logged.profile_completion.your_email_' | translate }} + ({{ssoEmail()}}) + {{ 'app.logged.profile_completion._is_currently_associated_with_another_account_on_this_platform' | translate }} + {{ 'app.logged.profile_completion.please_click_to_change_email_associated_with_your_PROVIDER_account' | translate:{PROVDER: activeProvider.name} }}

- {{ 'change_my_data' | translate }} + {{ 'app.logged.profile_completion.change_my_data' | translate }} -

{{ 'once_your_data_are_up_to_date_' | translate }} {{ '_click_on_the_synchronization_button_opposite_' }} {{ 'or' | translate}} {{ '_disconnect_then_reconnect_' }} {{ '_for_your_changes_to_take_effect' | translate }}

+

+ {{ 'app.logged.profile_completion.once_your_data_are_up_to_date_' | translate }} + {{ 'app.logged.profile_completion._click_on_the_synchronization_button_opposite_' }} + {{ 'app.logged.profile_completion.or' | translate}} + {{ 'app.logged.profile_completion._disconnect_then_reconnect_' }} + {{ 'app.logged.profile_completion._for_your_changes_to_take_effect' | translate }} +

@@ -138,11 +147,11 @@
- + '">
diff --git a/app/assets/templates/profile/resend_code_modal.html b/app/assets/templates/profile/resend_code_modal.html index b3e04dd02..f2eedc9f2 100644 --- a/app/assets/templates/profile/resend_code_modal.html +++ b/app/assets/templates/profile/resend_code_modal.html @@ -1,11 +1,11 @@ diff --git a/app/assets/templates/projects/_form.html.erb b/app/assets/templates/projects/_form.html.erb index 6d49016e2..4d0ccfae8 100644 --- a/app/assets/templates/projects/_form.html.erb +++ b/app/assets/templates/projects/_form.html.erb @@ -10,16 +10,16 @@
- +
- {{ 'name_is_required' }} + {{ 'app.shared.project.name_is_required' }}
- +
@@ -29,9 +29,9 @@
- {{ 'add_an_illustration' | translate }} {{ 'change' }} + {{ 'app.shared.project.add_an_illustration' | translate }} {{ 'app.shared.buttons.change' }} - {{ 'delete' }} + {{ 'app.shared.buttons.delete' }}
@@ -39,7 +39,7 @@
- +
@@ -50,40 +50,40 @@
{{file.attachment}}
- {{ 'browse' }} - {{ 'change' }} + {{ 'app.shared.buttons.browse' }} + {{ 'app.shared.buttons.change' }}
- +
- {{ 'add_a_new_file' | translate }} + {{ 'app.shared.project.add_a_new_file' | translate }}
- +
- {{ 'description_is_required' }} + {{ 'app.shared.project.description_is_required' }}
- +
@@ -94,7 +94,7 @@ type="text" name="project[project_steps_attributes][][title]" class="form-control m-b-sm m-t-xs" - placeholder="{{ 'step_title' | translate }}" + placeholder="{{ 'app.shared.project.step_title' | translate }}" required> @@ -114,25 +114,25 @@ {{image.attachment}}
- {{ 'browse' | translate }} {{ 'change' }} + {{ 'app.shared.buttons.browse' | translate }} {{ 'app.shared.buttons.change' }} - {{ 'delete' }} + {{ 'app.shared.buttons.delete' }}
- {{ 'add_a_new_step' }} + {{ 'app.shared.project.add_a_new_step' }} @@ -141,11 +141,11 @@ @@ -158,7 +158,7 @@
-

{{ 'employed_materials' }}

+

{{ 'app.shared.project.employed_materials' }}

@@ -176,7 +176,7 @@
-

{{ 'employed_machines' }}

+

{{ 'app.shared.project.employed_machines' }}

@@ -194,7 +194,7 @@
-

{{ 'collaborators' }}

+

{{ 'app.shared.project.collaborators' }}

@@ -212,7 +212,7 @@
-

{{ 'creative_commons_licences' }}

+

{{ 'app.shared.project.creative_commons_licences' }}

<%# TODO: be able to remove the selected option %> @@ -230,7 +230,7 @@
-

{{ 'themes' }}

+

{{ 'app.shared.project.themes' }}

@@ -248,7 +248,7 @@
-

{{ 'tags' }}

+

{{ 'app.shared.project.tags' }}

diff --git a/app/assets/templates/projects/edit.html.erb b/app/assets/templates/projects/edit.html.erb index e5e1a4b53..d01e8eff7 100644 --- a/app/assets/templates/projects/edit.html.erb +++ b/app/assets/templates/projects/edit.html.erb @@ -12,16 +12,16 @@
-

{{ 'edit_the_project' | translate }} {{ 'rough_draft' }}

+

{{ 'app.logged.projects_edit.edit_the_project' | translate }} {{ 'app.logged.projects_edit.rough_draft' }}

- -
{{ 'publish' }}
- +
{{ 'app.logged.projects_edit.publish' }}
+
@@ -29,7 +29,7 @@ - + '"> diff --git a/app/assets/templates/projects/index.html.erb b/app/assets/templates/projects/index.html.erb index f927f20b2..35ad72f89 100644 --- a/app/assets/templates/projects/index.html.erb +++ b/app/assets/templates/projects/index.html.erb @@ -7,13 +7,13 @@
-

{{ 'projects_list.the_fablab_projects' }}

+

{{ 'app.public.projects_list.the_fablab_projects' }}

@@ -23,16 +23,16 @@
- {{ 'projects_list.reset_all_filters' | translate }} + {{ 'app.public.projects_list.reset_all_filters' | translate }} - - + + @@ -44,44 +44,44 @@
- +
- +
- +
- {{ 'projects_list.project_search_result_is_empty' | translate }} + {{ 'app.public.projects_list.project_search_result_is_empty' | translate }}
@@ -99,13 +99,13 @@
- {{ 'projects_list.rough_draft' }} + {{ 'app.public.projects_list.rough_draft' }}
- {{ 'consult' | translate }} + {{ 'app.shared.buttons.consult' | translate }}
diff --git a/app/assets/templates/projects/new.html.erb b/app/assets/templates/projects/new.html.erb index 13a5d4239..933cf918c 100644 --- a/app/assets/templates/projects/new.html.erb +++ b/app/assets/templates/projects/new.html.erb @@ -9,7 +9,7 @@
-

{{ 'add_a_new_project' }}

+

{{ 'app.logged.projects_new.add_a_new_project' }}

@@ -18,7 +18,7 @@
- + '">
diff --git a/app/assets/templates/projects/show.html.erb b/app/assets/templates/projects/show.html.erb index 0918d3a98..90536f188 100644 --- a/app/assets/templates/projects/show.html.erb +++ b/app/assets/templates/projects/show.html.erb @@ -9,14 +9,14 @@
-

{{ project.name }} {{ 'rough_draft' }}

+

{{ project.name }} {{ 'app.public.projects_show.rough_draft' }}

-

{{ 'project_description' }}

+

{{ 'app.public.projects_show.project_description' }}

-

{{ 'step_N' | translate:{INDEX:step.step_nb} }} : {{step.title}}

+

{{ 'app.public.projects_show.step_N' | translate:{INDEX:step.step_nb} }} : {{step.title}}

@@ -58,8 +58,8 @@
@@ -78,11 +78,11 @@
- {{ 'posted_on_' | translate }} {{project.created_at | amDateFormat: 'LL'}} + {{ 'app.public.projects_show.posted_on_' | translate }} {{project.created_at | amDateFormat: 'LL'}}
@@ -90,7 +90,7 @@
{{project.project_caos_attributes.length}} -

{{ 'CAD_file_to_download' }}

+

{{ 'app.public.projects_show.CAD_file_to_download' }}

    @@ -103,7 +103,7 @@
    {{project.machines.length}} -

    {{ 'machines_and_materials' }}

    +

    {{ 'app.public.projects_show.machines_and_materials' }}

      @@ -122,7 +122,7 @@
      {{project.project_users.length}} -

      {{ 'collaborators' }}

      +

      {{ 'app.public.projects_show.collaborators' }}

        @@ -143,7 +143,7 @@
        -

        {{ 'licence' }}

        +

        {{ 'app.public.projects_show.licence' }}

        diff --git a/app/assets/templates/shared/ConfirmationNewModal.html b/app/assets/templates/shared/ConfirmationNewModal.html new file mode 100644 index 000000000..4118c567a --- /dev/null +++ b/app/assets/templates/shared/ConfirmationNewModal.html @@ -0,0 +1,35 @@ +
        + + +
        \ No newline at end of file diff --git a/app/assets/templates/shared/_admin_form.html b/app/assets/templates/shared/_admin_form.html index dae346c14..53fb425cf 100644 --- a/app/assets/templates/shared/_admin_form.html +++ b/app/assets/templates/shared/_admin_form.html @@ -1,76 +1,77 @@
        - - + + +
        - +
        - {{ 'pseudonym_is_required' }} + {{ 'app.admin.admins_new.pseudonym_is_required' }}
        - +
        - {{ 'surname_is_required' }} + {{ 'app.admin.admins_new.surname_is_required' }}
        - +
        - {{ 'first_name_is_required' }} + {{ 'app.admin.admins_new.first_name_is_required' }}
        - +
        - {{ 'email_is_required' }} + {{ 'app.admin.admins_new.email_is_required' }}
        @@ -83,7 +84,7 @@ uib-datepicker-popup="{{datePicker.format}}" datepicker-options="datePicker.options" is-open="datePicker.opened" - placeholder="{{ 'birth_date' | translate }}" + placeholder="{{ 'app.admin.admins_new.birth_date' | translate }}" ng-click="openDatePicker($event)" /> + placeholder="{{ 'app.admin.admins_new.address' | translate }}">
        @@ -114,7 +115,7 @@ type="text" name="admin[profile_attributes][phone]" class="form-control" id="user_phone" - placeholder="{{ 'phone_number' | translate }}"> + placeholder="{{ 'app.admin.admins_new.phone_number' | translate }}">
diff --git a/app/assets/templates/shared/_cart.html.erb b/app/assets/templates/shared/_cart.html.erb index 4a7179243..ae6dc7365 100644 --- a/app/assets/templates/shared/_cart.html.erb +++ b/app/assets/templates/shared/_cart.html.erb @@ -1,56 +1,70 @@
-

{{ 'cart.summary' }}

+

{{ 'app.shared.cart.summary' }}

<%= image_tag('fleche-left.png', class: 'fleche-left visible-lg') %> - {{ 'cart.select_one_or_more_slots_in_the_calendar' | translate:{SINGLE:limitToOneSlot}:"messageformat" }}

+ {{ 'app.shared.cart.select_one_or_more_slots_in_the_calendar' | translate:{SINGLE:limitToOneSlot} }}

-
{{ 'cart.you_ve_just_selected_the_slot' }}
+
{{ 'app.shared.cart.you_ve_just_selected_the_slot' }}
-
{{ 'cart.datetime_to_time' | translate:{START_DATETIME:(slot.start | amDateFormat:'LLLL'), END_TIME:(slot.end | amDateFormat:'LT') } }}
-
{{ 'cart.cost_of_TYPE' | translate:{TYPE:reservableType}:"messageformat" }} {{slot.price | currency}}
+
{{ 'app.shared.cart.datetime_to_time' | translate:{START_DATETIME:(slot.start | amDateFormat:'LLLL'), END_TIME:(slot.end | amDateFormat:'LT') } }}
+
{{ 'app.shared.cart.cost_of_TYPE' | translate:{TYPE:reservableType} }} {{slot.price | currency}}
- +
+
+
+
{{ 'app.shared.cart.slot_restrict_plans' }}
+
+
{{::group.name}}
+
    +
  • {{::plan.name}}
  • +
+
+
+
+
{{ 'app.shared.cart.slot_restrict_plans_of_others_groups' }}
+
+
-
- +
+
- +
-

{{ 'cart.to_benefit_from_attractive_prices' }}

-
-

{{ 'cart.or' }}

+

{{ 'app.shared.cart.to_benefit_from_attractive_prices' }}

+
+

{{ 'app.shared.cart.or' }}

-
{{ 'cart.you_ve_just_selected_a_' | translate }}
{{ 'cart._subscription' }} :
+
{{ 'app.shared.cart.you_ve_just_selected_a_' | translate }}
{{ 'app.shared.cart._subscription' }} :
{{selectedPlan | humanReadablePlanName }}
-
{{ 'cart.cost_of_the_subscription' | translate }} {{selectedPlan.amount | currency}}
+
{{ 'app.shared.cart.cost_of_the_subscription' | translate }} {{selectedPlan.amount | currency}}
@@ -59,29 +73,29 @@
- {{ 'cart.you_have_settled_the_following_TYPE' | translate:{TYPE:reservableType}:"messageformat" }} {{reservableName}}: + {{ 'app.shared.cart.you_have_settled_the_following_TYPE' | translate:{TYPE:reservableType} }} {{reservableName}}:
- {{ 'cart.datetime_to_time' | translate:{START_DATETIME:(paidSlot.start | amDateFormat:'LLLL'), END_TIME:(paidSlot.end | amDateFormat:'LT') } }} -
{{ 'cart.cost_of_TYPE' | translate:{TYPE:reservableType}:"messageformat" }} {{paidSlot.price | currency}}
+ {{ 'app.shared.cart.datetime_to_time' | translate:{START_DATETIME:(paidSlot.start | amDateFormat:'LLLL'), END_TIME:(paidSlot.end | amDateFormat:'LT') } }} +
{{ 'app.shared.cart.cost_of_TYPE' | translate:{TYPE:reservableType} }} {{paidSlot.price | currency}}
-
{{ 'cart.you_have_settled_a_' | translate }}
{{ 'cart._subscription' }} :
+
{{ 'app.shared.cart.you_have_settled_a_' | translate }}
{{ 'app.shared.cart._subscription' }} :
{{selectedPlan | humanReadablePlanName }} -
{{ 'cart.cost_of_the_subscription' | translate }} {{selectedPlan.amount | currency}}
+
{{ 'app.shared.cart.cost_of_the_subscription' | translate }} {{selectedPlan.amount | currency}}
-
{{ 'cart.total_' | translate }} {{amountPaid | currency}}
+
{{ 'app.shared.cart.total_' | translate }} {{amountPaid | currency}}
-
{{ 'cart.thank_you_your_payment_has_been_successfully_registered' | translate }}
- {{ 'cart.your_invoice_will_be_available_soon_from_your_' | translate }} {{ 'cart.dashboard' }} +
{{ 'app.shared.cart.thank_you_your_payment_has_been_successfully_registered' | translate }}
+ {{ 'app.shared.cart.your_invoice_will_be_available_soon_from_your_' | translate }} {{ 'app.shared.cart.dashboard' }}
@@ -90,48 +104,48 @@
-

{{ 'cart.summary' }}

+

{{ 'app.shared.cart.summary' }}

-
{{ 'cart.i_want_to_change_the_following_reservation' }}
+
{{ 'app.shared.cart.i_want_to_change_the_following_reservation' }}
-
{{ 'cart.datetime_to_time' | translate:{START_DATETIME:(events.modifiable.start | amDateFormat:'LLLL'), END_TIME:(events.modifiable.end | amDateFormat:'LT') } }}
+
{{ 'app.shared.cart.datetime_to_time' | translate:{START_DATETIME:(events.modifiable.start | amDateFormat:'LLLL'), END_TIME:(events.modifiable.end | amDateFormat:'LT') } }}
- +

<%= image_tag('fleche-left.png', class: 'fleche-left visible-lg') %> - {{ 'cart.select_a_new_slot_in_the_calendar' | translate }}

+ {{ 'app.shared.cart.select_a_new_slot_in_the_calendar' | translate }}

-
{{ 'cart.datetime_to_time' | translate:{START_DATETIME:(events.placable.start | amDateFormat:'LLLL'), END_TIME:(events.placable.end | amDateFormat:'LT') } }}
+
{{ 'app.shared.cart.datetime_to_time' | translate:{START_DATETIME:(events.placable.start | amDateFormat:'LLLL'), END_TIME:(events.placable.end | amDateFormat:'LT') } }}
- +
- {{ 'cart.tags_of_the_original_slot' | translate }}
+ {{ 'app.shared.cart.tags_of_the_original_slot' | translate }}
{{tag.name}} - {{ 'cart.none' }} + {{ 'app.shared.cart.none' }}

- {{ 'cart.tags_of_the_destination_slot' | translate }}
+ {{ 'app.shared.cart.tags_of_the_destination_slot' | translate }}
{{tag.name}} - {{ 'cart.none' }} + {{ 'app.shared.cart.none' }}
@@ -140,26 +154,26 @@
-
{{ 'cart.your_booking_slot_was_successfully_moved_from_' }}
+
{{ 'app.shared.cart.your_booking_slot_was_successfully_moved_from_' }}
-
{{ 'cart.datetime_to_time' | translate:{START_DATETIME:(events.moved.oldSlot.start | amDateFormat:'LLLL'), END_TIME:(events.moved.oldSlot.end | amDateFormat:'LT') } }}
+
{{ 'app.shared.cart.datetime_to_time' | translate:{START_DATETIME:(events.moved.oldSlot.start | amDateFormat:'LLLL'), END_TIME:(events.moved.oldSlot.end | amDateFormat:'LT') } }}
-

{{ 'cart.to_date' }}

+

{{ 'app.shared.cart.to_date' }}

-
{{ 'cart.datetime_to_time' | translate:{START_DATETIME:(events.moved.newSlot.start | amDateFormat:'LLLL'), END_TIME:(events.moved.newSlot.end | amDateFormat:'LT') } }}
+
{{ 'app.shared.cart.datetime_to_time' | translate:{START_DATETIME:(events.moved.newSlot.start | amDateFormat:'LLLL'), END_TIME:(events.moved.newSlot.end | amDateFormat:'LT') } }}
diff --git a/app/assets/templates/shared/_coupon.html.erb b/app/assets/templates/shared/_coupon.html.erb index bc4552ece..d24ce74d5 100644 --- a/app/assets/templates/shared/_coupon.html.erb +++ b/app/assets/templates/shared/_coupon.html.erb @@ -1,8 +1,8 @@
- {{ 'i_have_a_coupon' }} + {{ 'app.shared.coupon_input.i_have_a_coupon' }}
- +
- {{ 'add_an_avatar' }} - {{ 'change' }} + {{ 'app.shared.user.add_an_avatar' }} + {{ 'app.shared.buttons.change' }} @@ -47,7 +47,7 @@ value="true" ng-disabled="preventField['profile.gender'] && user.statistic_profile.gender && !userForm['user[statistic_profile_attributes][gender]'].$dirty" required/> - {{ 'man' | translate }} + {{ 'app.shared.user.man' | translate }} - + - {{ 'gender_is_required' }} + {{ 'app.shared.user.gender_is_required' }}
- +
- {{ 'pseudonym_is_required' }} + {{ 'app.shared.user.pseudonym_is_required' }}
- +
- {{ 'surname_is_required' }} + {{ 'app.shared.user.surname_is_required' }}
- +
- {{ 'first_name_is_required' }} + {{ 'app.shared.user.first_name_is_required' }}
- +
- {{ 'email_address_is_required' }} + {{ 'app.shared.user.email_address_is_required' }}
+ translate>{{ 'app.shared.user.change_password' }}
@@ -141,12 +141,12 @@ ng-model="user.password" class="form-control" id="user_password" - placeholder="{{ 'new_password' | translate }}" + placeholder="{{ 'app.shared.user.new_password' | translate }}" ng-minlength="8" required/>
- {{ 'password_is_required' }} - {{ 'password_is_too_short' }} + {{ 'app.shared.user.password_is_required' }} + {{ 'app.shared.user.password_is_too_short' }}
@@ -157,19 +157,19 @@ ng-model="user.password_confirmation" class="form-control" id="user_password_confirmation" - placeholder="{{ 'confirmation_of_new_password' | translate }}" + placeholder="{{ 'app.shared.user.confirmation_of_new_password' | translate }}" ng-minlength="8" required match="user.password"/>
- {{ 'confirmation_of_password_is_required' }} - {{ 'confirmation_of_password_is_too_short' }} - {{ 'confirmation_mismatch_with_password' }} + {{ 'app.shared.user.confirmation_of_password_is_required' }} + {{ 'app.shared.user.confirmation_of_password_is_too_short' }} + {{ 'app.shared.user.confirmation_mismatch_with_password' }}
- + @@ -177,16 +177,16 @@ name="user[invoicing_profile_attributes][organization_attributes][name]" ng-model="user.invoicing_profile.organization.name" class="form-control" - placeholder="{{ 'organization_name' | translate }}" + placeholder="{{ 'app.shared.user.organization_name' | translate }}" ng-required="user.invoicing_profile.organization" ng-disabled="preventField['profile.organization_name'] && user.invoicing_profile.organization.name && !userForm['user[invoicing_profile_attributes][organization_attributes][name]'].$dirty">
- {{ 'organization_name_is_required' }} + {{ 'app.shared.user.organization_name_is_required' }}
- + @@ -194,16 +194,16 @@ name="user[invoicing_profile_attributes][organization_attributes][address_attributes][address]" ng-model="user.invoicing_profile.organization.address.address" class="form-control" - placeholder="{{ 'organization_address' | translate }}" + placeholder="{{ 'app.shared.user.organization_address' | translate }}" ng-required="user.invoicing_profile.organization" ng-disabled="preventField['profile.organization_address'] && user.invoicing_profile.organization.address.address && !userForm['user[invoicing_profile_attributes][organization_attributes][address_attributes][address]'].$dirty">
- {{ 'organization_address_is_required' }} + {{ 'app.shared.user.organization_address_is_required' }}
- + @@ -219,12 +219,12 @@ name="user[statistic_profile_attributes][birthday]" value="{{user.statistic_profile.birthday | toIsoDate}}" />
- {{ 'date_of_birth_is_required' }} + {{ 'app.shared.user.date_of_birth_is_required' }}
- + @@ -234,54 +234,54 @@ class="form-control" id="user_address" ng-disabled="preventField['profile.address'] && user.invoicing_profile.address.address && !userForm['user[invoicing_profile_attributes][address_attributes][address]'].$dirty" - placeholder="{{ 'address' | translate }}"/> + placeholder="{{ 'app.shared.user.address' | translate }}"/>
- + + ng-required="phoneRequired"/>
- {{ 'phone_number_is_required' }} + {{ 'app.shared.user.phone_number_is_required' }}
- +
- +
- + - {{ 'message_is_required' }} + + {{ 'app.public.projects_show.message_is_required' }}
diff --git a/app/assets/templates/shared/signupModal.html.erb b/app/assets/templates/shared/signupModal.html.erb index 9c33debf2..b9f2639a8 100644 --- a/app/assets/templates/shared/signupModal.html.erb +++ b/app/assets/templates/shared/signupModal.html.erb @@ -1,6 +1,6 @@
{{alert.msg}} @@ -14,16 +14,16 @@ name="gender" ng-model="user.statistic_profile_attributes.gender" value="true" - required/> {{ 'man' | translate }} + required/> {{ 'app.public.common.man' | translate }} - - {{ 'gender_is_required'}} + + {{ 'app.public.common.gender_is_required'}}
@@ -34,10 +34,10 @@ name="first_name" ng-model="user.profile_attributes.first_name" class="form-control" - placeholder="{{ 'your_first_name' | translate }}" + placeholder="{{ 'app.public.common.your_first_name' | translate }}" required> - - {{ 'first_name_is_required' }} + + {{ 'app.public.common.first_name_is_required' }}
@@ -45,10 +45,10 @@ name="last_name" ng-model="user.profile_attributes.last_name" class="form-control" - placeholder="{{ 'your_surname' | translate }}" + placeholder="{{ 'app.public.common.your_surname' | translate }}" required> - - {{ 'surname_is_required' }} + + {{ 'app.public.common.surname_is_required' }}
@@ -60,11 +60,11 @@ name="username" ng-model="user.username" class="form-control" - placeholder="{{ 'your_pseudonym' | translate }}" + placeholder="{{ 'app.public.common.your_pseudonym' | translate }}" required>
- - {{ 'pseudonym_is_required' }} + + {{ 'app.public.common.pseudonym_is_required' }} @@ -76,11 +76,11 @@ name="email" ng-model="user.email" class="form-control" - placeholder="{{ 'your_email_address' | translate }}" + placeholder="{{ 'app.public.common.your_email_address' | translate }}" required> - - {{ 'email_is_required' }} + + {{ 'app.public.common.email_is_required' }} @@ -92,13 +92,13 @@ name="password" ng-model="user.password" class="form-control" - placeholder="{{ 'your_password' | translate }}" + placeholder="{{ 'app.public.common.your_password' | translate }}" required ng-minlength="8"> - {{ 'password_is_required' }} - {{ 'password_is_too_short' }} + {{ 'app.public.common.password_is_required' }} + {{ 'app.public.common.password_is_too_short' }} @@ -110,13 +110,13 @@ name="password_confirmation" ng-model="user.password_confirmation" class="form-control" - placeholder="{{ 'type_your_password_again' | translate }}" + placeholder="{{ 'app.public.common.type_your_password_again' | translate }}" required ng-minlength="8" match="user.password"> - {{ 'password_confirmation_is_required' }} - {{ 'password_does_not_match_with_confirmation' }} + {{ 'app.public.common.password_confirmation_is_required' }} + {{ 'app.public.common.password_does_not_match_with_confirmation' }} @@ -127,7 +127,7 @@ id="organization" ng-model="user.organization" value="false"/> - + @@ -139,11 +139,11 @@ name="organization_name" ng-model="user.profile_attributes.organization_attributes.name" class="form-control" - placeholder="{{ 'name_of_your_organization' | translate }}" + placeholder="{{ 'app.public.common.name_of_your_organization' | translate }}" ng-required="user.organization"> - - {{ 'organization_name_is_required' }} + + {{ 'app.public.common.organization_name_is_required' }} @@ -155,11 +155,11 @@ name="organization_address" ng-model="user.profile_attributes.organization_attributes.address_attributes.address" class="form-control" - placeholder="{{ 'address_of_your_organization' | translate }}" + placeholder="{{ 'app.public.common.address_of_your_organization' | translate }}" ng-required="user.organization"> - - {{ 'organization_address_is_required' }} + + {{ 'app.public.common.organization_address_is_required' }} @@ -167,11 +167,11 @@
- +
- {{ 'user_s_profile_is_required' }} + {{ 'app.public.common.user_s_profile_is_required' }}
@@ -186,12 +186,12 @@ uib-datepicker-popup="{{datePicker.format}}" datepicker-options="datePicker.options" is-open="datePicker.opened" - placeholder="{{ 'birth_date' | translate }}" + placeholder="{{ 'app.public.common.birth_date' | translate }}" ng-click="openDatePicker($event)" required/> - - {{ 'birth_date_is_required' }} + + {{ 'app.public.common.birth_date_is_required' }} @@ -203,11 +203,13 @@ type="text" name="phone" class="form-control" - placeholder="{{ 'phone_number' | translate }}" - required> + placeholder="{{ 'app.public.common.phone_number' | translate }}" + ng-required="phoneRequired"> - - {{ 'phone_number_is_required' }} + + + + {{ 'app.public.common.phone_number_is_required' }} @@ -218,7 +220,7 @@ id="is_allow_contact" ng-model="user.is_allow_contact" value="true"/> - + @@ -229,7 +231,7 @@ id="is_allow_newsletter" ng-model="user.is_allow_newsletter" value="true"/> - + @@ -242,8 +244,8 @@ value="true" ng-required="cgu != null"/> @@ -255,7 +257,7 @@ - {{ 'field_required' }} + {{ 'app.public.common.field_required' }}
@@ -264,5 +266,5 @@
diff --git a/app/assets/templates/shared/tour-step-template.html b/app/assets/templates/shared/tour-step-template.html new file mode 100644 index 000000000..fa9c83b0e --- /dev/null +++ b/app/assets/templates/shared/tour-step-template.html @@ -0,0 +1,12 @@ +
+
+
+
+ +
+ + +
+
+
+
diff --git a/app/assets/templates/shared/valid_reservation_modal.html.erb b/app/assets/templates/shared/valid_reservation_modal.html.erb index 510b44029..b74fb1ce1 100644 --- a/app/assets/templates/shared/valid_reservation_modal.html.erb +++ b/app/assets/templates/shared/valid_reservation_modal.html.erb @@ -1,17 +1,17 @@ diff --git a/app/assets/templates/spaces/_form.html b/app/assets/templates/spaces/_form.html index 117c03543..ee2204e51 100644 --- a/app/assets/templates/spaces/_form.html +++ b/app/assets/templates/spaces/_form.html @@ -1,21 +1,21 @@ {{alert.msg}}
- +
- {{ 'space.name_is_required' }} + {{ 'app.shared.space.name_is_required' }}
- +
@@ -26,8 +26,8 @@
- {{ 'space.add_an_illustration' | translate }} - {{ 'change' }} + {{ 'app.shared.space.add_an_illustration' | translate }} + {{ 'app.shared.buttons.change' }} - {{ 'delete' }} + {{ 'app.shared.buttons.delete' }}
@@ -44,7 +44,7 @@
- +
- {{ 'space.default_places_is_required' }} + {{ 'app.shared.space.default_places_is_required' }}
- +
- +
- +
@@ -98,20 +98,20 @@
{{file.attachment}}
- {{ 'space.attach_a_file' }} - {{ 'change' }} + {{ 'app.shared.space.attach_a_file' }} + {{ 'app.shared.buttons.change' }}
- {{ 'space.add_an_attachment' | translate }} + {{ 'app.shared.space.add_an_attachment' | translate }}
diff --git a/app/assets/templates/spaces/edit.html.erb b/app/assets/templates/spaces/edit.html.erb index d3fb4872b..e66de325f 100644 --- a/app/assets/templates/spaces/edit.html.erb +++ b/app/assets/templates/spaces/edit.html.erb @@ -7,7 +7,7 @@
-

{{ 'space_edit.edit_the_space_NAME' }}

+

{{ 'app.admin.space_edit.edit_the_space_NAME' }}

@@ -33,12 +33,12 @@
- + '">
diff --git a/app/assets/templates/spaces/index.html.erb b/app/assets/templates/spaces/index.html.erb index 08c49a2ef..a0bde7f95 100644 --- a/app/assets/templates/spaces/index.html.erb +++ b/app/assets/templates/spaces/index.html.erb @@ -7,27 +7,40 @@
-

{{ 'spaces_list.the_spaces' }}

+

{{ 'app.public.spaces_list.the_spaces' }}

-
+
'" + ui-tour-use-hotkeys="true" + ui-tour-scroll-parent-id="content-main" + post-render="setupSpacesTour"> -
-
- - +
+
+
+ + +
+
+
@@ -49,13 +62,13 @@
- +
-
+
- +
diff --git a/app/assets/templates/spaces/new.html.erb b/app/assets/templates/spaces/new.html.erb index b97a5347a..de8f9b079 100644 --- a/app/assets/templates/spaces/new.html.erb +++ b/app/assets/templates/spaces/new.html.erb @@ -7,7 +7,7 @@
-

{{ 'space_new.add_a_new_space' }}

+

{{ 'app.admin.space_new.add_a_new_space' }}

@@ -22,8 +22,8 @@
diff --git a/app/assets/templates/spaces/reserve.html.erb b/app/assets/templates/spaces/reserve.html.erb index 997251ee1..81a57c380 100644 --- a/app/assets/templates/spaces/reserve.html.erb +++ b/app/assets/templates/spaces/reserve.html.erb @@ -7,7 +7,7 @@
-

{{ 'space_reserve.planning_of_space_NAME' }}

+

{{ 'app.logged.space_reserve.planning_of_space_NAME' }}

@@ -17,7 +17,7 @@
- + '">
@@ -35,6 +35,8 @@ plan="selectedPlan" plan-selection-time="planSelectionTime" settings="settings" + plans="plans" + groups="groups" on-slot-added-to-cart="markSlotAsAdded" on-slot-removed-from-cart="markSlotAsRemoved" on-slot-start-to-modify="markSlotAsModifying" diff --git a/app/assets/templates/spaces/show.html b/app/assets/templates/spaces/show.html index 6cc32f7f4..8b9ad53e2 100644 --- a/app/assets/templates/spaces/show.html +++ b/app/assets/templates/spaces/show.html @@ -15,9 +15,9 @@
- {{ 'space_show.book_this_space' }} + {{ 'app.public.space_show.book_this_space' }} - {{ 'edit' | translate }} + {{ 'app.shared.buttons.edit' | translate }}
@@ -44,7 +44,7 @@
-

{{ 'space_show.characteristics' }}

+

{{ 'app.public.space_show.characteristics' }}

@@ -55,7 +55,7 @@
{{space.space_files_attributes.length}} -

{{ 'space_show.files_to_download' }}

+

{{ 'app.public.space_show.files_to_download' }}

    @@ -67,7 +67,7 @@
    -

    {{ 'space_show.projects_using_the_space' }}

    +

    {{ 'app.public.space_show.projects_using_the_space' }}

      diff --git a/app/assets/templates/stripe/payment_modal.html.erb b/app/assets/templates/stripe/payment_modal.html.erb index 955750e79..7f6f70be8 100644 --- a/app/assets/templates/stripe/payment_modal.html.erb +++ b/app/assets/templates/stripe/payment_modal.html.erb @@ -1,7 +1,7 @@
@@ -27,7 +27,7 @@
- + '">
@@ -47,6 +47,8 @@ plan="selectedPlan" plan-selection-time="planSelectionTime" settings="settings" + plans="plans" + groups="groups" on-slot-added-to-cart="markSlotAsAdded" on-slot-removed-from-cart="markSlotAsRemoved" on-slot-start-to-modify="markSlotAsModifying" diff --git a/app/assets/templates/trainings/show.html.erb b/app/assets/templates/trainings/show.html.erb index c1ef7f3bc..7cf8fadbb 100644 --- a/app/assets/templates/trainings/show.html.erb +++ b/app/assets/templates/trainings/show.html.erb @@ -15,9 +15,9 @@ diff --git a/app/assets/templates/wallet/credit_modal.html.erb b/app/assets/templates/wallet/credit_modal.html.erb index 6cf9ed08d..15a2e88cc 100644 --- a/app/assets/templates/wallet/credit_modal.html.erb +++ b/app/assets/templates/wallet/credit_modal.html.erb @@ -1,17 +1,17 @@