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

Merge branch 'dev' for release 4.3.0

This commit is contained in:
Sylvain 2020-03-04 12:24:15 +01:00
commit b15edb1fee
445 changed files with 20633 additions and 9989 deletions

View File

@ -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

View File

@ -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):

View File

@ -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

View File

@ -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

11
Gemfile
View File

@ -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'

View File

@ -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)

377
README.md
View File

@ -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)<br/>
4.1. [General Guidelines](#general-guidelines)<br/>
5. [PostgreSQL](#postgresql)<br/>
5.1. [Install PostgreSQL 9.6](#setup-postgresql)
6. [ElasticSearch](#elasticsearch)<br/>
6.1. [Install ElasticSearch](#setup-elasticsearch)<br/>
6.2. [Rebuild statistics](#rebuild-stats)<br/>
6.3. [Backup and Restore](#backup-and-restore-elasticsearch)
7. [Internationalization (i18n)](#i18n)<br/>
7.1. [Translation](#i18n-translation)<br/>
7.1.1. [Front-end translations](#i18n-translation-front)<br/>
7.1.2. [Back-end translations](#i18n-translation-back)<br/>
7.2. [Configuration](#i18n-configuration)<br/>
7.2.1. [Settings](#i18n-settings)<br/>
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)
<a name="software-stack"></a>
## 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
<a name="setup-a-production-environment"></a>
## 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).
<a name="setup-a-development-environment"></a>
## 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).
<a name="general-guidelines"></a>
### 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.
<a name="postgresql"></a>
## PostgreSQL
<a name="setup-postgresql"></a>
### 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).
<a name="elasticsearch"></a>
## 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.
<a name="setup-elasticsearch"></a>
### 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.
<a name="rebuild-stats"></a>
### 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]
```
<a name="backup-and-restore-elasticsearch"></a>
### 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).
<a name="i18n"></a>
## 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.
<a name="i18n-translation"></a>
### Translation
Check the files located in `config/locales`:
- Front app translations (angular.js) are located in `config/locales/app.scope.XX.yml`.
Where scope has one the following meaning :
- admin: translations of the administrator views (manage and configure the FabLab).
- logged: translations of the end-user's views accessible only to connected users.
- public: translation of end-user's views publicly accessible to anyone.
- shared: translations shared by many views (like forms or buttons).
- Back app translations (Ruby on Rails) are located in `config/locales/XX.yml`.
- Emails translations are located in `config/locales/mails.XX.yml`.
- Messages related to the authentication system are located in `config/locales/devise.XX.yml`.
If you plan to translate the application to a new locale, please consider that the reference translation is French.
Indeed, in some cases, the English texts/sentences can seems confuse or lack of context as they were originally translated from French.
To prevent syntax mistakes while translating locale files, we **STRONGLY advise** you to use a text editor which support syntax coloration for YML and Ruby.
<a name="i18n-translation-front"></a>
#### Front-end translations
Front-end translations uses [angular-translate](http://angular-translate.github.io) with some interpolations interpreted by angular.js and other interpreted by [MessageFormat](https://github.com/SlexAxton/messageformat.js/).
**These two kinds of interpolation use a near but different syntax witch SHOULD NOT be confused.**
Please refer to the official [angular-translate documentation](http://angular-translate.github.io/docs/#/guide/14_pluralization) before translating.
<a name="i18n-translation-back"></a>
#### Back-end translations
Back-end translations uses the [Ruby on Rails syntax](http://guides.rubyonrails.org/i18n.html) but some complex interpolations are interpreted by [MessageFormat](https://github.com/format-message/message-format-rb) and are marked as it in comments.
**DO NOT confuse the syntaxes.**
In each cases, some inline comments are included in the localisation files.
They can be recognized as they start with the sharp character (#).
These comments are not required to be translated, they are intended to help the translator to have some context 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.
<a name="i18n-configuration"></a>
### Configuration
Locales configurations are made in `config/application.yml`.
If you are in a development environment, your can keep the default values, otherwise, in production, values must be configured carefully.
<a name="i18n-settings"></a>
#### Settings
Please refer to the [environment configuration documentation](doc/environment.md#internationalization-settings)
<a name="i18n-apply"></a>
#### Applying changes
After modifying any values concerning the localisation, restart the application (ie. web server) to apply these changes in the i18n configuration.
Please refer to the [translation readme](doc/translation_readme.md) for instructions about configuring the language or to contribute to the translation.
<a name="open-projects"></a>
## 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
<a name="known-issues"></a>
## Known issues
- When browsing a machine page, you may encounter an "InterceptError" in the console and the loading bar will stop loading before reaching its ending.
This may happen if the machine was created through a seed file without any image.
To solve this, simply add an image to the machine's profile and refresh the web page.
- 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.
<a name="related-documentation"></a>
## Related Documentation

View File

@ -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

View File

@ -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

View File

@ -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'));
});
}
);

View File

@ -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);
}
]);

View File

@ -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') + '<br><strong>' +
_t('app.admin.calendar.beware_this_cannot_be_reverted') + '</strong>'
};
}
}
@ -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('<span class="remove-event">x&nbsp;</span>');
const eventRenderCb = function (event, element) {
if (event.available_type !== 'event') {
element.find('.fc-content').prepend('<span class="remove-event">x&nbsp;</span>');
}
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 = `<strong>${$scope.selectedTraining.name}</strong>`;
break;
case 'space':
$scope.reservableName = `<strong>${$scope.selectedSpace.name}</strong>`;
break;
default:
$scope.reservableName = `<span class="warning">${_t("app.admin.calendar.none")}</span>`;
}
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 `<span class="text-gray text-italic">${_t("app.admin.calendar.none")}</span>`;
const names = items.map(function (i) { return $sce.trustAsHtml(`<strong>${i.name}</strong>`); });
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);
}
)
}
}
]);

View File

@ -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();
}
};

View File

@ -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');
}
}
]);

View File

@ -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

View File

@ -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 })));
}
};
}

View File

@ -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') }
)
+ '<br/><br/><strong>'
+ _t('invoices.period_must_match_fiscal_year')
+ _t('app.admin.invoices.period_must_match_fiscal_year')
+ '</strong><br/><br/>'
+ _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);
});

View File

@ -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') + '<br/><br/>' + _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') + '<br/><br/>' + _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') + '<br/><br/>' + _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);
}
);
};

View File

@ -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();
}
]);

View File

@ -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');
}
};

View File

@ -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<Object>} 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}>}

View File

@ -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();
}
]);

View File

@ -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: [
`<div id="news">${_t('app.admin.settings.item_news')}</div>`,
`<div id="projects">${_t('app.admin.settings.item_projects')}</div>`,
`<div id="twitter">${_t('app.admin.settings.item_twitter')}</div>`,
`<div id="members">${_t('app.admin.settings.item_members')}</div>`,
`<div id="events">${_t('app.admin.settings.item_events')}</div>`
]
}
$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();
}
}
])

View File

@ -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'));
}
});

View File

@ -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')));
});
}
]);

View File

@ -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();
}
]);

View File

@ -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();
}
}]);

View File

@ -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 += `<span class='label label-success text-white'>${tag.name}</span> `;
}
element.find('.fc-title').append(`<br/>${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 += `<span class='label label-success text-white'>${tag.name}</span> `;
}
element.find('.fc-title').append(`<br/>${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();

View File

@ -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');
}
}
]);

View File

@ -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

View File

@ -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/
*/

View File

@ -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'
});
}

View File

@ -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') + '<br/>' +
'<strong>' + _t('edit_profile.all_data_will_be_lost') + '</strong><br/><br/>' +
_t('edit_profile.invoicing_data_kept') + '<br/>' +
_t('edit_profile.statistic_data_anonymized') + '<br/>' +
_t('edit_profile.no_further_access_to_projects')
_t('app.logged.dashboard.settings.confirm_delete_your_account') + '<br/>' +
'<strong>' + _t('app.logged.dashboard.settings.all_data_will_be_lost') + '</strong><br/><br/>' +
_t('app.logged.dashboard.settings.invoicing_data_kept') + '<br/>' +
_t('app.logged.dashboard.settings.statistic_data_anonymized') + '<br/>' +
_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'));
})
);

View File

@ -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;
}
);

View File

@ -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; });

View File

@ -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'));
}
);
};

View File

@ -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);

View File

@ -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);

View File

@ -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');
}
}
]);

View File

@ -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);
}
);
};
}]);

View File

@ -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}`) });
});
}
};

View File

@ -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();
}
});
}
]);

View File

@ -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();
}
});
}
]);

View File

@ -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();
}
});
}
]);

View File

@ -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);
}
});
}
]);

View File

@ -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();
}
});
}
]);

View File

@ -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); });
};

View File

@ -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) {

View File

@ -0,0 +1,43 @@
/* global twitterFetcher */
/**
* This directive will show the last tweet.
* Usage: <twitter />
*/
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();
}
});
}
]);

View File

@ -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;
}

View File

@ -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; }]
}
});
}

View File

@ -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;
}]);

View File

@ -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,

View File

@ -0,0 +1,10 @@
Application.Services.factory('FabAnalytics', ['$resource', function ($resource) {
return $resource('/api/analytics',
{}, {
data: {
method: 'GET',
url: '/api/analytics/data'
}
}
);
}]);

View File

@ -0,0 +1,5 @@
'use strict';
Application.Services.factory('Ical', ['$resource', function ($resource) {
return $resource('/api/ical/externals');
}]);

View File

@ -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' }
}
}
);
}]);

View File

@ -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;
}
}
}
}
);

View File

@ -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'
}
}
);

View File

@ -1,5 +0,0 @@
'use strict';
Application.Services.factory('Twitter', ['$resource', function ($resource) {
return $resource('/api/feeds/twitter_timelines');
}]);

View File

@ -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; }

View File

@ -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;
}

View File

@ -101,7 +101,6 @@
}
}
body.container{
padding: 0;
}
@ -636,4 +635,4 @@ body.container{
position: absolute;
left: -4px;
}
}
}

View File

@ -89,6 +89,11 @@
}
}
}
> li.menu-spacer {
height: 1px;
margin: 6px 80% 6px 5px;
background: linear-gradient(45deg, black, transparent);
}
ul{
display: none;
}

View File

@ -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;
}
}

View File

@ -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; }

View File

@ -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";

View File

@ -28,4 +28,4 @@ li.abuse {
margin-top: 1em;
}
}
}
}

View File

@ -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;
}

View File

@ -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;
}
}
}
}
}

View File

@ -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; }

View File

@ -7,7 +7,7 @@
</div>
<div class="col-xs-10 col-sm-10 col-md-8 b-l">
<section class="heading-title">
<h1 translate>{{ 'manage_abuses.abuses_list' }}</h1>
<h1 translate>{{ 'app.admin.manage_abuses.abuses_list' }}</h1>
</section>
</div>
</div>
@ -15,27 +15,27 @@
<section class="m-lg">
<div class="row m-b-md">
<span ng-show="abuses.length === 0" translate>{{ 'manage_abuses.no_reports' }}</span>
<span ng-show="abuses.length === 0" translate>{{ 'app.admin.manage_abuses.no_reports' }}</span>
<ul ng-show="abuses.length > 0">
<li class="abuse" ng-repeat="abuse in abuses">
<div class="signaled">
<a ui-sref="app.public.projects_show({id:abuse.signaled.slug})">{{abuse.signaled.name}}</a>,
<span translate>{{ 'manage_abuses.published_by' }}</span>
<span translate>{{ 'app.admin.manage_abuses.published_by' }}</span>
<a ui-sref="app.admin.members_edit({id:abuse.signaled.author.id})">{{abuse.signaled.author.full_name}}</a>,
<span translate>{{ 'manage_abuses.at_date' }}</span>
<span translate>{{ 'app.admin.manage_abuses.at_date' }}</span>
<span>{{abuse.signaled.published_at | amDateFormat:'L' }}</span>
<button class="btn btn-success" ng-click="confirmProcess(abuse.id)">
<i class="fa fa-check"></i>
</button>
</div>
<div class="report">
<span translate>{{ 'manage_abuses.at_date' }}</span>
<span translate>{{ 'app.admin.manage_abuses.at_date' }}</span>
<span>{{abuse.created_at | amDateFormat:'L' }}</span>,
<a href="mailto:{{abuse.email}}">{{abuse.first_name}} {{abuse.last_name}}</a>
<span translate>{{ 'manage_abuses.has_reported' }}</span>
<span translate>{{ 'app.admin.manage_abuses.has_reported' }}</span>
<cite>{{ abuse.message }}</cite>
</div>
</li>
</ul>
</div>
</section>
</section>

View File

@ -7,7 +7,7 @@
</div>
<div class="col-xs-10 col-sm-10 col-md-8 b-l">
<section class="heading-title">
<h1 translate>{{ 'add_an_administrator' }}</h1>
<h1 translate>{{ 'app.admin.admins_new.add_an_administrator' }}</h1>
</section>
</div>
@ -20,12 +20,12 @@
<form role="form" name="adminForm" class="form-horizontal" novalidate>
<section class="panel panel-default bg-light m-lg">
<div class="panel-body m-r">
<ng-include src="'<%= asset_path 'shared/_admin_form.html' %>'"></ng-include>
<ng-include src="'<%= asset_path "shared/_admin_form.html" %>'"></ng-include>
</div>
<div class="panel-footer no-padder">
<input type="submit" value="{{ 'save' | translate}}" class="r-b btn-valid btn btn-warning btn-block p-lg btn-lg text-u-c" ng-click="saveAdmin()" ng-disabled="adminForm.$invalid"/>
<input type="submit" value="{{ 'app.shared.buttons.save' | translate}}" class="r-b btn-valid btn btn-warning btn-block p-lg btn-lg text-u-c" ng-click="saveAdmin()" ng-disabled="adminForm.$invalid"/>
</div>
</section>
</form>

View File

@ -1,16 +1,16 @@
<div class="modal-header">
<h3 class="modal-title"><span translate>{{ 'data_mapping' }}</span> : {{field.local_field}}</h3>
<h3 class="modal-title"><span translate>{{ 'app.shared.authentication.data_mapping' }}</span> : {{field.local_field}}</h3>
</div>
<div class="modal-body m-lg">
<div>
<span translate>{{ 'expected_data_type' }}</span> : {{datatype}}
<span translate>{{ 'app.shared.authentication.expected_data_type' }}</span> : {{datatype}}
</div>
<form name="mappingForm" class="m-t-md">
<ng-switch on="datatype">
<!-- BOOLEAN -->
<div ng-switch-when="boolean">
<label for="add_mapping" translate>{{ 'mappings' }}</label>
<label for="add_mapping" translate>{{ 'app.shared.authentication.mappings' }}</label>
<ul class="list-unstyled">
<li class="m-t-sm m-l">
<input type="text"
@ -35,7 +35,7 @@
<!-- DATE -->
<div ng-switch-when="date">
<label for="date_format" translate>{{ 'input_format' }}</label>
<label for="date_format" translate>{{ 'app.shared.authentication.input_format' }}</label>
<select name="date_format"
id="date_format"
class="form-control"
@ -46,7 +46,7 @@
<!-- INTEGER -->
<div ng-switch-when="integer">
<label for="add_mapping" translate>{{ 'mappings' }}</label>
<label for="add_mapping" translate>{{ 'app.shared.authentication.mappings' }}</label>
<button class="btn btn-default pull-right" ng-click="addIntegerMapping()"><i class="fa fa-plus"></i></button>
<ul class="list-unstyled">
<li ng-repeat="map in transformation.rules.mapping" class="m-t-sm m-l">
@ -60,6 +60,6 @@
</form>
</div>
<div class="modal-footer">
<button class="btn btn-primary" ng-click="ok()" ng-disabled="!mappingForm.$valid" ng-if="datatype != 'string' && datatype != 'text'" translate>{{ 'confirm' }}</button>
<button class="btn btn-warning" ng-click="cancel()" translate>{{ 'cancel' }}</button>
</div>
<button class="btn btn-primary" ng-click="ok()" ng-disabled="!mappingForm.$valid" ng-if="datatype != 'string' && datatype != 'text'" translate>{{ 'app.shared.buttons.confirm' }}</button>
<button class="btn btn-warning" ng-click="cancel()" translate>{{ 'app.shared.buttons.cancel' }}</button>
</div>

View File

@ -1,5 +1,5 @@
<div class="form-group" ng-class="{'has-error': providerForm['auth_provider[name]'].$dirty && providerForm['auth_provider[name]'].$invalid}">
<label for="provider_name" class="col-sm-3 control-label" translate>{{ 'name' }}</label>
<label for="provider_name" class="col-sm-3 control-label" translate>{{ 'app.shared.authentication.name' }}</label>
<div class="col-sm-9">
<input type="text"
ng-model="provider.name"
@ -8,23 +8,23 @@
id="provider_name"
ng-disabled="mode == 'edition'"
required />
<span class="help-block" ng-show="providerForm['auth_provider[name]'].$dirty && providerForm['auth_provider[name]'].$error.required" translate>{{ 'provider_name_is_required' }}</span>
<span class="help-block" ng-show="providerForm['auth_provider[name]'].$dirty && providerForm['auth_provider[name]'].$error.required" translate>{{ 'app.shared.authentication.provider_name_is_required' }}</span>
</div>
</div>
<div class="form-group" ng-class="{'has-error': providerForm['auth_provider[providable_type]'].$dirty && providerForm['auth_provider[providable_type]'].$invalid}">
<label for="provider_type" class="col-sm-3 control-label" translate>{{ 'authentication_type' }}</label>
<label for="provider_type" class="col-sm-3 control-label" translate>{{ 'app.shared.authentication.authentication_type' }}</label>
<div class="col-sm-9">
<select ng-model="provider.providable_type"
ng-change="updateProvidable()"
class="form-control"
name="auth_provider[providable_type]"
id="provider_type"
ng-options="key as (value | translate) for (key, value) in authMethods"
ng-options="key as methodName(key) for (key, value) in authMethods"
ng-disabled="mode == 'edition'"
required>
</select>
<input type="hidden" name="auth_provider[type]" ng-value="provider.type" />
<span class="help-block" ng-show="providerForm['auth_provider[providable_type]'].$dirty && providerForm['auth_provider[providable_type]'].$error.required" translate>{{ 'authentication_type_is_required' }}</span>
<span class="help-block" ng-show="providerForm['auth_provider[providable_type]'].$dirty && providerForm['auth_provider[providable_type]'].$error.required" translate>{{ 'app.shared.authentication.authentication_type_is_required' }}</span>
</div>
</div>
</div>

View File

@ -1,7 +1,7 @@
<hr/>
<div class="form-group" ng-class="{'has-error': providerForm['auth_provider[base_url]'].$dirty && providerForm['auth_provider[base_url]'].$invalid}">
<label for="provider_base_url" class="col-sm-3 control-label" translate>{{ 'common_url' }}</label>
<label for="provider_base_url" class="col-sm-3 control-label" translate>{{ 'app.shared.oauth2.common_url' }}</label>
<div class="col-sm-9">
<input type="text"
ng-model="provider.providable_attributes.base_url"
@ -11,13 +11,13 @@
placeholder="https://sso.example.net..."
required
url>
<span class="help-block" ng-show="providerForm['auth_provider[base_url]'].$dirty && providerForm['auth_provider[base_url]'].$error.required" translate>{{ 'common_url_is_required' }}</span>
<span class="help-block" ng-show="providerForm['auth_provider[base_url]'].$error.url" translate>{{ 'provided_url_is_not_a_valid_url' }}</span>
<span class="help-block" ng-show="providerForm['auth_provider[base_url]'].$dirty && providerForm['auth_provider[base_url]'].$error.required" translate>{{ 'app.shared.oauth2.common_url_is_required' }}</span>
<span class="help-block" ng-show="providerForm['auth_provider[base_url]'].$error.url" translate>{{ 'app.shared.oauth2.provided_url_is_not_a_valid_url' }}</span>
</div>
</div>
<div class="form-group" ng-class="{'has-error': providerForm['auth_provider[authorization_endpoint]'].$dirty && providerForm['auth_provider[authorization_endpoint]'].$invalid}">
<label for="provider_authorization_endpoint" class="col-sm-3 control-label" translate>{{ 'authorization_endpoint' }}</label>
<label for="provider_authorization_endpoint" class="col-sm-3 control-label" translate>{{ 'app.shared.oauth2.authorization_endpoint' }}</label>
<div class="col-sm-9">
<input type="text"
ng-model="provider.providable_attributes.authorization_endpoint"
@ -27,13 +27,13 @@
placeholder="/oauth2/auth..."
required
endpoint>
<span class="help-block" ng-show="providerForm['auth_provider[authorization_endpoint]'].$dirty && providerForm['auth_provider[authorization_url]'].$error.required" translate>{{ 'oauth2_authorization_endpoint_is_required' }}</span>
<span class="help-block" ng-show="providerForm['auth_provider[authorization_endpoint]'].$error.endpoint" translate>{{ 'provided_endpoint_is_not_valid' }}</span>
<span class="help-block" ng-show="providerForm['auth_provider[authorization_endpoint]'].$dirty && providerForm['auth_provider[authorization_url]'].$error.required" translate>{{ 'app.shared.oauth2.oauth2_authorization_endpoint_is_required' }}</span>
<span class="help-block" ng-show="providerForm['auth_provider[authorization_endpoint]'].$error.endpoint" translate>{{ 'app.shared.oauth2.provided_endpoint_is_not_valid' }}</span>
</div>
</div>
<div class="form-group" ng-class="{'has-error': providerForm['auth_provider[token_endpoint]'].$dirty && providerForm['auth_provider[token_endpoint]'].$invalid}">
<label for="provider_token_endpoint" class="col-sm-3 control-label" translate>{{ 'token_acquisition_endpoint' }}</label>
<label for="provider_token_endpoint" class="col-sm-3 control-label" translate>{{ 'app.shared.oauth2.token_acquisition_endpoint' }}</label>
<div class="col-sm-9">
<input type="text"
ng-model="provider.providable_attributes.token_endpoint"
@ -43,13 +43,13 @@
placeholder="/oauth2/token..."
required
endpoint>
<span class="help-block" ng-show="providerForm['auth_provider[token_endpoint]'].$dirty && providerForm['auth_provider[token_endpoint]'].$error.required" translate>{{ 'oauth2_token_acquisition_endpoint_is_required' }}</span>
<span class="help-block" ng-show="providerForm['auth_provider[token_endpoint]'].$error.endpoint" translate>{{ 'provided_endpoint_is_not_valid' }}</span>
<span class="help-block" ng-show="providerForm['auth_provider[token_endpoint]'].$dirty && providerForm['auth_provider[token_endpoint]'].$error.required" translate>{{ 'app.shared.oauth2.oauth2_token_acquisition_endpoint_is_required' }}</span>
<span class="help-block" ng-show="providerForm['auth_provider[token_endpoint]'].$error.endpoint" translate>{{ 'app.shared.oauth2.provided_endpoint_is_not_valid' }}</span>
</div>
</div>
<div class="form-group" ng-class="{'has-error': providerForm['auth_provider[profile_url]'].$dirty && providerForm['auth_provider[profile_url]'].$invalid}">
<label for="provider_profile_url" class="col-sm-3 control-label" translate>{{ 'profil_edition_url' }}</label>
<label for="provider_profile_url" class="col-sm-3 control-label" translate>{{ 'app.shared.oauth2.profil_edition_url' }}</label>
<div class="col-sm-9">
<input type="text"
ng-model="provider.providable_attributes.profile_url"
@ -59,13 +59,13 @@
placeholder="https://exemple.net/user..."
required
url>
<span class="help-block" ng-show="providerForm['auth_provider[profile_url]'].$dirty && providerForm['auth_provider[profile_url]'].$error.required" translate>{{ 'profile_edition_url_is_required' }}</span>
<span class="help-block" ng-show="providerForm['auth_provider[profile_url]'].$error.url" translate>{{ 'provided_url_is_not_a_valid_url' }}</span>
<span class="help-block" ng-show="providerForm['auth_provider[profile_url]'].$dirty && providerForm['auth_provider[profile_url]'].$error.required" translate>{{ 'app.shared.oauth2.profile_edition_url_is_required' }}</span>
<span class="help-block" ng-show="providerForm['auth_provider[profile_url]'].$error.url" translate>{{ 'app.shared.oauth2.provided_url_is_not_a_valid_url' }}</span>
</div>
</div>
<div class="form-group" ng-class="{'has-error': providerForm['auth_provider[client_id]'].$dirty && providerForm['auth_provider[client_id]'].$invalid}">
<label for="provider_client_id" class="col-sm-3 control-label" translate>{{ 'client_identifier' }}</label>
<label for="provider_client_id" class="col-sm-3 control-label" translate>{{ 'app.shared.oauth2.client_identifier' }}</label>
<div class="col-sm-9">
<input type="text"
ng-model="provider.providable_attributes.client_id"
@ -73,12 +73,12 @@
name="auth_provider[client_id]"
id="provider_client_id"
required>
<span class="help-block" ng-show="providerForm['auth_provider[client_id]'].$dirty && providerForm['auth_provider[client_id]'].$error.required" translate>{{ 'oauth2_client_identifier_is_required' }} {{ 'obtain_it_when_registering_with_your_provider' }}</span>
<span class="help-block" ng-show="providerForm['auth_provider[client_id]'].$dirty && providerForm['auth_provider[client_id]'].$error.required" translate>{{ 'app.shared.oauth2.oauth2_client_identifier_is_required' }} {{ 'obtain_it_when_registering_with_your_provider' }}</span>
</div>
</div>
<div class="form-group" ng-class="{'has-error': providerForm['auth_provider[client_secret]'].$dirty && providerForm['auth_provider[client_secret]'].$invalid}">
<label for="provider_client_secret" class="col-sm-3 control-label" translate>{{ 'client_secret' }}</label>
<label for="provider_client_secret" class="col-sm-3 control-label" translate>{{ 'app.shared.oauth2.client_secret' }}</label>
<div class="col-sm-9">
<input type="text"
ng-model="provider.providable_attributes.client_secret"
@ -86,8 +86,8 @@
name="auth_provider[client_secret]"
id="provider_client_secret"
required>
<span class="help-block" ng-show="providerForm['auth_provider[client_secret]'].$dirty && providerForm['auth_provider[client_secret]'].$error.required" translate>{{ 'oauth2_client_secret_is_required' }} {{ 'obtain_it_when_registering_with_your_provider' }}</span>
<span class="help-block" ng-show="providerForm['auth_provider[client_secret]'].$dirty && providerForm['auth_provider[client_secret]'].$error.required" translate>{{ 'app.shared.oauth2.oauth2_client_secret_is_required' }} {{ 'obtain_it_when_registering_with_your_provider' }}</span>
</div>
</div>
<ng-include src="'<%= asset_path 'admin/authentications/_oauth2_mapping.html' %>'"></ng-include>
<ng-include src="'<%= asset_path "admin/authentications/_oauth2_mapping.html" %>'"></ng-include>

View File

@ -1,15 +1,15 @@
<h4 class="m-l m-t-xl" translate>{{ 'define_the_fields_mapping' }}</h4>
<h4 class="m-l m-t-xl" translate>{{ 'app.shared.oauth2.define_the_fields_mapping' }}</h4>
<button type="button" class="btn btn-success m-l m-b" ng-click="newMapping = {}"><i class="fa fa-plus"></i> {{ 'add_a_match' | translate }}</button>
<button type="button" class="btn btn-success m-l m-b" ng-click="newMapping = {}"><i class="fa fa-plus"></i> {{ 'app.shared.oauth2.add_a_match' | translate }}</button>
<table class="table">
<thead>
<tr>
<th translate>{{ 'model' }}</th>
<th translate>{{ 'field' }}</th>
<th translate>{{ 'api_endpoint_url' }}</th>
<th translate>{{ 'api_type' }}</th>
<th translate>{{ 'api_fields' }}</th>
<th translate>{{ 'app.shared.oauth2.model' }}</th>
<th translate>{{ 'app.shared.oauth2.field' }}</th>
<th translate>{{ 'app.shared.oauth2.api_endpoint_url' }}</th>
<th translate>{{ 'app.shared.oauth2.api_type' }}</th>
<th translate>{{ 'app.shared.oauth2.api_fields' }}</th>
<th style="width: 6.4em;"></th>
</tr>
</thead>
@ -76,4 +76,4 @@
</td>
</tr>
</tbody>
</table>
</table>

View File

@ -9,7 +9,7 @@
</div>
<div class="col-md-8 b-l b-r">
<section class="heading-title">
<h1>{{ 'provider' | translate }} {{provider.name}}</h1>
<h1>{{ 'app.admin.authentication_edit.provider' | translate }} {{provider.name}}</h1>
</section>
</div>
@ -17,7 +17,7 @@
<div class="col-md-3">
<section class="heading-actions wrapper">
<div class="btn btn-lg btn-block btn-default m-t-xs" ng-click="cancel()" translate>
{{ 'cancel' }}
{{ 'app.shared.buttons.cancel' }}
</div>
</section>
@ -35,13 +35,13 @@
<section class="panel panel-default bg-light m-lg">
<div class="panel-body m-r">
<ng-include src="'<%= asset_path 'admin/authentications/_form.html' %>'"></ng-include>
<ng-include src="'<%= asset_path 'admin/authentications/_oauth2.html'%>'" ng-if="provider.providable_type == 'OAuth2Provider'"></ng-include>
<ng-include src="'<%= asset_path "admin/authentications/_form.html" %>'"></ng-include>
<ng-include src="'<%= asset_path "admin/authentications/_oauth2.html"%>'" ng-if="provider.providable_type == 'OAuth2Provider'"></ng-include>
</div> <!-- ./panel-body -->
<div class="panel-footer no-padder">
<input type="button" value="{{ 'confirm_changes' | translate }}" class="r-b btn-valid btn btn-warning btn-block p-lg btn-lg text-u-c" ng-disabled="providerForm.$invalid" ng-click="updateProvider()"/>
<input type="button" value="{{ 'app.shared.buttons.confirm_changes' | translate }}" class="r-b btn-valid btn btn-warning btn-block p-lg btn-lg text-u-c" ng-disabled="providerForm.$invalid" ng-click="updateProvider()"/>
</div>
</section>
</form>

View File

@ -2,23 +2,23 @@
<div class="form-group">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-filter"></i></span>
<input type="text" ng-model="searchFilter" class="form-control" placeholder="{{ 'search_for_an_authentication_provider' | translate }}">
<input type="text" ng-model="searchFilter" class="form-control" placeholder="{{ 'app.admin.members.authentication_form.search_for_an_authentication_provider' | translate }}">
</div>
</div>
</div>
<div class="col-md-12">
<button type="button" class="btn btn-warning m-t m-b" ui-sref="app.admin.authentication_new" translate>{{ 'add_a_new_authentication_provider' }}</button>
<button type="button" class="btn btn-warning m-t m-b" ui-sref="app.admin.authentication_new" translate>{{ 'app.admin.members.authentication_form.add_a_new_authentication_provider' }}</button>
<table class="table">
<thead>
<tr>
<th style="width:15%" translate>{{ 'name' }}</th>
<th style="width:15%" translate>{{ 'app.admin.members.authentication_form.name' }}</th>
<th style="width:15%" translate>{{ 'strategy_name' }}</th>
<th style="width:15%" translate>{{ 'app.admin.members.authentication_form.strategy_name' }}</th>
<th style="width:15%" translate>{{ 'type' }}</th>
<th style="width:15%" translate>{{ 'app.admin.members.authentication_form.type' }}</th>
<th style="width:15%" translate>{{ 'state' }}</th>
<th style="width:15%" translate>{{ 'app.admin.members.authentication_form.state' }}</th>
<th style="width:10%"></th>
</tr>
@ -31,7 +31,7 @@
<td>{{ getState(provider.status) }}</td>
<td>
<button class="btn btn-default" ui-sref="app.admin.authentication_edit({id:provider.id})">
<i class="fa fa-pencil-square-o"></i> {{ 'edit' | translate }}
<i class="fa fa-pencil-square-o"></i> {{ 'app.shared.buttons.edit' | translate }}
</button>
<button class="btn btn-danger" ng-if="provider.status != 'active'" ng-click="destroyProvider(providers, provider)">
<i class="fa fa-trash-o"></i>
@ -40,4 +40,4 @@
</tr>
</tbody>
</table>
</div>
</div>

View File

@ -9,7 +9,7 @@
</div>
<div class="col-md-8 b-l b-r">
<section class="heading-title">
<h1 translate>{{ 'add_a_new_authentication_provider' }}</h1>
<h1 translate>{{ 'app.admin.authentication_new.add_a_new_authentication_provider' }}</h1>
</section>
</div>
@ -17,7 +17,7 @@
<div class="col-md-3">
<section class="heading-actions wrapper">
<div class="btn btn-lg btn-block btn-default m-t-xs" ng-click="cancel()" translate>
{{ 'cancel' }}
{{ 'app.shared.buttons.cancel' }}
</div>
</section>
@ -36,13 +36,13 @@
<section class="panel panel-default bg-light m-lg">
<div class="panel-body m-r">
<ng-include src="'<%= asset_path 'admin/authentications/_form.html' %>'"></ng-include>
<ng-include src="'<%= asset_path 'admin/authentications/_oauth2.html'%>'" ng-if="provider.providable_type == 'OAuth2Provider'"></ng-include>
<ng-include src="'<%= asset_path "admin/authentications/_form.html" %>'"></ng-include>
<ng-include src="'<%= asset_path "admin/authentications/_oauth2.html" %>'" ng-if="provider.providable_type == 'OAuth2Provider'"></ng-include>
</div> <!-- ./panel-body -->
<div class="panel-footer no-padder">
<input type="button" value="{{ 'save' | translate }}" class="r-b btn-valid btn btn-warning btn-block p-lg btn-lg text-u-c" ng-disabled="providerForm.$invalid" ng-click="registerProvider()"/>
<input type="button" value="{{ 'app.shared.buttons.save' | translate }}" class="r-b btn-valid btn btn-warning btn-block p-lg btn-lg text-u-c" ng-disabled="providerForm.$invalid" ng-click="registerProvider()"/>
</div>
</section>
</form>

View File

@ -5,17 +5,17 @@
<a href="#" ng-click="backPrevLocation($event)"><i class="fa fa-long-arrow-left "></i></a>
</section>
</div>
<div class="col-xs-10 col-sm-10 col-md-8 b-l b-r-md">
<div class="col-md-8 b-l b-r-md">
<section class="heading-title">
<h1 translate>{{ 'admin_calendar.calendar_management' }}</h1>
<h1 translate>{{ 'app.admin.calendar.calendar_management' }}</h1>
</section>
</div>
<div class="col-xs-12 col-sm-12 col-md-3 b-t hide-b-md">
<section class="heading-actions wrapper" ng-class="{'p-s': !fablabWithoutSpaces}">
<span class="badge text-sm bg-formation" ng-class="{'m-t-sm': fablabWithoutSpaces}" translate>{{ 'admin_calendar.trainings' }}</span><br>
<span class="badge text-sm bg-machine" translate>{{ 'admin_calendar.machines' }}</span><br>
<span class="badge text-sm bg-space" ng-hide="fablabWithoutSpaces" translate>{{ 'admin_calendar.spaces' }}</span>
<div class="col-md-3">
<section class="heading-actions wrapper">
<a role="button" ui-sref="app.admin.calendar.icalendar" class="btn btn-lg btn-default rounded m-t-sm text-sm import-ics-button">
<i class="fa fa-exchange m-r" aria-hidden="true"></i><span translate>{{ 'app.admin.calendar.external_sync' }}</span>
</a>
</section>
</div>
@ -23,29 +23,44 @@
</section>
<section class="row no-gutter">
<section class="row no-gutter admin-calendar"
ui-tour="calendar"
ui-tour-backdrop="true"
ui-tour-template-url="'<%= asset_path "shared/tour-step-template.html" %>'"
ui-tour-use-hotkeys="true"
ui-tour-scroll-parent-id="content-main"
post-render="setupCalendarTour">
<div class="col-sm-12 col-md-12 col-lg-9">
<div ui-calendar="calendarConfig" ng-model="eventSources" calendar="calendar" class="wrapper-lg"></div>
<div class="calendar-legend-block">
<h4 translate>{{ 'app.admin.calendar.legend' }}</h4>
<div class="legends">
<span class="calendar-legend text-sm border-formation" translate>{{ 'app.admin.calendar.trainings' }}</span><br>
<span class="calendar-legend text-sm border-machine" translate>{{ 'app.admin.calendar.machines' }}</span><br>
<span class="calendar-legend text-sm border-space" ng-hide="fablabWithoutSpaces" translate>{{ 'app.admin.calendar.spaces' }}</span>
<span class="calendar-legend text-sm border-event" ng-show="eventsInCalendar" translate>{{ 'app.admin.calendar.events' }}</span>
</div>
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-3">
<div class="m text-center">
<a class="btn btn-default"
<a class="btn btn-default export-xls-button"
ng-href="api/availabilities/export_index.xlsx"
target="export-frame"
ng-click="alertExport('index')"
uib-popover="{{ 'admin_calendar.availabilities_notice' | translate}}"
uib-popover="{{ 'app.admin.calendar.availabilities_notice' | translate}}"
popover-trigger="mouseenter"
popover-placement="bottom">
<i class="fa fa-file-excel-o"></i> {{ 'admin_calendar.availabilities' | translate }}
<i class="fa fa-file-excel-o"></i> {{ 'app.admin.calendar.availabilities' | translate }}
</a>
<iframe name="export-frame" height="0" width="0" class="none"></iframe>
</div>
<div class="widget panel b-a m m-t-lg" ng-if="availability">
<div class="widget panel b-a m m-t-lg" ng-if="availability" ng-hide="availability.available_type == 'event'">
<div class="panel-heading b-b small">
<h3 translate>{{ 'admin_calendar.ongoing_reservations' }}</h3>
<h3 translate>{{ 'app.admin.calendar.ongoing_reservations' }}</h3>
</div>
<div class="widget-content no-bg auto wrapper" ng-class="{'reservations-locked': availability.lock}">
<ul class="list-unstyled" ng-if="reservations.length > 0">
@ -56,14 +71,14 @@
<span class="btn btn-warning btn-xs" ng-click="cancelBooking(r)" ng-if="!r.canceled_at"><i class="fa fa-times red"></i></span>
</li>
</ul>
<div ng-show="reservations.length == 0" translate>{{ 'admin_calendar.no_reservations' }}</div>
<div class="m-t" ng-show="availability.lock"><i class="fa fa-ban"/> <span class="m-l-xs" translate>{{ 'admin_calendar.reservations_locked' }}</span></div>
<div ng-show="reservations.length == 0" translate>{{ 'app.admin.calendar.no_reservations' }}</div>
<div class="m-t" ng-show="availability.lock"><i class="fa fa-ban"></i> <span class="m-l-xs" translate>{{ 'app.admin.calendar.reservations_locked' }}</span></div>
</div>
</div>
<div class="widget panel b-a m m-t-lg" ng-if="availability.machine_ids.length > 0">
<div class="panel-heading b-b small">
<h3 translate>{{ 'admin_calendar.machines' }}</h3>
<h3 translate>{{ 'app.admin.calendar.machines' }}</h3>
</div>
<div class="widget-content no-bg auto wrapper">
<ul class="list-unstyled">
@ -75,29 +90,62 @@
</div>
</div>
<div class="widget panel b-a m m-t-lg" ng-if="availability">
<div class="widget panel b-a m m-t-lg" ng-if="availability.plan_ids.length > 0">
<div class="panel-heading b-b small">
<h3 translate>{{ 'admin_calendar.actions' }}</h3>
<h3 translate>{{ 'app.admin.calendar.plans' }}</h3>
</div>
<div class="widget-content no-bg auto wrapper">
<ul class="list-unstyled">
<li ng-repeat="g in availability.plans" class="m-b-xs">
<div class="font-sbold">{{::g.name}}</div>
<ul class="m-n" ng-repeat="plan in g.plans">
<li>
{{::plan.name}}
<span class="btn btn-warning btn-xs" ng-click="removePlan(plan)" ><i class="fa fa-times red"></i></span>
</li>
</ul>
</li>
</ul>
</div>
</div>
<div class="widget panel b-a m m-t-lg" ng-if="availability" >
<div class="panel-heading b-b small">
<h3 translate>{{ 'app.admin.calendar.actions' }}</h3>
</div>
<div class="widget-content no-bg auto wrapper" ng-hide="availability.available_type == 'event'">
<button class="btn btn-default" ng-click="toggleLockReservations()">
<span ng-hide="availability.lock">
<i class="fa fa-stop" />
<span class="m-l-xs" translate>{{ 'admin_calendar.block_reservations' }}</span>
<i class="fa fa-stop"></i>
<span class="m-l-xs" translate>{{ 'app.admin.calendar.block_reservations' }}</span>
</span>
<span ng-show="availability.lock">
<i class="fa fa-play" />
<span class="m-l-xs" translate>{{ 'admin_calendar.allow_reservations' }}</span>
<i class="fa fa-play"></i>
<span class="m-l-xs" translate>{{ 'app.admin.calendar.allow_reservations' }}</span>
</span>
</button>
<button class="btn btn-default m-t" ng-click="removeSlot()">
<span>
<i class="fa fa-trash" />
<span class="m-l-xs" translate>{{ 'admin_calendar.delete_slot' }}</span>
<i class="fa fa-trash"></i>
<span class="m-l-xs" translate>{{ 'app.admin.calendar.delete_slot' }}</span>
</span>
</button>
</div>
<div class="widget-content no-bg auto wrapper" ng-show="availability.available_type == 'event'">
<a class="btn btn-default pointer" ui-sref="app.admin.events_edit({id: availability.event_id})">
<span>
<i class="fa fa-edit"></i>
<span class="m-l-xs" translate>{{ 'app.admin.calendar.edit_event' }}</span>
</span>
</a>
<a class="btn btn-default m-t pointer" ui-sref="app.admin.event_reservations({id: availability.event_id})">
<span>
<i class="fa fa-bookmark"></i>
<span class="m-l-xs" translate>{{ 'app.admin.calendar.view_reservations' }}</span>
</span>
</a>
</div>
</div>
</div>
</section>
</section>

View File

@ -0,0 +1,26 @@
<div class="modal-header">
<img ng-src="{{logoBlack.custom_asset_file_attributes.attachment_url}}" alt="{{logo.custom_asset_file_attributes.attachment}}" class="modal-logo"/>
<h1 translate>{{ 'app.admin.calendar.confirmation_required' }}</h1>
</div>
<div class="modal-body">
<p ng-hide="isRecurrent" translate>{{ 'app.admin.calendar.do_you_really_want_to_delete_this_slot' }}</p>
<p ng-show="isRecurrent" translate>{{ 'app.admin.calendar.delete_recurring_slot' }}</p>
<div ng-show="isRecurrent" class="form-group">
<label class="checkbox">
<input type="radio" name="delete_mode" ng-model="deleteMode" value="single" required/>
<span translate>{{ 'app.admin.calendar.delete_this_slot' }}</span>
</label>
<label class="checkbox">
<input type="radio" name="delete_mode" ng-model="deleteMode" value="next" required/>
<span translate>{{ 'app.admin.calendar.delete_this_and_next' }}</span>
</label>
<label class="checkbox">
<input type="radio" name="delete_mode" ng-model="deleteMode" value="all" required/>
<span translate>{{ 'app.admin.calendar.delete_all' }}</span>
</label>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-info" ng-click="ok()" translate>{{ 'app.shared.buttons.delete' }}</button>
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'app.shared.buttons.cancel' }}</button>
</div>

View File

@ -1,27 +1,27 @@
<div class="modal-header">
<h3 class="text-center red">
{{ 'admin_calendar.DATE_slot' | translate:{DATE:(start | amDateFormat: 'LL')} }} {{start | amDateFormat:'LT'}} - {{end | amDateFormat:'LT'}}
{{ 'app.admin.calendar.DATE_slot' | translate:{DATE:(start | amDateFormat: 'LL')} }} {{start | amDateFormat:'LT'}} - {{end | amDateFormat:'LT'}}
</h3>
</div>
<div class="modal-body" ng-show="step === 1">
<label class="m-t-sm" translate>{{ 'admin_calendar.what_kind_of_slot_do_you_want_to_create' }}</label>
<label class="m-t-sm" translate>{{ 'app.admin.calendar.what_kind_of_slot_do_you_want_to_create' }}</label>
<div class="form-group">
<div class="radio">
<label>
<input type="radio" id="training" name="available_type" value="training" ng-model="availability.available_type">
<span translate>{{ 'admin_calendar.training' }}</span>
<span translate>{{ 'app.admin.calendar.training' }}</span>
</label>
</div>
<div class="radio">
<label>
<input type="radio" id="machine" name="available_type" value="machines" ng-model="availability.available_type">
<span translate>{{ 'admin_calendar.machine' }}</span>
<span translate>{{ 'app.admin.calendar.machine' }}</span>
</label>
</div>
<div class="radio" ng-hide="fablabWithoutSpaces">
<label>
<input type="radio" id="space" name="available_type" value="space" ng-model="availability.available_type" ng-disabled="spaces.length === 0">
<span translate>{{ 'admin_calendar.space' }}</span>
<span translate>{{ 'app.admin.calendar.space' }}</span>
</label>
</div>
</div>
@ -29,13 +29,17 @@
<div class="modal-body" ng-show="step === 2">
<div ng-show="availability.available_type == 'machines'">
<p class="text-center font-sbold m-t-sm">{{ 'admin_calendar.select_some_machines' | translate }}</p>
<p class="text-center font-sbold m-t-sm">{{ 'app.admin.calendar.select_some_machines' | translate }}</p>
<div class="form-group m-l-xl">
<button class="btn btn-default pull-right m-t-n-xl" ng-click="toggleAll()" translate>{{ selectedMachines.length == 0 ? 'app.admin.calendar.select_all' : 'app.admin.calendar.select_none' }}</button>
<label class="checkbox" ng-repeat="machine in machines">
<input type="checkbox" ng-click="toggleSelection(machine)"> {{machine.name}} <span class="text-xs">(id {{machine.id}})</span>
<input type="checkbox" ng-click="toggleSelection(machine)" ng-model="selectedMachinesBinding[machine.id]"> {{machine.name}} <span class="text-xs">(id {{machine.id}})</span>
</label>
</div>
<div class="alert alert-info m-b-none text-xs wrapper-sm">
<i class="fa fa-lightbulb-o m-r" aria-hidden="true"></i> <a ui-sref="app.public.machines_list" ng-click="cancel()" translate> {{ 'app.admin.calendar.manage_machines' }}</a>
</div>
</div>
<div ng-show="availability.available_type == 'training'">
@ -43,12 +47,15 @@
</select>
<div class="row m-t">
<div class="form-group">
<label class="col-sm-6 control-label" for="nb_places_training" translate>{{ 'admin_calendar.number_of_tickets' }}</label>
<label class="col-sm-6 control-label" for="nb_places_training" translate>{{ 'app.admin.calendar.number_of_tickets' }}</label>
<div class="col-sm-6">
<input type="number" id="nb_places_training" class="form-control" ng-model="availability.nb_total_places">
</div>
</div>
</div>
<div class="alert alert-info m-b-none text-xs wrapper-sm m-t">
<i class="fa fa-lightbulb-o m-r" aria-hidden="true"></i> <a ui-sref="app.public.trainings_list" ng-click="cancel()" translate> {{ 'app.admin.calendar.manage_trainings' }}</a>
</div>
</div>
<div ng-show="availability.available_type == 'space'">
@ -56,29 +63,37 @@
</select>
<div class="row m-t">
<div class="form-group">
<label class="col-sm-6 control-label" for="nb_places_space" translate>{{ 'admin_calendar.number_of_tickets' }}</label>
<label class="col-sm-6 control-label" for="nb_places_space" translate>{{ 'app.admin.calendar.number_of_tickets' }}</label>
<div class="col-sm-6">
<input type="number" id="nb_places_space" class="form-control" ng-model="availability.nb_total_places">
</div>
</div>
</div>
<div class="alert alert-info m-b-none text-xs wrapper-sm m-t">
<i class="fa fa-lightbulb-o m-r" aria-hidden="true"></i> <a ui-sref="app.public.spaces_list" ng-click="cancel()" translate> {{ 'app.admin.calendar.manage_spaces' }}</a>
</div>
</div>
</div>
<div class="modal-body" ng-show="step === 3">
<div id="timeAdjust" class="m-t-sm">
<p class="text-center font-sbold" translate>{{ 'admin_calendar.adjust_the_opening_hours' }}</p>
<p class="text-center font-sbold" translate>{{ 'app.admin.calendar.adjust_the_opening_hours' }}</p>
<div class="row">
<div class="col-md-3 col-md-offset-2">
<uib-timepicker ng-model="start" hour-step="timepickers.start.hstep" readonly-input="true" minute-step="timepickers.start.mstep" show-meridian="false"></uib-timepicker>
</div>
<span class="col-md-1 m-t-xl m-l" translate>{{ 'admin_calendar.to_time' }}</span>
<span class="col-md-1 m-t-xl m-l" translate>{{ 'app.admin.calendar.to_time' }}</span>
<fieldset ng-disabled="endDateReadOnly" class="col-md-5">
<uib-timepicker ng-model="end" hour-step="timepickers.end.hstep" readonly-input="true" minute-step="timepickers.end.mstep" show-meridian="false"></uib-timepicker>
</fieldset>
</div>
</div>
</div>
<div class="modal-body" ng-show="step === 4">
<div class="m-t-sm">
<p class="text-center font-sbold" translate>{{ 'app.admin.calendar.restrict_options' }}</p>
</div>
<div id="tagAssociate" class="m-t-lg">
<p class="text-center font-sbold" translate>{{ 'admin_calendar.restrict_this_slot_with_labels_optional' }}</p>
<p class="text-center font-sbold" translate>{{ 'app.admin.calendar.restrict_with_labels' }}</p>
<div class="row">
<div class="col-sm-12">
<ui-select multiple ng-model="availability.tag_ids" class="form-control">
@ -92,14 +107,124 @@
</div>
</div>
</div>
<div class="m-t-sm">
<p class="text-center font-sbold" translate>{{ 'app.admin.calendar.restrict_for_subscriptions' }}</p>
<div class="row">
<div class="form-group col-md-12">
<label for="is_only_subscriptions" translate>{{ 'app.admin.calendar.enabled' }}</label>
<input bs-switch
ng-model="isOnlySubscriptions"
id="is_only_subscriptions"
type="checkbox"
class="form-control"
switch-on-text="{{ 'app.shared.buttons.yes' | translate }}"
switch-off-text="{{ 'app.shared.buttons.no' | translate }}"
switch-animate="true"/>
</div>
<div class="col-md-12" ng-show="isOnlySubscriptions">
<p class="font-sbold m-t-sm">{{ 'app.admin.calendar.select_some_plans' | translate }}</p>
<div class="form-group">
<button class="btn btn-default pull-right m-t-n-xl" ng-click="toggleAllPlans()" translate>{{ selectedPlans.length == 0 ? 'app.admin.calendar.select_all' : 'app.admin.calendar.select_none' }}</button>
<div ng-repeat="group in plansClassifiedByGroup">
<div class="text-center font-sbold">{{::group.name}}</div>
<label class="checkbox m-l-md" ng-repeat="plan in group.plans">
<input type="checkbox" ng-click="toggleSelectPlan(plan)" ng-model="selectedPlansBinding[plan.id]"> {{::plan.name}}</span>
</label>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer" ng-show="step < 3">
<button class="btn btn-info" ng-click="previous()" ng-disabled="step === 1" translate>{{ 'admin_calendar.previous' }}</button>
<button class="btn btn-info" ng-click="next()" translate>{{ 'admin_calendar.next' }}</button>
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'cancel' }}</button>
<div class="modal-body m-h" ng-show="step === 5">
<div class="m-t-sm">
<p class="text-center font-sbold" translate>{{ 'app.admin.calendar.recurrence' }}</p>
<div class="row">
<div class="form-group col-md-12">
<label for="is_recurrent" translate>{{ 'app.admin.calendar.enabled' }}</label>
<input bs-switch
ng-model="availability.is_recurrent"
id="is_recurrent"
type="checkbox"
class="form-control"
switch-on-text="{{ 'app.shared.buttons.yes' | translate }}"
switch-off-text="{{ 'app.shared.buttons.no' | translate }}"
switch-animate="true"/>
<input type="hidden" name="availability[is_recurrent]" value="{{availability.is_recurrent}}"/>
</div>
</div>
</div>
<div class="row">
<div class="form-group">
<label for="period">{{ 'app.admin.calendar.period' | translate }}</label>
<select id="period"
name="period"
class="form-control"
ng-model="availability.period"
ng-required="availability.is_recurrent"
ng-disabled="!availability.is_recurrent">
<option value="week" ng-selected="availability.period == 'week'" translate>{{ 'app.admin.calendar.week' }}</option>
<option value="month" ng-selected="availability.period == 'month'" translate>{{ 'app.admin.calendar.month' }}</option>
</select>
</div>
</div>
<div class="row">
<div class="form-group">
<label for="nb_periods">{{ 'app.admin.calendar.number_of_periods' | translate }}</label>
<input id="nb_periods"
name="nb_periods"
class="form-control"
ng-model="availability.nb_periods"
type="number"
ng-required="availability.is_recurrent"
ng-disabled="!availability.is_recurrent" />
</div>
</div>
<div class="row">
<div class="form-group">
<label for="end_date">{{ 'app.admin.calendar.end_date' | translate }}</label>
<input id="end_date"
name="end_date"
class="form-control"
ng-model="availability.end_date"
type="date"
ng-required="availability.is_recurrent"
ng-disabled="!availability.is_recurrent" />
</div>
</div>
</div>
<div class="modal-footer" ng-show="step === 3">
<button class="btn btn-info" ng-click="previous()" translate>{{ 'admin_calendar.previous' }}</button>
<button class="btn btn-warning" ng-click="ok()" translate>{{ 'confirm' }}</button>
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'cancel' }}</button>
<div class="modal-body m-h" ng-show="step === 6">
<div class="m-t-sm">
<p class="text-center font-sbold" translate>{{ 'app.admin.calendar.summary' }}</p>
<div class="row">
<span>{{ 'app.admin.calendar.about_to_create' | translate:{NUMBER:occurrences.length,TYPE:availability.available_type} }}</span>
<ul>
<li ng-repeat="slot in occurrences">{{slot.start_at | amDateFormat:'L LT'}} - {{slot.end_at | amDateFormat:'LT'}}</li>
</ul>
<div class="alert alert-warning" translate translate-values="{DURATION: slotDuration}"> {{ 'app.admin.calendar.divided_in_slots' }} </div>
<div>
<span class="underline" translate>{{ 'app.admin.calendar.reservable' }}</span>
<span ng-bind-html="reservableName"></span>
</div>
<div class="m-t">
<span class="underline" translate>{{ 'app.admin.calendar.labels' }}</span>
<span ng-bind-html="tagsName"></span>
</div>
<div class="m-t" ng-show="isOnlySubscriptions">
<span class="underline" translate>{{ 'app.admin.calendar.plans' }}</span>
<span ng-bind-html="plansName"></span>
</div>
</div>
</div>
</div>
<div class="modal-footer" ng-show="step < 6">
<button class="btn btn-info" ng-click="previous()" ng-disabled="step === 1" translate>{{ 'app.admin.calendar.previous' }}</button>
<button class="btn btn-info" ng-click="next()" translate>{{ 'app.admin.calendar.next' }}</button>
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'app.shared.buttons.cancel' }}</button>
</div>
<div class="modal-footer" ng-show="step === 6">
<button class="btn btn-info" ng-click="previous()" translate>{{ 'app.admin.calendar.previous' }}</button>
<button class="btn btn-warning" ng-click="ok()" translate>{{ 'app.shared.buttons.confirm' }}</button>
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'app.shared.buttons.cancel' }}</button>
</div>

View File

@ -0,0 +1,96 @@
<section class="heading b-b">
<div class="row no-gutter">
<div class="col-xs-2 col-sm-2 col-md-1">
<section class="heading-btn">
<a role="button" ng-click="backPrevLocation($event)"><i class="fa fa-long-arrow-left "></i></a>
</section>
</div>
<div class="col-md-8 b-l b-r-md">
<section class="heading-title">
<h1 translate>{{ 'app.admin.icalendar.icalendar_import' }}</h1>
</section>
</div>
<div class="col-md-3">
<section class="heading-actions wrapper">
</section>
</div>
</div>
</section>
<section class="row no-gutter">
<div class="col-sm-12 col-md-12 col-lg-9">
<div class="alert alert-info m-lg" translate>
{{ 'app.admin.icalendar.intro' }}
</div>
<div class="wrapper-lg">
<table class="table" ng-show="calendars.length > 0">
<thead>
<tr>
<th style="width: 35%;" translate>{{ 'app.admin.icalendar.name' }}</th>
<th style="width: 35%;" translate>{{ 'app.admin.icalendar.url' }}</th>
<th translate>{{ 'app.admin.icalendar.display' }}</th>
<th style="width: 20%;"></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="calendar in calendars">
<td class="calendar-name">{{calendar.name}}</td>
<td class="calendar-url"><a href="{{calendar.url}}" target="_blank">{{calendar.url}}</a></td>
<td class="calendar-legend-block text-left"><span class="calendar-legend" ng-style="calendarStyle(calendar)" translate> {{ calendar.text_hidden ? '' : 'app.admin.icalendar.example' }}</span>
<td class="calendar-actions">
<button class="btn btn-info" ng-click="sync(calendar)"><i class="fa fa-refresh"></i></button>
<button class="btn btn-danger" ng-click="delete(calendar)"><i class="fa fa-trash"></i></button>
</td>
</tr>
</tbody>
</table>
<form class="calendar-form" name="newImportForm">
<h4 translate>{{ 'app.admin.icalendar.new_import' }}</h4>
<div class="input-group">
<div class="input-group-addon">
<i class="fa fa-font"></i>
</div>
<input type="text" ng-model="newCalendar.name" class="form-control" placeholder="{{ 'app.admin.icalendar.name' | translate }}" required>
</div>
<div class="input-group">
<div class="input-group-addon">
<i class="fa fa-link"></i>
</div>
<input type="url" ng-model="newCalendar.url" class="form-control" placeholder="{{ 'app.admin.icalendar.url' | translate }}" required>
</div>
<div class="input-group">
<div class="input-group-addon">
<i class="fa fa-paint-brush"></i>
</div>
<input type="text" minicolors ng-model="newCalendar.color" class="form-control" placeholder="{{ 'app.admin.icalendar.color' | translate}}" required/>
</div>
<div class="input-group">
<div class="input-group-addon">
<i class="fa fa-paint-brush"></i>
</div>
<input type="text" minicolors ng-model="newCalendar.text_color" class="form-control" placeholder="{{ 'app.admin.icalendar.text_color' | translate}}" ng-required="!newCalendar.text_hidden"/>
</div>
<div class="input-group">
<label for="hideText" class="control-label m-r" translate>{{ 'app.admin.icalendar.hide_text' }}</label>
<input bs-switch
ng-model="newCalendar.text_hidden"
id="hideText"
type="checkbox"
class="form-control"
switch-on-text="{{ 'app.admin.icalendar.hidden' | translate }}"
switch-off-text="{{ 'app.admin.icalendar.shown' | translate }}"
switch-animate="true"/>
</div>
<div class="m-t text-right">
<button role="button" class="btn btn-warning" ng-click="save()" ng-disabled="newImportForm.$invalid" translate>
{{ 'app.shared.buttons.confirm' }}
</button>
</div>
</form>
</div>
</div>
</section>

View File

@ -1,15 +1,15 @@
<div class="form-group" ng-class="{'has-error': couponForm['coupon[name]'].$dirty && couponForm['coupon[name]'].$invalid}">
<label for="coupon[name]">{{ 'name' | translate }} *</label>
<label for="coupon[name]">{{ 'app.shared.coupon.name' | translate }} *</label>
<input type="text" id="coupon[name]"
name="coupon[name]"
class="form-control"
ng-model="coupon.name"
required="required"/>
<span class="help-block error" ng-show="couponForm['coupon[name]'].$dirty && couponForm['coupon[name]'].$error.required" translate>{{ 'name_is_required' }}</span>
<span class="help-block error" ng-show="couponForm['coupon[name]'].$dirty && couponForm['coupon[name]'].$error.required" translate>{{ 'app.shared.coupon.name_is_required' }}</span>
</div>
<div class="form-group" ng-class="{'has-error': couponForm['coupon[code]'].$dirty && couponForm['coupon[code]'].$invalid}">
<label for="coupon[code]">{{ 'code' | translate }} *</label>
<label for="coupon[code]">{{ 'app.shared.coupon.code' | translate }} *</label>
<input type="text" id="coupon[code]"
name="coupon[code]"
class="form-control"
@ -17,25 +17,25 @@
ng-pattern="/^[A-Z0-9\-]+$/"
ng-disabled="mode == 'EDIT'"
required="required"/>
<span class="help-block error" ng-show="couponForm['coupon[code]'].$dirty && couponForm['coupon[code]'].$error.required" translate>{{ 'code_is_required' }}</span>
<span class="help-block error" ng-show="couponForm['coupon[code]'].$dirty && couponForm['coupon[code]'].$error.pattern" translate>{{ 'code_must_be_composed_of_capital_letters_digits_and_or_dashes' }}</span>
<span class="help-block error" ng-show="couponForm['coupon[code]'].$dirty && couponForm['coupon[code]'].$error.required" translate>{{ 'app.shared.coupon.code_is_required' }}</span>
<span class="help-block error" ng-show="couponForm['coupon[code]'].$dirty && couponForm['coupon[code]'].$error.pattern" translate>{{ 'app.shared.coupon.code_must_be_composed_of_capital_letters_digits_and_or_dashes' }}</span>
</div>
<div class="form-group">
<label for="coupon[type]">{{ 'kind_of_coupon' | translate }} *</label>
<label for="coupon[type]">{{ 'app.shared.coupon.kind_of_coupon' | translate }} *</label>
<select id="coupon[type]"
name="coupon[type]"
class="form-control"
ng-model="coupon.type"
ng-disabled="mode == 'EDIT'"
required="required">
<option value="percent_off" translate>{{ 'percentage' }}</option>
<option value="amount_off" translate>{{ 'amount' }}</option>
<option value="percent_off" translate>{{ 'app.shared.coupon.percentage' }}</option>
<option value="amount_off" translate>{{ 'app.shared.coupon.amount' }}</option>
</select>
</div>
<div class="form-group" ng-class="{'has-error': couponForm['coupon[percent_off]'].$dirty && couponForm['coupon[percent_off]'].$invalid}" ng-show="coupon.type == 'percent_off'">
<label for="coupon[percent_off]">{{ 'percent_off' | translate }} *</label>
<label for="coupon[percent_off]">{{ 'app.shared.coupon.percent_off' | translate }} *</label>
<div class="input-group">
<input type="number" id="coupon[percent_off]"
name="coupon[percent_off]"
@ -47,13 +47,13 @@
ng-required="coupon.type == 'percent_off'"/>
<span class="input-group-addon"><i class="fa fa-percent"></i></span>
</div>
<span class="help-block error" ng-show="couponForm['coupon[percent_off]'].$dirty && couponForm['coupon[percent_off]'].$error.required" translate>{{ 'percent_off_is_required' }}</span>
<span class="help-block error" ng-show="couponForm['coupon[percent_off]'].$dirty && (couponForm['coupon[percent_off]'].$error.min || couponForm['coupon[percent_off]'].$error.max)" translate>{{ 'percentage_must_be_between_0_and_100' }}</span>
<span class="help-block error" ng-show="couponForm['coupon[percent_off]'].$dirty && couponForm['coupon[percent_off]'].$error.required" translate>{{ 'app.shared.coupon.percent_off_is_required' }}</span>
<span class="help-block error" ng-show="couponForm['coupon[percent_off]'].$dirty && (couponForm['coupon[percent_off]'].$error.min || couponForm['coupon[percent_off]'].$error.max)" translate>{{ 'app.shared.coupon.percentage_must_be_between_0_and_100' }}</span>
</div>
<div class="form-group" ng-class="{'has-error': couponForm['coupon[amount_off]'].$dirty && couponForm['coupon[amount_off]'].$invalid}" ng-show="coupon.type == 'amount_off'">
<label for="coupon[amount_off]">{{ 'amount_off' | translate }} *</label>
<label for="coupon[amount_off]">{{ 'app.shared.coupon.amount_off' | translate }} *</label>
<div class="input-group">
<span class="input-group-addon">{{currencySymbol}}</span>
<input type="number" id="coupon[amount_off]"
@ -64,25 +64,25 @@
ng-disabled="mode == 'EDIT'"
ng-required="coupon.type == 'amount_off'"/>
</div>
<span class="help-block error" ng-show="couponForm['coupon[percent_off]'].$dirty && couponForm['coupon[percent_off]'].$error.required" translate>{{ 'percent_off_is_required' }}</span>
<span class="help-block error" ng-show="couponForm['coupon[percent_off]'].$dirty && (couponForm['coupon[percent_off]'].$error.min || couponForm['coupon[percent_off]'].$error.max)" translate>{{ 'percentage_must_be_between_0_and_100' }}</span>
<span class="help-block error" ng-show="couponForm['coupon[percent_off]'].$dirty && couponForm['coupon[percent_off]'].$error.required" translate>{{ 'app.shared.coupon.percent_off_is_required' }}</span>
<span class="help-block error" ng-show="couponForm['coupon[percent_off]'].$dirty && (couponForm['coupon[percent_off]'].$error.min || couponForm['coupon[percent_off]'].$error.max)" translate>{{ 'app.shared.coupon.percentage_must_be_between_0_and_100' }}</span>
</div>
<div class="form-group" ng-class="{'has-error': couponForm['coupon[validity_per_user]'].$dirty && couponForm['coupon[validity_per_user]'].$invalid}">
<label for="coupon[validity_per_user]">{{ 'validity_per_user' | translate }} *</label>
<label for="coupon[validity_per_user]">{{ 'app.shared.coupon.validity_per_user' | translate }} *</label>
<select id="coupon[validity_per_user]"
name="coupon[validity_per_user]"
class="form-control"
ng-model="coupon.validity_per_user"
required="required"
ng-disabled="mode == 'EDIT'"
ng-options="( validity | translate ) for validity in validities">
ng-options="validityName(validity) for validity in validities">
</select>
<span class="help-block error" ng-show="couponForm['coupon[validity_per_user]'].$dirty && couponForm['coupon[validity_per_user]'].$error.required" translate>{{ 'validity_per_user_is_required' }}</span>
<span class="help-block error" ng-show="couponForm['coupon[validity_per_user]'].$dirty && couponForm['coupon[validity_per_user]'].$error.required" translate>{{ 'app.shared.coupon.validity_per_user_is_required' }}</span>
</div>
<div class="form-group" ng-class="{'has-error': errors['valid_until']}">
<label for="coupon[valid_until]" translate>{{ 'valid_until' }}</label>
<label for="coupon[valid_until]" translate>{{ 'app.shared.coupon.valid_until' }}</label>
<div class="input-group">
<input type="text" id="coupon[valid_until]"
name="coupon[valid_until]"
@ -100,34 +100,34 @@
<span class="help-block error" ng-show="errors['valid_until']">{{ errors['valid_until'].join(' ; ') }}</span>
<span class="text-info text-xs">
<i class="fa fa-lightbulb-o"></i> {{ 'leave_empty_for_no_limit' | translate }}
<i class="fa fa-lightbulb-o"></i> {{ 'app.shared.coupon.leave_empty_for_no_limit' | translate }}
</span>
</div>
<div class="form-group" ng-class="{'has-error': couponForm['coupon[max_usages]'].$dirty && couponForm['coupon[max_usages]'].$invalid}">
<label for="coupon[max_usages]">{{ 'max_usages' | translate }}</label>
<label for="coupon[max_usages]">{{ 'app.shared.coupon.max_usages' | translate }}</label>
<input type="number" id="coupon[max_usages]"
name="coupon[max_usages]"
class="form-control"
ng-model="coupon.max_usages"
ng-disabled="mode == 'EDIT'"
min="0"/>
<span class="help-block error" ng-show="couponForm['coupon[max_usages]'].$dirty && couponForm['coupon[max_usages]'].$error.min" translate>{{ 'max_usages_must_be_equal_or_greater_than_0' }}</span>
<span class="help-block error" ng-show="couponForm['coupon[max_usages]'].$dirty && couponForm['coupon[max_usages]'].$error.min" translate>{{ 'app.shared.coupon.max_usages_must_be_equal_or_greater_than_0' }}</span>
<span class="text-info text-xs">
<i class="fa fa-lightbulb-o"></i> {{ 'leave_empty_for_no_limit' | translate }}
<i class="fa fa-lightbulb-o"></i> {{ 'app.shared.coupon.leave_empty_for_no_limit' | translate }}
</span>
</div>
<div class="form-group">
<label for="coupon[active]" translate>{{ 'enabled' }}</label>
<label for="coupon[active]" translate>{{ 'app.shared.coupon.enabled' }}</label>
<input bs-switch
ng-model="coupon.active"
id="coupon[active]"
type="checkbox"
class="form-control"
switch-on-text="{{ 'yes' | translate }}"
switch-off-text="{{ 'no' | translate }}"
switch-on-text="{{ 'app.shared.buttons.yes' | translate }}"
switch-off-text="{{ 'app.shared.buttons.no' | translate }}"
switch-animate="true" />
<input type="hidden" name="coupon[active]" value="{{coupon.active}}"/>
</div>

View File

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

View File

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

View File

@ -1,11 +1,11 @@
<div class="m-t">
<h3 translate>{{ 'categories' }}</h3>
<p translate>{{ 'at_least_one_category_is_required' }}</p>
<button type="button" class="btn btn-warning m-b m-t" ng-click="addElement('category')" translate>{{ 'add_a_category' }}</button>
<h3 class="events-categories" translate>{{ 'app.admin.events.categories' }}</h3>
<p translate>{{ 'app.admin.events.at_least_one_category_is_required' }}</p>
<button type="button" class="btn btn-warning m-b m-t" ng-click="addElement('category')" translate>{{ 'app.admin.events.add_a_category' }}</button>
<table class="table">
<thead>
<tr>
<th style="width:80%" translate>{{ 'name' }}</th>
<th style="width:80%" translate>{{ 'app.admin.events.name' }}</th>
<th style="width:20%"></th>
</tr>
</thead>
@ -28,7 +28,7 @@
</form>
<div class="buttons" ng-show="!rowform.$visible">
<button class="btn btn-default" ng-click="rowform.$show()">
<i class="fa fa-edit"></i> <span class="hidden-xs hidden-sm" translate>{{ 'edit' }}</span>
<i class="fa fa-edit"></i> <span class="hidden-xs hidden-sm" translate>{{ 'app.shared.buttons.edit' }}</span>
</button>
<button class="btn btn-danger" ng-click="removeElement('category', $index)">
<i class="fa fa-trash-o"></i>
@ -39,12 +39,12 @@
</tbody>
</table>
<h3 translate>{{ 'themes' }}</h3>
<button type="button" class="btn btn-warning m-b m-t" ng-click="addElement('theme')" translate>{{ 'add_a_theme' }}</button>
<h3 class="events-themes" translate>{{ 'app.admin.events.themes' }}</h3>
<button type="button" class="btn btn-warning m-b m-t" ng-click="addElement('theme')" translate>{{ 'app.admin.events.add_a_theme' }}</button>
<table class="table">
<thead>
<tr>
<th style="width:80%" translate>{{ 'name' }}</th>
<th style="width:80%" translate>{{ 'app.admin.events.name' }}</th>
<th style="width:20%"></th>
</tr>
</thead>
@ -67,7 +67,7 @@
</form>
<div class="buttons" ng-show="!rowform.$visible">
<button class="btn btn-default" ng-click="rowform.$show()">
<i class="fa fa-edit"></i> <span class="hidden-xs hidden-sm" translate>{{ 'edit' }}</span>
<i class="fa fa-edit"></i> <span class="hidden-xs hidden-sm" translate>{{ 'app.shared.buttons.edit' }}</span>
</button>
<button class="btn btn-danger" ng-click="removeElement('theme', $index)">
<i class="fa fa-trash-o"></i>
@ -78,12 +78,12 @@
</tbody>
</table>
<h3 translate>{{ 'age_ranges' }}</h3>
<button type="button" class="btn btn-warning m-b m-t" ng-click="addElement('age_range')" translate>{{ 'add_a_range' }}</button>
<h3 class="events-age-ranges" translate>{{ 'app.admin.events.age_ranges' }}</h3>
<button type="button" class="btn btn-warning m-b m-t" ng-click="addElement('age_range')" translate>{{ 'app.admin.events.add_a_range' }}</button>
<table class="table">
<thead>
<tr>
<th style="width:80%" translate>{{ 'name' }}</th>
<th style="width:80%" translate>{{ 'app.admin.events.name' }}</th>
<th style="width:20%"></th>
</tr>
</thead>
@ -106,7 +106,7 @@
</form>
<div class="buttons" ng-show="!rowform.$visible">
<button class="btn btn-default" ng-click="rowform.$show()">
<i class="fa fa-edit"></i> <span class="hidden-xs hidden-sm" translate>{{ 'edit' }}</span>
<i class="fa fa-edit"></i> <span class="hidden-xs hidden-sm" translate>{{ 'app.shared.buttons.edit' }}</span>
</button>
<button class="btn btn-danger" ng-click="removeElement('age_range', $index)">
<i class="fa fa-trash-o"></i>
@ -117,4 +117,4 @@
</tbody>
</table>
</div>
</div>

View File

@ -7,31 +7,37 @@
</div>
<div class="col-xs-10 col-sm-10 col-md-8 b-l b-r-md">
<section class="heading-title">
<h1 translate>{{ 'fablab_events' }}</h1>
<h1 translate>{{ 'app.admin.events.fablab_events' }}</h1>
</section>
</div>
<div class="col-xs-12 col-sm-12 col-md-3 b-t hide-b-md" ng-if="isAuthorized(['admin'])">
<section class="heading-actions wrapper">
<a class="btn btn-lg btn-warning bg-white b-2x rounded m-t-sm upper text-sm" ui-sref="app.admin.events_new" role="button" translate>{{ 'add_an_event' }}</a>
<a class="btn btn-lg btn-warning bg-white b-2x rounded m-t-sm upper text-sm" ui-sref="app.admin.events_new" role="button" translate>{{ 'app.admin.events.add_an_event' }}</a>
</section>
</div>
</div>
<section class="m-lg">
<section class="m-lg events-management"
ui-tour="events"
ui-tour-backdrop="true"
ui-tour-template-url="'<%= asset_path "shared/tour-step-template.html" %>'"
ui-tour-use-hotkeys="true"
ui-tour-scroll-parent-id="content-main"
post-render="setupEventsTour">
<div class="row">
<div class="col-md-12">
<uib-tabset justified="true">
<uib-tab heading="{{ 'events_monitoring' | translate }}">
<ng-include src="'<%= asset_path 'admin/events/monitoring.html' %>'"></ng-include>
<uib-tabset justified="true" active="tabs.active">
<uib-tab heading="{{ 'app.admin.events.events_monitoring' | translate }}" index="0">
<ng-include src="'<%= asset_path "admin/events/monitoring.html" %>'"></ng-include>
</uib-tab>
<uib-tab heading="{{ 'manage_filters' | translate }}">
<ng-include src="'<%= asset_path 'admin/events/filters.html' %>'"></ng-include>
<uib-tab heading="{{ 'app.admin.events.manage_filters' | translate }}" index="1">
<ng-include src="'<%= asset_path "admin/events/filters.html" %>'"></ng-include>
</uib-tab>
<uib-tab heading="{{ 'manage_prices_categories' | translate }}">
<ng-include src="'<%= asset_path 'admin/events/prices.html' %>'"></ng-include>
<uib-tab heading="{{ 'app.admin.events.manage_prices_categories' | translate }}" index="2" class="prices-tab">
<ng-include src="'<%= asset_path "admin/events/prices.html" %>'"></ng-include>
</uib-tab>
</uib-tabset>
</div>

View File

@ -1,18 +1,18 @@
<div class="col-md-6 m-b m-t">
<select ng-model="eventsScope.selected" class="form-control" ng-change="changeScope()">
<option value="" translate>{{ 'all_events' }}</option>
<option value="passed" translate>{{ 'passed_events' }}</option>
<option value="future" translate>{{ 'events_to_come' }}</option>
<option value="future_asc" translate>{{ 'events_to_come_asc' }}</option>
<select ng-model="eventsScope.selected" class="form-control events-list-filter" ng-change="changeScope()">
<option value="" translate>{{ 'app.admin.events.all_events' }}</option>
<option value="passed" translate>{{ 'app.admin.events.passed_events' }}</option>
<option value="future" translate>{{ 'app.admin.events.events_to_come' }}</option>
<option value="future_asc" translate>{{ 'app.admin.events.events_to_come_asc' }}</option>
</select>
</div>
<table class="table">
<table class="table events-list">
<thead>
<tr>
<th style="width:30%" translate>{{ 'title' }}</th>
<th style="width:30%" translate>{{ 'dates' }}</th>
<th style="width:10%" translate>{{ 'booking' }}</th>
<th style="width:30%" translate>{{ 'app.admin.events.title' }}</th>
<th style="width:30%" translate>{{ 'app.admin.events.dates' }}</th>
<th style="width:10%" translate>{{ 'app.admin.events.booking' }}</th>
<th style="width:30%"></th>
</tr>
</thead>
@ -27,22 +27,22 @@
<!--One day event-->
<span ng-if="(event.start_date | amDateFormat:'LL')==(event.end_date | amDateFormat:'LL')">
{{ 'on_DATE' | translate:{DATE:(event.start_date | amDateFormat:'LL')} }}
{{ 'app.admin.events.on_DATE' | translate:{DATE:(event.start_date | amDateFormat:'LL')} }}
<span ng-if="event.all_day == 'false'">
{{ 'from_TIME' | translate:{TIME:(event.start_date | amDateFormat:'LT')} }}
<span class="text-sm font-thin" translate>{{ 'to_time' }}</span>
{{ 'app.admin.events.from_TIME' | translate:{TIME:(event.start_date | amDateFormat:'LT')} }}
<span class="text-sm font-thin" translate>{{ 'app.admin.events.to_time' }}</span>
{{event.end_date | amDateFormat:'LT'}}
</span>
</span>
<!--Multiple days event-->
<span ng-if="(event.start_date | amDateFormat:'LL')!=(event.end_date | amDateFormat:'LL')">
{{'from_DATE' | translate:{DATE:(event.start_date | amDateFormat:'LL')} }}
{{'to_date' | translate}} {{event.end_date | amDateFormat:'LL'}}
{{'app.admin.events.from_DATE' | translate:{DATE:(event.start_date | amDateFormat:'LL')} }}
{{'app.admin.events.to_date' | translate}} {{event.end_date | amDateFormat:'LL'}}
<br ng-if="event.all_day == 'false'"/>
<span ng-if="event.all_day == 'false'">
{{ 'from_TIME' | translate:{TIME:(event.start_date | amDateFormat:'LT')} }}
<span class="text-sm font-thin" translate>{{ 'to_time' }}</span>
{{ 'app.admin.events.from_TIME' | translate:{TIME:(event.start_date | amDateFormat:'LT')} }}
<span class="text-sm font-thin" translate>{{ 'app.admin.events.to_time' }}</span>
{{event.end_date | amDateFormat:'LT'}}
</span>
</span>
@ -50,17 +50,17 @@
<td style="vertical-align:middle">
<span class="ng-binding" ng-if="event.nb_total_places > 0">{{ event.nb_total_places - event.nb_free_places }} / {{ event.nb_total_places }}</span>
<span class="badge font-sbold cancelled" ng-if="event.nb_total_places == -1" translate>{{ 'cancelled' }}</span>
<span class="badge font-sbold" ng-if="!event.nb_total_places" translate>{{ 'free_entry' }}</span>
<span class="badge font-sbold cancelled" ng-if="event.nb_total_places == -1" translate>{{ 'app.admin.events.cancelled' }}</span>
<span class="badge font-sbold" ng-if="!event.nb_total_places" translate>{{ 'app.admin.events.free_entry' }}</span>
</td>
<td style="vertical-align:middle">
<div class="buttons">
<a class="btn btn-default" ui-sref="app.admin.event_reservations({id: event.id})">
<i class="fa fa-bookmark"></i> {{ 'view_reservations' | translate }}
<i class="fa fa-bookmark"></i> {{ 'app.admin.events.view_reservations' | translate }}
</a>
<a class="btn btn-default" ui-sref="app.admin.events_edit({id: event.id})">
<i class="fa fa-edit"></i> {{ 'edit' | translate }}
<i class="fa fa-edit"></i> {{ 'app.shared.buttons.edit' | translate }}
</a>
</div>
</td>
@ -70,6 +70,6 @@
<div class="row">
<div class="col-lg-12 text-center">
<a class="btn btn-warning" ng-click="loadMoreEvents()" ng-if="paginateActive" translate>{{ 'load_the_next_events' }}</a>
<a class="btn btn-warning" ng-click="loadMoreEvents()" ng-if="paginateActive" translate>{{ 'app.admin.events.load_the_next_events' }}</a>
</div>
</div>

View File

@ -1,6 +1,6 @@
<div class="modal-header">
<img ng-src="{{logoBlack.custom_asset_file_attributes.attachment_url}}" alt="{{logo.custom_asset_file_attributes.attachment}}" class="modal-logo"/>
<h1 translate>{{ 'price_category' }}</h1>
<h1 translate>{{ 'app.admin.events.price_category' }}</h1>
</div>
<div class="modal-body">
<form role="form" name="priceCategoryForm" class="form-horizontal" novalidate autocomplete="off" ng-keydown="priceCategoryForm.$valid && $event.which == 13 && ok()">
@ -12,10 +12,10 @@
name="name"
ng-model="category.name"
class="form-control"
placeholder="{{ 'category_name' | translate }}"
placeholder="{{ 'app.admin.events.category_name' | translate }}"
required />
</div>
<span class="help-block" ng-show="priceCategoryForm.name.$dirty && priceCategoryForm.name.$error.required" translate>{{ 'category_name_is_required' }}</span>
<span class="help-block" ng-show="priceCategoryForm.name.$dirty && priceCategoryForm.name.$error.required" translate>{{ 'app.admin.events.category_name_is_required' }}</span>
</div>
</div>
@ -25,16 +25,16 @@
rows="10"
class="form-control"
id="conditions"
placeholder="{{ 'enter_here_the_conditions_under_which_this_price_is_applicable' | translate }}"
placeholder="{{ 'app.admin.events.enter_here_the_conditions_under_which_this_price_is_applicable' | translate }}"
name="conditions"
required>
</textarea>
<span class="help-block" ng-show="priceCategoryForm.conditions.$dirty && priceCategoryForm.conditions.$error.required" translate>{{ 'conditions_are_required' }}</span>
<span class="help-block" ng-show="priceCategoryForm.conditions.$dirty && priceCategoryForm.conditions.$error.required" translate>{{ 'app.admin.events.conditions_are_required' }}</span>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-info" ng-click="ok()" ng-disabled="priceCategoryForm.$invalid" translate>{{ 'confirm' }}</button>
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'cancel' }}</button>
<button class="btn btn-info" ng-click="ok()" ng-disabled="priceCategoryForm.$invalid" translate>{{ 'app.shared.buttons.confirm' }}</button>
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'app.shared.buttons.cancel' }}</button>
</div>

View File

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

View File

@ -7,7 +7,7 @@
</div>
<div class="col-xs-10 col-sm-10 col-md-8 b-l">
<section class="heading-title">
<h1>{{ 'the_reservations' | translate }} {{event.title}}</h1>
<h1>{{ 'app.admin.event_reservations.the_reservations' | translate }} {{event.title}}</h1>
</section>
</div>
</div>
@ -20,35 +20,36 @@
<table class="table" ng-if="reservations.length > 0">
<thead>
<tr>
<th style="width:25%" translate>{{ 'user' }}</th>
<th style="width:25%" translate>{{ 'payment_date' }}</th>
<th style="width:25%" translate>{{ 'reserved_tickets' }}</th>
<th style="width:25%" translate>{{ 'app.admin.event_reservations.user' }}</th>
<th style="width:25%" translate>{{ 'app.admin.event_reservations.payment_date' }}</th>
<th style="width:25%" translate>{{ 'app.admin.event_reservations.reserved_tickets' }}</th>
<th style="width:25%"></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="reservation in reservations">
<tr ng-repeat="reservation in reservations" ng-class="{'disabled': isCancelled(reservation)}">
<td class="text-c">
<a ui-sref="app.logged.members_show({id: reservation.user_id})">{{ reservation.user_full_name }} </a>
</td>
<td>{{ reservation.created_at | amDateFormat:'LL LTS' }}</td>
<td>
<span ng-if="reservation.nb_reserve_places > 0">{{ 'full_price_' | translate }} {{reservation.nb_reserve_places}}<br/></span>
<span ng-if="reservation.nb_reserve_places > 0">{{ 'app.admin.event_reservations.full_price_' | translate }} {{reservation.nb_reserve_places}}<br/></span>
<span ng-repeat="ticket in reservation.tickets">{{ticket.event_price_category.price_category.name}} : {{ticket.booked}}</span>
<div ng-show="isCancelled(reservation)" class="canceled-marker" translate>{{ 'app.admin.event_reservations.canceled' }}</div>
</td>
<td>
<div class="buttons">
<button class="btn btn-default" ui-sref="app.public.events_show({id: event.id})">
<i class="fa fa-tag"></i> {{ 'show_the_event' | translate }}
<i class="fa fa-tag"></i> {{ 'app.admin.event_reservations.show_the_event' | translate }}
</button>
</div>
</td>
</tr>
</tbody>
</table>
<p ng-if="reservations.length == 0" translate>{{ 'no_reservations_for_now' }}</p>
<p ng-if="reservations.length == 0" translate>{{ 'app.admin.event_reservations.no_reservations_for_now' }}</p>
<button type="button" class="btn btn-warning m-t m-b" ui-sref="app.admin.events" translate>{{ 'back_to_monitoring' }}</button>
<button type="button" class="btn btn-warning m-t m-b" ui-sref="app.admin.events" translate>{{ 'app.admin.event_reservations.back_to_monitoring' }}</button>
</div>
</div>
</section>

View File

@ -1,13 +1,13 @@
<div class="m-t-lg m-b">
<button type="button" class="btn btn-warning" ng-click="addGroup()">
<i class="fa fa-plus m-r"></i>
<span translate>{{ 'group_form.add_a_group' }}</span>
<span translate>{{ 'app.admin.members.group_form.add_a_group' }}</span>
</button>
<div class="form-group pull-right">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-filter"></i></span>
<select ng-model="groupFiltering" class="form-control">
<option ng-repeat="status in filterDisabled" value="{{status}}" translate>{{ 'group_form.status_'+status }}</option>
<option ng-repeat="status in filterDisabled" value="{{status}}" translate>{{ 'app.admin.members.group_form.status_'+status }}</option>
</select>
</div>
</div>
@ -16,7 +16,7 @@
<table class="table">
<thead>
<tr>
<th style="width: 75%;" translate>{{ 'group_form.group_name' }}</th>
<th style="width: 75%;" translate>{{ 'app.admin.members.group_form.group_name' }}</th>
<th style="width: 25%"></th>
</tr>
</thead>
@ -39,11 +39,11 @@
</form>
<div class="buttons" ng-hide="rowform.$visible || group.slug === 'admins'">
<button class="btn btn-default" ng-click="rowform.$show()">
<i class="fa fa-edit"></i> <span class="hidden-xs hidden-sm" translate>{{ 'edit' }}</span>
<i class="fa fa-edit"></i> <span class="hidden-xs hidden-sm" translate>{{ 'app.shared.buttons.edit' }}</span>
</button>
<button class="btn btn-default" ng-click="toggleDisableGroup($index)">
<span ng-hide="group.disabled"><i class="fa fa-eye-slash"></i> <span translate>{{ 'group_form.disable' }}</span></span>
<span ng-show="group.disabled"><i class="fa fa-eye"></i> <span translate>{{ 'group_form.enable' }}</span></span>
<span ng-hide="group.disabled"><i class="fa fa-eye-slash"></i> <span translate>{{ 'app.admin.members.group_form.disable' }}</span></span>
<span ng-show="group.disabled"><i class="fa fa-eye"></i> <span translate>{{ 'app.admin.members.group_form.enable' }}</span></span>
</button>
<button class="btn btn-danger" ng-click="removeGroup($index)">
<i class="fa fa-trash-o"></i>

View File

@ -1,10 +1,10 @@
<ul>
<li><span class="period-info-title" translate>{{ 'invoices.closed_at' }}</span> : <span>{{period.closed_at | amDateFormat:'L'}}</span></li>
<li><span class="period-info-title" translate>{{ 'invoices.closed_by' }}</span> : <span>{{period.user_name}}</span></li>
<li><span class="period-info-title" translate>{{ 'invoices.period_total' }}</span> : <span>{{period.period_total | currency}}</span></li>
<li><span class="period-info-title" translate>{{ 'invoices.perpetual_total' }}</span> : <span>{{period.perpetual_total | currency}}</span></li>
<li><span class="period-info-title" translate>{{ 'app.admin.invoices.closed_at' }}</span> : <span>{{period.closed_at | amDateFormat:'L'}}</span></li>
<li><span class="period-info-title" translate>{{ 'app.admin.invoices.closed_by' }}</span> : <span>{{period.user_name}}</span></li>
<li><span class="period-info-title" translate>{{ 'app.admin.invoices.period_total' }}</span> : <span>{{period.period_total | currency}}</span></li>
<li><span class="period-info-title" translate>{{ 'app.admin.invoices.perpetual_total' }}</span> : <span>{{period.perpetual_total | currency}}</span></li>
<li>
<span class="period-info-title" translate>{{ 'invoices.integrity' }}</span> :
<span class="period-info-title" translate>{{ 'app.admin.invoices.integrity' }}</span> :
<i class="fa fa-link chained" ng-show="period.chained_footprint"></i>
<i class="fa fa-chain-broken broken" ng-hide="period.chained_footprint"></i>
</li>

View File

@ -1,11 +1,11 @@
<div class="modal-header">
<h3 class="text-center red" translate>{{ 'invoices.export_accounting_data' }}</h3>
<h3 class="text-center red" translate>{{ 'app.admin.invoices.export_accounting_data' }}</h3>
</div>
<div class="modal-body">
<form role="form" name="exportForm">
<div class="row">
<div class="form-group col-md-6">
<label for="start_date" translate>{{ 'invoices.export_form_date' }}</label>
<label for="start_date" translate>{{ 'app.admin.invoices.export_form_date' }}</label>
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-calendar"></i></span>
<input type="text"
@ -23,7 +23,7 @@
</div>
</div>
<div class="form-group col-md-6">
<label for="end_date" translate>{{ 'invoices.export_to_date' }}</label>
<label for="end_date" translate>{{ 'app.admin.invoices.export_to_date' }}</label>
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-calendar"></i></span>
<input type="text"
@ -42,36 +42,36 @@
</div>
</div>
<div class="row">
<h4 class="control-label m-l" translate>{{ 'invoices.export_to' }}</h4>
<h4 class="control-label m-l" translate>{{ 'app.admin.invoices.export_to' }}</h4>
<div class="form-group m-l-lg">
<label for="acd">
<input type="radio" name="acd" id="acd" ng-model="exportTarget.software" ng-value="'acd'" ng-click="fillSettings()" required/>
{{ 'invoices.acd' | translate }}
{{ 'app.admin.invoices.acd' | translate }}
</label>
</div>
</div>
<div class="row" ng-show="exportTarget.settings">
<div class="col-md-4 font-bold" translate>{{ 'invoices.format' }}</div>
<div class="col-md-4 font-bold" translate>{{ 'app.admin.invoices.format' }}</div>
<div class="col-md-8">{{ exportTarget.settings.format }}</div>
<div class="col-md-4 font-bold" translate>{{ 'invoices.encoding' }}</div>
<div class="col-md-4 font-bold" translate>{{ 'app.admin.invoices.encoding' }}</div>
<div class="col-md-8">{{ exportTarget.settings.encoding }}</div>
<div class="col-md-4 font-bold" translate>{{ 'invoices.separator' }}</div>
<div class="col-md-4 font-bold" translate>{{ 'app.admin.invoices.separator' }}</div>
<div class="col-md-8">{{ exportTarget.settings.separator }}</div>
<div class="col-md-4 font-bold" translate>{{ 'invoices.dateFormat' }}</div>
<div class="col-md-4 font-bold" translate>{{ 'app.admin.invoices.dateFormat' }}</div>
<div class="col-md-8">
<a href="https://apidock.com/ruby/DateTime/strftime" class="help-cursor" target="_blank">{{ exportTarget.settings.dateFormat }}</a>
</div>
<div class="col-md-4 font-bold" translate>{{ 'invoices.labelMaxLength' }}</div>
<div class="col-md-4 font-bold" translate>{{ 'app.admin.invoices.labelMaxLength' }}</div>
<div class="col-md-8">{{ exportTarget.settings.labelMaxLength }}</div>
<div class="col-md-4 font-bold" translate>{{ 'invoices.decimalSeparator' }}</div>
<div class="col-md-4 font-bold" translate>{{ 'app.admin.invoices.decimalSeparator' }}</div>
<div class="col-md-8">{{ exportTarget.settings.decimalSeparator }}</div>
<div class="col-md-4 font-bold" translate>{{ 'invoices.exportInvoicesAtZero' }}</div>
<div class="col-md-8" translate>{{ exportTarget.settings.exportInvoicesAtZero ? 'yes' : 'no' }}</div>
<div class="col-md-4 font-bold" translate>{{ 'invoices.columns' }}</div>
<div class="col-md-4 font-bold" translate>{{ 'app.admin.invoices.exportInvoicesAtZero' }}</div>
<div class="col-md-8" translate>{{ exportTarget.settings.exportInvoicesAtZero ? 'app.shared.buttons.yes' : 'app.shared.buttons.no' }}</div>
<div class="col-md-4 font-bold" translate>{{ 'app.admin.invoices.columns' }}</div>
<table class="col-md-12 export-table-template">
<thead>
<tr>
<td ng-repeat="column in exportTarget.settings.columns" translate>{{ 'invoices.exportColumns.' + column }}</td>
<td ng-repeat="column in exportTarget.settings.columns" translate>{{ 'app.admin.invoices.exportColumns.' + column }}</td>
</tr>
</thead>
<tbody>
@ -91,7 +91,7 @@
<input name="type" type="hidden" ng-value="exportTarget.software"/>
<input name="key" type="hidden" ng-value="query.key"/>
<input name="query" type="hidden" ng-value="query.query"/>
<input type="submit" class="btn btn-warning" value="{{ 'confirm' | translate }}" formtarget="export-frame"/>
<input type="submit" class="btn btn-warning" value="{{ 'app.shared.buttons.confirm' | translate }}" formtarget="export-frame"/>
</form>
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'cancel' }}</button>
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'app.shared.buttons.cancel' }}</button>
</div>

View File

@ -1,10 +1,10 @@
<div class="modal-header">
<h3 class="text-center red" translate>{{ 'invoices.create_a_refund_on_this_invoice' }}</h3>
<h3 class="text-center red" translate>{{ 'app.admin.invoices.create_a_refund_on_this_invoice' }}</h3>
</div>
<div class="modal-body">
<form name="avoirForm" novalidate="novalidate">
<div class="form-group" ng-class="{'has-error': avoirForm.avoir_date.$dirty && avoirForm.avoir_date.$invalid }">
<label translate>{{ 'invoices.creation_date_for_the_refund' }}</label>
<label translate>{{ 'app.admin.invoices.creation_date_for_the_refund' }}</label>
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-calendar"></i></span>
<input type="text"
@ -19,24 +19,24 @@
ng-click="openDatePicker($event)"
required/>
</div>
<span class="help-block" ng-show="avoirForm.avoir_date.$dirty && avoirForm.avoir_date.$error.required" translate>{{ 'invoices.creation_date_is_required' }}</span>
<span class="help-block" ng-show="avoirForm.avoir_date.$dirty && avoirForm.avoir_date.$error.required" translate>{{ 'app.admin.invoices.creation_date_is_required' }}</span>
</div>
<div class="form-group">
<label translate>{{ 'invoices.refund_mode' }}</label>
<label translate>{{ 'app.admin.invoices.refund_mode' }}</label>
<select class="form-control m-t-sm" name="payment_method" ng-model="avoir.payment_method" ng-options="mode.value as mode.name for mode in avoirModes" required></select>
</div>
<div class="form-group" ng-if="invoice.is_subscription_invoice">
<label translate>{{ 'invoices.do_you_want_to_disable_the_user_s_subscription' }}</label>
<label translate>{{ 'app.admin.invoices.do_you_want_to_disable_the_user_s_subscription' }}</label>
<select class="form-control m-t-sm" name="subscription_to_expire" ng-model="avoir.subscription_to_expire" ng-options="value as key for (key, value) in subscriptionExpireOptions" required></select>
</div>
<div ng-show="!invoice.is_subscription_invoice && invoice.items.length > 1" class="form-group">
<label translate>{{ 'invoices.elements_to_refund' }}</label>
<label translate>{{ 'app.admin.invoices.elements_to_refund' }}</label>
<table class="table partial-avoir-table">
<thead>
<tr>
<th class="input-col"></th>
<th class="label-col" translate>{{ 'invoices.description' }}</th>
<th class="amount-col" translate>{{ 'invoices.price' }}</th>
<th class="label-col" translate>{{ 'app.admin.invoices.description' }}</th>
<th class="amount-col" translate>{{ 'app.admin.invoices.price' }}</th>
</tr>
</thead>
<tbody>
@ -49,13 +49,13 @@
</table>
</div>
<div>
<label for="description" translate>{{ 'invoices.description_optional' }}</label>
<p translate>{{ 'invoices.will_appear_on_the_refund_invoice' }}</p>
<label for="description" translate>{{ 'app.admin.invoices.description_optional' }}</label>
<p translate>{{ 'app.admin.invoices.will_appear_on_the_refund_invoice' }}</p>
<textarea class="form-control m-t-sm" name="description" ng-model="avoir.description"></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-warning" ng-click="ok()" ng-disabled="avoirForm.$invalid" translate>{{ 'confirm' }}</button>
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'cancel' }}</button>
<button class="btn btn-warning" ng-click="ok()" ng-disabled="avoirForm.$invalid" translate>{{ 'app.shared.buttons.confirm' }}</button>
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'app.shared.buttons.cancel' }}</button>
</div>

View File

@ -1,10 +1,10 @@
<div class="modal-header">
<h3 class="text-center red" translate>{{ 'invoices.close_accounting_period' }}</h3>
<h3 class="text-center red" translate>{{ 'app.admin.invoices.close_accounting_period' }}</h3>
</div>
<div class="modal-body">
<form name="closePeriodForm" novalidate="novalidate" class="row">
<div class="form-group col-md-6" ng-class="{'has-error': closePeriodForm.start_at.$dirty && closePeriodForm.start_at.$invalid }">
<label translate>{{ 'invoices.close_from_date' }}</label>
<label translate>{{ 'app.admin.invoices.close_from_date' }}</label>
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-calendar"></i></span>
<input type="text"
@ -21,11 +21,11 @@
readonly
required/>
</div>
<span class="help-block" ng-show="closePeriodForm.start_at.$dirty && closePeriodForm.start_at.$error.required" translate>{{ 'invoices.start_date_is_required' }}</span>
<span class="help-block" ng-show="closePeriodForm.start_at.$dirty && closePeriodForm.start_at.$error.required" translate>{{ 'app.admin.invoices.start_date_is_required' }}</span>
<span class="help-block error" ng-show="errors.start_at">{{ errors.start_at[0] }}</span>
</div>
<div class="form-group col-md-6" ng-class="{'has-error': closePeriodForm.end_at.$dirty && closePeriodForm.end_at.$invalid }">
<label translate>{{ 'invoices.close_until_date' }}</label>
<label translate>{{ 'app.admin.invoices.close_until_date' }}</label>
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-calendar"></i></span>
<input type="text"
@ -43,7 +43,7 @@
required
readonly/>
</div>
<span class="help-block" ng-show="closePeriodForm.end_at.$dirty && closePeriodForm.end_at.$error.required" translate>{{ 'invoices.end_date_is_required' }}</span>
<span class="help-block" ng-show="closePeriodForm.end_at.$dirty && closePeriodForm.end_at.$error.required" translate>{{ 'app.admin.invoices.end_date_is_required' }}</span>
<span class="help-block error" ng-show="errors.end_at">{{ errors.end_at[0] }}</span>
</div>
</form>
@ -51,12 +51,12 @@
<span class="help-block error">{{ $parent.invoiceErrorRE.exec(key)[1] }} : {{ value[0] }}</span>
</div>
<div>
<h4 translate>{{ 'invoices.previous_closings' }}</h4>
<h4 translate>{{ 'app.admin.invoices.previous_closings' }}</h4>
<table class="table closings-table" ng-show="accountingPeriods.length > 0">
<thead>
<tr>
<th translate>{{ 'invoices.start_date' }}</th>
<th translate>{{ 'invoices.end_date' }}</th>
<th translate>{{ 'app.admin.invoices.start_date' }}</th>
<th translate>{{ 'app.admin.invoices.end_date' }}</th>
<th></th>
</tr>
</thead>
@ -72,10 +72,10 @@
</tr>
</tbody>
</table>
<div ng-show="accountingPeriods.length === 0" translate>{{ 'invoices.no_periods'}}</div>
<div ng-show="accountingPeriods.length === 0" translate>{{ 'app.admin.invoices.no_periods'}}</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-warning" ng-click="ok()" ng-disabled="closePeriodForm.$invalid || pendingCreation" translate>{{ 'confirm' }}</button>
<button class="btn btn-default" ng-click="cancel()" ng-disabled="pendingCreation" translate>{{ 'cancel' }}</button>
<button class="btn btn-warning" ng-click="ok()" ng-disabled="closePeriodForm.$invalid || pendingCreation" translate>{{ 'app.shared.buttons.confirm' }}</button>
<button class="btn btn-default" ng-click="cancel()" ng-disabled="pendingCreation" translate>{{ 'app.shared.buttons.cancel' }}</button>
</div>

View File

@ -5,33 +5,39 @@
<a href="#" ng-click="backPrevLocation($event)"><i class="fa fa-long-arrow-left "></i></a>
</section>
</div>
<div class="col-xs-10 col-sm-10 col-md-8 b-l">
<div class="col-xs-10 col-sm-10 col-md-8 b-l b-r">
<section class="heading-title">
<h1 translate>{{ 'invoices.invoices' }}</h1>
<h1 translate>{{ 'app.admin.invoices.invoices' }}</h1>
</section>
</div>
<div class="col-xs-12 col-sm-12 col-md-3 b-t hide-b-md">
<div class="col-xs-12 col-sm-12 col-md-3">
<section class="heading-actions wrapper">
<a class="btn btn-default rounded m-t-sm" ng-click="toggleExportModal()"><i class="fa fa-book"></i></a>
<a class="btn btn-default rounded m-t-sm export-accounting-button" ng-click="toggleExportModal()"><i class="fa fa-book"></i></a>
<iframe name="export-frame" height="0" width="0" class="none" id="accounting-export-frame"></iframe>
<a class="btn btn-lg btn-default rounded m-t-sm text-sm" ng-click="closeAnAccountingPeriod()"><i class="fa fa-calendar-check-o"></i> {{ 'invoices.accounting_periods' | translate }}</a>
<a class="btn btn-lg btn-default rounded m-t-sm text-sm close-accounting-periods-button" ng-click="closeAnAccountingPeriod()"><i class="fa fa-calendar-check-o"></i> {{ 'app.admin.invoices.accounting_periods' | translate }}</a>
</section>
</div>
</div>
</section>
<section class="m-lg">
<section class="m-lg invoices-management"
ui-tour="invoices"
ui-tour-backdrop="true"
ui-tour-template-url="'<%= asset_path "shared/tour-step-template.html" %>'"
ui-tour-use-hotkeys="true"
ui-tour-scroll-parent-id="content-main"
post-render="setupInvoicesTour">
<div class="row">
<div class="col-md-12">
<uib-tabset justified="true">
<uib-tab heading="{{ 'invoices.invoices_list' | translate }}" ng-hide="fablabWithoutInvoices" active="tabs.listing.active">
<h3 class="m-t-xs"><i class="fa fa-filter"></i> {{ 'invoices.filter_invoices' | translate }}</h3>
<uib-tabset justified="true" active="tabs.active">
<uib-tab heading="{{ 'app.admin.invoices.invoices_list' | translate }}" ng-hide="fablabWithoutInvoices" index="0">
<h3 class="m-t-xs"><i class="fa fa-filter"></i> {{ 'app.admin.invoices.filter_invoices' | translate }}</h3>
<div class="row">
<div class="col-md-4">
<div class="form-group">
<div class="input-group">
<span class="input-group-addon" translate>{{ 'invoices.invoice_num_' }}</span>
<span class="input-group-addon" translate>{{ 'app.admin.invoices.invoice_num_' }}</span>
<input type="text" ng-model="searchInvoice.reference" class="form-control" placeholder="" ng-change="handleFilterChange()">
</div>
</div>
@ -40,7 +46,7 @@
<div class="col-md-4">
<div class="form-group">
<div class="input-group">
<span class="input-group-addon" translate>{{ 'invoices.customer_' }}</span>
<span class="input-group-addon" translate>{{ 'app.admin.invoices.customer_' }}</span>
<input type="text" ng-model="searchInvoice.name" class="form-control" placeholder="" ng-change="handleFilterChange()">
</div>
</div>
@ -49,7 +55,7 @@
<div class="col-md-4">
<div class="form-group">
<div class="input-group">
<span class="input-group-addon">{{ "invoices.date_" | translate }}</span>
<span class="input-group-addon">{{ "app.admin.invoices.date_" | translate }}</span>
<input type="date" ng-model="searchInvoice.date" class="form-control" ng-change="handleFilterChange()">
</div>
</div>
@ -60,24 +66,24 @@
<div class="row">
<div class="col-md-12">
<table class="table" ng-if="invoices.length > 0">
<table class="table invoices-list" ng-if="invoices.length > 0">
<thead>
<tr>
<th style="width:5%"></th>
<th style="width:15%"><a href="" ng-click="setOrderInvoice('reference')">{{ 'invoices.invoice_num' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': orderInvoice=='reference', 'fa fa-sort-numeric-desc': orderInvoice=='-reference', 'fa fa-arrows-v': orderInvoice }"></i></a></th>
<th style="width:15%"><a href="" ng-click="setOrderInvoice('reference')">{{ 'app.admin.invoices.invoice_num' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': orderInvoice=='reference', 'fa fa-sort-numeric-desc': orderInvoice=='-reference', 'fa fa-arrows-v': orderInvoice }"></i></a></th>
<th style="width:20%"><a href="" ng-click="setOrderInvoice('date')">{{ 'invoices.date' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': orderInvoice=='date', 'fa fa-sort-numeric-desc': orderInvoice=='-date', 'fa fa-arrows-v': orderInvoice }"></i></a></th>
<th style="width:20%"><a href="" ng-click="setOrderInvoice('date')">{{ 'app.admin.invoices.date' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': orderInvoice=='date', 'fa fa-sort-numeric-desc': orderInvoice=='-date', 'fa fa-arrows-v': orderInvoice }"></i></a></th>
<th style="width:10%"><a href="" ng-click="setOrderInvoice('total')"> {{ 'invoices.price' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': orderInvoice=='total', 'fa fa-sort-numeric-desc': orderInvoice=='-total', 'fa fa-arrows-v': orderInvoice }"></i></a></th>
<th style="width:10%"><a href="" ng-click="setOrderInvoice('total')"> {{ 'app.admin.invoices.price' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': orderInvoice=='total', 'fa fa-sort-numeric-desc': orderInvoice=='-total', 'fa fa-arrows-v': orderInvoice }"></i></a></th>
<th style="width:20%"><a href="" ng-click="setOrderInvoice('name')">{{ 'invoices.customer' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderInvoice=='name', 'fa fa-sort-alpha-desc': orderInvoice=='-name', 'fa fa-arrows-v': orderInvoice }"></i></a></th>
<th style="width:20%"><a href="" ng-click="setOrderInvoice('name')">{{ 'app.admin.invoices.customer' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderInvoice=='name', 'fa fa-sort-alpha-desc': orderInvoice=='-name', 'fa fa-arrows-v': orderInvoice }"></i></a></th>
<th style="width:30%"></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="invoice in invoices">
<td>
<td class="chained-indicator">
<i class="fa fa-link chained" ng-show="invoice.chained_footprint"/>
<i class="fa fa-chain-broken broken" ng-hide="invoice.chained_footprint"/>
</td>
@ -90,14 +96,14 @@
<span ng-hide="invoice.user_id">{{ invoice.name }}</span>
<td>
<div class="buttons">
<a class="btn btn-default" ng-href="api/invoices/{{invoice.id}}/download" target="_blank" ng-if="!invoice.is_avoir">
<i class="fa fa-file-pdf-o"></i> {{ 'invoices.download_the_invoice' | translate }}
<a class="btn btn-default download-button" ng-href="api/invoices/{{invoice.id}}/download" target="_blank" ng-if="!invoice.is_avoir">
<i class="fa fa-file-pdf-o"></i> {{ 'app.admin.invoices.download_the_invoice' | translate }}
</a>
<a class="btn btn-default" ng-href="api/invoices/{{invoice.id}}/download" target="_blank" ng-if="invoice.is_avoir">
<i class="fa fa-file-pdf-o"></i> {{ 'invoices.download_the_credit_note' | translate }}
<i class="fa fa-file-pdf-o"></i> {{ 'app.admin.invoices.download_the_credit_note' | translate }}
</a>
<a class="btn btn-default" ng-click="generateAvoirForInvoice(invoice)" ng-if="(!invoice.has_avoir || invoice.has_avoir == 'partial') && !invoice.is_avoir && !invoice.prevent_refund && !isDateClosed(invoice.created_at)">
<i class="fa fa-reply"></i> {{ 'invoices.credit_note' | translate }}
<a class="btn btn-default refund-button" ng-click="generateAvoirForInvoice(invoice)" ng-if="(!invoice.has_avoir || invoice.has_avoir == 'partial') && !invoice.is_avoir && !invoice.prevent_refund && !isDateClosed(invoice.created_at)">
<i class="fa fa-reply"></i> {{ 'app.admin.invoices.credit_note' | translate }}
</a>
</div>
</td>
@ -105,9 +111,9 @@
</tbody>
</table>
<div class="text-center">
<button class="btn btn-warning" ng-click="showNextInvoices()" ng-hide="noMoreResults"><i class="fa fa-search-plus" aria-hidden="true"></i> {{ 'invoices.display_more_invoices' | translate }}</button>
<button class="btn btn-warning" ng-click="showNextInvoices()" ng-hide="noMoreResults"><i class="fa fa-search-plus" aria-hidden="true"></i> {{ 'app.admin.invoices.display_more_invoices' | translate }}</button>
</div>
<p ng-if="invoices.length == 0" translate>{{ 'invoices.no_invoices_for_now' }}</p>
<p ng-if="invoices.length == 0" translate>{{ 'app.admin.invoices.no_invoices_for_now' }}</p>
</div>
</div>
@ -116,211 +122,211 @@
<uib-tab heading="{{ 'invoices.invoicing_settings' | translate }}" active="tabs.settings.active">
<div class="alert alert-warning p-md m-t" role="alert" ng-show="fablabWithoutInvoices">
<i class="fa fa-warning m-r"></i>
<span translate>{{ 'invoices.warning_invoices_disabled' }}</span>
</div>
<form class="invoice-placeholder">
<div class="invoice-logo">
<img src="data:image/png;base64," data-src="holder.js/100%x100%/text:&#xf03e;/font:FontAwesome/icon" bs-holder ng-if="!invoice.logo" class="img-responsive">
<img base-sixty-four-image="invoice.logo" ng-if="invoice.logo && invoice.logo.base64">
<div class="tools-box">
<div class="btn-group">
<div class="btn btn-default btn-file">
<i class="fa fa-edit"></i> {{ 'invoices.change_logo' | translate }}
<input type="file" accept="image/png,image/jpeg,image/x-png,image/pjpeg" name="invoice[logo][attachment]" ng-model="invoice.logo" base-sixty-four-input>
</div>
</div>
</div>
</div>
<div class="invoice-buyer-infos">
<strong translate>{{ 'invoices.john_smith' }}</strong>
<div translate>{{ 'invoices.john_smith_at_example_com' }}</div>
<uib-tab heading="{{ 'app.admin.invoices.invoicing_settings' | translate }}" index="1" class="invoices-settings">
<div class="alert alert-warning p-md m-t" role="alert" ng-show="fablabWithoutInvoices">
<i class="fa fa-warning m-r"></i>
<span translate>{{ 'app.admin.invoices.warning_invoices_disabled' }}</span>
</div>
<div class="invoice-reference invoice-editable" ng-click="openEditReference()">{{ 'invoices.invoice_reference_' | translate }} {{mkReference()}}</div>
<div class="invoice-code invoice-editable" ng-show="invoice.code.active" ng-click="openEditCode()">{{ 'invoices.code_' | translate }} {{invoice.code.model}}</div>
<div class="invoice-code invoice-activable" ng-show="!invoice.code.active" ng-click="openEditCode()" translate>{{ 'invoices.code_disabled' }}</div>
<div class="invoice-order invoice-editable" ng-click="openEditInvoiceNb()"> {{ 'invoices.order_num' | translate }} {{mkNumber()}}</div>
<div class="invoice-date">{{ 'invoices.invoice_issued_on_DATE_at_TIME' | translate:{DATE:(today | amDateFormat:'L'), TIME:(today | amDateFormat:'LT')} }}</div>
<div class="invoice-object">
{{ 'invoices.object_reservation_of_john_smith_on_DATE_at_TIME' | translate:{DATE:(inOneWeek | amDateFormat:'L'), TIME:(inOneWeek | amDateFormat:'LT')} }}
</div>
<div class="invoice-data">
{{ 'invoices.order_summary' | translate }}
<table>
<thead>
<tr>
<th translate>{{ 'invoices.details' }}</th>
<th class="right" translate>{{ 'invoices.amount' }}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ 'invoices.machine_booking-3D_printer' | translate }} {{inOneWeek | amDateFormat:'LLL'}} - {{inOneWeekAndOneHour | amDateFormat:'LT'}}</td>
<td class="right">{{30.0 | currency}}</td>
</tr>
<form class="invoice-placeholder">
<div class="invoice-logo">
<img src="data:image/png;base64," data-src="holder.js/100%x100%/text:&#xf03e;/font:FontAwesome/icon" bs-holder ng-if="!invoice.logo" class="img-responsive">
<img base-sixty-four-image="invoice.logo" ng-if="invoice.logo && invoice.logo.base64">
<div class="tools-box">
<div class="btn-group">
<div class="btn btn-default btn-file">
<i class="fa fa-edit"></i> {{ 'app.admin.invoices.change_logo' | translate }}
<input type="file" accept="image/png,image/jpeg,image/x-png,image/pjpeg" name="invoice[logo][attachment]" ng-model="invoice.logo" base-sixty-four-input>
</div>
</div>
</div>
</div>
<div class="invoice-buyer-infos">
<strong translate>{{ 'app.admin.invoices.john_smith' }}</strong>
<div translate>{{ 'app.admin.invoices.john_smith_at_example_com' }}</div>
</div>
<div class="invoice-reference invoice-editable" ng-click="openEditReference()">{{ 'app.admin.invoices.invoice_reference_' | translate }} {{mkReference()}}</div>
<div class="invoice-code invoice-editable" ng-show="invoice.code.active" ng-click="openEditCode()">{{ 'app.admin.invoices.code_' | translate }} {{invoice.code.model}}</div>
<div class="invoice-code invoice-activable" ng-show="!invoice.code.active" ng-click="openEditCode()" translate>{{ 'app.admin.invoices.code_disabled' }}</div>
<div class="invoice-order invoice-editable" ng-click="openEditInvoiceNb()"> {{ 'app.admin.invoices.order_num' | translate }} {{mkNumber()}}</div>
<div class="invoice-date">{{ 'app.admin.invoices.invoice_issued_on_DATE_at_TIME' | translate:{DATE:(today | amDateFormat:'L'), TIME:(today | amDateFormat:'LT')} }}</div>
<div class="invoice-object">
{{ 'app.admin.invoices.object_reservation_of_john_smith_on_DATE_at_TIME' | translate:{DATE:(inOneWeek | amDateFormat:'L'), TIME:(inOneWeek | amDateFormat:'LT')} }}
</div>
<div class="invoice-data">
{{ 'app.admin.invoices.order_summary' | translate }}
<table>
<thead>
<tr>
<th translate>{{ 'app.admin.invoices.details' }}</th>
<th class="right" translate>{{ 'app.admin.invoices.amount' }}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ 'app.admin.invoices.machine_booking-3D_printer' | translate }} {{inOneWeek | amDateFormat:'LLL'}} - {{inOneWeekAndOneHour | amDateFormat:'LT'}}</td>
<td class="right">{{30.0 | currency}}</td>
</tr>
<tr class="invoice-total" ng-class="{'bold vat-line':invoice.VAT.active}">
<td ng-show="!invoice.VAT.active" translate>{{ 'invoices.total_amount' }}</td>
<td ng-show="invoice.VAT.active" translate>{{ 'invoices.total_including_all_taxes' }}</td>
<td class="right">{{30.0 | currency}}</td>
</tr>
<tr class="invoice-total" ng-class="{'bold vat-line':invoice.VAT.active}">
<td ng-show="!invoice.VAT.active" translate>{{ 'app.admin.invoices.total_amount' }}</td>
<td ng-show="invoice.VAT.active" translate>{{ 'app.admin.invoices.total_including_all_taxes' }}</td>
<td class="right">{{30.0 | currency}}</td>
</tr>
<tr class="invoice-vat invoice-activable" ng-click="openEditVAT()" ng-show="!invoice.VAT.active">
<td translate>{{ 'invoices.VAT_disabled' }}</td>
<td></td>
</tr>
<tr class="invoice-vat invoice-activable" ng-click="openEditVAT()" ng-show="!invoice.VAT.active">
<td translate>{{ 'app.admin.invoices.VAT_disabled' }}</td>
<td></td>
</tr>
<tr class="invoice-vat invoice-editable vat-line italic" ng-click="openEditVAT()" ng-show="invoice.VAT.active">
<td>{{ 'invoices.including_VAT' | translate }} {{invoice.VAT.rate}} %</td>
<td>{{30-(30/(invoice.VAT.rate/100+1)) | currency}}</td>
</tr>
<tr class="invoice-ht vat-line italic" ng-show="invoice.VAT.active">
<td translate>{{ 'invoices.including_total_excluding_taxes' }}</td>
<td>{{30/(invoice.VAT.rate/100+1) | currency}}</td>
</tr>
<tr class="invoice-payed vat-line bold" ng-show="invoice.VAT.active">
<td translate>{{ 'invoices.including_amount_payed_on_ordering' }}</td>
<td>{{30.0 | currency}}</td>
</tr>
<tr class="invoice-vat invoice-editable vat-line italic" ng-click="openEditVAT()" ng-show="invoice.VAT.active">
<td>{{ 'app.admin.invoices.including_VAT' | translate }} {{invoice.VAT.rate}} %</td>
<td>{{30-(30/(invoice.VAT.rate/100+1)) | currency}}</td>
</tr>
<tr class="invoice-ht vat-line italic" ng-show="invoice.VAT.active">
<td translate>{{ 'app.admin.invoices.including_total_excluding_taxes' }}</td>
<td>{{30/(invoice.VAT.rate/100+1) | currency}}</td>
</tr>
<tr class="invoice-payed vat-line bold" ng-show="invoice.VAT.active">
<td translate>{{ 'app.admin.invoices.including_amount_payed_on_ordering' }}</td>
<td>{{30.0 | currency}}</td>
</tr>
</tbody>
</table>
<p class="invoice-payment" translate translate-values="{DATE:(today | amDateFormat:'L'), TIME:(today | amDateFormat:'LT'), AMOUNT:(30.0 | currency)}">
{{ 'invoices.settlement_by_debit_card_on_DATE_at_TIME_for_an_amount_of_AMOUNT' }}
</p>
</div>
<div medium-editor class="invoice-text invoice-editable" ng-model="invoice.text.content"
options='{
"placeholder": "{{ "invoices.important_notes" | translate }}",
"buttons": ["underline"]
}'
ng-blur="textEditEnd($event)">
</div>
<div medium-editor class="invoice-legals invoice-editable" ng-model="invoice.legals.content"
options='{
"placeholder": "{{ "invoices.address_and_legal_information" | translate }}",
"buttons": ["bold", "underline"]
}'
ng-blur="legalsEditEnd($event)">
</div>
</form>
</tbody>
</table>
<p class="invoice-payment" translate translate-values="{DATE:(today | amDateFormat:'L'), TIME:(today | amDateFormat:'LT'), AMOUNT:(30.0 | currency)}">
{{ 'app.admin.invoices.settlement_by_debit_card_on_DATE_at_TIME_for_an_amount_of_AMOUNT' }}
</p>
</div>
<div medium-editor class="invoice-text invoice-editable" ng-model="invoice.text.content"
options='{
"placeholder": "{{ "app.admin.invoices.important_notes" | translate }}",
"buttons": ["underline"]
}'
ng-blur="textEditEnd($event)">
</div>
<div medium-editor class="invoice-legals invoice-editable" ng-model="invoice.legals.content"
options='{
"placeholder": "{{ "app.admin.invoices.address_and_legal_information" | translate }}",
"buttons": ["bold", "underline"]
}'
ng-blur="legalsEditEnd($event)">
</div>
</form>
</uib-tab>
<uib-tab heading="{{ 'invoices.accounting_codes' | translate }}">
<uib-tab heading="{{ 'app.admin.invoices.accounting_codes' | translate }}" index="2" class="accounting-codes-tab">
<div class="panel panel-default m-t-md accounting-codes">
<div class="panel-body">
<div class="row">
<div class="col-md-6">
<label for="journalCode" translate>{{ 'invoices.accounting_journal_code' }}</label>
<input type="text" id="journalCode" ng-model="settings.journalCode.value" class="form-control" placeholder="{{ 'invoices.general_journal_code' | translate }}"/>
<label for="journalCode" translate>{{ 'app.admin.invoices.accounting_journal_code' }}</label>
<input type="text" id="journalCode" ng-model="settings.journalCode.value" class="form-control" placeholder="{{ 'app.admin.invoices.general_journal_code' | translate }}"/>
</div>
</div>
<div class="row">
<div class="col-md-6">
<label for="cardClientCode" translate>{{ 'invoices.accounting_card_client_code' }}</label>
<input type="text" id="cardClientCode" ng-model="settings.cardClientCode.value" class="form-control" placeholder="{{ 'invoices.card_client_code' | translate }}" />
<label for="cardClientCode" translate>{{ 'app.admin.invoices.accounting_card_client_code' }}</label>
<input type="text" id="cardClientCode" ng-model="settings.cardClientCode.value" class="form-control" placeholder="{{ 'app.admin.invoices.card_client_code' | translate }}" />
</div>
<div class="col-md-6">
<label for="cardClientLabel" translate>{{ 'invoices.accounting_card_client_label' }}</label>
<input type="text" id="cardClientLabel" ng-model="settings.cardClientLabel.value" class="form-control" placeholder="{{ 'invoices.card_client_label' | translate }}"/>
<label for="cardClientLabel" translate>{{ 'app.admin.invoices.accounting_card_client_label' }}</label>
<input type="text" id="cardClientLabel" ng-model="settings.cardClientLabel.value" class="form-control" placeholder="{{ 'app.admin.invoices.card_client_label' | translate }}"/>
</div>
</div>
<div class="row">
<div class="col-md-6">
<label for="walletClientCode" translate>{{ 'invoices.accounting_wallet_client_code' }}</label>
<input type="text" id="walletClientCode" ng-model="settings.walletClientCode.value" class="form-control" placeholder="{{ 'invoices.wallet_client_code' | translate }}" />
<label for="walletClientCode" translate>{{ 'app.admin.invoices.accounting_wallet_client_code' }}</label>
<input type="text" id="walletClientCode" ng-model="settings.walletClientCode.value" class="form-control" placeholder="{{ 'app.admin.invoices.wallet_client_code' | translate }}" />
</div>
<div class="col-md-6">
<label for="walletClientLabel" translate>{{ 'invoices.accounting_wallet_client_label' }}</label>
<input type="text" id="walletClientLabel" ng-model="settings.walletClientLabel.value" class="form-control" placeholder="{{ 'invoices.wallet_client_label' | translate }}"/>
<label for="walletClientLabel" translate>{{ 'app.admin.invoices.accounting_wallet_client_label' }}</label>
<input type="text" id="walletClientLabel" ng-model="settings.walletClientLabel.value" class="form-control" placeholder="{{ 'app.admin.invoices.wallet_client_label' | translate }}"/>
</div>
</div>
<div class="row">
<div class="col-md-6">
<label for="otherClientCode" translate>{{ 'invoices.accounting_other_client_code' }}</label>
<input type="text" id="otherClientCode" ng-model="settings.otherClientCode.value" class="form-control" placeholder="{{ 'invoices.other_client_code' | translate }}" />
<label for="otherClientCode" translate>{{ 'app.admin.invoices.accounting_other_client_code' }}</label>
<input type="text" id="otherClientCode" ng-model="settings.otherClientCode.value" class="form-control" placeholder="{{ 'app.admin.invoices.other_client_code' | translate }}" />
</div>
<div class="col-md-6">
<label for="otherClientLabel" translate>{{ 'invoices.accounting_other_client_label' }}</label>
<input type="text" id="otherClientLabel" ng-model="settings.otherClientLabel.value" class="form-control" placeholder="{{ 'invoices.other_client_label' | translate }}"/>
<label for="otherClientLabel" translate>{{ 'app.admin.invoices.accounting_other_client_label' }}</label>
<input type="text" id="otherClientLabel" ng-model="settings.otherClientLabel.value" class="form-control" placeholder="{{ 'app.admin.invoices.other_client_label' | translate }}"/>
</div>
</div>
<div class="row">
<div class="col-md-6">
<label for="walletCode" translate>{{ 'invoices.accounting_wallet_code' }}</label>
<input type="text" id="walletCode" ng-model="settings.walletCode.value" class="form-control" placeholder="{{ 'invoices.general_wallet_code' | translate }}" />
<label for="walletCode" translate>{{ 'app.admin.invoices.accounting_wallet_code' }}</label>
<input type="text" id="walletCode" ng-model="settings.walletCode.value" class="form-control" placeholder="{{ 'app.admin.invoices.general_wallet_code' | translate }}" />
</div>
<div class="col-md-6">
<label for="walletLabel" translate>{{ 'invoices.accounting_wallet_label' }}</label>
<input type="text" id="walletLabel" ng-model="settings.walletLabel.value" class="form-control" placeholder="{{ 'invoices.general_wallet_label' | translate }}"/>
<label for="walletLabel" translate>{{ 'app.admin.invoices.accounting_wallet_label' }}</label>
<input type="text" id="walletLabel" ng-model="settings.walletLabel.value" class="form-control" placeholder="{{ 'app.admin.invoices.general_wallet_label' | translate }}"/>
</div>
</div>
<div class="row">
<div class="col-md-6">
<label for="vatCode" translate>{{ 'invoices.accounting_vat_code' }}</label>
<input type="text" id="vatCode" ng-model="settings.vatCode.value" class="form-control" placeholder="{{ 'invoices.general_vat_code' | translate }}"/>
<label for="vatCode" translate>{{ 'app.admin.invoices.accounting_vat_code' }}</label>
<input type="text" id="vatCode" ng-model="settings.vatCode.value" class="form-control" placeholder="{{ 'app.admin.invoices.general_vat_code' | translate }}"/>
</div>
<div class="col-md-6">
<label for="vatLabel" translate>{{ 'invoices.accounting_vat_label' }}</label>
<input type="text" id="vatLabel" ng-model="settings.vatLabel.value" class="form-control" placeholder="{{ 'invoices.general_vat_label' | translate }}"/>
<label for="vatLabel" translate>{{ 'app.admin.invoices.accounting_vat_label' }}</label>
<input type="text" id="vatLabel" ng-model="settings.vatLabel.value" class="form-control" placeholder="{{ 'app.admin.invoices.general_vat_label' | translate }}"/>
</div>
</div>
<div class="row">
<div class="col-md-6">
<label for="subscriptionCode" translate>{{ 'invoices.accounting_subscription_code' }}</label>
<input type="text" id="subscriptionCode" ng-model="settings.subscriptionCode.value" class="form-control" placeholder="{{ 'invoices.general_subscription_code' | translate }}" />
<label for="subscriptionCode" translate>{{ 'app.admin.invoices.accounting_subscription_code' }}</label>
<input type="text" id="subscriptionCode" ng-model="settings.subscriptionCode.value" class="form-control" placeholder="{{ 'app.admin.invoices.general_subscription_code' | translate }}" />
</div>
<div class="col-md-6">
<label for="subscriptionLabel" translate>{{ 'invoices.accounting_subscription_label' }}</label>
<input type="text" id="subscriptionLabel" ng-model="settings.subscriptionLabel.value" class="form-control" placeholder="{{ 'invoices.general_subscription_label' | translate }}"/>
<label for="subscriptionLabel" translate>{{ 'app.admin.invoices.accounting_subscription_label' }}</label>
<input type="text" id="subscriptionLabel" ng-model="settings.subscriptionLabel.value" class="form-control" placeholder="{{ 'app.admin.invoices.general_subscription_label' | translate }}"/>
</div>
</div>
<div class="row">
<div class="col-md-6">
<label for="machineCode" translate>{{ 'invoices.accounting_Machine_code' }}</label>
<input type="text" id="machineCode" ng-model="settings.machineCode.value" class="form-control" placeholder="{{ 'invoices.general_machine_code' | translate }}"/>
<label for="machineCode" translate>{{ 'app.admin.invoices.accounting_Machine_code' }}</label>
<input type="text" id="machineCode" ng-model="settings.machineCode.value" class="form-control" placeholder="{{ 'app.admin.invoices.general_machine_code' | translate }}"/>
</div>
<div class="col-md-6">
<label for="machineLabel" translate>{{ 'invoices.accounting_Machine_label' }}</label>
<input type="text" id="machineLabel" ng-model="settings.machineLabel.value" class="form-control" placeholder="{{ 'invoices.general_machine_label' | translate }}"/>
<label for="machineLabel" translate>{{ 'app.admin.invoices.accounting_Machine_label' }}</label>
<input type="text" id="machineLabel" ng-model="settings.machineLabel.value" class="form-control" placeholder="{{ 'app.admin.invoices.general_machine_label' | translate }}"/>
</div>
</div>
<div class="row">
<div class="col-md-6">
<label for="trainingCode" translate>{{ 'invoices.accounting_Training_code' }}</label>
<input type="text" id="trainingCode" ng-model="settings.trainingCode.value" class="form-control" placeholder="{{ 'invoices.general_training_code' | translate }}" />
<label for="trainingCode" translate>{{ 'app.admin.invoices.accounting_Training_code' }}</label>
<input type="text" id="trainingCode" ng-model="settings.trainingCode.value" class="form-control" placeholder="{{ 'app.admin.invoices.general_training_code' | translate }}" />
</div>
<div class="col-md-6">
<label for="trainingLabel" translate>{{ 'invoices.accounting_Training_label' }}</label>
<input type="text" id="trainingLabel" ng-model="settings.trainingLabel.value" class="form-control" placeholder="{{ 'invoices.general_training_label' | translate }}"/>
<label for="trainingLabel" translate>{{ 'app.admin.invoices.accounting_Training_label' }}</label>
<input type="text" id="trainingLabel" ng-model="settings.trainingLabel.value" class="form-control" placeholder="{{ 'app.admin.invoices.general_training_label' | translate }}"/>
</div>
</div>
<div class="row">
<div class="col-md-6">
<label for="eventCode" translate>{{ 'invoices.accounting_Event_code' }}</label>
<input type="text" id="eventCode" ng-model="settings.eventCode.value" class="form-control" placeholder="{{ 'invoices.general_event_code' | translate }}"/>
<label for="eventCode" translate>{{ 'app.admin.invoices.accounting_Event_code' }}</label>
<input type="text" id="eventCode" ng-model="settings.eventCode.value" class="form-control" placeholder="{{ 'app.admin.invoices.general_event_code' | translate }}"/>
</div>
<div class="col-md-6">
<label for="eventLabel" translate>{{ 'invoices.accounting_Event_label' }}</label>
<input type="text" id="eventLabel" ng-model="settings.eventLabel.value" class="form-control" placeholder="{{ 'invoices.general_event_label' | translate }}"/>
<label for="eventLabel" translate>{{ 'app.admin.invoices.accounting_Event_label' }}</label>
<input type="text" id="eventLabel" ng-model="settings.eventLabel.value" class="form-control" placeholder="{{ 'app.admin.invoices.general_event_label' | translate }}"/>
</div>
</div>
<div class="row">
<div class="col-md-6">
<label for="spaceCode" translate>{{ 'invoices.accounting_Space_code' }}</label>
<input type="text" id="spaceCode" ng-model="settings.spaceCode.value" class="form-control" placeholder="{{ 'invoices.general_space_code' | translate }}" />
<label for="spaceCode" translate>{{ 'app.admin.invoices.accounting_Space_code' }}</label>
<input type="text" id="spaceCode" ng-model="settings.spaceCode.value" class="form-control" placeholder="{{ 'app.admin.invoices.general_space_code' | translate }}" />
</div>
<div class="col-md-6">
<label for="spaceLabel" translate>{{ 'invoices.accounting_Space_label' }}</label>
<input type="text" id="spaceLabel" ng-model="settings.spaceLabel.value" class="form-control" placeholder="{{ 'invoices.general_space_label' | translate }}"/>
<label for="spaceLabel" translate>{{ 'app.admin.invoices.accounting_Space_label' }}</label>
<input type="text" id="spaceLabel" ng-model="settings.spaceLabel.value" class="form-control" placeholder="{{ 'app.admin.invoices.general_space_label' | translate }}"/>
</div>
</div>
<button name="button" class="btn btn-warning m-t-lg" ng-click="save()" translate>{{ 'save' }}</button>
<button name="button" class="btn btn-warning m-t-lg" ng-click="save()" translate>{{ 'app.shared.buttons.save' }}</button>
</div>
</div>
</uib-tab>
@ -333,125 +339,125 @@
<script type="text/ng-template" id="editReference.html">
<div class="custom-invoice">
<div class="modal-header">
<h3 class="modal-title" translate>{{ 'invoices.invoice_reference' }}</h3>
<h3 class="modal-title" translate>{{ 'app.admin.invoices.invoice_reference' }}</h3>
</div>
<div class="modal-body row">
<div class="elements col-md-4">
<h4>Éléments</h4>
<ul>
<li ng-click="invoice.reference.help = 'addYear.html'">{{ 'invoices.year' | translate }}</li>
<li ng-click="invoice.reference.help = 'addMonth.html'">{{ 'invoices.month' | translate }}</li>
<li ng-click="invoice.reference.help = 'addDay.html'">{{ 'invoices.day' | translate }}</li>
<li ng-click="invoice.reference.help = 'addInvoiceNumber.html'">{{ 'invoices.num_of_invoice' | translate }}</li>
<li ng-click="invoice.reference.help = 'addOnlineInfo.html'">{{ 'invoices.online_sales' | translate }}</li>
<%# <li ng-click="invoice.reference.help = 'addWalletInfo.html'">{{ 'invoices.wallet' | translate }}</li> %>
<li ng-click="invoice.reference.help = 'addRefundInfo.html'">{{ 'invoices.refund' | translate }}</li>
<li ng-click="invoice.reference.help = 'addYear.html'">{{ 'app.admin.invoices.year' | translate }}</li>
<li ng-click="invoice.reference.help = 'addMonth.html'">{{ 'app.admin.invoices.month' | translate }}</li>
<li ng-click="invoice.reference.help = 'addDay.html'">{{ 'app.admin.invoices.day' | translate }}</li>
<li ng-click="invoice.reference.help = 'addInvoiceNumber.html'">{{ 'app.admin.invoices.num_of_invoice' | translate }}</li>
<li ng-click="invoice.reference.help = 'addOnlineInfo.html'">{{ 'app.admin.invoices.online_sales' | translate }}</li>
<%# <li ng-click="invoice.reference.help = 'addWalletInfo.html'">{{ 'app.admin.invoices.wallet' | translate }}</li> %>
<li ng-click="invoice.reference.help = 'addRefundInfo.html'">{{ 'app.admin.invoices.refund' | translate }}</li>
</ul>
</div>
<div class="col-md-8">
<div class="model">
<h4 translate>{{ 'invoices.model' }}</h4>
<h4 translate>{{ 'app.admin.invoices.model' }}</h4>
<input type="text" class="form-control" ng-model="model">
</div>
<div class="help">
<h4 translate>{{ 'invoices.documentation' }}</h4>
<h4 translate>{{ 'app.admin.invoices.documentation' }}</h4>
<ng-include src="invoice.reference.help" autoscroll="true">
</ng-include>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-warning" ng-click="ok()" translate>{{ 'confirm' }}</button>
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'cancel' }}</button>
<button class="btn btn-warning" ng-click="ok()" translate>{{ 'app.shared.buttons.confirm' }}</button>
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'app.shared.buttons.cancel' }}</button>
</div>
</div>
</script>
<script type="text/ng-template" id="addYear.html">
<table class="invoice-element-legend">
<tr><td><strong>YY</strong></td><td translate>{{ 'invoices.2_digits_year' }}</td></tr>
<tr><td><strong>YYYY</strong></td><td translate>{{ 'invoices.4_digits_year' }}</td></tr>
<tr><td><strong>YY</strong></td><td translate>{{ 'app.admin.invoices.2_digits_year' }}</td></tr>
<tr><td><strong>YYYY</strong></td><td translate>{{ 'app.admin.invoices.4_digits_year' }}</td></tr>
</table>
</script>
<script type="text/ng-template" id="addMonth.html">
<table class="invoice-element-legend">
<tr><td><strong>M</strong></td><td translate>{{ 'invoices.month_number' }}</td></tr>
<tr><td><strong>MM</strong></td><td translate>{{ 'invoices.2_digits_month_number' }}</td></tr>
<tr><td><strong>MMM</strong></td><td translate>{{ 'invoices.3_characters_month_name' }}</td></tr>
<tr><td><strong>M</strong></td><td translate>{{ 'app.admin.invoices.month_number' }}</td></tr>
<tr><td><strong>MM</strong></td><td translate>{{ 'app.admin.invoices.2_digits_month_number' }}</td></tr>
<tr><td><strong>MMM</strong></td><td translate>{{ 'app.admin.invoices.3_characters_month_name' }}</td></tr>
</table>
</script>
<script type="text/ng-template" id="addDay.html">
<table class="invoice-element-legend">
<tr><td><strong>D</strong></td><td translate>{{ 'invoices.day_in_the_month' }}</td></tr>
<tr><td><strong>DD</strong></td><td translate>{{ 'invoices.2_digits_day_in_the_month' }}</td></tr>
<tr><td><strong>D</strong></td><td translate>{{ 'app.admin.invoices.day_in_the_month' }}</td></tr>
<tr><td><strong>DD</strong></td><td translate>{{ 'app.admin.invoices.2_digits_day_in_the_month' }}</td></tr>
</table>
</script>
<script type="text/ng-template" id="addInvoiceNumber.html">
<table class="invoice-element-legend">
<tr><td><strong>dd...dd</strong></td><td translate>{{ 'invoices.n_digits_daily_count_of_invoices' }}</td></tr>
<tr><td><strong>mm...mm</strong></td><td translate>{{ 'invoices.n_digits_monthly_count_of_invoices' }}</td></tr>
<tr><td><strong>yy...yy</strong></td><td translate>{{ 'invoices.n_digits_annual_amount_of_invoices' }}</td></tr>
<tr><td><strong>dd...dd</strong></td><td translate>{{ 'app.admin.invoices.n_digits_daily_count_of_invoices' }}</td></tr>
<tr><td><strong>mm...mm</strong></td><td translate>{{ 'app.admin.invoices.n_digits_monthly_count_of_invoices' }}</td></tr>
<tr><td><strong>yy...yy</strong></td><td translate>{{ 'app.admin.invoices.n_digits_annual_amount_of_invoices' }}</td></tr>
</table>
<span class="bottom-notes" translate>{{ 'invoices.beware_if_the_number_exceed_the_specified_length_it_will_be_truncated_by_the_left' }}</span>
<span class="bottom-notes" translate>{{ 'app.admin.invoices.beware_if_the_number_exceed_the_specified_length_it_will_be_truncated_by_the_left' }}</span>
</script>
<script type="text/ng-template" id="addOrderNumber.html">
<table class="invoice-element-legend">
<tr><td><strong>nn...nn</strong></td><td translate>{{ 'invoices.n_digits_count_of_orders' }}</td></tr>
<tr><td><strong>dd...dd</strong></td><td translate>{{ 'invoices.n_digits_daily_count_of_orders' }}</td></tr>
<tr><td><strong>mm...mm</strong></td><td translate>{{ 'invoices.n_digits_monthly_count_of_orders' }}</td></tr>
<tr><td><strong>yy...yy</strong></td><td translate>{{ 'invoices.n_digits_annual_amount_of_orders' }}</td></tr>
<tr><td><strong>nn...nn</strong></td><td translate>{{ 'app.admin.invoices.n_digits_count_of_orders' }}</td></tr>
<tr><td><strong>dd...dd</strong></td><td translate>{{ 'app.admin.invoices.n_digits_daily_count_of_orders' }}</td></tr>
<tr><td><strong>mm...mm</strong></td><td translate>{{ 'app.admin.invoices.n_digits_monthly_count_of_orders' }}</td></tr>
<tr><td><strong>yy...yy</strong></td><td translate>{{ 'app.admin.invoices.n_digits_annual_amount_of_orders' }}</td></tr>
</table>
<span class="bottom-notes" translate>{{ 'invoices.beware_if_the_number_exceed_the_specified_length_it_will_be_truncated_by_the_left' }}</span>
<span class="bottom-notes" translate>{{ 'app.admin.invoices.beware_if_the_number_exceed_the_specified_length_it_will_be_truncated_by_the_left' }}</span>
</script>
<script type="text/ng-template" id="addOnlineInfo.html">
<table class="invoice-element-legend">
<tr><td><strong>X[texte]</strong></td><td>{{ 'invoices.add_a_notice_regarding_the_online_sales_only_if_the_invoice_is_concerned' | translate }} <mark translate>{{ 'invoices.this_will_never_be_added_when_a_refund_notice_is_present' }}</mark> {{ 'invoices.eg_XVL_will_add_VL_to_the_invoices_settled_with_stripe' | translate }}</td></tr>
<tr><td><strong>X[texte]</strong></td><td>{{ 'app.admin.invoices.add_a_notice_regarding_the_online_sales_only_if_the_invoice_is_concerned' | translate }} <mark translate>{{ 'app.admin.invoices.this_will_never_be_added_when_a_refund_notice_is_present' }}</mark> {{ 'app.admin.invoices.eg_XVL_will_add_VL_to_the_invoices_settled_with_stripe' | translate }}</td></tr>
</table>
</script>
<script type="text/ng-template" id="addWalletInfo.html">
<table class="invoice-element-legend">
<tr><td><strong>W[texte]</strong></td><td>{{ 'invoices.add_a_notice_regarding_the_wallet_only_if_the_invoice_is_concerned' | translate }} <mark translate>{{ 'invoices.this_will_never_be_added_when_a_refund_notice_is_present' }}</mark> {{ 'invoices.eg_WPM_will_add_PM_to_the_invoices_settled_with_wallet' | translate }}</td></tr>
<tr><td><strong>W[texte]</strong></td><td>{{ 'app.admin.invoices.add_a_notice_regarding_the_wallet_only_if_the_invoice_is_concerned' | translate }} <mark translate>{{ 'app.admin.invoices.this_will_never_be_added_when_a_refund_notice_is_present' }}</mark> {{ 'app.admin.invoices.eg_WPM_will_add_PM_to_the_invoices_settled_with_wallet' | translate }}</td></tr>
</table>
</script>
<script type="text/ng-template" id="addRefundInfo.html">
<table class="invoice-element-legend">
<tr><td><strong>R[texte]</strong></td><td>{{ 'invoices.add_a_notice_regarding_refunds_only_if_the_invoice_is_concerned' | translate }}<mark translate>{{ 'invoices.this_will_never_be_added_when_an_online_sales_notice_is_present' }}</mark> {{ 'invoices.eg_RA_will_add_A_to_the_refund_invoices' | translate }}</td></tr>
<tr><td><strong>R[texte]</strong></td><td>{{ 'app.admin.invoices.add_a_notice_regarding_refunds_only_if_the_invoice_is_concerned' | translate }}<mark translate>{{ 'app.admin.invoices.this_will_never_be_added_when_an_online_sales_notice_is_present' }}</mark> {{ 'app.admin.invoices.eg_RA_will_add_A_to_the_refund_invoices' | translate }}</td></tr>
</table>
</script>
<script type="text/ng-template" id="editCode.html">
<div class="custom-invoice">
<div class="modal-header">
<h3 class="modal-title" translate>{{ 'invoices.code' }}</h3>
<h3 class="modal-title" translate>{{ 'app.admin.invoices.code' }}</h3>
</div>
<div class="modal-body">
<div class="form-group">
<label for="enableCode" class="control-label" translate>{{ 'invoices.enable_the_code' }}</label>
<label for="enableCode" class="control-label" translate>{{ 'app.admin.invoices.enable_the_code' }}</label>
<input bs-switch
ng-model="isSelected"
id="enableCode"
type="checkbox"
class="form-control m-l-sm"
switch-on-text="{{ 'invoices.enabled' | translate }}"
switch-off-text="{{ 'invoices.disabled' | translate }}"
switch-on-text="{{ 'app.admin.invoices.enabled' | translate }}"
switch-off-text="{{ 'app.admin.invoices.disabled' | translate }}"
switch-animate="true"/>
</div>
<div class="form-group" ng-show="isSelected">
<label for="codeModel" class="control-label" translate>{{ 'invoices.code' }}</label>
<label for="codeModel" class="control-label" translate>{{ 'app.admin.invoices.code' }}</label>
<input id="codeModel" type="text" ng-model="codeModel" class="form-control"/>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-warning" ng-click="ok()" translate>{{ 'confirm' }}</button>
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'cancel' }}</button>
<button class="btn btn-warning" ng-click="ok()" translate>{{ 'app.shared.buttons.confirm' }}</button>
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'app.shared.buttons.cancel' }}</button>
</div>
</div>
</script>
@ -461,33 +467,33 @@
<script type="text/ng-template" id="editNumber.html">
<div class="custom-invoice">
<div class="modal-header">
<h3 class="modal-title" translate>{{ 'invoices.order_number' }}</h3>
<h3 class="modal-title" translate>{{ 'app.admin.invoices.order_number' }}</h3>
</div>
<div class="modal-body row">
<div class="elements col-md-4">
<h4 translate>{{ 'invoices.elements' }}</h4>
<h4 translate>{{ 'app.admin.invoices.elements' }}</h4>
<ul>
<li ng-click="invoice.number.help = 'addYear.html'">{{ 'invoices.year' | translate }}</li>
<li ng-click="invoice.number.help = 'addMonth.html'">{{ 'invoices.month' | translate }}</li>
<li ng-click="invoice.number.help = 'addDay.html'">{{ 'invoices.day' | translate }}</li>
<li ng-click="invoice.number.help = 'addOrderNumber.html'">{{ 'invoices.order_num' | translate }}</li>
<li ng-click="invoice.number.help = 'addYear.html'">{{ 'app.admin.invoices.year' | translate }}</li>
<li ng-click="invoice.number.help = 'addMonth.html'">{{ 'app.admin.invoices.month' | translate }}</li>
<li ng-click="invoice.number.help = 'addDay.html'">{{ 'app.admin.invoices.day' | translate }}</li>
<li ng-click="invoice.number.help = 'addOrderNumber.html'">{{ 'app.admin.invoices.order_num' | translate }}</li>
</ul>
</div>
<div class="col-md-8">
<div class="model">
<h4 translate>{{ 'invoices.model' }}</h4>
<h4 translate>{{ 'app.admin.invoices.model' }}</h4>
<input type="text" class="form-control" ng-model="model">
</div>
<div class="help">
<h4 translate>{{ 'invoices.documentation' }}</h4>
<h4 translate>{{ 'app.admin.invoices.documentation' }}</h4>
<ng-include src="invoice.number.help" autoscroll="true">
</ng-include>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-warning" ng-click="ok()" translate>{{ 'confirm' }}</button>
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'cancel' }}</button>
<button class="btn btn-warning" ng-click="ok()" translate>{{ 'app.shared.buttons.confirm' }}</button>
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'app.shared.buttons.cancel' }}</button>
</div>
</div>
</script>
@ -496,23 +502,23 @@
<script type="text/ng-template" id="editVAT.html">
<div class="custom-invoice">
<div class="modal-header">
<h3 class="modal-title" translate>{{ 'invoices.VAT' }}</h3>
<h3 class="modal-title" translate>{{ 'app.admin.invoices.VAT' }}</h3>
</div>
<div class="modal-body">
<div class="form-group">
<label for="enableVAT" class="control-label" translate>{{ 'invoices.enable_VAT' }}</label>
<label for="enableVAT" class="control-label" translate>{{ 'app.admin.invoices.enable_VAT' }}</label>
<input bs-switch
ng-model="isSelected"
id="enableVAT"
type="checkbox"
class="form-control m-l-sm"
switch-on-text="{{ 'invoices.enabled' | translate }}"
switch-off-text="{{ 'invoices.disabled' | translate }}"
switch-on-text="{{ 'app.admin.invoices.enabled' | translate }}"
switch-off-text="{{ 'app.admin.invoices.disabled' | translate }}"
switch-animate="true"/>
</div>
<div class="form-group" ng-show="isSelected">
<label for="vatRate" class="control-label" translate>{{ 'invoices.VAT_rate' }}</label>
<label for="vatRate" class="control-label" translate>{{ 'app.admin.invoices.VAT_rate' }}</label>
<div class="input-group">
<span class="input-group-addon">% </span>
<input id="vatRate" type="number" ng-model="rate" class="form-control" min="0" max="100"/>
@ -520,32 +526,32 @@
</div>
<div class="m-t-lg">
<h4 translate>{{ 'invoices.VAT_history' }}</h4>
<h4 translate>{{ 'app.admin.invoices.VAT_history' }}</h4>
<table class="table scrollable-3-cols">
<thead>
<tr>
<th translate>{{ 'invoices.VAT_rate' }}</th>
<th translate>{{ 'invoices.changed_at' }}</th>
<th translate>{{ 'invoices.changed_by' }}</th>
<th translate>{{ 'app.admin.invoices.VAT_rate' }}</th>
<th translate>{{ 'app.admin.invoices.changed_at' }}</th>
<th translate>{{ 'app.admin.invoices.changed_by' }}</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="value in history | orderBy:'-date'">
<td>
<span class="no-user-label" ng-show="value.enabled === false" translate>{{'invoices.VAT_disabled'}}</span>
<span class="no-user-label" ng-show="value.enabled === true" translate>{{'invoices.VAT_enabled'}}</span>
<span class="no-user-label" ng-show="value.enabled === false" translate>{{'app.admin.invoices.VAT_disabled'}}</span>
<span class="no-user-label" ng-show="value.enabled === true" translate>{{'app.admin.invoices.VAT_enabled'}}</span>
<span ng-show="value.rate">{{value.rate}}</span>
</td>
<td>{{value.date | amDateFormat:'L LT'}}</td>
<td>{{value.user.name}}<span class="no-user-label" ng-hide="value.user" translate>{{ 'invoices.deleted_user' }}</span></td>
<td>{{value.user.name}}<span class="no-user-label" ng-hide="value.user" translate>{{ 'app.admin.invoices.deleted_user' }}</span></td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-warning" ng-click="ok()" translate>{{ 'confirm' }}</button>
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'cancel' }}</button>
<button class="btn btn-warning" ng-click="ok()" translate>{{ 'app.shared.buttons.confirm' }}</button>
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'app.shared.buttons.cancel' }}</button>
</div>
</div>
</script>

View File

@ -1,18 +1,18 @@
<div class="form-group" ng-class="{'has-error': userForm['user[group_id]'].$dirty && userForm['user[group_id]'].$invalid}">
<label for="user_group_id" class="col-sm-3 control-label">
<span translate>{{ 'group' }}</span>
<span translate>{{ 'app.shared.user_admin.group' }}</span>
<span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
</label>
<div class="col-sm-9">
<select ng-model="user.group_id" ng-disabled="user.subscribed_plan" class="form-control" name="user[group_id]" id="user_group_id" ng-options="g.id as g.name for g in groups" required>
</select>
<input type="hidden" name="user[group_id]" ng-value="user.group_id" />
<span class="help-block" ng-show="userForm['user[group_id]'].$dirty && userForm['user[group_id]'].$error.required" translate>{{ 'group_is_required' }}</span>
<span class="help-block" ng-show="userForm['user[group_id]'].$dirty && userForm['user[group_id]'].$error.required" translate>{{ 'app.shared.user_admin.group_is_required' }}</span>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label" translate>{{ 'trainings' }}</label>
<label class="col-sm-2 control-label" translate>{{ 'app.shared.user_admin.trainings' }}</label>
<div class="col-sm-10">
<input type="hidden" name="user[statistic_profile_attributes][training_ids][]" value="" />
<ui-select multiple ng-model="user.training_ids" class="form-control">
@ -28,7 +28,7 @@
</div>
<div class="form-group">
<label class="col-sm-2 control-label" translate>{{ 'tags' }}</label>
<label class="col-sm-2 control-label" translate>{{ 'app.shared.user_admin.tags' }}</label>
<div class="col-sm-10">
<input type="hidden" name="user[tag_ids][]" value="" />
<ui-select multiple ng-model="user.tag_ids" name="user[tag_ids][]" class="form-control">

View File

@ -2,23 +2,23 @@
<div class="form-group">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-filter"></i></span>
<input type="text" ng-model="searchFilter" class="form-control" placeholder="{{ 'search_for_an_administrator' | translate }}">
<input type="text" ng-model="searchFilter" class="form-control" placeholder="{{ 'app.admin.members.search_for_an_administrator' | translate }}">
</div>
</div>
</div>
<div class="col-md-12">
<button type="button" class="btn btn-warning m-t m-b" ui-sref="app.admin.admins_new" translate>{{ 'add_a_new_administrator' }}</button>
<button type="button" class="btn btn-warning m-t m-b" ui-sref="app.admin.admins_new" translate>{{ 'app.admin.members.add_a_new_administrator' }}</button>
<table class="table">
<thead>
<tr>
<th style="width:15%"><a href="" ng-click="setOrderAdmin('profile_attributes.last_name')">{{ 'surname' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderAdmin =='profile_attributes.last_name', 'fa fa-sort-alpha-desc': orderAdmin =='-profile_attributes.last_name', 'fa fa-arrows-v': orderAdmin }"></i></a></th>
<th style="width:15%"><a href="" ng-click="setOrderAdmin('profile_attributes.last_name')">{{ 'app.admin.members.surname' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderAdmin =='profile_attributes.last_name', 'fa fa-sort-alpha-desc': orderAdmin =='-profile_attributes.last_name', 'fa fa-arrows-v': orderAdmin }"></i></a></th>
<th style="width:15%"><a href="" ng-click="setOrderAdmin('profile_attributes.first_name')">{{ 'first_name' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderAdmin =='profile_attributes.first_name', 'fa fa-sort-alpha-desc': orderAdmin =='-profile_attributes.first_name', 'fa fa-arrows-v': orderAdmin }"></i></a></th>
<th style="width:15%"><a href="" ng-click="setOrderAdmin('profile_attributes.first_name')">{{ 'app.admin.members.first_name' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderAdmin =='profile_attributes.first_name', 'fa fa-sort-alpha-desc': orderAdmin =='-profile_attributes.first_name', 'fa fa-arrows-v': orderAdmin }"></i></a></th>
<th style="width:15%"><a href="" ng-click="setOrderAdmin('email')">{{ 'email' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderAdmin =='email', 'fa fa-sort-alpha-desc': orderAdmin =='-email', 'fa fa-arrows-v': orderAdmin }"></i></a></th>
<th style="width:15%"><a href="" ng-click="setOrderAdmin('email')">{{ 'app.admin.members.email' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderAdmin =='email', 'fa fa-sort-alpha-desc': orderAdmin =='-email', 'fa fa-arrows-v': orderAdmin }"></i></a></th>
<th style="width:10%"><a href="" ng-click="setOrderAdmin('profile_attributes.phone')">{{ 'phone' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': orderAdmin =='profile_attributes.phone', 'fa fa-sort-numeric-desc': orderAdmin =='-profile_attributes.phone', 'fa fa-arrows-v': orderAdmin }"></i></a></th>
<th style="width:10%"><a href="" ng-click="setOrderAdmin('profile_attributes.phone')">{{ 'app.admin.members.phone' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': orderAdmin =='profile_attributes.phone', 'fa fa-sort-numeric-desc': orderAdmin =='-profile_attributes.phone', 'fa fa-arrows-v': orderAdmin }"></i></a></th>
<th style="width:10%"></th>
</tr>
</thead>
@ -36,4 +36,4 @@
</tr>
</tbody>
</table>
</div>
</div>

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