mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-01-19 08:52:25 +01:00
Merge branch 'dev' for release 5.4.14
This commit is contained in:
commit
089ab09dc4
5
.codeclimate.yml
Normal file
5
.codeclimate.yml
Normal file
@ -0,0 +1,5 @@
|
||||
plugins:
|
||||
rubocop:
|
||||
enabled: true
|
||||
config:
|
||||
file: .rubocop.yml
|
9
.gitlab-ci.yml
Normal file
9
.gitlab-ci.yml
Normal file
@ -0,0 +1,9 @@
|
||||
stages:
|
||||
- test
|
||||
|
||||
include:
|
||||
- template: Code-Quality.gitlab-ci.yml
|
||||
|
||||
code_quality:
|
||||
artifacts:
|
||||
paths: [ gl-code-quality-report.json ]
|
@ -1,4 +1,8 @@
|
||||
Metrics/LineLength:
|
||||
require: rubocop-rails
|
||||
|
||||
AllCops:
|
||||
NewCops: enable
|
||||
Layout/LineLength:
|
||||
Max: 140
|
||||
Metrics/MethodLength:
|
||||
Max: 35
|
||||
@ -19,8 +23,6 @@ Metrics/BlockLength:
|
||||
- 'test/**/*.rb'
|
||||
Metrics/ParameterLists:
|
||||
CountKeywordArgs: false
|
||||
Style/BracesAroundHashParameters:
|
||||
EnforcedStyle: context_dependent
|
||||
Style/RegexpLiteral:
|
||||
EnforcedStyle: slashes
|
||||
Style/EmptyElse:
|
||||
|
57
CHANGELOG.md
57
CHANGELOG.md
@ -1,6 +1,61 @@
|
||||
# Changelog Fab-manager
|
||||
|
||||
## next deploy
|
||||
## next release
|
||||
|
||||
## v5.4.14 2022 August 1
|
||||
|
||||
- Added a test for multiple reservations on the same space slot
|
||||
- Display the tag and theme field on the detail view of a project
|
||||
- Improved file validation, validation is now based on content of the file in addition of the validation of the extension
|
||||
- Fix a security issue: xss vulnerability in projects
|
||||
- Fix a security issue: updated moment to 2.29.4 to fix [CVE-2022-31129](https://cve.mitre.org/cgi-bin/cvename.cgi?CVE-2022-31129)
|
||||
|
||||
## v5.4.13 2022 July 27
|
||||
|
||||
- Improved calendars loading time
|
||||
- Refactored and documented the availability-slot-reservation data model
|
||||
- Display bookers names to connected users now apply to all resources
|
||||
- Updated rails locales files
|
||||
- Usage of the rails logger instead of printing to standard output
|
||||
- Optionnaly add a username column to the member list (#375)
|
||||
- Improved attached ICS file texts, in reservations emails
|
||||
- Fix a bug: user validation required alert is displayed and disappears instantly
|
||||
- Fix a bug: canceled trainings are still shown on the public profile page
|
||||
- Fix a bug: prevent same slot booking feature ignores canceled reservations
|
||||
- Fix a bug: wrong currency on invoices files
|
||||
- Fix a bug: unable to reserve if user's subscription plan is disabled
|
||||
- Fix a bug: for admins and managers, the current password is not requested before changing their own password
|
||||
- Fix a bug: missing translations
|
||||
- Fix a bug: unable to book a space's slot with an existing reservation
|
||||
- Fix a bug: Unable to import accounts from SSO when the transformation modal was opened but leaved empty
|
||||
- Fix a bug: Unable to change the group of a user
|
||||
- Fix a bug: As admin, unable to create a new member (#374)
|
||||
- Fix a bug: profile completion form is not shown is T&C were not filled
|
||||
- Fix a bug: Erroneous "cancelation failed" message if elasticsearch was disabled
|
||||
- Fix a bug: admin group being replaced in SSO authentication (#376)
|
||||
- Fix a bug: SSO data being overridden when it is empty and the user can change it (#377)
|
||||
- Fix a security issue: updated terser to 4.8.1 to fix [CVE-2022-25858](https://cve.mitre.org/cgi-bin/cvename.cgi?CVE-2022-25858)
|
||||
- Fix a security issue: updated tzinfo to 1.2.10 to fix [CVE-2022-31163](https://cve.mitre.org/cgi-bin/cvename.cgi?CVE-2022-31163)
|
||||
- Fix a security issue: updated rails to 5.2.8.1 to fix [CVE-2022-32224](https://cve.mitre.org/cgi-bin/cvename.cgi?CVE-2022-32224)
|
||||
|
||||
## v5.4.12 2022 July 06
|
||||
|
||||
- Fix a bug: Gender, Address and Birthday are not mapped properly from SSO (#365)
|
||||
- Fix a bug: unable to import a new account from an SSO provider
|
||||
- Fix a security issue: updated rails-html-sanitizer to 1.4.3 to fix [CVE-2022-32209](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-32209)
|
||||
|
||||
## v5.4.11 2022 July 06
|
||||
|
||||
- Fix a bug: social networks icons not shown in firefox
|
||||
- Fix a bug: Gender, Address and Birthday are not mapped properly from SSO (#365)
|
||||
- Fix a bug: OIDC scopes are not shown in the configuration form select
|
||||
- Fix a bug: OIDC scopes are not saved
|
||||
|
||||
## v5.4.10 2022 July 05
|
||||
|
||||
- Increased About page title's size
|
||||
- Fix a bug: script mount-proof-of-identity-files creates a volume with an invalid path
|
||||
- Fix a bug: unable to access the new OIDC provider form
|
||||
|
||||
## v5.4.9 2022 June 29
|
||||
|
||||
|
5
Gemfile
5
Gemfile
@ -3,7 +3,7 @@
|
||||
source 'https://rubygems.org'
|
||||
|
||||
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
|
||||
gem 'rails', '~> 5.2.7'
|
||||
gem 'rails', '~> 5.2.8'
|
||||
# Used by rails 5.2 to reduce the app boot time by over 50%
|
||||
gem 'bootsnap'
|
||||
# Use Puma as web server
|
||||
@ -39,7 +39,8 @@ group :development do
|
||||
gem 'rb-readline'
|
||||
# Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
|
||||
gem 'railroady'
|
||||
gem 'rubocop', '~> 0.61.1', require: false
|
||||
gem 'rubocop', '~> 1.31', require: false
|
||||
gem 'rubocop-rails', require: false
|
||||
gem 'spring'
|
||||
gem 'spring-watcher-listen', '~> 2.0.0'
|
||||
end
|
||||
|
125
Gemfile.lock
125
Gemfile.lock
@ -4,46 +4,46 @@ GEM
|
||||
Ascii85 (1.0.3)
|
||||
aasm (5.0.8)
|
||||
concurrent-ruby (~> 1.0)
|
||||
actioncable (5.2.7.1)
|
||||
actionpack (= 5.2.7.1)
|
||||
actioncable (5.2.8.1)
|
||||
actionpack (= 5.2.8.1)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
actionmailer (5.2.7.1)
|
||||
actionpack (= 5.2.7.1)
|
||||
actionview (= 5.2.7.1)
|
||||
activejob (= 5.2.7.1)
|
||||
actionmailer (5.2.8.1)
|
||||
actionpack (= 5.2.8.1)
|
||||
actionview (= 5.2.8.1)
|
||||
activejob (= 5.2.8.1)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (5.2.7.1)
|
||||
actionview (= 5.2.7.1)
|
||||
activesupport (= 5.2.7.1)
|
||||
actionpack (5.2.8.1)
|
||||
actionview (= 5.2.8.1)
|
||||
activesupport (= 5.2.8.1)
|
||||
rack (~> 2.0, >= 2.0.8)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
||||
actionpack-page_caching (1.2.2)
|
||||
actionpack (>= 5.0.0)
|
||||
actionview (5.2.7.1)
|
||||
activesupport (= 5.2.7.1)
|
||||
actionview (5.2.8.1)
|
||||
activesupport (= 5.2.8.1)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.0.3)
|
||||
active_record_query_trace (1.7)
|
||||
activejob (5.2.7.1)
|
||||
activesupport (= 5.2.7.1)
|
||||
activejob (5.2.8.1)
|
||||
activesupport (= 5.2.8.1)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (5.2.7.1)
|
||||
activesupport (= 5.2.7.1)
|
||||
activerecord (5.2.7.1)
|
||||
activemodel (= 5.2.7.1)
|
||||
activesupport (= 5.2.7.1)
|
||||
activemodel (5.2.8.1)
|
||||
activesupport (= 5.2.8.1)
|
||||
activerecord (5.2.8.1)
|
||||
activemodel (= 5.2.8.1)
|
||||
activesupport (= 5.2.8.1)
|
||||
arel (>= 9.0)
|
||||
activestorage (5.2.7.1)
|
||||
actionpack (= 5.2.7.1)
|
||||
activerecord (= 5.2.7.1)
|
||||
activestorage (5.2.8.1)
|
||||
actionpack (= 5.2.8.1)
|
||||
activerecord (= 5.2.8.1)
|
||||
marcel (~> 1.0.0)
|
||||
activesupport (5.2.7.1)
|
||||
activesupport (5.2.8.1)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 0.7, < 2)
|
||||
minitest (~> 5.1)
|
||||
@ -57,7 +57,7 @@ GEM
|
||||
apipie-rails (0.5.17)
|
||||
rails (>= 4.1)
|
||||
arel (9.0.0)
|
||||
ast (2.4.0)
|
||||
ast (2.4.2)
|
||||
attr_required (1.0.1)
|
||||
awesome_print (1.8.0)
|
||||
axiom-types (0.1.1)
|
||||
@ -167,7 +167,7 @@ GEM
|
||||
mime-types (~> 3.0)
|
||||
multi_xml (>= 0.5.2)
|
||||
httpclient (2.8.3)
|
||||
i18n (1.10.0)
|
||||
i18n (1.12.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
icalendar (2.7.1)
|
||||
ice_cube (~> 0.16)
|
||||
@ -176,7 +176,6 @@ GEM
|
||||
image_processing (1.12.2)
|
||||
mini_magick (>= 4.9.5, < 5)
|
||||
ruby-vips (>= 2.0.17, < 3)
|
||||
jaro_winkler (1.5.4)
|
||||
jbuilder (2.10.0)
|
||||
activesupport (>= 5.0.0)
|
||||
jbuilder_cache_multi (0.1.0)
|
||||
@ -204,7 +203,7 @@ GEM
|
||||
listen (3.0.8)
|
||||
rb-fsevent (~> 0.9, >= 0.9.4)
|
||||
rb-inotify (~> 0.9, >= 0.9.7)
|
||||
loofah (2.17.0)
|
||||
loofah (2.18.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.5.9)
|
||||
mail (2.7.1)
|
||||
@ -222,7 +221,7 @@ GEM
|
||||
mini_magick (4.10.1)
|
||||
mini_mime (1.1.2)
|
||||
mini_portile2 (2.8.0)
|
||||
minitest (5.15.0)
|
||||
minitest (5.16.2)
|
||||
minitest-reporters (1.4.2)
|
||||
ansi
|
||||
builder
|
||||
@ -233,7 +232,7 @@ GEM
|
||||
multi_xml (0.6.0)
|
||||
multipart-post (2.1.1)
|
||||
nio4r (2.5.8)
|
||||
nokogiri (1.13.6)
|
||||
nokogiri (1.13.8)
|
||||
mini_portile2 (~> 2.8.0)
|
||||
racc (~> 1.4)
|
||||
notify_with (0.0.2)
|
||||
@ -274,8 +273,8 @@ GEM
|
||||
httparty (~> 0.20)
|
||||
orm_adapter (0.5.0)
|
||||
parallel (1.19.1)
|
||||
parser (2.7.0.4)
|
||||
ast (~> 2.4.0)
|
||||
parser (3.1.2.0)
|
||||
ast (~> 2.4.1)
|
||||
pdf-core (0.7.0)
|
||||
pdf-reader (2.4.0)
|
||||
Ascii85 (~> 1.0.0)
|
||||
@ -287,7 +286,6 @@ GEM
|
||||
pg_search (2.3.2)
|
||||
activerecord (>= 5.2)
|
||||
activesupport (>= 5.2)
|
||||
powerpack (0.1.2)
|
||||
prawn (2.2.2)
|
||||
pdf-core (~> 0.7.0)
|
||||
ttfunk (~> 1.5)
|
||||
@ -300,7 +298,7 @@ GEM
|
||||
activesupport (>= 3.0.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.6.0)
|
||||
rack (2.2.3.1)
|
||||
rack (2.2.4)
|
||||
rack-oauth2 (1.19.0)
|
||||
activesupport
|
||||
attr_required
|
||||
@ -309,26 +307,26 @@ GEM
|
||||
rack (>= 2.1.0)
|
||||
rack-proxy (0.7.2)
|
||||
rack
|
||||
rack-test (1.1.0)
|
||||
rack (>= 1.0, < 3)
|
||||
rack-test (2.0.2)
|
||||
rack (>= 1.3)
|
||||
railroady (1.5.3)
|
||||
rails (5.2.7.1)
|
||||
actioncable (= 5.2.7.1)
|
||||
actionmailer (= 5.2.7.1)
|
||||
actionpack (= 5.2.7.1)
|
||||
actionview (= 5.2.7.1)
|
||||
activejob (= 5.2.7.1)
|
||||
activemodel (= 5.2.7.1)
|
||||
activerecord (= 5.2.7.1)
|
||||
activestorage (= 5.2.7.1)
|
||||
activesupport (= 5.2.7.1)
|
||||
rails (5.2.8.1)
|
||||
actioncable (= 5.2.8.1)
|
||||
actionmailer (= 5.2.8.1)
|
||||
actionpack (= 5.2.8.1)
|
||||
actionview (= 5.2.8.1)
|
||||
activejob (= 5.2.8.1)
|
||||
activemodel (= 5.2.8.1)
|
||||
activerecord (= 5.2.8.1)
|
||||
activestorage (= 5.2.8.1)
|
||||
activesupport (= 5.2.8.1)
|
||||
bundler (>= 1.3.0)
|
||||
railties (= 5.2.7.1)
|
||||
railties (= 5.2.8.1)
|
||||
sprockets-rails (>= 2.0.0)
|
||||
rails-dom-testing (2.0.3)
|
||||
activesupport (>= 4.2.0)
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.4.2)
|
||||
rails-html-sanitizer (1.4.3)
|
||||
loofah (~> 2.3)
|
||||
rails-observers (0.1.5)
|
||||
activemodel (>= 4.0)
|
||||
@ -337,9 +335,9 @@ GEM
|
||||
rails_stdout_logging
|
||||
rails_serve_static_assets (0.0.5)
|
||||
rails_stdout_logging (0.0.5)
|
||||
railties (5.2.7.1)
|
||||
actionpack (= 5.2.7.1)
|
||||
activesupport (= 5.2.7.1)
|
||||
railties (5.2.8.1)
|
||||
actionpack (= 5.2.8.1)
|
||||
activesupport (= 5.2.8.1)
|
||||
method_source
|
||||
rake (>= 0.8.7)
|
||||
thor (>= 0.19.0, < 2.0)
|
||||
@ -353,19 +351,29 @@ GEM
|
||||
activesupport
|
||||
i18n
|
||||
redis (4.6.0)
|
||||
regexp_parser (2.5.0)
|
||||
repost (0.3.2)
|
||||
responders (2.4.1)
|
||||
actionpack (>= 4.2.0, < 6.0)
|
||||
railties (>= 4.2.0, < 6.0)
|
||||
rexml (3.2.5)
|
||||
rolify (5.2.0)
|
||||
rubocop (0.61.1)
|
||||
jaro_winkler (~> 1.5.1)
|
||||
rubocop (1.31.2)
|
||||
json (~> 2.3)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 2.5, != 2.5.1.1)
|
||||
powerpack (~> 0.1)
|
||||
parser (>= 3.1.0.0)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 1.8, < 3.0)
|
||||
rexml (>= 3.2.5, < 4.0)
|
||||
rubocop-ast (>= 1.18.0, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (~> 1.4.0)
|
||||
unicode-display_width (>= 1.4.0, < 3.0)
|
||||
rubocop-ast (1.19.1)
|
||||
parser (>= 3.1.1.0)
|
||||
rubocop-rails (2.15.2)
|
||||
activesupport (>= 4.2.0)
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 1.7.0, < 2.0)
|
||||
ruby-progressbar (1.10.1)
|
||||
ruby-rc4 (0.1.5)
|
||||
ruby-vips (2.1.4)
|
||||
@ -412,7 +420,7 @@ GEM
|
||||
spring-watcher-listen (2.0.1)
|
||||
listen (>= 2.7, < 4.0)
|
||||
spring (>= 1.2, < 3.0)
|
||||
sprockets (4.0.3)
|
||||
sprockets (4.1.1)
|
||||
concurrent-ruby (~> 1.0)
|
||||
rack (> 1, < 3)
|
||||
sprockets-rails (3.4.2)
|
||||
@ -440,7 +448,7 @@ GEM
|
||||
camertron-eprun
|
||||
cldr-plurals-runtime-rb (~> 1.0)
|
||||
tzinfo
|
||||
tzinfo (1.2.9)
|
||||
tzinfo (1.2.10)
|
||||
thread_safe (~> 0.1)
|
||||
tzinfo-data (1.2020.4)
|
||||
tzinfo (>= 1.0.0)
|
||||
@ -529,7 +537,7 @@ DEPENDENCIES
|
||||
puma (= 4.3.12)
|
||||
pundit
|
||||
railroady
|
||||
rails (~> 5.2.7)
|
||||
rails (~> 5.2.8)
|
||||
rails-observers
|
||||
rails_12factor
|
||||
rb-readline
|
||||
@ -537,7 +545,8 @@ DEPENDENCIES
|
||||
repost
|
||||
responders (~> 2.0)
|
||||
rolify
|
||||
rubocop (~> 0.61.1)
|
||||
rubocop (~> 1.31)
|
||||
rubocop-rails
|
||||
rubyXL
|
||||
rubyzip (>= 1.3.0)
|
||||
sassc (= 2.1.0)
|
||||
|
@ -85,10 +85,10 @@ class API::AuthProvidersController < API::ApiController
|
||||
|
||||
def provider_params
|
||||
if params['auth_provider']['providable_type'] == DatabaseProvider.name
|
||||
params.require(:auth_provider).permit(:name, :providable_type, providable_attributes: [:id])
|
||||
params.require(:auth_provider).permit(:id, :name, :providable_type, providable_attributes: [:id])
|
||||
elsif params['auth_provider']['providable_type'] == OAuth2Provider.name
|
||||
params.require(:auth_provider)
|
||||
.permit(:name, :providable_type,
|
||||
.permit(:id, :name, :providable_type,
|
||||
providable_attributes: %i[id base_url token_endpoint authorization_endpoint
|
||||
profile_url client_id client_secret scopes],
|
||||
auth_provider_mappings_attributes: [:id, :local_model, :local_field, :api_field, :api_endpoint, :api_data_type,
|
||||
@ -96,10 +96,11 @@ class API::AuthProvidersController < API::ApiController
|
||||
mapping: %i[from to]]])
|
||||
elsif params['auth_provider']['providable_type'] == OpenIdConnectProvider.name
|
||||
params.require(:auth_provider)
|
||||
.permit(:name, :providable_type,
|
||||
providable_attributes: %i[id issuer discovery client_auth_method scope prompt send_scope_to_token_endpoint
|
||||
client__identifier client__secret client__authorization_endpoint client__token_endpoint
|
||||
client__userinfo_endpoint client__jwks_uri client__end_session_endpoint profile_url],
|
||||
.permit(:id, :name, :providable_type,
|
||||
providable_attributes: [:id, :issuer, :discovery, :client_auth_method, :prompt, :send_scope_to_token_endpoint,
|
||||
:client__identifier, :client__secret, :client__authorization_endpoint, :client__token_endpoint,
|
||||
:client__userinfo_endpoint, :client__jwks_uri, :client__end_session_endpoint, :profile_url,
|
||||
scope: []],
|
||||
auth_provider_mappings_attributes: [:id, :local_model, :local_field, :api_field, :api_endpoint, :api_data_type,
|
||||
:_destroy, transformation: [:type, :format, :true_value, :false_value,
|
||||
mapping: %i[from to]]])
|
||||
|
@ -4,15 +4,15 @@
|
||||
class API::AvailabilitiesController < API::ApiController
|
||||
before_action :authenticate_user!, except: [:public]
|
||||
before_action :set_availability, only: %i[show update reservations lock]
|
||||
before_action :define_max_visibility, only: %i[machine trainings spaces]
|
||||
before_action :set_operator_role, only: %i[machine spaces trainings]
|
||||
before_action :set_customer, only: %i[machine spaces trainings]
|
||||
respond_to :json
|
||||
|
||||
def index
|
||||
authorize Availability
|
||||
start_date = ActiveSupport::TimeZone[params[:timezone]].parse(params[:start])
|
||||
end_date = ActiveSupport::TimeZone[params[:timezone]].parse(params[:end]).end_of_day
|
||||
display_window = window
|
||||
@availabilities = Availability.includes(:machines, :tags, :trainings, :spaces)
|
||||
.where('start_at >= ? AND end_at <= ?', start_date, end_date)
|
||||
.where('start_at >= ? AND end_at <= ?', display_window[:start], display_window[:end])
|
||||
|
||||
@availabilities = @availabilities.where.not(available_type: 'event') unless Setting.get('events_in_calendar')
|
||||
|
||||
@ -20,19 +20,14 @@ class API::AvailabilitiesController < API::ApiController
|
||||
end
|
||||
|
||||
def public
|
||||
start_date = ActiveSupport::TimeZone[params[:timezone]].parse(params[:start])
|
||||
end_date = ActiveSupport::TimeZone[params[:timezone]].parse(params[:end]).end_of_day
|
||||
@reservations = Reservation.includes(:slots, :statistic_profile)
|
||||
.references(:slots)
|
||||
.where('slots.start_at >= ? AND slots.end_at <= ?', start_date, end_date)
|
||||
display_window = window
|
||||
|
||||
machine_ids = params[:m] || []
|
||||
service = Availabilities::PublicAvailabilitiesService.new(current_user)
|
||||
@availabilities = service.public_availabilities(
|
||||
start_date,
|
||||
end_date,
|
||||
@reservations,
|
||||
machines: machine_ids, spaces: params[:s]
|
||||
display_window,
|
||||
{ machines: machine_ids, spaces: params[:s], trainings: params[:t] },
|
||||
(params[:evt] && params[:evt] == 'true')
|
||||
)
|
||||
|
||||
@title_filter = { machine_ids: machine_ids.map(&:to_i) }
|
||||
@ -47,10 +42,8 @@ class API::AvailabilitiesController < API::ApiController
|
||||
authorize Availability
|
||||
@availability = Availability.new(availability_params)
|
||||
if @availability.save
|
||||
if params[:availability][:occurrences]
|
||||
service = Availabilities::CreateAvailabilitiesService.new
|
||||
service.create(@availability, params[:availability][:occurrences])
|
||||
end
|
||||
render :show, status: :created, location: @availability
|
||||
else
|
||||
render json: @availability.errors, status: :unprocessable_entity
|
||||
@ -78,27 +71,32 @@ class API::AvailabilitiesController < API::ApiController
|
||||
end
|
||||
|
||||
def machine
|
||||
@current_user_role = current_user.role
|
||||
|
||||
service = Availabilities::AvailabilitiesService.new(current_user, other: @visi_max_other, year: @visi_max_year)
|
||||
@slots = service.machines(params[:machine_id], user)
|
||||
service = Availabilities::AvailabilitiesService.new(current_user)
|
||||
@machine = Machine.friendly.find(params[:machine_id])
|
||||
@slots = service.machines([@machine], @customer, window)
|
||||
end
|
||||
|
||||
def trainings
|
||||
service = Availabilities::AvailabilitiesService.new(current_user, other: @visi_max_other, year: @visi_max_year)
|
||||
@availabilities = service.trainings(params[:training_id], user)
|
||||
service = Availabilities::AvailabilitiesService.new(current_user)
|
||||
@trainings = if params[:training_id].is_number? || (params[:training_id].length.positive? && params[:training_id] != 'all')
|
||||
[Training.friendly.find(params[:training_id])]
|
||||
else
|
||||
Training.all
|
||||
end
|
||||
@slots = service.trainings(@trainings, @customer, window)
|
||||
end
|
||||
|
||||
def spaces
|
||||
@current_user_role = current_user.role
|
||||
|
||||
service = Availabilities::AvailabilitiesService.new(current_user, other: @visi_max_other, year: @visi_max_year)
|
||||
@slots = service.spaces(params[:space_id], user)
|
||||
service = Availabilities::AvailabilitiesService.new(current_user)
|
||||
@space = Space.friendly.find(params[:space_id])
|
||||
@slots = service.spaces([@space], @customer, window)
|
||||
end
|
||||
|
||||
def reservations
|
||||
authorize Availability
|
||||
@reservation_slots = @availability.slots.includes(reservations: [statistic_profile: [user: [:profile]]]).order('slots.start_at ASC')
|
||||
@slots_reservations = @availability.slots_reservations
|
||||
.includes(:slot, reservation: [statistic_profile: [user: [:profile]]])
|
||||
.order('slots.start_at ASC')
|
||||
end
|
||||
|
||||
def export_availabilities
|
||||
@ -131,14 +129,24 @@ class API::AvailabilitiesController < API::ApiController
|
||||
|
||||
private
|
||||
|
||||
def user
|
||||
if params[:member_id]
|
||||
def window
|
||||
start_date = ActiveSupport::TimeZone[params[:timezone]]&.parse(params[:start])
|
||||
end_date = ActiveSupport::TimeZone[params[:timezone]]&.parse(params[:end])&.end_of_day
|
||||
{ start: start_date, end: end_date }
|
||||
end
|
||||
|
||||
def set_customer
|
||||
@customer = if params[:member_id]
|
||||
User.find(params[:member_id])
|
||||
else
|
||||
current_user
|
||||
end
|
||||
end
|
||||
|
||||
def set_operator_role
|
||||
@operator_role = current_user.role
|
||||
end
|
||||
|
||||
def set_availability
|
||||
@availability = Availability.find(params[:id])
|
||||
end
|
||||
@ -155,43 +163,10 @@ class API::AvailabilitiesController < API::ApiController
|
||||
end
|
||||
|
||||
def filter_availabilites(availabilities)
|
||||
availabilities_filtered = []
|
||||
availabilities.to_ary.each do |a|
|
||||
# machine slot
|
||||
if !a.try(:available_type)
|
||||
availabilities_filtered << a
|
||||
else
|
||||
availabilities_filtered << a if filter_training?(a)
|
||||
availabilities_filtered << a if filter_space?(a)
|
||||
availabilities_filtered << a if filter_machine?(a)
|
||||
availabilities_filtered << a if filter_event?(a)
|
||||
end
|
||||
end
|
||||
availabilities_filtered.delete_if(&method(:remove_full?))
|
||||
end
|
||||
|
||||
def filter_training?(availability)
|
||||
params[:t] && availability.available_type == 'training' && params[:t].include?(availability.trainings.first.id.to_s)
|
||||
end
|
||||
|
||||
def filter_space?(availability)
|
||||
params[:s] && availability.available_type == 'space' && params[:s].include?(availability.spaces.first.id.to_s)
|
||||
end
|
||||
|
||||
def filter_machine?(availability)
|
||||
params[:m] && availability.available_type == 'machines' && (params[:m].map(&:to_i) & availability.machine_ids).any?
|
||||
end
|
||||
|
||||
def filter_event?(availability)
|
||||
params[:evt] && params[:evt] == 'true' && availability.available_type == 'event'
|
||||
availabilities.delete_if(&method(:remove_full?))
|
||||
end
|
||||
|
||||
def remove_full?(availability)
|
||||
params[:dispo] == 'false' && (availability.is_reserved || (availability.try(:full?) && availability.full?))
|
||||
end
|
||||
|
||||
def define_max_visibility
|
||||
@visi_max_year = Setting.get('visibility_yearly').to_i.months.since
|
||||
@visi_max_other = Setting.get('visibility_others').to_i.months.since
|
||||
end
|
||||
end
|
||||
|
@ -55,6 +55,8 @@ class API::EventsController < API::ApiController
|
||||
authorize Event
|
||||
@event = Event.new(event_params.permit!)
|
||||
if @event.save
|
||||
service = Availabilities::CreateAvailabilitiesService.new
|
||||
service.create_slots(@event.availability)
|
||||
render :show, status: :created, location: @event
|
||||
else
|
||||
render json: @event.errors, status: :unprocessable_entity
|
||||
|
@ -40,7 +40,7 @@ class API::MembersController < API::ApiController
|
||||
def create
|
||||
authorize :user, :create_member?
|
||||
|
||||
@member = User.new(user_params.permit!)
|
||||
@member = User.new(Members::MembersService.handle_organization(user_params.permit!))
|
||||
members_service = Members::MembersService.new(@member)
|
||||
|
||||
if members_service.create(current_user, user_params)
|
||||
|
@ -55,9 +55,8 @@ class API::PayzenController < API::PaymentsController
|
||||
|
||||
def check_cart
|
||||
cart = shopping_cart
|
||||
unless cart.valid?
|
||||
render json: { error: 'unable to pay' }, status: :unprocessable_entity and return
|
||||
end
|
||||
render json: { error: cart.errors }, status: :unprocessable_entity and return unless cart.valid?
|
||||
|
||||
render json: { cart: 'ok' }, status: :ok
|
||||
end
|
||||
|
||||
|
@ -43,6 +43,6 @@ class API::ReservationsController < API::ApiController
|
||||
def reservation_params
|
||||
params.require(:reservation).permit(:message, :reservable_id, :reservable_type, :nb_reserve_places,
|
||||
tickets_attributes: %i[event_price_category_id booked],
|
||||
slots_attributes: %i[id start_at end_at availability_id offered])
|
||||
slots_reservations_attributes: %i[id slot_id offered])
|
||||
end
|
||||
end
|
||||
|
@ -1,35 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# API Controller for resources of type Slot
|
||||
# Slots are used to cut Availabilities into reservable slots. The duration of these slots is configured per
|
||||
# availability by Availability.slot_duration, or otherwise globally by Setting.get('slot_duration')
|
||||
class API::SlotsController < API::ApiController
|
||||
before_action :authenticate_user!
|
||||
before_action :set_slot, only: %i[update cancel]
|
||||
respond_to :json
|
||||
|
||||
def update
|
||||
authorize @slot
|
||||
if @slot.update(slot_params)
|
||||
SubscriptionExtensionAfterReservation.new(@slot.reservation).extend_subscription_if_eligible
|
||||
render :show, status: :created, location: @slot
|
||||
else
|
||||
render json: @slot.errors, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def cancel
|
||||
authorize @slot
|
||||
SlotService.new.cancel(@slot)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_slot
|
||||
@slot = Slot.find(params[:id])
|
||||
end
|
||||
|
||||
def slot_params
|
||||
params.require(:slot).permit(:start_at, :end_at, :availability_id)
|
||||
end
|
||||
end
|
35
app/controllers/api/slots_reservations_controller.rb
Normal file
35
app/controllers/api/slots_reservations_controller.rb
Normal file
@ -0,0 +1,35 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# API Controller for resources of type Slot
|
||||
# Slots are used to cut Availabilities into reservable slots. The duration of these slots is configured per
|
||||
# availability by Availability.slot_duration, or otherwise globally by Setting.get('slot_duration')
|
||||
class API::SlotsReservationsController < API::ApiController
|
||||
before_action :authenticate_user!
|
||||
before_action :set_slots_reservation, only: %i[update cancel]
|
||||
respond_to :json
|
||||
|
||||
def update
|
||||
authorize @slot_reservation
|
||||
if @slot_reservation.update(slot_params)
|
||||
SubscriptionExtensionAfterReservation.new(@slot_reservation.reservation).extend_subscription_if_eligible
|
||||
render :show, status: :created, location: @slot_reservation
|
||||
else
|
||||
render json: @slot_reservation.errors, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def cancel
|
||||
authorize @slot_reservation
|
||||
SlotsReservationsService.cancel(@slot_reservation)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_slots_reservation
|
||||
@slot_reservation = SlotsReservation.find(params[:id])
|
||||
end
|
||||
|
||||
def slot_params
|
||||
params.require(:slots_reservation).permit(:slot_id)
|
||||
end
|
||||
end
|
@ -19,9 +19,8 @@ class API::StripeController < API::PaymentsController
|
||||
res = nil # json of the API answer
|
||||
|
||||
cart = shopping_cart
|
||||
unless cart.valid?
|
||||
render json: { error: 'unable to pay' }, status: :unprocessable_entity and return
|
||||
end
|
||||
render json: { error: cart.errors }, status: :unprocessable_entity and return unless cart.valid?
|
||||
|
||||
begin
|
||||
amount = debit_amount(cart) # will contains the amount and the details of each invoice lines
|
||||
if params[:payment_method_id].present?
|
||||
@ -74,7 +73,7 @@ class API::StripeController < API::PaymentsController
|
||||
|
||||
def setup_subscription
|
||||
cart = shopping_cart
|
||||
raise InvalidSubscriptionError unless cart.valid?
|
||||
render json: { error: cart.errors }, status: :unprocessable_entity and return unless cart.valid?
|
||||
|
||||
service = Stripe::Service.new
|
||||
method = service.attach_method_as_default(
|
||||
|
@ -52,8 +52,8 @@ class API::TrainingsController < API::ApiController
|
||||
authorize Training
|
||||
@training = Training.find(params[:id])
|
||||
@availabilities = @training.availabilities
|
||||
.includes(slots: { reservations: { statistic_profile: [:trainings, user: [:profile]] } })
|
||||
.where('slots.canceled_at': nil)
|
||||
.includes(slots: { slots_reservations: { reservations: { statistic_profile: [:trainings, user: [:profile]] } } })
|
||||
.where('slots_reservations.canceled_at': nil)
|
||||
.order('availabilities.start_at DESC')
|
||||
end
|
||||
|
||||
|
@ -14,7 +14,7 @@ class SocialBotController < ActionController::Base
|
||||
@training = Training.friendly.find(Regexp.last_match(3).to_s)
|
||||
render :training, status: :ok
|
||||
else
|
||||
puts "unknown bot request : #{request.original_url}"
|
||||
Rails.logger.warn "unknown bot request : #{request.original_url}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -29,6 +29,8 @@ export const OpenidConnectForm = <TFieldValues extends FieldValues, TContext ext
|
||||
// saves the state of the discovery endpoint
|
||||
const [discoveryAvailable, setDiscoveryAvailable] = useState<boolean>(false);
|
||||
const [scopesAvailable, setScopesAvailable] = useState<string[]>(null);
|
||||
// this is a workaround for https://github.com/JedWatson/react-select/issues/1879
|
||||
const [selectKey, setSelectKey] = useState<number>(0);
|
||||
|
||||
// when we have detected a discovery endpoint, we mark it as available
|
||||
useEffect(() => {
|
||||
@ -38,6 +40,11 @@ export const OpenidConnectForm = <TFieldValues extends FieldValues, TContext ext
|
||||
);
|
||||
}, [discoveryAvailable]);
|
||||
|
||||
// this will force the scope "select" to re-fetch the options
|
||||
useEffect(() => {
|
||||
setSelectKey(selectKey + 1);
|
||||
}, [scopesAvailable]);
|
||||
|
||||
// when the component is mounted, we try to discover the discovery endpoint for the current configuration (if any)
|
||||
useEffect(() => {
|
||||
checkForDiscoveryEndpoint({ target: { value: currentFormValues?.issuer } } as React.ChangeEvent<HTMLInputElement>);
|
||||
@ -69,7 +76,7 @@ export const OpenidConnectForm = <TFieldValues extends FieldValues, TContext ext
|
||||
* The resulting list is provided through the callback parameter.
|
||||
*/
|
||||
const loadScopes = (inputValue: string, callback: (options: Array<{ value: string, label: string }>) => void): void => {
|
||||
const current = currentFormValues.scope || [];
|
||||
const current = currentFormValues?.scope || [];
|
||||
if (scopesAvailable) {
|
||||
// add custom scopes to the list of available scopes
|
||||
const unlisted = difference(current, scopesAvailable);
|
||||
@ -125,6 +132,7 @@ export const OpenidConnectForm = <TFieldValues extends FieldValues, TContext ext
|
||||
label={t('app.admin.authentication.openid_connect_form.scope')}
|
||||
tooltip={<HtmlTranslate trKey="app.admin.authentication.openid_connect_form.scope_help_html" />}
|
||||
loadOptions={loadScopes}
|
||||
selectKey={selectKey.toString()}
|
||||
creatable
|
||||
control={control} />
|
||||
<FormSelect id="providable_attributes.prompt"
|
||||
|
@ -9,6 +9,7 @@ import { mappingType } from '../../models/authentication-provider';
|
||||
import { BooleanMappingForm } from './boolean-mapping-form';
|
||||
import { DateMappingForm } from './date-mapping-form';
|
||||
import { StringMappingForm } from './string-mapping-form';
|
||||
import { FormInput } from '../form/form-input';
|
||||
|
||||
interface TypeMappingModalProps<TFieldValues, TContext extends object> {
|
||||
model: string,
|
||||
@ -38,6 +39,10 @@ export const TypeMappingModal = <TFieldValues extends FieldValues, TContext exte
|
||||
confirmButton={<i className="fa fa-check" />}
|
||||
onConfirm={toggleModal}>
|
||||
<span>{model} > {field} ({t('app.admin.authentication.type_mapping_modal.TYPE_expected', { TYPE: t(`app.admin.authentication.type_mapping_modal.types.${type}`) })})</span>
|
||||
<FormInput register={register}
|
||||
id={`auth_provider_mappings_attributes.${fieldMappingId}.transformation.type`}
|
||||
type="hidden"
|
||||
defaultValue={type} />
|
||||
{type === 'integer' && <IntegerMappingForm register={register} control={control} fieldMappingId={fieldMappingId} />}
|
||||
{type === 'boolean' && <BooleanMappingForm register={register} fieldMappingId={fieldMappingId} />}
|
||||
{type === 'date' && <DateMappingForm control={control} fieldMappingId={fieldMappingId} />}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { ReactNode, useEffect, useState } from 'react';
|
||||
import { FabPanel } from '../../base/fab-panel';
|
||||
import { Reservation, ReservationSlot } from '../../../models/reservation';
|
||||
import { Reservation, SlotsReservation } from '../../../models/reservation';
|
||||
import ReservationAPI from '../../../api/reservation';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import moment from 'moment';
|
||||
@ -38,16 +38,16 @@ const ReservationsPanel: React.FC<SpaceReservationsProps> = ({ userId, onError,
|
||||
*/
|
||||
const reservationsByDate = (state: 'past' | 'futur'): Array<Reservation> => {
|
||||
return reservations.filter(r => {
|
||||
return !!r.slots_attributes.find(s => filterSlot(s, state));
|
||||
return !!r.slots_reservations_attributes.find(s => filterSlot(s, state));
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the given slot if past of futur
|
||||
* Check if the given slot reservation if past of futur
|
||||
*/
|
||||
const filterSlot = (slot: ReservationSlot, state: 'past' | 'futur'): boolean => {
|
||||
return (state === 'past' && moment(slot.start_at).isBefore()) ||
|
||||
(state === 'futur' && moment(slot.start_at).isAfter());
|
||||
const filterSlot = (sr: SlotsReservation, state: 'past' | 'futur'): boolean => {
|
||||
return (state === 'past' && moment(sr.slot_attributes.start_at).isBefore()) ||
|
||||
(state === 'futur' && moment(sr.slot_attributes.start_at).isAfter());
|
||||
};
|
||||
|
||||
/**
|
||||
@ -95,12 +95,12 @@ const ReservationsPanel: React.FC<SpaceReservationsProps> = ({ userId, onError,
|
||||
return (
|
||||
<li key={reservation.id} className="reservation">
|
||||
<a className={`reservation-title ${details[reservation.id] ? 'clicked' : ''}`} onClick={toggleDetails(reservation.id)}>
|
||||
{reservation.reservable.name} - {FormatLib.date(reservation.slots_attributes[0].start_at)}
|
||||
{reservation.reservable.name} - {FormatLib.date(reservation.slots_reservations_attributes[0].slot_attributes.start_at)}
|
||||
</a>
|
||||
{details[reservation.id] && <FabPopover title={t('app.logged.dashboard.reservations.reservations_panel.slots_details')}>
|
||||
{reservation.slots_attributes.filter(s => filterSlot(s, state)).map(
|
||||
slot => <span key={slot.id} className="slot-details">
|
||||
{FormatLib.date(slot.start_at)}, {FormatLib.time(slot.start_at)} - {FormatLib.time(slot.end_at)}
|
||||
{reservation.slots_reservations_attributes.filter(s => filterSlot(s, state)).map(
|
||||
slotReservation => <span key={slotReservation.id} className="slot-details">
|
||||
{FormatLib.date(slotReservation.slot_attributes.start_at)}, {FormatLib.time(slotReservation.slot_attributes.start_at)} - {FormatLib.time(slotReservation.slot_attributes.end_at)}
|
||||
</span>
|
||||
)}
|
||||
</FabPopover>}
|
||||
@ -109,7 +109,7 @@ const ReservationsPanel: React.FC<SpaceReservationsProps> = ({ userId, onError,
|
||||
};
|
||||
|
||||
const futur = reservationsByDate('futur');
|
||||
const past = _.orderBy(reservationsByDate('past'), r => r.slots_attributes[0].start_at, 'desc');
|
||||
const past = _.orderBy(reservationsByDate('past'), r => r.slots_reservations_attributes[0].slot_attributes.start_at, 'desc');
|
||||
|
||||
return (
|
||||
<FabPanel className="reservations-panel" header={header()}>
|
||||
|
@ -10,13 +10,14 @@ export interface AbstractFormItemProps<TFieldValues> extends PropsWithChildren<A
|
||||
className?: string,
|
||||
disabled?: boolean|((id: string) => boolean),
|
||||
onLabelClick?: (event: React.MouseEvent<HTMLLabelElement, MouseEvent>) => void,
|
||||
inLine?: boolean,
|
||||
}
|
||||
|
||||
/**
|
||||
* This abstract component should not be used directly.
|
||||
* Other forms components that are intended to be used with react-hook-form must extend this component.
|
||||
*/
|
||||
export const AbstractFormItem = <TFieldValues extends FieldValues>({ id, label, tooltip, className, disabled, error, warning, rules, formState, onLabelClick, children }: AbstractFormItemProps<TFieldValues>) => {
|
||||
export const AbstractFormItem = <TFieldValues extends FieldValues>({ id, label, tooltip, className, disabled, error, warning, rules, formState, onLabelClick, inLine, children }: AbstractFormItemProps<TFieldValues>) => {
|
||||
const [isDirty, setIsDirty] = useState<boolean>(false);
|
||||
const [fieldError, setFieldError] = useState<{ message: string }>(error);
|
||||
const [isDisabled, setIsDisabled] = useState<boolean>(false);
|
||||
@ -59,14 +60,21 @@ export const AbstractFormItem = <TFieldValues extends FieldValues>({ id, label,
|
||||
|
||||
return (
|
||||
<label className={`form-item ${classNames}`} onClick={handleLabelClick}>
|
||||
{label && <div className='form-item-header'>
|
||||
{(label && !inLine) && <div className='form-item-header'>
|
||||
<p>{label}</p>
|
||||
{tooltip && <div className="item-tooltip">
|
||||
<span className="trigger"><i className="fa fa-question-circle" /></span>
|
||||
<div className="content">{tooltip}</div>
|
||||
</div>}
|
||||
</div>}
|
||||
|
||||
<div className='form-item-field'>
|
||||
{inLine && <div className='form-item-header'><p>{label}</p>
|
||||
{tooltip && <div className="item-tooltip">
|
||||
<span className="trigger"><i className="fa fa-question-circle" /></span>
|
||||
<div className="content">{tooltip}</div>
|
||||
</div>}
|
||||
</div>}
|
||||
{children}
|
||||
</div>
|
||||
{(isDirty && fieldError) && <div className="form-item-error">{fieldError.message}</div> }
|
||||
|
@ -16,6 +16,7 @@ interface CommonProps<TFieldValues, TContext extends object, TOptionValue> exten
|
||||
onChange?: (values: Array<TOptionValue>) => void,
|
||||
placeholder?: string,
|
||||
creatable?: boolean,
|
||||
selectKey?: string,
|
||||
}
|
||||
|
||||
// we should provide either an array of options or a function that returns a promise, but not both
|
||||
@ -35,7 +36,7 @@ type selectOption<TOptionValue> = { value: TOptionValue, label: string, select?:
|
||||
* This component is a wrapper around react-select to use with react-hook-form.
|
||||
* It is a multi-select component.
|
||||
*/
|
||||
export const FormMultiSelect = <TFieldValues extends FieldValues, TContext extends object, TOptionValue>({ id, label, tooltip, className, control, placeholder, options, valuesDefault, error, rules, disabled, onChange, formState, warning, loadOptions, creatable }: FormSelectProps<TFieldValues, TContext, TOptionValue>) => {
|
||||
export const FormMultiSelect = <TFieldValues extends FieldValues, TContext extends object, TOptionValue>({ id, label, tooltip, className, control, placeholder, options, valuesDefault, error, rules, disabled, onChange, formState, warning, loadOptions, creatable, selectKey }: FormSelectProps<TFieldValues, TContext, TOptionValue>) => {
|
||||
const { t } = useTranslation('shared');
|
||||
|
||||
const [isDisabled, setIsDisabled] = React.useState<boolean>(false);
|
||||
@ -52,7 +53,7 @@ export const FormMultiSelect = <TFieldValues extends FieldValues, TContext exten
|
||||
useEffect(() => {
|
||||
if (typeof loadOptions === 'function') {
|
||||
loadOptions('', options => {
|
||||
setAllOptions(options);
|
||||
setTimeout(() => setAllOptions(options), 1);
|
||||
});
|
||||
}
|
||||
}, [loadOptions]);
|
||||
@ -73,7 +74,7 @@ export const FormMultiSelect = <TFieldValues extends FieldValues, TContext exten
|
||||
* This function will return the currently selected options, according to the selectedOptions state.
|
||||
*/
|
||||
const getCurrentValues = (value: Array<TOptionValue>): Array<selectOption<TOptionValue>> => {
|
||||
return allOptions.filter(c => value?.includes(c.value));
|
||||
return allOptions?.filter(c => value?.includes(c.value));
|
||||
};
|
||||
|
||||
/**
|
||||
@ -119,6 +120,7 @@ export const FormMultiSelect = <TFieldValues extends FieldValues, TContext exten
|
||||
classNamePrefix: 'rs',
|
||||
className: 'rs',
|
||||
ref,
|
||||
key: selectKey,
|
||||
value: getCurrentValues(value),
|
||||
placeholder,
|
||||
isDisabled,
|
||||
|
@ -29,6 +29,7 @@ export const FormSwitch = <TFieldValues, TContext extends object>({ id, label, t
|
||||
<AbstractFormItem id={id} formState={formState} label={label}
|
||||
className={`form-switch ${className || ''}`} tooltip={tooltip}
|
||||
disabled={disabled}
|
||||
inLine
|
||||
rules={rules} error={error} warning={warning}>
|
||||
<Controller name={id as FieldPath<TFieldValues>}
|
||||
control={control}
|
||||
|
@ -163,7 +163,7 @@ const ReserveButton: React.FC<ReserveButtonProps> = ({ currentUser, machineId, o
|
||||
</button>
|
||||
<PendingTrainingModal isOpen={pendingTraining}
|
||||
toggleModal={togglePendingTrainingModal}
|
||||
nextReservation={machine?.current_user_next_training_reservation?.slots_attributes[0]?.start_at} />
|
||||
nextReservation={machine?.current_user_next_training_reservation?.slots_reservations_attributes[0]?.slot_attributes.start_at} />
|
||||
<RequiredTrainingModal isOpen={trainingRequired}
|
||||
toggleModal={toggleRequiredTrainingModal}
|
||||
user={user}
|
||||
|
@ -249,7 +249,7 @@ export const AbstractPaymentModal: React.FC<AbstractPaymentModalProps> = ({ isOp
|
||||
};
|
||||
|
||||
AbstractPaymentModal.defaultProps = {
|
||||
title: 'app.shared.payment.online_payment',
|
||||
title: 'app.shared.abstract_payment_modal.online_payment',
|
||||
preventCgv: false,
|
||||
preventScheduleInfo: false,
|
||||
modalSize: ModalSize.medium
|
||||
|
@ -59,7 +59,9 @@ export const EditSocials = <TFieldValues extends FieldValues>({ register, setVal
|
||||
<>
|
||||
<div className='social-icons'>
|
||||
{userNetworks.map((network, index) =>
|
||||
!selectedNetworks.includes(network) && <img key={index} src={`${Icons}#${network.name}`} onClick={() => selectNetwork(network)}></img>
|
||||
!selectedNetworks.includes(network) && <svg key={index} onClick={() => selectNetwork(network)} viewBox="0 0 24 24" >
|
||||
<use href={`${Icons}#${network.name}`} />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
{selectNetwork.length && <div className='social-inputs'>
|
||||
@ -79,7 +81,7 @@ export const EditSocials = <TFieldValues extends FieldValues>({ register, setVal
|
||||
label={network.name}
|
||||
disabled={disabled}
|
||||
placeholder={t('app.shared.edit_socials.url_placeholder')}
|
||||
icon={<img src={`${Icons}#${network.name}`}></img>}
|
||||
icon={<svg viewBox="0 0 24 24"><use href={`${Icons}#${network.name}`}/></svg>}
|
||||
addOn={<Trash size={16} />}
|
||||
addOnAction={() => dispatch({ type: 'delete', payload: { network, field: `profile_attributes.${network.name}` } })} />
|
||||
)}
|
||||
|
@ -85,7 +85,7 @@ export const FabSocials: React.FC<FabSocialsProps> = ({ show = false, onError, o
|
||||
{fabNetworks.map((network, index) =>
|
||||
selectedNetworks.includes(network) &&
|
||||
<a key={index} href={network.url} target='_blank' rel="noreferrer">
|
||||
<img src={`${Icons}#${network.name}`}></img>
|
||||
<svg viewBox="0 0 24 24"><use href={`${Icons}#${network.name}`}/></svg>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
@ -95,7 +95,9 @@ export const FabSocials: React.FC<FabSocialsProps> = ({ show = false, onError, o
|
||||
<div className='social-icons'>
|
||||
{fabNetworks.map((network, index) =>
|
||||
!selectedNetworks.includes(network) &&
|
||||
<img key={index} src={`${Icons}#${network.name}`} onClick={() => selectNetwork(network)}></img>
|
||||
<svg viewBox="0 0 24 24" key={index} onClick={() => selectNetwork(network)}>
|
||||
<use href={`${Icons}#${network.name}`} />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
{selectNetwork.length && <div className='social-inputs'>
|
||||
@ -114,7 +116,7 @@ export const FabSocials: React.FC<FabSocialsProps> = ({ show = false, onError, o
|
||||
defaultValue={network.url}
|
||||
label={network.name}
|
||||
placeholder={t('app.shared.fab_socials.url_placeholder')}
|
||||
icon={<img src={`${Icons}#${network.name}`}></img>}
|
||||
icon={<svg viewBox="0 0 24 24"><use href={`${Icons}#${network.name}`}/></svg>}
|
||||
addOn={<Trash size={16} />}
|
||||
addOnAction={() => remove(network)} />
|
||||
)}
|
||||
|
@ -7,6 +7,7 @@ import { FieldPathValue } from 'react-hook-form/dist/types/path';
|
||||
import { FieldValues } from 'react-hook-form/dist/types/fields';
|
||||
import { FormInput } from '../form/form-input';
|
||||
import { Avatar } from './avatar';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface AvatarInputProps<TFieldValues> {
|
||||
register: UseFormRegister<TFieldValues>,
|
||||
@ -20,6 +21,8 @@ interface AvatarInputProps<TFieldValues> {
|
||||
* This component allows to set the user's avatar, in forms managed by react-hook-form.
|
||||
*/
|
||||
export const AvatarInput = <TFieldValues extends FieldValues>({ currentAvatar, userName, register, setValue, size }: AvatarInputProps<TFieldValues>) => {
|
||||
const { t } = useTranslation('shared');
|
||||
|
||||
const [avatar, setAvatar] = useState<string|ArrayBuffer>(currentAvatar);
|
||||
/**
|
||||
* Check if the provided user has a configured avatar
|
||||
@ -70,8 +73,8 @@ export const AvatarInput = <TFieldValues extends FieldValues>({ currentAvatar, u
|
||||
<Avatar avatar={avatar} userName={userName} size="large" />
|
||||
<div className="buttons">
|
||||
<FabButton onClick={onAddAvatar} className="select-button">
|
||||
{!hasAvatar() && <span>Add an avatar</span>}
|
||||
{hasAvatar() && <span>Change</span>}
|
||||
{!hasAvatar() && <span>{t('app.shared.avatar_input.add_an_avatar')}</span>}
|
||||
{hasAvatar() && <span>{t('app.shared.avatar_input.change')}</span>}
|
||||
<FormInput className="avatar-file-input"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
|
@ -9,19 +9,21 @@ import { FieldValues } from 'react-hook-form/dist/types/fields';
|
||||
import { PasswordInput } from './password-input';
|
||||
import { FormState } from 'react-hook-form/dist/types/form';
|
||||
import MemberAPI from '../../api/member';
|
||||
import { User } from '../../models/user';
|
||||
|
||||
interface ChangePasswordProp<TFieldValues> {
|
||||
register: UseFormRegister<TFieldValues>,
|
||||
onError: (message: string) => void,
|
||||
currentFormPassword: string,
|
||||
formState: FormState<TFieldValues>,
|
||||
user: User,
|
||||
}
|
||||
|
||||
/**
|
||||
* This component shows a button that trigger a modal dialog to verify the user's current password.
|
||||
* If the user's current password is correct, the modal dialog is closed and the button is replaced by a form to set the new password.
|
||||
*/
|
||||
export const ChangePassword = <TFieldValues extends FieldValues>({ register, onError, currentFormPassword, formState }: ChangePasswordProp<TFieldValues>) => {
|
||||
export const ChangePassword = <TFieldValues extends FieldValues>({ register, onError, currentFormPassword, formState, user }: ChangePasswordProp<TFieldValues>) => {
|
||||
const { t } = useTranslation('shared');
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = React.useState<boolean>(false);
|
||||
@ -31,8 +33,8 @@ export const ChangePassword = <TFieldValues extends FieldValues>({ register, onE
|
||||
const { handleSubmit, register: passwordRegister } = useForm<{ password: string }>();
|
||||
|
||||
useEffect(() => {
|
||||
MemberAPI.current().then(user => {
|
||||
setIsPrivileged(user.role === 'admin' || user.role === 'manager');
|
||||
MemberAPI.current().then(operator => {
|
||||
setIsPrivileged((operator.role === 'admin' || operator.role === 'manager') && user.id !== operator.id);
|
||||
}).catch(error => onError(error));
|
||||
}, []);
|
||||
|
||||
|
@ -82,7 +82,9 @@ export const UserProfileForm: React.FC<UserProfileFormProps> = ({ action, size,
|
||||
}).catch(error => onError(error));
|
||||
}
|
||||
if (showTermsAndConditionsInput) {
|
||||
CustomAssetAPI.get(CustomAssetName.CguFile).then(setTermsAndConditions).catch(error => onError(error));
|
||||
CustomAssetAPI.get(CustomAssetName.CguFile).then(cgu => {
|
||||
if (cgu?.custom_asset_file_attributes) setTermsAndConditions(cgu);
|
||||
}).catch(error => onError(error));
|
||||
}
|
||||
ProfileCustomFieldAPI.index().then(data => {
|
||||
const fData = data.filter(f => f.actived);
|
||||
@ -252,6 +254,7 @@ export const UserProfileForm: React.FC<UserProfileFormProps> = ({ action, size,
|
||||
{ action === 'update' && <ChangePassword register={register}
|
||||
onError={onError}
|
||||
currentFormPassword={output.password}
|
||||
user={user}
|
||||
formState={formState} />}
|
||||
{action === 'create' && <PasswordInput register={register}
|
||||
currentFormPassword={output.password}
|
||||
|
@ -18,8 +18,8 @@
|
||||
* Controller used in the calendar management page
|
||||
*/
|
||||
|
||||
Application.Controllers.controller('AdminCalendarController', ['$scope', '$state', '$uibModal', 'moment', 'AuthService', 'Availability', 'Slot', 'Setting', 'Export', 'growl', 'dialogs', 'bookingWindowStart', 'bookingWindowEnd', 'machinesPromise', 'plansPromise', 'groupsPromise', 'settingsPromise', '_t', 'uiCalendarConfig', 'CalendarConfig', 'Member', 'uiTourService',
|
||||
function ($scope, $state, $uibModal, moment, AuthService, Availability, Slot, Setting, Export, growl, dialogs, bookingWindowStart, bookingWindowEnd, machinesPromise, plansPromise, groupsPromise, settingsPromise, _t, uiCalendarConfig, CalendarConfig, Member, uiTourService) {
|
||||
Application.Controllers.controller('AdminCalendarController', ['$scope', '$state', '$uibModal', 'moment', 'AuthService', 'Availability', 'SlotsReservation', 'Setting', 'Export', 'growl', 'dialogs', 'bookingWindowStart', 'bookingWindowEnd', 'machinesPromise', 'plansPromise', 'groupsPromise', 'settingsPromise', '_t', 'uiCalendarConfig', 'CalendarConfig', 'Member', 'uiTourService',
|
||||
function ($scope, $state, $uibModal, moment, AuthService, Availability, SlotsReservation, Setting, Export, growl, dialogs, bookingWindowStart, bookingWindowEnd, machinesPromise, plansPromise, groupsPromise, settingsPromise, _t, uiCalendarConfig, CalendarConfig, Member, uiTourService) {
|
||||
/* PRIVATE STATIC CONSTANTS */
|
||||
|
||||
// The calendar is divided in slots of 30 minutes
|
||||
@ -78,9 +78,9 @@ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state
|
||||
|
||||
/**
|
||||
* Open a confirmation modal to cancel the booking of a user for the currently selected event.
|
||||
* @param slot {Object} reservation slot of a user, inherited from $resource
|
||||
* @param slot_reservation {Object} reserved slot, as returned by /api/availabilities/:id/reservations
|
||||
*/
|
||||
$scope.cancelBooking = function (slot) {
|
||||
$scope.cancelBooking = function (slot_reservation) {
|
||||
// open a confirmation dialog
|
||||
dialogs.confirm(
|
||||
{
|
||||
@ -89,19 +89,19 @@ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state
|
||||
return {
|
||||
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 })
|
||||
, { GENDER: getGender($scope.currentUser), USER: slot_reservation.user.name, DATE: moment(slot_reservation.start_at).format('L'), TIME: moment(slot_reservation.start_at).format('LT'), RESERVATION: slot_reservation.reservable.name })
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
function () {
|
||||
// the admin has confirmed, cancel the subscription
|
||||
Slot.cancel(
|
||||
{ id: slot.slot_id },
|
||||
// the admin has confirmed, cancel the reservation
|
||||
SlotsReservation.cancel(
|
||||
{ id: slot_reservation.id },
|
||||
function (data, status) { // success
|
||||
// update the canceled_at attribute
|
||||
for (const resa of Array.from($scope.reservations)) {
|
||||
if (resa.slot_id === data.id) {
|
||||
if (resa.id === data.id) {
|
||||
resa.canceled_at = data.canceled_at;
|
||||
break;
|
||||
}
|
||||
|
@ -548,7 +548,7 @@ Application.Controllers.controller('ShowEventReservationsController', ['$scope',
|
||||
* @returns {boolean}
|
||||
*/
|
||||
$scope.isCancelled = function (reservation) {
|
||||
return !!(reservation.slots_attributes[0].canceled_at);
|
||||
return !!(reservation.slots_reservations_attributes[0].canceled_at);
|
||||
};
|
||||
}]);
|
||||
|
||||
|
@ -163,6 +163,9 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce',
|
||||
// is user validation required
|
||||
$scope.enableUserValidationRequired = (settingsPromise.user_validation_required === 'true');
|
||||
|
||||
// should we display the username in the list?
|
||||
$scope.displayUsername = (settingsPromise.show_username_in_admin_list === 'true');
|
||||
|
||||
// Admins ordering/sorting. Default: not sorted
|
||||
$scope.orderAdmin = null;
|
||||
|
||||
|
@ -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', 'iCalendarPromise',
|
||||
function ($scope, $state, $aside, moment, Availability, Slot, Setting, growl, dialogs, bookingWindowStart, bookingWindowEnd, _t, uiCalendarConfig, CalendarConfig, trainingsPromise, machinesPromise, spacesPromise, iCalendarPromise) {
|
||||
Application.Controllers.controller('CalendarController', ['$scope', '$state', '$aside', 'moment', 'Availability', 'Setting', 'growl', 'dialogs', 'bookingWindowStart', 'bookingWindowEnd', '_t', 'uiCalendarConfig', 'CalendarConfig', 'trainingsPromise', 'machinesPromise', 'spacesPromise', 'iCalendarPromise',
|
||||
function ($scope, $state, $aside, moment, Availability, Setting, growl, dialogs, bookingWindowStart, bookingWindowEnd, _t, uiCalendarConfig, CalendarConfig, trainingsPromise, machinesPromise, spacesPromise, iCalendarPromise) {
|
||||
/* PRIVATE STATIC CONSTANTS */
|
||||
let currentMachineEvent = null;
|
||||
machinesPromise.forEach(m => m.checked = true);
|
||||
@ -216,8 +216,9 @@ Application.Controllers.controller('CalendarController', ['$scope', '$state', '$
|
||||
} 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 });
|
||||
if (event.machine_ids) {
|
||||
// TODO open modal to ask the user to select the machine to show
|
||||
$state.go('app.public.machines_show', { id: event.machine_ids[0] });
|
||||
} else if (event.space_id) {
|
||||
$state.go('app.public.space_show', { id: event.space_id });
|
||||
}
|
||||
|
@ -129,8 +129,8 @@ Application.Controllers.controller('EventsController', ['$scope', '$state', 'Eve
|
||||
}
|
||||
]);
|
||||
|
||||
Application.Controllers.controller('ShowEventController', ['$scope', '$state', '$rootScope', 'Event', '$uibModal', 'Member', 'Reservation', 'Price', 'CustomAsset', 'Slot', 'eventPromise', 'growl', '_t', 'Wallet', 'AuthService', 'helpers', 'dialogs', 'priceCategoriesPromise', 'settingsPromise', 'LocalPayment',
|
||||
function ($scope, $state,$rootScope, Event, $uibModal, Member, Reservation, Price, CustomAsset, Slot, eventPromise, growl, _t, Wallet, AuthService, helpers, dialogs, priceCategoriesPromise, settingsPromise, LocalPayment) {
|
||||
Application.Controllers.controller('ShowEventController', ['$scope', '$state', '$rootScope', 'Event', '$uibModal', 'Member', 'Reservation', 'Price', 'CustomAsset', 'SlotsReservation', 'eventPromise', 'growl', '_t', 'Wallet', 'AuthService', 'helpers', 'dialogs', 'priceCategoriesPromise', 'settingsPromise', 'LocalPayment',
|
||||
function ($scope, $state,$rootScope, Event, $uibModal, Member, Reservation, Price, CustomAsset, SlotsReservation, eventPromise, growl, _t, Wallet, AuthService, helpers, dialogs, priceCategoriesPromise, settingsPromise, LocalPayment) {
|
||||
/* PUBLIC SCOPE */
|
||||
|
||||
// reservations for the currently shown event
|
||||
@ -353,36 +353,8 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
|
||||
* Callback to validate the booking of a free event
|
||||
*/
|
||||
$scope.validReserveEvent = function () {
|
||||
const cartItems = {
|
||||
customer_id: $scope.ctrl.member.id,
|
||||
items: [
|
||||
{
|
||||
reservation: {
|
||||
reservable_id: $scope.event.id,
|
||||
reservable_type: 'Event',
|
||||
slots_attributes: [],
|
||||
nb_reserve_places: $scope.reserve.nbReservePlaces,
|
||||
tickets_attributes: []
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
// a single slot is used for events
|
||||
cartItems.items[0].reservation.slots_attributes.push({
|
||||
start_at: $scope.event.start_date,
|
||||
end_at: $scope.event.end_date,
|
||||
availability_id: $scope.event.availability.id
|
||||
});
|
||||
// iterate over reservations per prices
|
||||
for (let price_id in $scope.reserve.tickets) {
|
||||
if (Object.prototype.hasOwnProperty.call($scope.reserve.tickets, price_id)) {
|
||||
const seats = $scope.reserve.tickets[price_id];
|
||||
cartItems.items[0].reservation.tickets_attributes.push({
|
||||
event_price_category_id: price_id,
|
||||
booked: seats
|
||||
});
|
||||
}
|
||||
}
|
||||
const reservation = mkReservation($scope.reserve, $scope.event)
|
||||
const cartItems = mkCartItems(reservation, $scope.coupon.applied);
|
||||
// set the attempting marker
|
||||
$scope.attempting = true;
|
||||
// save the reservation to the API
|
||||
@ -393,19 +365,15 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
|
||||
}
|
||||
, function (response) {
|
||||
// reservation failed
|
||||
$scope.alerts = [];
|
||||
$scope.alerts.push({
|
||||
msg: response.data.card[0],
|
||||
type: 'danger'
|
||||
});
|
||||
growl.error(response && response.data && response.data.card && response.data.card[0] || 'server error');
|
||||
// unset the attempting marker
|
||||
return $scope.attempting = false;
|
||||
});
|
||||
$scope.attempting = false;
|
||||
})
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback to cancel a reservation
|
||||
* @param reservation {{id:number, reservable_id:number, nb_reserve_places:number, slots_attributes:[{id: number, canceled_at: string}], total_booked_seats: number}}
|
||||
* @param reservation {Reservation}
|
||||
*/
|
||||
$scope.cancelReservation = function(reservation) {
|
||||
dialogs.confirm({
|
||||
@ -418,14 +386,14 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
|
||||
}
|
||||
}
|
||||
}, function() { // cancel confirmed
|
||||
Slot.cancel({
|
||||
id: reservation.slots_attributes[0].id
|
||||
SlotsReservation.cancel({
|
||||
id: reservation.slots_reservations_attributes[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_attributes[0].canceled_at = new Date();
|
||||
$scope.reservations[index].slots_reservations_attributes[0].canceled_at = new Date();
|
||||
}, function(error) {
|
||||
growl.warning(_t('app.public.events_show.cancellation_failed'));
|
||||
});
|
||||
@ -434,17 +402,17 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
|
||||
|
||||
/**
|
||||
* Test if the provided reservation has been cancelled
|
||||
* @param reservation {{slots_attributes: [{canceled_at: string}]}}
|
||||
* @param reservation {Reservation}
|
||||
* @returns {boolean}
|
||||
*/
|
||||
$scope.isCancelled = function(reservation) {
|
||||
return !!(reservation.slots_attributes[0].canceled_at);
|
||||
return !!(reservation.slots_reservations_attributes[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 reservation {Reservation}
|
||||
*/
|
||||
$scope.modifyReservation = function (reservation) {
|
||||
const index = $scope.reservations.indexOf(reservation);
|
||||
@ -475,9 +443,7 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
|
||||
return eventToPlace = e;
|
||||
}
|
||||
});
|
||||
$scope.reservation.slots_attributes[0].start_at = eventToPlace.start_date;
|
||||
$scope.reservation.slots_attributes[0].end_at = eventToPlace.end_date;
|
||||
$scope.reservation.slots_attributes[0].availability_id = eventToPlace.availability_id;
|
||||
$scope.reservation.slots_reservations_attributes[0].slot_id = eventToPlace.slot_id;
|
||||
$scope.attempting = true;
|
||||
Reservation.update({ id: reservation.id }, { reservation: $scope.reservation }, function (reservation) {
|
||||
$uibModalInstance.close(reservation);
|
||||
@ -514,10 +480,10 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
|
||||
|
||||
/**
|
||||
* Checks if the provided reservation is able to be moved (date change)
|
||||
* @param reservation {{slots_attributes:[], total_booked_seats:number}}
|
||||
* @param reservation {Reservation}
|
||||
*/
|
||||
$scope.reservationCanModify = function (reservation) {
|
||||
const slotStart = moment(reservation.slots_attributes[0].start_at);
|
||||
const slotStart = moment(reservation.slots_reservations_attributes[0].slot_attributes.start_at);
|
||||
const now = moment();
|
||||
|
||||
let isAble = false;
|
||||
@ -529,10 +495,10 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
|
||||
|
||||
/**
|
||||
* Checks if the provided reservation is able to be cancelled
|
||||
* @param reservation {{slots_attributes:[]}}
|
||||
* @param reservation {Reservation}
|
||||
*/
|
||||
$scope.reservationCanCancel = function(reservation) {
|
||||
const slotStart = moment(reservation.slots_attributes[0].start_at);
|
||||
const slotStart = moment(reservation.slots_reservations_attributes[0].slot_attributes.start_at);
|
||||
const now = moment();
|
||||
return $scope.enableBookingCancel && slotStart.diff(now, "hours") >= $scope.cancelBookingDelay;
|
||||
};
|
||||
@ -653,22 +619,20 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
|
||||
* Create a hash map implementing the Reservation specs
|
||||
* @param reserve {Object} Reservation parameters (places...)
|
||||
* @param event {Object} Current event
|
||||
* @return {{reservation: {reservable_id:number, reservable_type:string, slots_attributes:Array<Object>, nb_reserve_places:number}}}
|
||||
* @return {{reservation: Reservation}}
|
||||
*/
|
||||
const mkReservation = function (reserve, event) {
|
||||
const reservation = {
|
||||
reservable_id: event.id,
|
||||
reservable_type: 'Event',
|
||||
slots_attributes: [],
|
||||
slots_reservations_attributes: [],
|
||||
nb_reserve_places: reserve.nbReservePlaces,
|
||||
tickets_attributes: []
|
||||
};
|
||||
|
||||
reservation.slots_attributes.push({
|
||||
start_at: event.start_date,
|
||||
end_at: event.end_date,
|
||||
availability_id: event.availability.id,
|
||||
offered: event.offered || false
|
||||
reservation.slots_reservations_attributes.push({
|
||||
offered: event.offered || false,
|
||||
slot_id: event.availability.slot_id
|
||||
});
|
||||
|
||||
for (let evt_px_cat of Array.from(event.prices)) {
|
||||
@ -764,9 +728,12 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
|
||||
cartItems () {
|
||||
return mkCartItems(reservation, $scope.coupon.applied);
|
||||
},
|
||||
event () {
|
||||
return $scope.event;
|
||||
}
|
||||
},
|
||||
controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'Auth', 'LocalPayment', 'wallet', 'helpers', '$filter', 'coupon', 'cartItems',
|
||||
function ($scope, $uibModalInstance, $state, reservation, price, Auth, LocalPayment, wallet, helpers, $filter, coupon, cartItems) {
|
||||
controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'Auth', 'LocalPayment', 'wallet', 'helpers', '$filter', 'coupon', 'cartItems', 'event',
|
||||
function ($scope, $uibModalInstance, $state, reservation, price, Auth, LocalPayment, wallet, helpers, $filter, coupon, cartItems, event) {
|
||||
// User's wallet amount
|
||||
$scope.wallet = wallet;
|
||||
|
||||
@ -782,6 +749,9 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
|
||||
// Reservation
|
||||
$scope.reservation = reservation;
|
||||
|
||||
// the event
|
||||
$scope.bookedEvent = event;
|
||||
|
||||
// Used in wallet info template to interpolate some translations
|
||||
$scope.numberFilter = $filter('number');
|
||||
|
||||
|
@ -451,6 +451,7 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$tran
|
||||
*/
|
||||
$scope.markSlotAsAdded = function () {
|
||||
$scope.selectedEvent.backgroundColor = FREE_SLOT_BORDER_COLOR;
|
||||
$scope.selectedEvent.oldTitle = $scope.selectedEvent.title;
|
||||
$scope.selectedEvent.title = _t('app.logged.machines_reserve.i_reserve');
|
||||
updateEvents($scope.selectedEvent);
|
||||
};
|
||||
@ -461,25 +462,21 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$tran
|
||||
$scope.markSlotAsRemoved = function (slot) {
|
||||
slot.backgroundColor = 'white';
|
||||
slot.borderColor = FREE_SLOT_BORDER_COLOR;
|
||||
slot.title = '';
|
||||
slot.isValid = false;
|
||||
slot.slot_id = null;
|
||||
slot.is_reserved = false;
|
||||
slot.can_modify = false;
|
||||
slot.offered = false;
|
||||
slot.title = slot.oldTitle;
|
||||
updateEvents(slot);
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback when a slot was successfully cancelled. Reset the slot style as 'ready to book'
|
||||
*/
|
||||
$scope.slotCancelled = function () { $scope.markSlotAsRemoved($scope.selectedEvent); };
|
||||
$scope.slotCancelled = function () { refreshCalendar() };
|
||||
|
||||
/**
|
||||
* 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.oldTitle = $scope.selectedEvent.title;
|
||||
$scope.selectedEvent.title = _t('app.logged.machines_reserve.i_change');
|
||||
updateEvents($scope.selectedEvent);
|
||||
};
|
||||
@ -505,31 +502,7 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$tran
|
||||
* When modifying an already booked reservation, callback when the modification was successfully done.
|
||||
*/
|
||||
$scope.modifyMachineSlot = function () {
|
||||
const save = {
|
||||
slotId: $scope.events.modifiable.slot_id,
|
||||
borderColor: $scope.events.modifiable.borderColor,
|
||||
user: angular.copy($scope.events.modifiable.user),
|
||||
title: (!$scope.events.modifiable.user || $scope.currentUser.id === $scope.events.modifiable.user.id) ? _t('app.logged.machines_reserve.i_ve_reserved') : _t('app.logged.machines_reserve.not_available')
|
||||
};
|
||||
|
||||
$scope.events.modifiable.backgroundColor = 'white';
|
||||
$scope.events.modifiable.title = '';
|
||||
$scope.events.modifiable.borderColor = FREE_SLOT_BORDER_COLOR;
|
||||
$scope.events.modifiable.slot_id = null;
|
||||
$scope.events.modifiable.is_reserved = false;
|
||||
$scope.events.modifiable.can_modify = false;
|
||||
updateEvents($scope.events.modifiable);
|
||||
|
||||
$scope.events.placable.title = save.title;
|
||||
$scope.events.placable.backgroundColor = 'white';
|
||||
$scope.events.placable.borderColor = save.borderColor;
|
||||
$scope.events.placable.slot_id = save.slotId;
|
||||
$scope.events.placable.is_reserved = true;
|
||||
$scope.events.placable.can_modify = true;
|
||||
$scope.events.placable.user = angular.copy(save.user);
|
||||
updateEvents($scope.events.placable);
|
||||
|
||||
refetchCalendar();
|
||||
refreshCalendar();
|
||||
};
|
||||
|
||||
/**
|
||||
@ -540,7 +513,7 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$tran
|
||||
$scope.events.placable.backgroundColor = 'white';
|
||||
$scope.events.placable.title = '';
|
||||
}
|
||||
$scope.events.modifiable.title = $scope.currentUser.id === $scope.events.modifiable.user.id ? _t('app.logged.machines_reserve.i_ve_reserved') : _t('app.logged.machines_reserve.not_available');
|
||||
$scope.events.modifiable.title = $scope.events.modifiable.oldTitle;
|
||||
$scope.events.modifiable.backgroundColor = 'white';
|
||||
|
||||
updateEvents($scope.events.placable, $scope.events.modifiable);
|
||||
@ -553,7 +526,10 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$tran
|
||||
$scope.updateMember = function () {
|
||||
$scope.plansAreShown = false;
|
||||
$scope.selectedPlan = null;
|
||||
Member.get({ id: $scope.ctrl.member.id }, function (member) { $scope.ctrl.member = member; });
|
||||
Member.get({ id: $scope.ctrl.member.id }, function (member) {
|
||||
$scope.ctrl.member = member;
|
||||
refreshCalendar();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
@ -608,23 +584,6 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$tran
|
||||
*/
|
||||
$scope.afterPayment = function (paymentDocument) {
|
||||
Reservation.get({ id: paymentDocument.main_object.id }, function (reservation) {
|
||||
angular.forEach($scope.events.reserved, function (machineSlot, key) {
|
||||
machineSlot.is_reserved = true;
|
||||
machineSlot.can_modify = true;
|
||||
if ($scope.currentUser.role === 'admin' || ($scope.currentUser.role === 'manager' && reservation.user_id !== $scope.currentUser.id)) {
|
||||
// an admin or a manager booked for someone else
|
||||
machineSlot.title = _t('app.logged.machines_reserve.not_available');
|
||||
machineSlot.borderColor = UNAVAILABLE_SLOT_BORDER_COLOR;
|
||||
updateMachineSlot(machineSlot, reservation, $scope.ctrl.member);
|
||||
} else {
|
||||
// booked for "myself"
|
||||
machineSlot.title = _t('app.logged.machines_reserve.i_ve_reserved');
|
||||
machineSlot.borderColor = BOOKED_SLOT_BORDER_COLOR;
|
||||
updateMachineSlot(machineSlot, reservation, $scope.currentUser);
|
||||
}
|
||||
machineSlot.backgroundColor = 'white';
|
||||
});
|
||||
|
||||
if ($scope.selectedPlan) {
|
||||
$scope.ctrl.member.subscribed_plan = angular.copy($scope.selectedPlan);
|
||||
if ($scope.ctrl.member.id === Auth._currentUser.id) {
|
||||
@ -633,8 +592,14 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$tran
|
||||
$scope.plansAreShown = false;
|
||||
$scope.selectedPlan = null;
|
||||
}
|
||||
$scope.ctrl.member.training_credits = angular.copy(reservation.user.training_credits);
|
||||
$scope.ctrl.member.machine_credits = angular.copy(reservation.user.machine_credits);
|
||||
if ($scope.ctrl.member.id === Auth._currentUser.id) {
|
||||
Auth._currentUser.training_credits = angular.copy(reservation.user.training_credits);
|
||||
Auth._currentUser.machine_credits = angular.copy(reservation.user.machine_credits);
|
||||
}
|
||||
|
||||
refetchCalendar();
|
||||
refreshCalendar();
|
||||
|
||||
// trigger the refresh of react components
|
||||
setTimeout(() => {
|
||||
@ -677,15 +642,12 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$tran
|
||||
*/
|
||||
const initialize = function () {
|
||||
$scope.eventSources.push({
|
||||
events: function (start, end, timezone, callback) {
|
||||
Availability.machine({ machineId: $transition$.params().id }, function (availabilities) {
|
||||
callback(availabilities);
|
||||
});
|
||||
},
|
||||
url: `/api/availabilities/machines/${$transition$.params().id}`,
|
||||
textColor: 'black'
|
||||
});
|
||||
|
||||
if ($scope.currentUser.role !== 'admin') {
|
||||
$scope.ctrl.member = $scope.currentUser;
|
||||
return Member.get({ id: $scope.currentUser.id }, function (member) { $scope.ctrl.member = member; });
|
||||
}
|
||||
};
|
||||
@ -701,9 +663,30 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$tran
|
||||
return;
|
||||
}
|
||||
$scope.selectedEvent = event;
|
||||
return $scope.selectionTime = new Date();
|
||||
$scope.selectionTime = new Date();
|
||||
};
|
||||
|
||||
/**
|
||||
* Refetch all events from the API and re-populate the calendar with the resulting slots
|
||||
*/
|
||||
const refreshCalendar = function () {
|
||||
const view = uiCalendarConfig.calendars.calendar.fullCalendar('getView');
|
||||
return Availability.machine({
|
||||
machineId: $scope.machine.id,
|
||||
member_id: $scope.ctrl.member.id,
|
||||
start: view.start,
|
||||
end: view.end,
|
||||
timezone: Fablab.timezone
|
||||
}, function (slots) {
|
||||
uiCalendarConfig.calendars.calendar.fullCalendar('removeEvents');
|
||||
return $scope.eventSources.splice(0, 1, {
|
||||
events: slots,
|
||||
textColor: 'black'
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggered when fullCalendar tries to graphically render an event block.
|
||||
* Append the event tag into the block, just after the event title.
|
||||
@ -719,24 +702,6 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$tran
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* After payment, update the id of the newly reserved slot with the id returned by the server.
|
||||
* This will allow the user to modify the reservation he just booked. The associated user will also be registered
|
||||
* with the slot.
|
||||
* @param slot {Object}
|
||||
* @param reservation {Object}
|
||||
* @param user {Object} user associated with the slot
|
||||
*/
|
||||
const updateMachineSlot = function (slot, reservation, user) {
|
||||
angular.forEach(reservation.slots, function (s) {
|
||||
if (slot.start.isSame(s.start_at)) {
|
||||
slot.slot_id = s.id;
|
||||
slot.user = user;
|
||||
}
|
||||
});
|
||||
updateEvents(slot);
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the calendar's display to render the new attributes of the events
|
||||
* @param events Object[] events to update in full-calendar
|
||||
|
@ -391,6 +391,7 @@ Application.Controllers.controller('ReserveSpaceController', ['$scope', '$transi
|
||||
*/
|
||||
$scope.markSlotAsAdded = function () {
|
||||
$scope.selectedEvent.backgroundColor = SELECTED_EVENT_BG_COLOR;
|
||||
$scope.selectedEvent.oldTitle = $scope.selectedEvent.title;
|
||||
updateEvents($scope.selectedEvent);
|
||||
};
|
||||
|
||||
@ -399,27 +400,22 @@ Application.Controllers.controller('ReserveSpaceController', ['$scope', '$transi
|
||||
*/
|
||||
$scope.markSlotAsRemoved = function (slot) {
|
||||
slot.backgroundColor = 'white';
|
||||
slot.title = '';
|
||||
slot.title = slot.oldTitle;
|
||||
slot.borderColor = FREE_SLOT_BORDER_COLOR;
|
||||
slot.slot_id = null;
|
||||
slot.isValid = false;
|
||||
slot.is_reserved = false;
|
||||
slot.can_modify = false;
|
||||
slot.offered = false;
|
||||
if (slot.is_completed) { slot.is_completed = false; }
|
||||
updateEvents(slot);
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback when a slot was successfully cancelled. Reset the slot style as 'ready to book'
|
||||
*/
|
||||
$scope.slotCancelled = function () { $scope.markSlotAsRemoved($scope.selectedEvent); };
|
||||
$scope.slotCancelled = function () { refreshCalendar(); };
|
||||
|
||||
/**
|
||||
* 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.oldTitle = $scope.selectedEvent.title;
|
||||
$scope.selectedEvent.title = _t('app.logged.space_reserve.i_change');
|
||||
updateEvents($scope.selectedEvent);
|
||||
};
|
||||
@ -445,30 +441,7 @@ Application.Controllers.controller('ReserveSpaceController', ['$scope', '$transi
|
||||
* When modifying an already booked reservation, callback when the modification was successfully done.
|
||||
*/
|
||||
$scope.modifySpaceSlot = function () {
|
||||
const save = {
|
||||
slotId: $scope.events.modifiable.slot_id,
|
||||
borderColor: $scope.events.modifiable.borderColor,
|
||||
title: _t('app.logged.space_reserve.i_ve_reserved')
|
||||
};
|
||||
|
||||
$scope.events.modifiable.backgroundColor = 'white';
|
||||
$scope.events.modifiable.title = '';
|
||||
$scope.events.modifiable.borderColor = FREE_SLOT_BORDER_COLOR;
|
||||
$scope.events.modifiable.slot_id = null;
|
||||
$scope.events.modifiable.is_reserved = false;
|
||||
$scope.events.modifiable.can_modify = false;
|
||||
if ($scope.events.modifiable.is_completed) { $scope.events.modifiable.is_completed = false; }
|
||||
updateEvents($scope.events.modifiable);
|
||||
|
||||
$scope.events.placable.title = save.title;
|
||||
$scope.events.placable.backgroundColor = 'white';
|
||||
$scope.events.placable.borderColor = save.borderColor;
|
||||
$scope.events.placable.slot_id = save.slotId;
|
||||
$scope.events.placable.is_reserved = true;
|
||||
$scope.events.placable.can_modify = true;
|
||||
updateEvents($scope.events.placable);
|
||||
|
||||
refetchCalendar();
|
||||
refreshCalendar();
|
||||
};
|
||||
|
||||
/**
|
||||
@ -493,14 +466,7 @@ Application.Controllers.controller('ReserveSpaceController', ['$scope', '$transi
|
||||
if ($scope.ctrl.member) {
|
||||
Member.get({ id: $scope.ctrl.member.id }, function (member) {
|
||||
$scope.ctrl.member = member;
|
||||
return Availability.spaces({ spaceId: $scope.space.id, member_id: $scope.ctrl.member.id }, function (spaces) {
|
||||
uiCalendarConfig.calendars.calendar.fullCalendar('removeEvents');
|
||||
return $scope.eventSources.splice(0, 1, {
|
||||
events: spaces,
|
||||
textColor: 'black'
|
||||
}
|
||||
);
|
||||
});
|
||||
refreshCalendar();
|
||||
});
|
||||
}
|
||||
// as the events are re-fetched for the new user, we must re-init the cart
|
||||
@ -561,16 +527,6 @@ Application.Controllers.controller('ReserveSpaceController', ['$scope', '$transi
|
||||
*/
|
||||
$scope.afterPayment = function (paymentDocument) {
|
||||
Reservation.get({ id: paymentDocument.main_object.id }, function (reservation) {
|
||||
angular.forEach($scope.events.paid, function (spaceSlot, key) {
|
||||
spaceSlot.is_reserved = true;
|
||||
spaceSlot.can_modify = true;
|
||||
spaceSlot.title = _t('app.logged.space_reserve.i_ve_reserved');
|
||||
spaceSlot.backgroundColor = 'white';
|
||||
spaceSlot.borderColor = RESERVED_SLOT_BORDER_COLOR;
|
||||
updateSpaceSlotId(spaceSlot, reservation);
|
||||
updateEvents(spaceSlot);
|
||||
});
|
||||
|
||||
if ($scope.selectedPlan) {
|
||||
$scope.ctrl.member.subscribed_plan = angular.copy($scope.selectedPlan);
|
||||
if ($scope.ctrl.member.id === Auth._currentUser.id) {
|
||||
@ -586,7 +542,7 @@ Application.Controllers.controller('ReserveSpaceController', ['$scope', '$transi
|
||||
Auth._currentUser.machine_credits = angular.copy(reservation.user.machine_credits);
|
||||
}
|
||||
|
||||
refetchCalendar();
|
||||
refreshCalendar();
|
||||
});
|
||||
};
|
||||
|
||||
@ -602,16 +558,13 @@ Application.Controllers.controller('ReserveSpaceController', ['$scope', '$transi
|
||||
*/
|
||||
const initialize = function () {
|
||||
if ($scope.currentUser.role !== 'admin') {
|
||||
$scope.ctrl.member = $scope.currentUser;
|
||||
Member.get({ id: $scope.currentUser.id }, function (member) { $scope.ctrl.member = member; });
|
||||
}
|
||||
// we load the availabilities from a callback function of the $scope.eventSources, instead of resolving a promise
|
||||
// in the router because this allows to refetchEvents from fullCalendar API.
|
||||
$scope.eventSources.push({
|
||||
events: function (start, end, timezone, callback) {
|
||||
Availability.spaces({ spaceId: $transition$.params().id }, function (availabilities) {
|
||||
callback(availabilities);
|
||||
});
|
||||
},
|
||||
url: `/api/availabilities/spaces/${$transition$.params().id}`,
|
||||
textColor: 'black'
|
||||
});
|
||||
};
|
||||
@ -631,6 +584,27 @@ Application.Controllers.controller('ReserveSpaceController', ['$scope', '$transi
|
||||
$scope.selectionTime = new Date();
|
||||
};
|
||||
|
||||
/**
|
||||
* Refetch all events from the API and re-populate the calendar with the resulting slots
|
||||
*/
|
||||
const refreshCalendar = function () {
|
||||
const view = uiCalendarConfig.calendars.calendar.fullCalendar('getView');
|
||||
return Availability.spaces({
|
||||
spaceId: $scope.space.id,
|
||||
member_id: $scope.ctrl.member.id,
|
||||
start: view.start,
|
||||
end: view.end,
|
||||
timezone: Fablab.timezone
|
||||
}, function (spaces) {
|
||||
uiCalendarConfig.calendars.calendar.fullCalendar('removeEvents');
|
||||
return $scope.eventSources.splice(0, 1, {
|
||||
events: spaces,
|
||||
textColor: 'black'
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Triggered when fullCalendar tries to graphically render an event block.
|
||||
* Append the event tag into the block, just after the event title.
|
||||
@ -646,20 +620,6 @@ Application.Controllers.controller('ReserveSpaceController', ['$scope', '$transi
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* After payment, update the id of the newly reserved slot with the id returned by the server.
|
||||
* This will allow the user to modify the reservation he just booked.
|
||||
* @param slot {Object}
|
||||
* @param reservation {Object}
|
||||
*/
|
||||
const updateSpaceSlotId = function (slot, reservation) {
|
||||
angular.forEach(reservation.slots, function (s) {
|
||||
if (slot.start.isSame(s.start_at)) {
|
||||
slot.slot_id = s.id;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the calendar's display to render the new attributes of the events
|
||||
* @param events Object[] events to update in full-calendar
|
||||
|
@ -188,19 +188,13 @@ Application.Controllers.controller('ReserveTrainingController', ['$scope', '$tra
|
||||
slot.backgroundColor = 'white';
|
||||
slot.title = slot.training.name;
|
||||
slot.borderColor = FREE_SLOT_BORDER_COLOR;
|
||||
slot.id = null;
|
||||
slot.isValid = false;
|
||||
slot.is_reserved = false;
|
||||
slot.can_modify = false;
|
||||
slot.offered = false;
|
||||
if (slot.is_completed) { slot.is_completed = false; }
|
||||
updateEvents(slot);
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback when a slot was successfully cancelled. Reset the slot style as 'ready to book'
|
||||
*/
|
||||
$scope.slotCancelled = function () { $scope.markSlotAsRemoved($scope.selectedEvent); };
|
||||
$scope.slotCancelled = function () { refreshCalendar() };
|
||||
|
||||
/**
|
||||
* Change the last selected slot's appearence to looks like 'currently looking for a new destination to exchange'
|
||||
@ -234,30 +228,7 @@ Application.Controllers.controller('ReserveTrainingController', ['$scope', '$tra
|
||||
* When modifying an already booked reservation, callback when the modification was successfully done.
|
||||
*/
|
||||
$scope.modifyTrainingSlot = function () {
|
||||
const save = {
|
||||
slotId: $scope.events.modifiable.slot_id,
|
||||
borderColor: $scope.events.modifiable.borderColor,
|
||||
title: !AuthService.isAuthorized(['admin', 'manager']) ? $scope.events.placable.training.name + ' - ' + _t('app.logged.trainings_reserve.i_ve_reserved') : $scope.events.placable.training.name,
|
||||
};
|
||||
|
||||
$scope.events.modifiable.backgroundColor = 'white';
|
||||
$scope.events.modifiable.title = $scope.events.modifiable.training.name;
|
||||
$scope.events.modifiable.borderColor = FREE_SLOT_BORDER_COLOR;
|
||||
$scope.events.modifiable.slot_id = null;
|
||||
$scope.events.modifiable.is_reserved = false;
|
||||
$scope.events.modifiable.can_modify = false;
|
||||
if ($scope.events.modifiable.is_completed) { $scope.events.modifiable.is_completed = false; }
|
||||
updateEvents($scope.events.modifiable);
|
||||
|
||||
$scope.events.placable.title = save.title;
|
||||
$scope.events.placable.backgroundColor = 'white';
|
||||
$scope.events.placable.borderColor = save.borderColor;
|
||||
$scope.events.placable.slot_id = save.slotId;
|
||||
$scope.events.placable.is_reserved = true;
|
||||
$scope.events.placable.can_modify = true;
|
||||
updateEvents($scope.events.placable);
|
||||
|
||||
refetchCalendar();
|
||||
refreshCalendar();
|
||||
};
|
||||
|
||||
/**
|
||||
@ -282,21 +253,13 @@ Application.Controllers.controller('ReserveTrainingController', ['$scope', '$tra
|
||||
if ($scope.ctrl.member) {
|
||||
Member.get({ id: $scope.ctrl.member.id }, function (member) {
|
||||
$scope.ctrl.member = member;
|
||||
const id = $transition$.params().id === 'all' ? $transition$.params().id : $scope.training.id;
|
||||
return Availability.trainings({ trainingId: id, member_id: $scope.ctrl.member.id }, function (trainings) {
|
||||
uiCalendarConfig.calendars.calendar.fullCalendar('removeEvents');
|
||||
return $scope.eventSources.splice(0, 1, {
|
||||
events: trainings,
|
||||
textColor: 'black'
|
||||
}
|
||||
);
|
||||
});
|
||||
refreshCalendar();
|
||||
});
|
||||
}
|
||||
// as the events are re-fetched for the new user, we must re-init the cart
|
||||
$scope.events.reserved = [];
|
||||
$scope.selectedPlan = null;
|
||||
return $scope.plansAreShown = false;
|
||||
$scope.plansAreShown = false;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -351,16 +314,6 @@ Application.Controllers.controller('ReserveTrainingController', ['$scope', '$tra
|
||||
*/
|
||||
$scope.afterPayment = function (paymentDocument) {
|
||||
Reservation.get({ id: paymentDocument.main_object.id }, function (reservation) {
|
||||
angular.forEach($scope.events.paid, function (trainingSlot, key) {
|
||||
trainingSlot.backgroundColor = 'white';
|
||||
trainingSlot.is_reserved = true;
|
||||
trainingSlot.can_modify = true;
|
||||
updateTrainingSlotId(trainingSlot, reservation);
|
||||
trainingSlot.borderColor = '#b2e774';
|
||||
trainingSlot.title = trainingSlot.training.name + ' - ' + _t('app.logged.trainings_reserve.i_ve_reserved');
|
||||
updateEvents(trainingSlot);
|
||||
});
|
||||
|
||||
if ($scope.selectedPlan) {
|
||||
$scope.ctrl.member.subscribed_plan = angular.copy($scope.selectedPlan);
|
||||
if ($scope.ctrl.member.id === Auth._currentUser.id) {
|
||||
@ -376,7 +329,7 @@ Application.Controllers.controller('ReserveTrainingController', ['$scope', '$tra
|
||||
Auth._currentUser.machine_credits = angular.copy(reservation.user.machine_credits);
|
||||
}
|
||||
|
||||
refetchCalendar();
|
||||
refreshCalendar();
|
||||
});
|
||||
};
|
||||
|
||||
@ -392,16 +345,13 @@ Application.Controllers.controller('ReserveTrainingController', ['$scope', '$tra
|
||||
*/
|
||||
const initialize = function () {
|
||||
if ($scope.currentUser.role !== 'admin') {
|
||||
$scope.ctrl.member = $scope.currentUser;
|
||||
Member.get({ id: $scope.currentUser.id }, function (member) { $scope.ctrl.member = member; });
|
||||
}
|
||||
// we load the availabilities from a callback function of the $scope.eventSources, instead of resolving a promise
|
||||
// in the router because this allows to refetchEvents from fullCalendar API.
|
||||
$scope.eventSources.push({
|
||||
events: function (start, end, timezone, callback) {
|
||||
Availability.trainings({ trainingId: $transition$.params().id }, function (availabilities) {
|
||||
callback(availabilities);
|
||||
});
|
||||
},
|
||||
url: `/api/availabilities/trainings/${$transition$.params().id}`,
|
||||
textColor: 'black'
|
||||
});
|
||||
};
|
||||
@ -424,6 +374,27 @@ Application.Controllers.controller('ReserveTrainingController', ['$scope', '$tra
|
||||
return $scope.selectionTime = new Date();
|
||||
};
|
||||
|
||||
/**
|
||||
* Refetch all events from the API and re-populate the calendar with the resulting slots
|
||||
*/
|
||||
const refreshCalendar = function () {
|
||||
const view = uiCalendarConfig.calendars.calendar.fullCalendar('getView');
|
||||
const id = $transition$.params().id === 'all' ? $transition$.params().id : $scope.training.id;
|
||||
Availability.trainings({
|
||||
trainingId: id,
|
||||
member_id: $scope.ctrl.member.id,
|
||||
start: view.start,
|
||||
end: view.end,
|
||||
timezone: Fablab.timezone
|
||||
}, function (trainings) {
|
||||
uiCalendarConfig.calendars.calendar.fullCalendar('removeEvents');
|
||||
$scope.eventSources.splice(0, 1, {
|
||||
events: trainings,
|
||||
textColor: 'black'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggered when fullCalendar tries to graphicaly render an event block.
|
||||
* Append the event tag into the block, just after the event title.
|
||||
|
@ -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', 'AuthService', 'Payment', 'helpers', '_t',
|
||||
function ($rootScope, $uibModal, dialogs, growl, Auth, Price, Wallet, CustomAsset, Slot, AuthService, Payment, helpers, _t) {
|
||||
Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs', 'growl', 'Auth', 'Price', 'Wallet', 'CustomAsset', 'SlotsReservation', 'AuthService', 'Payment', 'helpers', '_t',
|
||||
function ($rootScope, $uibModal, dialogs, growl, Auth, Price, Wallet, CustomAsset, SlotsReservation, AuthService, Payment, helpers, _t) {
|
||||
return ({
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
@ -232,19 +232,17 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
|
||||
* When modifying an already booked reservation, confirm the modification.
|
||||
*/
|
||||
$scope.modifySlot = function () {
|
||||
Slot.update({ id: $scope.events.modifiable.slot_id }, {
|
||||
slot: {
|
||||
start_at: $scope.events.placable.start,
|
||||
end_at: $scope.events.placable.end,
|
||||
availability_id: $scope.events.placable.availability_id
|
||||
SlotsReservation.update({ id: $scope.events.modifiable.slots_reservations_ids[0] }, {
|
||||
slots_reservation: {
|
||||
slot_id: $scope.events.placable.slot_id
|
||||
}
|
||||
}
|
||||
, function () { // success
|
||||
, function (slotReservation) { // success
|
||||
// -> run the callback
|
||||
if (typeof $scope.onSlotModifySuccess === 'function') { $scope.onSlotModifySuccess(); }
|
||||
// -> set the events as successfully moved (to display a summary)
|
||||
$scope.events.moved = {
|
||||
newSlot: $scope.events.placable,
|
||||
newSlot: Object.assign($scope.events.placable, { slots_reservations_ids: [slotReservation.id] }),
|
||||
oldSlot: $scope.events.modifiable
|
||||
};
|
||||
// -> reset the 'moving' status
|
||||
@ -462,11 +460,13 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
|
||||
*/
|
||||
const validateSameTimeReservations = function (slot, callback) {
|
||||
let sameTimeReservations = $scope.settings.overlapping_categories.split(',').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));
|
||||
return _.filter($scope.user[k], function (sr) {
|
||||
return !sr.canceled_at && (
|
||||
slot.start.isSame(sr.start_at) ||
|
||||
(slot.end.isAfter(sr.start_at) && slot.end.isBefore(sr.end_at)) ||
|
||||
(slot.start.isAfter(sr.start_at) && slot.start.isBefore(sr.end_at)) ||
|
||||
(slot.start.isBefore(sr.start_at) && slot.end.isAfter(sr.end_at))
|
||||
);
|
||||
});
|
||||
});
|
||||
sameTimeReservations = _.union.apply(null, sameTimeReservations);
|
||||
@ -519,10 +519,10 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
|
||||
$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
|
||||
if (!$scope.slot.is_completed && $scope.slot.slots_reservations_ids.length === 0 && !$scope.events.modifiable) {
|
||||
// slot is not fully reserved, and not reserved by the current user, and we are not currently modifying a slot
|
||||
// -> can be added to cart or removed if already present
|
||||
const index = _.findIndex($scope.events.reserved, (e) => e._id === $scope.slot._id);
|
||||
const index = _.findIndex($scope.events.reserved, (e) => e.slot_id === $scope.slot.slot_id);
|
||||
if (index === -1) {
|
||||
if (($scope.limitToOneSlot === 'true') && $scope.events.reserved[0]) {
|
||||
// if we limit the number of slots in the cart to 1, and there is already
|
||||
@ -540,25 +540,30 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
|
||||
resetCartState();
|
||||
// finally, we update the prices
|
||||
return updateCartPrice();
|
||||
} else if (!$scope.slot.is_reserved && !$scope.slot.is_completed && $scope.events.modifiable) {
|
||||
// slot is not reserved but we are currently modifying a slot
|
||||
// -> we request the calender to change the rendering
|
||||
} else if (!$scope.slot.is_completed && $scope.slot.slots_reservations_ids.length === 0 && $scope.events.modifiable) {
|
||||
// slot is not fully reserved, not reserved by the current user, and we are currently modifying a slot
|
||||
// -> we request the calendar to change the rendering
|
||||
if (typeof $scope.onSlotModifyUnselect === 'function') {
|
||||
// if the callback return false, cancel the selection for the current modification
|
||||
const res = $scope.onSlotModifyUnselect();
|
||||
if (!res) return;
|
||||
}
|
||||
// -> then, we re-affect the destination slot
|
||||
if (!$scope.events.placable || ($scope.events.placable._id !== $scope.slot._id)) {
|
||||
if (!$scope.events.placable || ($scope.events.placable.slot_id !== $scope.slot.slot_id)) {
|
||||
return $scope.events.placable = $scope.slot;
|
||||
} else {
|
||||
return $scope.events.placable = null;
|
||||
}
|
||||
} else if ($scope.slot.is_reserved && $scope.events.modifiable && ($scope.slot.is_reserved._id === $scope.events.modifiable._id)) {
|
||||
} else if ($scope.slot.slots_reservations_ids.length > 0 &&
|
||||
$scope.events.modifiable &&
|
||||
($scope.slot._id === $scope.events.modifiable._id)) {
|
||||
// slot is reserved and currently modified
|
||||
// -> we cancel the modification
|
||||
$scope.cancelModifySlot();
|
||||
} else if ($scope.slot.is_reserved && (slotCanBeModified($scope.slot) || slotCanBeCanceled($scope.slot)) && !$scope.events.modifiable && ($scope.events.reserved.length === 0)) {
|
||||
} else if ($scope.slot.slots_reservations_ids.length > 0 &&
|
||||
(slotCanBeModified($scope.slot) || slotCanBeCanceled($scope.slot)) &&
|
||||
!$scope.events.modifiable &&
|
||||
$scope.events.reserved.length === 0) {
|
||||
// slot is reserved and is ok to be modified or cancelled
|
||||
// but we are not currently running a modification or having any slots in the cart
|
||||
// -> first affect the modification/cancellation rights attributes to the current slot
|
||||
@ -595,7 +600,7 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
|
||||
}
|
||||
},
|
||||
function () { // cancel confirmed
|
||||
Slot.cancel({ id: $scope.slot.slot_id }, function () { // successfully canceled
|
||||
SlotsReservation.cancel({ id: $scope.slot.slots_reservations_ids[0] }, function () { // successfully canceled
|
||||
growl.success(_t('app.shared.cart.reservation_was_cancelled_successfully'));
|
||||
if (typeof $scope.onSlotCancelSuccess === 'function') { return $scope.onSlotCancelSuccess(); }
|
||||
}
|
||||
@ -707,20 +712,18 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
|
||||
/**
|
||||
* Create a hash map implementing the Reservation specs
|
||||
* @param slots {Array<Object>} Array of fullCalendar events: slots selected on the calendar
|
||||
* @return {{reservation: {reservable_type: string, reservable_id: string, slots_attributes: []}}}
|
||||
* @return {{reservation: Reservation}}
|
||||
*/
|
||||
const mkReservation = function (slots) {
|
||||
const reservation = {
|
||||
reservable_id: $scope.reservableId,
|
||||
reservable_type: $scope.reservableType,
|
||||
slots_attributes: []
|
||||
slots_reservations_attributes: []
|
||||
};
|
||||
angular.forEach(slots, function (slot) {
|
||||
reservation.slots_attributes.push({
|
||||
start_at: slot.start,
|
||||
end_at: slot.end,
|
||||
availability_id: slot.availability_id,
|
||||
offered: slot.offered || false
|
||||
reservation.slots_reservations_attributes.push({
|
||||
offered: slot.offered || false,
|
||||
slot_id: slot.slot_id
|
||||
});
|
||||
});
|
||||
|
||||
@ -730,7 +733,7 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
|
||||
/**
|
||||
* Create a hash map implementing the Subscription specs
|
||||
* @param planId {number}
|
||||
* @return {{subscription: {plan_id: number}}}
|
||||
* @return {{subscription: SubscriptionRequest}}
|
||||
*/
|
||||
const mkSubscription = function (planId) {
|
||||
return {
|
||||
@ -742,7 +745,7 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
|
||||
|
||||
/**
|
||||
* Build the ShoppingCart object, from the current reservation
|
||||
* @param items {Array<{reservation:{reservable_type: string, reservable_id: string, slots_attributes: []}}|{subscription: {plan_id: number}}>}
|
||||
* @param items {Array<CartItem>}
|
||||
* @param paymentMethod {string}
|
||||
* @return {ShoppingCart}
|
||||
*/
|
||||
|
@ -191,6 +191,14 @@ Application.Filters.filter('humanReadablePlanName', ['$filter', function ($filte
|
||||
};
|
||||
}]);
|
||||
|
||||
Application.Filters.filter('canceledReservationsFilter', [function () {
|
||||
return function (elements) {
|
||||
if (!angular.isUndefined(elements) && (elements != null)) {
|
||||
return elements.filter(e => e.canceled_at === null);
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
Application.Filters.filter('trainingReservationsFilter', [function () {
|
||||
return function (elements, selectedScope) {
|
||||
if (!angular.isUndefined(elements) && !angular.isUndefined(selectedScope) && (elements != null) && (selectedScope != null)) {
|
||||
|
@ -25,10 +25,12 @@ export interface AuthenticationProviderMapping {
|
||||
format: 'iso8601' | 'rfc2822' | 'rfc3339' | 'timestamp-s' | 'timestamp-ms',
|
||||
true_value: string,
|
||||
false_value: string,
|
||||
mapping: {
|
||||
mapping: [
|
||||
{
|
||||
from: string,
|
||||
to: number|string
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,15 +3,19 @@ import { ApiFilter } from './api';
|
||||
|
||||
export type ReservableType = 'Training' | 'Event' | 'Space' | 'Machine';
|
||||
|
||||
export interface ReservationSlot {
|
||||
export interface SlotsReservation {
|
||||
id?: number,
|
||||
canceled_at?: TDateISO,
|
||||
offered?: boolean,
|
||||
slot_id?: number,
|
||||
slot_attributes?: {
|
||||
id: number,
|
||||
start_at: TDateISO,
|
||||
end_at: TDateISO,
|
||||
canceled_at?: TDateISO,
|
||||
availability_id?: number,
|
||||
offered?: boolean,
|
||||
is_reserved?: boolean
|
||||
availability_id: number
|
||||
}
|
||||
}
|
||||
// TODO, refactor Reservation for cart_items (in payment) => should use slot_id instead of (start_at + end_at)
|
||||
|
||||
export interface Reservation {
|
||||
id?: number,
|
||||
@ -20,7 +24,7 @@ export interface Reservation {
|
||||
message?: string,
|
||||
reservable_id: number,
|
||||
reservable_type: ReservableType,
|
||||
slots_attributes: Array<ReservationSlot>,
|
||||
slots_reservations_attributes: Array<SlotsReservation>,
|
||||
reservable?: {
|
||||
id: number,
|
||||
name: string
|
||||
|
@ -136,7 +136,8 @@ export enum SettingName {
|
||||
MachinesModule = 'machines_module',
|
||||
UserChangeGroup = 'user_change_group',
|
||||
UserValidationRequired = 'user_validation_required',
|
||||
UserValidationRequiredList = 'user_validation_required_list'
|
||||
UserValidationRequiredList = 'user_validation_required_list',
|
||||
ShowUsernameInAdminList = 'show_username_in_admin_list'
|
||||
}
|
||||
|
||||
export type SettingValue = string|boolean|number;
|
||||
|
@ -936,7 +936,7 @@ angular.module('application.router', ['ui.router'])
|
||||
groupsPromise: ['Group', function (Group) { return Group.query().$promise; }],
|
||||
tagsPromise: ['Tag', function (Tag) { return Tag.query().$promise; }],
|
||||
authProvidersPromise: ['AuthProvider', function (AuthProvider) { return AuthProvider.query().$promise; }],
|
||||
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['feature_tour_display', 'user_validation_required']" }).$promise; }]
|
||||
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['feature_tour_display', 'user_validation_required', 'show_username_in_admin_list']" }).$promise; }]
|
||||
}
|
||||
})
|
||||
.state('app.admin.members_new', {
|
||||
@ -1093,7 +1093,7 @@ angular.module('application.router', ['ui.router'])
|
||||
"'link_name', 'home_content', 'home_css', 'phone_required', 'upcoming_events_shown', 'public_agenda_module'," +
|
||||
"'renew_pack_threshold', 'pack_only_for_subscription', 'overlapping_categories', 'public_registrations'," +
|
||||
"'extended_prices_in_same_day', 'recaptcha_site_key', 'recaptcha_secret_key', 'user_validation_required', " +
|
||||
"'user_validation_required_list', 'machines_module', 'user_change_group']"
|
||||
"'user_validation_required_list', 'machines_module', 'user_change_group', 'show_username_in_admin_list']"
|
||||
}).$promise;
|
||||
}],
|
||||
privacyDraftsPromise: ['Setting', function (Setting) { return Setting.get({ name: 'privacy_draft', history: true }).$promise; }],
|
||||
|
@ -1,15 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
Application.Services.factory('Slot', ['$resource', function ($resource) {
|
||||
return $resource('/api/slots/:id',
|
||||
{ id: '@id' }, {
|
||||
update: {
|
||||
method: 'PUT'
|
||||
},
|
||||
cancel: {
|
||||
method: 'PUT',
|
||||
url: '/api/slots/:id/cancel'
|
||||
}
|
||||
}
|
||||
);
|
||||
}]);
|
15
app/frontend/src/javascript/services/slots_reservation.js
Normal file
15
app/frontend/src/javascript/services/slots_reservation.js
Normal file
@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
Application.Services.factory('SlotsReservation', ['$resource', function ($resource) {
|
||||
return $resource('/api/slots_reservations/:id',
|
||||
{ id: '@id' }, {
|
||||
update: {
|
||||
method: 'PUT'
|
||||
},
|
||||
cancel: {
|
||||
method: 'PUT',
|
||||
url: '/api/slots_reservations/:id/cancel'
|
||||
}
|
||||
}
|
||||
);
|
||||
}]);
|
@ -408,9 +408,10 @@
|
||||
.about-title, .about-title p {
|
||||
margin: 0;
|
||||
font-size: rem-calc(50);
|
||||
line-height: rem-calc(48);
|
||||
line-height: rem-calc(56);
|
||||
color: #fff;
|
||||
font-weight: 900;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.about-title-aside {
|
||||
@ -418,14 +419,6 @@
|
||||
font-size: rem-calc(18);
|
||||
}
|
||||
|
||||
.about-title,
|
||||
.about-title p
|
||||
{
|
||||
font-size: rem-calc(18);
|
||||
line-height: rem-calc(30);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&.ng-hide {
|
||||
opacity: 0;
|
||||
|
||||
|
@ -103,12 +103,20 @@
|
||||
}
|
||||
|
||||
.heading-title {
|
||||
//overflow: hidden;
|
||||
height: 94px;
|
||||
min-height: 94px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
|
||||
& > *:not(:last-child) {
|
||||
margin-right: 3rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 0 15px;
|
||||
padding: 36px 15px;
|
||||
margin: 0;
|
||||
padding: 15px 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -581,7 +589,7 @@ body.container {
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
& > img {
|
||||
& > svg {
|
||||
border: 1px solid var(--gray-soft-dark);
|
||||
background-color: var(--gray-soft-lightest);
|
||||
&:hover { opacity: 0.65; }
|
||||
@ -589,7 +597,7 @@ body.container {
|
||||
& > a {
|
||||
transition: transform 200ms ease-in-out;
|
||||
&:hover { transform: translateY(-4px); }
|
||||
img {
|
||||
svg {
|
||||
max-width: 100%;
|
||||
height: inherit;
|
||||
}
|
||||
@ -665,12 +673,17 @@ body.container {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.profile-bio {
|
||||
.bio-title {
|
||||
display: inherit;
|
||||
text-align: center;
|
||||
height: 50px;
|
||||
}
|
||||
a {
|
||||
color: var(--gray-soft-lightest) !important;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.calendar-filter {
|
||||
h3 {
|
||||
|
@ -34,7 +34,6 @@
|
||||
.heading-title {
|
||||
h1 {
|
||||
font-size: rem-calc(16);
|
||||
padding: 26px 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@
|
||||
p {
|
||||
@include text-sm;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
&::first-letter { text-transform: uppercase; }
|
||||
}
|
||||
|
||||
|
@ -1,28 +1,22 @@
|
||||
.form-switch {
|
||||
position: relative;
|
||||
|
||||
.form-item-header {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
left: 16px;
|
||||
padding-top: 2px;
|
||||
width: fit-content;
|
||||
margin-bottom: 0;
|
||||
|
||||
.item-tooltip {
|
||||
margin-left: 12px;
|
||||
position: unset;
|
||||
|
||||
.content {
|
||||
right: unset;
|
||||
left: 0;
|
||||
& > *:not(:first-child) {
|
||||
margin-left: 1.5rem;
|
||||
}
|
||||
|
||||
.item-tooltip .content {
|
||||
max-width: min(75vw, 30ch);
|
||||
}
|
||||
}
|
||||
|
||||
.form-item-field {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
background-color: white;
|
||||
padding: 16px;
|
||||
background-color: white;
|
||||
& > *:not(:first-child) {
|
||||
margin-left: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -24,3 +24,7 @@
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-select-bootstrap .ui-select-choices-row > span {
|
||||
white-space: normal;
|
||||
}
|
@ -11,7 +11,7 @@
|
||||
<section class="heading-title">
|
||||
<h1 class="inline">{{ 'app.shared.user_admin.user' | translate }} {{ user.name }}</h1>
|
||||
<span class="label label-danger text-white" ng-show="user.need_completion" translate>{{ 'app.shared.user_admin.incomplete_profile' }}</span>
|
||||
<div class="pull-right" style="top: 35%;position: relative;right: 10px;" ng-if="enableUserValidationRequired">
|
||||
<div ng-if="enableUserValidationRequired">
|
||||
<user-validation member="user"
|
||||
on-error="onError"
|
||||
on-success="onValidateMemberSuccess" />
|
||||
|
@ -38,13 +38,14 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:4%" class="hidden-xs" ng-if="enableUserValidationRequired"></th>
|
||||
<th style="width:15%"><a ng-click="setOrderMember('last_name')">{{ 'app.admin.members.surname' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': member.order=='last_name', 'fa fa-sort-alpha-desc': member.order=='-last_name', 'fa fa-arrows-v': member.order }"></i></a></th>
|
||||
<th style="width:15%"><a ng-click="setOrderMember('first_name')">{{ 'app.admin.members.first_name' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': member.order=='first_name', 'fa fa-sort-alpha-desc': member.order=='-first_name', 'fa fa-arrows-v': member.order }"></i></a></th>
|
||||
<th style="width:15%" class="hidden-xs"><a ng-click="setOrderMember('email')">{{ 'app.admin.members.email' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': member.order=='email', 'fa fa-sort-alpha-desc': member.order=='-email', 'fa fa-arrows-v': member.order }"></i></a></th>
|
||||
<th style="width:9%" class="hidden-xs hidden-sm hidden-md"><a ng-click="setOrderMember('phone')">{{ 'app.admin.members.phone' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': member.order=='phone', 'fa fa-sort-numeric-desc': member.order=='-phone', 'fa fa-arrows-v': member.order }"></i></a></th>
|
||||
<th style="width:14%" class="hidden-xs hidden-sm"><a ng-click="setOrderMember('group')">{{ 'app.admin.members.user_type' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': member.order=='group', 'fa fa-sort-alpha-desc': member.order=='-group', 'fa fa-arrows-v': member.order }"></i></a></th>
|
||||
<th style="width:14%" class="hidden-xs hidden-sm hidden-md"><a ng-click="setOrderMember('plan')">{{ 'app.admin.members.subscription' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': member.order=='plan', 'fa fa-sort-alpha-desc': member.order=='-plan', 'fa fa-arrows-v': member.order }"></i></a></th>
|
||||
<th style="width:14%" class="buttons-col"></th>
|
||||
<th style="width:8%" ng-show="displayUsername"><a ng-click="setOrderMember('username')">{{ 'app.admin.members.username' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': member.order=='username', 'fa fa-sort-alpha-desc': member.order=='-username', 'fa fa-arrows-v': member.order }"></i></a></th>
|
||||
<th style="width:14%"><a ng-click="setOrderMember('last_name')">{{ 'app.admin.members.surname' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': member.order=='last_name', 'fa fa-sort-alpha-desc': member.order=='-last_name', 'fa fa-arrows-v': member.order }"></i></a></th>
|
||||
<th style="width:14%"><a ng-click="setOrderMember('first_name')">{{ 'app.admin.members.first_name' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': member.order=='first_name', 'fa fa-sort-alpha-desc': member.order=='-first_name', 'fa fa-arrows-v': member.order }"></i></a></th>
|
||||
<th style="width:14%" class="hidden-xs"><a ng-click="setOrderMember('email')">{{ 'app.admin.members.email' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': member.order=='email', 'fa fa-sort-alpha-desc': member.order=='-email', 'fa fa-arrows-v': member.order }"></i></a></th>
|
||||
<th style="width:8%" class="hidden-xs hidden-sm hidden-md"><a ng-click="setOrderMember('phone')">{{ 'app.admin.members.phone' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': member.order=='phone', 'fa fa-sort-numeric-desc': member.order=='-phone', 'fa fa-arrows-v': member.order }"></i></a></th>
|
||||
<th style="width:13%" class="hidden-xs hidden-sm"><a ng-click="setOrderMember('group')">{{ 'app.admin.members.user_type' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': member.order=='group', 'fa fa-sort-alpha-desc': member.order=='-group', 'fa fa-arrows-v': member.order }"></i></a></th>
|
||||
<th style="width:13%" class="hidden-xs hidden-sm hidden-md"><a ng-click="setOrderMember('plan')">{{ 'app.admin.members.subscription' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': member.order=='plan', 'fa fa-sort-alpha-desc': member.order=='-plan', 'fa fa-arrows-v': member.order }"></i></a></th>
|
||||
<th style="width:12%" class="buttons-col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -52,6 +53,7 @@
|
||||
<td class="text-center" ng-if="enableUserValidationRequired">
|
||||
<span ng-class="{ 'text-success': !!m.validated_at }"><i class="fa fa-user-check"></i></span>
|
||||
</td>
|
||||
<td class="text-c" ng-show="displayUsername">{{ m.username }}</td>
|
||||
<td class="text-c">{{ m.profile.last_name }}</td>
|
||||
<td class="text-c">{{ m.profile.first_name }}</td>
|
||||
<td class="hidden-xs">{{ m.email }}</td>
|
||||
|
@ -73,6 +73,27 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default m-t-md">
|
||||
<div class="panel-heading">
|
||||
<span class="font-sbold" translate>{{ 'app.admin.settings.accounts_management' }}</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="row">
|
||||
<h3 class="m-l" translate>{{ 'app.admin.settings.members_list' }}</h3>
|
||||
<p class="alert alert-warning m-h-md" translate>
|
||||
{{ 'app.admin.settings.members_list_info' }}
|
||||
</p>
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<boolean-setting name="'show_username_in_admin_list'"
|
||||
settings="allSettings"
|
||||
label="'app.admin.settings.show_username_in_admin_list' | translate"
|
||||
on-success="onSuccess"
|
||||
on-error="onError">
|
||||
</boolean-setting>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel panel-default m-t-md">
|
||||
<div class="panel-heading">
|
||||
|
@ -179,7 +179,7 @@
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="row" ng-show="$root.modules.machines">
|
||||
<h3 class="m-l" translate>{{ 'app.admin.settings.display_machine_reservation_user_name' }}</h3>
|
||||
<h3 class="m-l" translate>{{ 'app.admin.settings.display_reservation_user_name' }}</h3>
|
||||
<p class="alert alert-warning m-h-md" ng-bind-html="'app.admin.settings.display_name_info_html' | translate"></p>
|
||||
<boolean-setting name="'display_name_enable'"
|
||||
label="'app.admin.settings.display_name' | translate"
|
||||
|
@ -39,7 +39,7 @@
|
||||
</div>
|
||||
<div class="widget-content bg-light wrapper r-b">
|
||||
<ul class="list-unstyled" ng-if="user.training_reservations.length > 0">
|
||||
<li ng-repeat="r in user.training_reservations | trainingReservationsFilter:'future'" class="m-b" data-label="{{ 'app.admin.event_reservations.canceled' | translate}}" ng-class="{'reservation-canceled':r.canceled_at}">
|
||||
<li ng-repeat="r in user.training_reservations | trainingReservationsFilter:'future'" class="m-b" data-label="{{ 'app.logged.dashboard.trainings.canceled' | translate}}" ng-class="{'reservation-canceled':r.canceled_at}">
|
||||
<span class="font-sbold">{{r.reservable.name}}</span> - <span class="label label-warning wrapper-sm">{{ r.start_at | amDateFormat:'LLL' }} - {{ r.end_at | amDateFormat:'LT' }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
@ -54,7 +54,7 @@
|
||||
</div>
|
||||
<div class="widget-content bg-light auto wrapper r-b">
|
||||
<ul class="list-unstyled" ng-if="user.training_reservations.length > 0">
|
||||
<li ng-repeat="r in user.training_reservations | trainingReservationsFilter:'passed'" class="m-b" data-label="{{ 'app.admin.event_reservations.canceled' | translate}}" ng-class="{'reservation-canceled':r.canceled_at}">
|
||||
<li ng-repeat="r in user.training_reservations | trainingReservationsFilter:'passed'" class="m-b" data-label="{{ 'app.logged.dashboard.trainings.canceled' | translate}}" ng-class="{'reservation-canceled':r.canceled_at}">
|
||||
<span class="font-sbold">{{r.reservable.name}}</span> - <span class="label label-info text-white wrapper-sm">{{ r.start_at | amDateFormat:'LLL' }} - {{ r.end_at | amDateFormat:'LT' }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -84,9 +84,15 @@
|
||||
</div>
|
||||
<small class="text-xs m-b"><i>{{ 'app.public.projects_show.posted_on_' | translate }} {{project.created_at | amDateFormat: 'LL'}}</i></small>
|
||||
|
||||
<div class="m" ng-if="project.themes">
|
||||
<span ng-repeat="theme in project.themes" class="badge m-r-sm">
|
||||
{{theme.name}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<section class="widget panel b-a m" ng-if="project.project_caos_attributes">
|
||||
<div class="panel-heading b-b">
|
||||
<span class="badge bg-warning pull-right">{{project.project_caos_attributes.length}}</span>
|
||||
@ -150,6 +156,15 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="widget panel b-a m" ng-if="project.tags">
|
||||
<div class="panel-heading b-b">
|
||||
<h3 translate>{{ 'app.shared.project.tags' }}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<pre>{{ project.tags }}</pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="widget b-t">
|
||||
|
||||
<div class="widget-content text-center m-t">
|
||||
|
@ -48,11 +48,11 @@
|
||||
|
||||
|
||||
<div class="wrapper">
|
||||
<section class="widget panel no-border bg-black-light text-white lt">
|
||||
<section class="profile-bio widget panel no-border bg-black-light text-white lt">
|
||||
<div class="panel-body">
|
||||
<div class="wrapper m-t-xl m-b">
|
||||
<div class="row m-b">
|
||||
<div class="col-xs-5 text-right">
|
||||
<div class="col-xs-5">
|
||||
<span class="font-bold bio-title" translate>{{ 'app.shared.public_profile.interests' }}</span>
|
||||
<div class="m-b m-t-sm" ng-bind-html="user.profile_attributes.interest"></div>
|
||||
</div>
|
||||
@ -74,7 +74,7 @@
|
||||
<h1 class="red text-u-c" translate>{{ 'app.shared.public_profile.trainings' }}</h1>
|
||||
<!-- <h3 class="text-u-c">Formations</h3> -->
|
||||
<ul class="list-unstyled" ng-if="user.training_reservations.length > 0 || user.trainings.length > 0">
|
||||
<li ng-repeat="r in user.training_reservations | trainingReservationsFilter:'future'">
|
||||
<li ng-repeat="r in user.training_reservations | trainingReservationsFilter:'future' | canceledReservationsFilter">
|
||||
{{r.reservable.name}} - <span class="label label-info text-white" translate>{{ 'app.shared.public_profile.to_come' }}</span>
|
||||
</li>
|
||||
<li ng-repeat="t in user.trainings">
|
||||
|
@ -7,10 +7,11 @@
|
||||
<uib-alert ng-repeat="alert in alerts" type="{{alert.type}}" close="closeAlert($index)">{{alert.msg}}</uib-alert>
|
||||
<div class="row">
|
||||
<div ng-class="{'col-md-6': schedule, 'm-h-sm': !schedule}">
|
||||
<div ng-if="reservation">
|
||||
<!-- this modal dialog is only still used in events reservation, so the following workaround can do the trick -->
|
||||
<div ng-if="reservation && bookedEvent">
|
||||
<p translate>{{ 'app.shared.valid_reservation_modal.here_is_the_summary_of_the_slots_to_book_for_the_current_user' }}</p>
|
||||
<ul ng-repeat="slot in reservation.slots_attributes">
|
||||
<li><strong>{{slot.start_at | amDateFormat: 'LL'}} : {{slot.start_at | amDateFormat:'LT'}} - {{slot.end_at | amDateFormat:'LT'}}</strong></li>
|
||||
<ul ng-repeat="sr in reservation.reservation.slots_reservations_attributes">
|
||||
<li><strong>{{bookedEvent.availability.start_at | amDateFormat: 'LL'}} : {{bookedEvent.availability.start_at | amDateFormat:'LT'}} - {{bookedEvent.availability.end_at | amDateFormat:'LT'}}</strong></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div ng-if="subscription">
|
||||
|
@ -8,7 +8,7 @@ module AvailabilityHelper
|
||||
EVENT_COLOR = '#dd7e6b'
|
||||
IS_RESERVED_BY_CURRENT_USER = '#b2e774'
|
||||
MACHINE_IS_RESERVED_BY_USER = '#1d98ec'
|
||||
IS_COMPLETED = '#eeeeee'
|
||||
IS_FULL = '#eeeeee'
|
||||
|
||||
def availability_border_color(availability)
|
||||
case availability.available_type
|
||||
@ -25,7 +25,7 @@ module AvailabilityHelper
|
||||
|
||||
def machines_slot_border_color(slot)
|
||||
if slot.is_reserved
|
||||
slot.is_reserved_by_current_user ? IS_RESERVED_BY_CURRENT_USER : IS_COMPLETED
|
||||
slot.current_user_slots_reservations_ids.empty? ? IS_FULL : IS_RESERVED_BY_CURRENT_USER
|
||||
else
|
||||
MACHINE_COLOR
|
||||
end
|
||||
@ -34,8 +34,8 @@ module AvailabilityHelper
|
||||
def space_slot_border_color(slot)
|
||||
if slot.is_reserved
|
||||
IS_RESERVED_BY_CURRENT_USER
|
||||
elsif slot.complete?
|
||||
IS_COMPLETED
|
||||
elsif slot.full?
|
||||
IS_FULL
|
||||
else
|
||||
SPACE_COLOR
|
||||
end
|
||||
@ -45,7 +45,7 @@ module AvailabilityHelper
|
||||
if availability.is_reserved
|
||||
IS_RESERVED_BY_CURRENT_USER
|
||||
elsif availability.full?
|
||||
IS_COMPLETED
|
||||
IS_FULL
|
||||
else
|
||||
case availability.available_type
|
||||
when 'training'
|
||||
|
@ -25,7 +25,7 @@ class NotificationsMailer < NotifyWith::NotificationsMailer
|
||||
|
||||
send(notification.notification_type)
|
||||
rescue StandardError => e
|
||||
STDERR.puts "[NotificationsMailer] notification cannot be sent: #{e}"
|
||||
Rails.logger.error "[NotificationsMailer] notification cannot be sent: #{e}"
|
||||
end
|
||||
|
||||
def helpers
|
||||
@ -54,7 +54,7 @@ class NotificationsMailer < NotifyWith::NotificationsMailer
|
||||
end
|
||||
|
||||
def notify_member_create_reservation
|
||||
attachments[@attached_object.ics_filename] = @attached_object.to_ics.encode(Encoding::ISO_8859_15)
|
||||
attachments[@attached_object.ics_filename] = @attached_object.to_ics.encode(Encoding::UTF_8)
|
||||
mail(to: @recipient.email,
|
||||
subject: t('notifications_mailer.notify_member_create_reservation.subject'),
|
||||
template_name: 'notify_member_create_reservation')
|
||||
|
@ -20,6 +20,7 @@ class Availability < ApplicationRecord
|
||||
has_many :spaces, through: :spaces_availabilities
|
||||
|
||||
has_many :slots
|
||||
has_many :slots_reservations, through: :slots
|
||||
has_many :reservations, through: :slots
|
||||
|
||||
has_one :event
|
||||
@ -36,7 +37,7 @@ class Availability < ApplicationRecord
|
||||
scope :trainings, -> { includes(:trainings).where(available_type: 'training') }
|
||||
scope :spaces, -> { includes(:spaces).where(available_type: 'space') }
|
||||
|
||||
attr_accessor :is_reserved, :slot_id, :can_modify
|
||||
attr_accessor :is_reserved, :current_user_slots_reservations_ids, :can_modify
|
||||
|
||||
validates :start_at, :end_at, presence: true
|
||||
validate :length_must_be_slot_multiple, unless: proc { end_at.blank? or start_at.blank? }
|
||||
@ -73,7 +74,7 @@ class Availability < ApplicationRecord
|
||||
.joins(:slots)
|
||||
.where('slots.availability_id = ?', id)
|
||||
else
|
||||
STDERR.puts "[safe_destroy] Availability with unknown type #{available_type}"
|
||||
Rails.logger.warn "[safe_destroy] Availability with unknown type #{available_type}"
|
||||
reservations = []
|
||||
end
|
||||
if reservations.size.zero?
|
||||
@ -106,7 +107,7 @@ class Availability < ApplicationRecord
|
||||
when 'space'
|
||||
spaces.map(&:name).join(' - ')
|
||||
else
|
||||
STDERR.puts "[title] Availability with unknown type #{available_type}"
|
||||
Rails.logger.warn "[title] Availability with unknown type #{available_type}"
|
||||
'???'
|
||||
end
|
||||
end
|
||||
@ -116,23 +117,25 @@ class Availability < ApplicationRecord
|
||||
def full?
|
||||
return false if nb_total_places.blank?
|
||||
|
||||
if available_type == 'training' || available_type == 'space'
|
||||
nb_total_places <= slots.to_a.select { |s| s.canceled_at.nil? }.size
|
||||
elsif available_type == 'event'
|
||||
if available_type == 'event'
|
||||
event.nb_free_places.zero?
|
||||
else
|
||||
slots.map(&:full?).reduce(:&)
|
||||
end
|
||||
end
|
||||
|
||||
def nb_total_places
|
||||
def available_places_per_slot(reservable = nil)
|
||||
case available_type
|
||||
when 'training'
|
||||
super.presence || trainings.map(&:nb_total_places).reduce(:+)
|
||||
nb_total_places || reservable&.nb_total_places || trainings.map(&:nb_total_places).max
|
||||
when 'event'
|
||||
event.nb_total_places
|
||||
when 'space'
|
||||
super.presence || spaces.map(&:default_places).reduce(:+)
|
||||
nb_total_places || reservable&.default_places || spaces.map(&:default_places).max
|
||||
when 'machines'
|
||||
reservable.nil? ? machines.count : 1
|
||||
else
|
||||
nil
|
||||
raise TypeError
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -38,7 +38,7 @@ class CartItem::EventReservation < CartItem::Reservation
|
||||
::Reservation.new(
|
||||
reservable_id: @reservable.id,
|
||||
reservable_type: Event.name,
|
||||
slots_attributes: slots_params,
|
||||
slots_reservations_attributes: slots_params,
|
||||
tickets_attributes: tickets_params,
|
||||
nb_reserve_places: @normal_tickets,
|
||||
statistic_profile_id: StatisticProfile.find_by(user: @customer).id
|
||||
|
@ -16,7 +16,7 @@ class CartItem::MachineReservation < CartItem::Reservation
|
||||
::Reservation.new(
|
||||
reservable_id: @reservable.id,
|
||||
reservable_type: Machine.name,
|
||||
slots_attributes: slots_params,
|
||||
slots_reservations_attributes: slots_params,
|
||||
statistic_profile_id: StatisticProfile.find_by(user: @customer).id
|
||||
)
|
||||
end
|
||||
@ -25,6 +25,22 @@ class CartItem::MachineReservation < CartItem::Reservation
|
||||
'machine'
|
||||
end
|
||||
|
||||
def valid?(all_items)
|
||||
@slots.each do |slot|
|
||||
same_hour_slots = SlotsReservation.joins(:reservation).where(
|
||||
reservations: { reservable: @reservable },
|
||||
slot_id: slot[:slot_id],
|
||||
canceled_at: nil
|
||||
).count
|
||||
if same_hour_slots.positive?
|
||||
@errors[:slot] = 'slot is reserved'
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def credits
|
||||
|
@ -36,6 +36,8 @@ class CartItem::PaymentSchedule
|
||||
end
|
||||
|
||||
def valid?(_all_items)
|
||||
return true unless @requested && @plan&.monthly_payment
|
||||
|
||||
if @plan&.disabled
|
||||
@errors[:item] = 'plan is disabled'
|
||||
return false
|
||||
|
@ -11,7 +11,7 @@ class CartItem::Reservation < CartItem::BaseItem
|
||||
@customer = customer
|
||||
@operator = operator
|
||||
@reservable = reservable
|
||||
@slots = slots
|
||||
@slots = slots.map { |s| expand_slot(s) }
|
||||
super
|
||||
end
|
||||
|
||||
@ -43,33 +43,27 @@ class CartItem::Reservation < CartItem::BaseItem
|
||||
def valid?(all_items)
|
||||
pending_subscription = all_items.find { |i| i.is_a?(CartItem::Subscription) }
|
||||
@slots.each do |slot|
|
||||
availability = Availability.find_by(id: slot[:availability_id])
|
||||
if availability.nil?
|
||||
@errors[:slot] = 'slot availability does not exist'
|
||||
slot_db = Slot.find(slot[:slot_id])
|
||||
if slot_db.nil?
|
||||
@errors[:slot] = 'slot does not exist'
|
||||
return false
|
||||
end
|
||||
|
||||
if availability.available_type == 'machines'
|
||||
s = Slot.includes(:reservations).where(start_at: slot[:start_at], end_at: slot[:end_at], availability_id: slot[:availability_id], canceled_at: nil, "reservations.reservable": @reservable)
|
||||
unless s.empty?
|
||||
@errors[:slot] = 'slot is reserved'
|
||||
availability = Availability.find_by(id: slot[:slot_attributes][:availability_id])
|
||||
if availability.nil?
|
||||
@errors[:availability] = 'availability does not exist'
|
||||
return false
|
||||
end
|
||||
elsif availability.available_type == 'space' && availability.spaces.first.disabled
|
||||
@errors[:slot] = 'space is disabled'
|
||||
return false
|
||||
elsif availability.full?
|
||||
@errors[:slot] = 'availability is complete'
|
||||
|
||||
if slot_db.full?
|
||||
@errors[:slot] = 'availability is full'
|
||||
return false
|
||||
end
|
||||
|
||||
next if availability.plan_ids.empty?
|
||||
next if (@customer.subscribed_plan && availability.plan_ids.include?(@customer.subscribed_plan.id)) ||
|
||||
(pending_subscription && availability.plan_ids.include?(pending_subscription.plan.id)) ||
|
||||
(@operator.manager? && @customer.id != @operator.id) ||
|
||||
@operator.admin?
|
||||
next if required_subscription?(availability, pending_subscription)
|
||||
|
||||
@errors[:slot] = 'slot is restricted for subscribers'
|
||||
@errors[:availability] = 'availability is restricted for subscribers'
|
||||
return false
|
||||
end
|
||||
|
||||
@ -88,7 +82,11 @@ class CartItem::Reservation < CartItem::BaseItem
|
||||
def grouped_slots
|
||||
return { all: @slots } unless Setting.get('extended_prices_in_same_day')
|
||||
|
||||
@slots.group_by { |slot| slot[:start_at].to_date }
|
||||
@slots.group_by { |slot| slot[:slot_attributes][:start_at].to_date }
|
||||
end
|
||||
|
||||
def expand_slot(slot)
|
||||
slot.merge({ slot_attributes: Slot.find(slot[:slot_id]) })
|
||||
end
|
||||
|
||||
##
|
||||
@ -99,7 +97,7 @@ class CartItem::Reservation < CartItem::BaseItem
|
||||
def get_slot_price_from_prices(prices, slot, is_privileged, options = {})
|
||||
options = GET_SLOT_PRICE_DEFAULT_OPTS.merge(options)
|
||||
|
||||
slot_minutes = (slot[:end_at].to_time - slot[:start_at].to_time) / SECONDS_PER_MINUTE
|
||||
slot_minutes = (slot[:slot_attributes][:end_at].to_time - slot[:slot_attributes][:start_at].to_time) / SECONDS_PER_MINUTE
|
||||
price = prices[:prices].find { |p| p[:duration] <= slot_minutes && p[:duration].positive? }
|
||||
price = prices[:prices].first if price.nil?
|
||||
hourly_rate = (price[:price].amount.to_f / price[:price].duration) * MINUTES_PER_HOUR
|
||||
@ -129,7 +127,7 @@ class CartItem::Reservation < CartItem::BaseItem
|
||||
options = GET_SLOT_PRICE_DEFAULT_OPTS.merge(options)
|
||||
|
||||
slot_rate = options[:has_credits] || (slot[:offered] && is_privileged) ? 0 : hourly_rate
|
||||
slot_minutes = (slot[:end_at].to_time - slot[:start_at].to_time) / SECONDS_PER_MINUTE
|
||||
slot_minutes = (slot[:slot_attributes][:end_at].to_time - slot[:slot_attributes][:start_at].to_time) / SECONDS_PER_MINUTE
|
||||
# apply the base price to the real slot duration
|
||||
real_price = if options[:is_division]
|
||||
(slot_rate / MINUTES_PER_HOUR) * slot_minutes
|
||||
@ -146,7 +144,7 @@ class CartItem::Reservation < CartItem::BaseItem
|
||||
|
||||
unless options[:elements].nil?
|
||||
options[:elements][:slots].push(
|
||||
start_at: slot[:start_at],
|
||||
start_at: slot[:slot_attributes][:start_at],
|
||||
price: real_price,
|
||||
promo: (slot_rate != hourly_rate)
|
||||
)
|
||||
@ -160,7 +158,7 @@ class CartItem::Reservation < CartItem::BaseItem
|
||||
# and the base price (1 hours), we use the 7 hours price, then 3 hours price, and finally the base price twice (7+3+1+1 = 12).
|
||||
# All these prices are returned to be applied to the reservation.
|
||||
def applicable_prices(slots)
|
||||
total_duration = slots.map { |slot| (slot[:end_at].to_time - slot[:start_at].to_time) / SECONDS_PER_MINUTE }.reduce(:+)
|
||||
total_duration = slots.map { |slot| (slot[:slot_attributes][:end_at].to_time - slot[:slot_attributes][:start_at].to_time) / SECONDS_PER_MINUTE }.reduce(:+)
|
||||
rates = { prices: [] }
|
||||
|
||||
remaining_duration = total_duration
|
||||
@ -196,6 +194,17 @@ class CartItem::Reservation < CartItem::BaseItem
|
||||
end
|
||||
|
||||
def slots_params
|
||||
@slots.map { |slot| slot.permit(:id, :start_at, :end_at, :availability_id, :offered) }
|
||||
@slots.map { |slot| slot.permit(:id, :slot_id, :offered) }
|
||||
end
|
||||
|
||||
##
|
||||
# Check if the given availability requires a valid subscription. If so, check if the current customer
|
||||
# has the required susbcription, otherwise, check if the operator is privileged
|
||||
##
|
||||
def required_subscription?(availability, pending_subscription)
|
||||
(@customer.subscribed_plan && availability.plan_ids.include?(@customer.subscribed_plan.id)) ||
|
||||
(pending_subscription && availability.plan_ids.include?(pending_subscription.plan.id)) ||
|
||||
(@operator.manager? && @customer.id != @operator.id) ||
|
||||
@operator.admin?
|
||||
end
|
||||
end
|
||||
|
@ -9,6 +9,7 @@ class CartItem::SpaceReservation < CartItem::Reservation
|
||||
|
||||
super(customer, operator, space, slots)
|
||||
@plan = plan
|
||||
@space = space
|
||||
@new_subscription = new_subscription
|
||||
end
|
||||
|
||||
@ -16,7 +17,7 @@ class CartItem::SpaceReservation < CartItem::Reservation
|
||||
::Reservation.new(
|
||||
reservable_id: @reservable.id,
|
||||
reservable_type: Space.name,
|
||||
slots_attributes: slots_params,
|
||||
slots_reservations_attributes: slots_params,
|
||||
statistic_profile_id: StatisticProfile.find_by(user: @customer).id
|
||||
)
|
||||
end
|
||||
@ -25,6 +26,15 @@ class CartItem::SpaceReservation < CartItem::Reservation
|
||||
'space'
|
||||
end
|
||||
|
||||
def valid?(all_items)
|
||||
if @space.disabled
|
||||
@errors[:reservable] = 'space is disabled'
|
||||
return false
|
||||
end
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def credits
|
||||
|
@ -36,7 +36,7 @@ class CartItem::TrainingReservation < CartItem::Reservation
|
||||
::Reservation.new(
|
||||
reservable_id: @reservable.id,
|
||||
reservable_type: Training.name,
|
||||
slots_attributes: slots_params,
|
||||
slots_reservations_attributes: slots_params,
|
||||
statistic_profile_id: StatisticProfile.find_by(user: @customer).id
|
||||
)
|
||||
end
|
||||
|
@ -24,8 +24,8 @@ module ICalendarConcern
|
||||
cal.event do |e|
|
||||
e.dtstart = start_time
|
||||
e.dtend = group_slots.last[:end_at]
|
||||
e.summary = I18n.t('reservation_ics.summary', TYPE: I18n.t("reservation_ics.type.#{reservable.class.name}"))
|
||||
e.description = I18n.t('reservation_ics.description', COUNT: group_slots.count, ITEM: reservable.name)
|
||||
e.summary = title
|
||||
e.description = description(group_slots)
|
||||
e.ip_class = 'PRIVATE'
|
||||
|
||||
e.alarm do |a|
|
||||
@ -38,5 +38,33 @@ module ICalendarConcern
|
||||
end
|
||||
cal
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def title
|
||||
case reservable_type
|
||||
when 'Machine', 'Training', 'Space'
|
||||
reservable.name
|
||||
when 'Event'
|
||||
reservable.title
|
||||
else
|
||||
Rails.logger.warn "Unexpected reservable type #{reservable_type}"
|
||||
reservable_type
|
||||
end
|
||||
end
|
||||
|
||||
def description(group_slots)
|
||||
case reservable_type
|
||||
when 'Machine', 'Space'
|
||||
I18n.t('reservation_ics.description_slot', COUNT: group_slots.count, ITEM: reservable.name)
|
||||
when 'Training'
|
||||
I18n.t('reservation_ics.description_training', TYPE: reservable.name)
|
||||
when 'Event'
|
||||
I18n.t('reservation_ics.description_event', NUMBER: nb_reserve_places + (tickets.map(&:booked).reduce(:+) || 0))
|
||||
else
|
||||
Rails.logger.warn "Unexpected reservable type #{reservable_type}"
|
||||
I18n.t('reservation_ics.description_slot', COUNT: group_slots.count, ITEM: reservable_type)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -40,37 +40,40 @@ module SingleSignOnConcern
|
||||
## @param sso_mapping {String} must be of form 'user._field_' or 'profile._field_'. Eg. 'user.email'
|
||||
## @param data {*} the data to put in the given key. Eg. 'user@example.com'
|
||||
def set_data_from_sso_mapping(sso_mapping, data)
|
||||
return if data.nil? || data.blank?
|
||||
|
||||
if sso_mapping.to_s.start_with? 'user.'
|
||||
self[sso_mapping[5..-1].to_sym] = data unless data.nil? || data.blank?
|
||||
self[sso_mapping[5..-1].to_sym] = data
|
||||
elsif sso_mapping.to_s.start_with? 'profile.'
|
||||
case sso_mapping.to_s
|
||||
when 'profile.avatar'
|
||||
profile.user_avatar ||= UserAvatar.new
|
||||
profile.user_avatar.remote_attachment_url = data
|
||||
when 'profile.address'
|
||||
invoicing_profile ||= InvoicingProfile.new
|
||||
invoicing_profile.address ||= Address.new
|
||||
invoicing_profile.address.address = data
|
||||
self.invoicing_profile ||= InvoicingProfile.new
|
||||
self.invoicing_profile.address ||= Address.new
|
||||
self.invoicing_profile.address.address = data
|
||||
when 'profile.organization_name'
|
||||
invoicing_profile ||= InvoicingProfile.new
|
||||
invoicing_profile.organization ||= Organization.new
|
||||
invoicing_profile.organization.name = data
|
||||
self.invoicing_profile ||= InvoicingProfile.new
|
||||
self.invoicing_profile.organization ||= Organization.new
|
||||
self.invoicing_profile.organization.name = data
|
||||
when 'profile.organization_address'
|
||||
invoicing_profile ||= InvoicingProfile.new
|
||||
invoicing_profile.organization ||= Organization.new
|
||||
invoicing_profile.organization.address ||= Address.new
|
||||
invoicing_profile.organization.address.address = data
|
||||
self.invoicing_profile ||= InvoicingProfile.new
|
||||
self.invoicing_profile.organization ||= Organization.new
|
||||
self.invoicing_profile.organization.address ||= Address.new
|
||||
self.invoicing_profile.organization.address.address = data
|
||||
when 'profile.gender'
|
||||
statistic_profile ||= StatisticProfile.new
|
||||
statistic_profile.gender = data
|
||||
self.statistic_profile ||= StatisticProfile.new
|
||||
self.statistic_profile.gender = data
|
||||
when 'profile.birthday'
|
||||
statistic_profile ||= StatisticProfile.new
|
||||
statistic_profile.birthday = data
|
||||
self.statistic_profile ||= StatisticProfile.new
|
||||
self.statistic_profile.birthday = data
|
||||
else
|
||||
profile[sso_mapping[8..-1].to_sym] = data unless data.nil?
|
||||
profile[sso_mapping[8..-1].to_sym] = data
|
||||
end
|
||||
end
|
||||
return if data.nil? || data.blank? || mapped_from_sso&.include?(sso_mapping)
|
||||
|
||||
return if mapped_from_sso&.include?(sso_mapping)
|
||||
|
||||
self.mapped_from_sso = [mapped_from_sso, sso_mapping].compact.join(',')
|
||||
end
|
||||
@ -121,7 +124,7 @@ module SingleSignOnConcern
|
||||
logger.debug "mapping sso field #{field} with value=#{value}"
|
||||
# we do not merge the email field if its end with the special value '-duplicate' as this means
|
||||
# that the user is currently merging with the account that have the same email than the sso
|
||||
set_data_from_sso_mapping(field, value) unless field == 'user.email' && value.end_with?('-duplicate')
|
||||
set_data_from_sso_mapping(field, value) unless (field == 'user.email' && value.end_with?('-duplicate')) || (field == 'user.group_id' && user.admin?)
|
||||
end
|
||||
|
||||
# run the account transfer in an SQL transaction to ensure data integrity
|
||||
|
@ -25,8 +25,8 @@ class Event < ApplicationRecord
|
||||
|
||||
attr_accessor :recurrence, :recurrence_end_at
|
||||
|
||||
after_create :event_recurrence
|
||||
before_save :update_nb_free_places
|
||||
after_create :event_recurrence
|
||||
# update event updated_at for index cache
|
||||
after_save -> { touch }
|
||||
|
||||
@ -79,7 +79,10 @@ class Event < ApplicationRecord
|
||||
if nb_total_places.nil?
|
||||
self.nb_free_places = nil
|
||||
else
|
||||
reserved_places = reservations.joins(:slots).where('slots.canceled_at': nil).map(&:total_booked_seats).inject(0) { |sum, t| sum + t }
|
||||
reserved_places = reservations.joins(:slots_reservations)
|
||||
.where('slots_reservations.canceled_at': nil)
|
||||
.map(&:total_booked_seats)
|
||||
.inject(0) { |sum, t| sum + t }
|
||||
self.nb_free_places = (nb_total_places - reserved_places)
|
||||
end
|
||||
end
|
||||
@ -106,6 +109,7 @@ class Event < ApplicationRecord
|
||||
nil
|
||||
end
|
||||
r = Recurrence.new(every: recurrence, on: on, starts: availability.start_at + 1.day, until: recurrence_end_at)
|
||||
service = Availabilities::CreateAvailabilitiesService.new
|
||||
r.events.each do |date|
|
||||
days_diff = availability.end_at.day - availability.start_at.day
|
||||
start_at = DateTime.new(
|
||||
@ -154,6 +158,7 @@ class Event < ApplicationRecord
|
||||
recurrence_id: id
|
||||
)
|
||||
event.save
|
||||
service.create_slots(event.availability)
|
||||
end
|
||||
update_columns(recurrence_id: id)
|
||||
end
|
||||
|
@ -129,6 +129,11 @@ class Invoice < PaymentDocument
|
||||
def prevent_refund?
|
||||
return true if user.nil?
|
||||
|
||||
if main_item.nil?
|
||||
Rails.logger.error "Invoice (id: #{id}) does not have a main_item and is probably in error"
|
||||
return true
|
||||
end
|
||||
|
||||
if main_item.object_type == 'Reservation' && main_item.object&.reservable_type == 'Training'
|
||||
user.trainings.include?(main_item.object.reservable_id)
|
||||
else
|
||||
@ -175,7 +180,8 @@ class Invoice < PaymentDocument
|
||||
return unless Setting.get('invoicing_module')
|
||||
|
||||
unless Rails.env.test?
|
||||
puts "Creating an InvoiceWorker job to generate the following invoice: id(#{id}), main_item.object_id(#{main_item.object_id}), " \
|
||||
Rails.logger.info "Creating an InvoiceWorker job to generate the following invoice: id(#{id}), " \
|
||||
"main_item.object_id(#{main_item.object_id}), " \
|
||||
"main_item.object_type(#{main_item.object_type}), user_id(#{invoicing_profile.user_id})"
|
||||
end
|
||||
InvoiceWorker.perform_async(id, user&.subscription&.expired_at)
|
||||
@ -185,9 +191,7 @@ class Invoice < PaymentDocument
|
||||
return if Rails.env.test?
|
||||
return unless changed?
|
||||
|
||||
puts "WARNING: Invoice update triggered [ id: #{id}, reference: #{reference} ]"
|
||||
puts '---------- changes ----------'
|
||||
puts changes
|
||||
puts '---------------------------------'
|
||||
Rails.logger.warn "Invoice update triggered [ id: #{id}, reference: #{reference} ]\n" \
|
||||
"---------- changes ----------#{changes}\n---------------------------------"
|
||||
end
|
||||
end
|
||||
|
@ -50,9 +50,7 @@ class InvoiceItem < Footprintable
|
||||
return if Rails.env.test?
|
||||
return unless changed?
|
||||
|
||||
puts "WARNING: InvoiceItem update triggered [ id: #{id}, invoice reference: #{invoice.reference} ]"
|
||||
puts '---------- changes ----------'
|
||||
puts changes
|
||||
puts '---------------------------------'
|
||||
Rails.logger.warn "InvoiceItem update triggered [ id: #{id}, invoice reference: #{invoice.reference} ]\n" \
|
||||
"---------- changes ----------\n#{changes}\n---------------------------------"
|
||||
end
|
||||
end
|
||||
|
@ -100,7 +100,8 @@ class PaymentSchedule < PaymentDocument
|
||||
return unless Setting.get('invoicing_module')
|
||||
|
||||
unless Rails.env.test?
|
||||
puts "Creating an PaymentScheduleWorker job to generate the following payment schedule: id(#{id}), main_object.object_id(#{main_object.object_id}), " \
|
||||
Rails.logger.info "Creating an PaymentScheduleWorker job to generate the following payment schedule: id(#{id}), " \
|
||||
"main_object.object_id(#{main_object.object_id}), " \
|
||||
"main_object.object_type(#{main_object.object_type}), user_id(#{invoicing_profile.user_id})"
|
||||
end
|
||||
PaymentScheduleWorker.perform_async(id)
|
||||
|
@ -122,7 +122,7 @@ class Plan < ApplicationRecord
|
||||
if !stat_type.nil? && !stat_subtype.nil?
|
||||
StatisticTypeSubType.create!(statistic_type: stat_type, statistic_sub_type: stat_subtype)
|
||||
else
|
||||
puts 'ERROR: Unable to create the statistics association for the new plan. ' \
|
||||
Rails.logger.error 'Unable to create the statistics association for the new plan. ' \
|
||||
'Possible causes: the type or the subtype were not created successfully.'
|
||||
end
|
||||
end
|
||||
|
@ -12,7 +12,7 @@ class Reservation < ApplicationRecord
|
||||
has_many :slots_reservations, dependent: :destroy
|
||||
has_many :slots, through: :slots_reservations
|
||||
|
||||
accepts_nested_attributes_for :slots, allow_destroy: true
|
||||
accepts_nested_attributes_for :slots_reservations, allow_destroy: true
|
||||
belongs_to :reservable, polymorphic: true
|
||||
|
||||
has_many :tickets
|
||||
@ -35,13 +35,18 @@ class Reservation < ApplicationRecord
|
||||
# @param canceled if true, count the number of seats for this reservation, including canceled seats
|
||||
def total_booked_seats(canceled: false)
|
||||
# cases:
|
||||
# - machine/training/space reservation => 1 slot = 1 seat (currently not covered by this function)
|
||||
if reservable_type == 'Event'
|
||||
# - event reservation => seats = nb_reserve_place (normal price) + tickets.booked (other prices)
|
||||
return 0 if slots.first.canceled_at && !canceled
|
||||
|
||||
total = nb_reserve_places
|
||||
total += tickets.map(&:booked).map(&:to_i).reduce(:+) if tickets.count.positive?
|
||||
|
||||
total = 0 unless slots_reservations.first&.canceled_at.nil?
|
||||
else
|
||||
# - machine/training/space reservation => 1 slot_reservation = 1 seat
|
||||
total = slots_reservations.count
|
||||
total -= slots_reservations.where.not(canceled_at: nil).count unless canceled
|
||||
end
|
||||
|
||||
total
|
||||
end
|
||||
|
||||
@ -93,26 +98,22 @@ class Reservation < ApplicationRecord
|
||||
private
|
||||
|
||||
def machine_not_already_reserved
|
||||
already_reserved = false
|
||||
slots.each do |slot|
|
||||
same_hour_slots = Slot.joins(:reservations).where(
|
||||
slots_reservations.each do |slot|
|
||||
same_hour_slots = SlotsReservation.joins(:reservation).where(
|
||||
reservations: { reservable_type: reservable_type, reservable_id: reservable_id },
|
||||
start_at: slot.start_at,
|
||||
end_at: slot.end_at,
|
||||
availability_id: slot.availability_id,
|
||||
slot_id: slot.slot_id,
|
||||
canceled_at: nil
|
||||
)
|
||||
if same_hour_slots.any?
|
||||
already_reserved = true
|
||||
).count
|
||||
if same_hour_slots.positive?
|
||||
errors.add(:reservable, 'already reserved')
|
||||
break
|
||||
end
|
||||
end
|
||||
errors.add(:machine, 'already reserved') if already_reserved
|
||||
end
|
||||
|
||||
def training_not_fully_reserved
|
||||
slot = slots.first
|
||||
errors.add(:training, 'already fully reserved') if Availability.find(slot.availability_id).full?
|
||||
full = slots_reservations.map(&:slot).map(&:full?).reduce(:&)
|
||||
errors.add(:reservable, 'already fully reserved') if full
|
||||
end
|
||||
|
||||
def slots_not_locked
|
||||
|
@ -147,7 +147,8 @@ class Setting < ApplicationRecord
|
||||
machines_module
|
||||
user_change_group
|
||||
user_validation_required
|
||||
user_validation_required_list] }
|
||||
user_validation_required_list
|
||||
show_username_in_admin_list] }
|
||||
# WARNING: when adding a new key, you may also want to add it in:
|
||||
# - config/locales/en.yml#settings
|
||||
# - app/frontend/src/javascript/models/setting.ts#SettingName
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
# Stores data about a shopping data
|
||||
class ShoppingCart
|
||||
attr_accessor :customer, :operator, :payment_method, :items, :coupon, :payment_schedule
|
||||
attr_accessor :customer, :operator, :payment_method, :items, :coupon, :payment_schedule, :errors
|
||||
|
||||
# @param items {Array<CartItem::BaseItem>}
|
||||
# @param coupon {CartItem::Coupon}
|
||||
@ -18,6 +18,7 @@ class ShoppingCart
|
||||
@items = items
|
||||
@coupon = coupon
|
||||
@payment_schedule = payment_schedule
|
||||
@errors = {}
|
||||
end
|
||||
|
||||
# compute the price details of the current shopping cart
|
||||
@ -55,13 +56,9 @@ class ShoppingCart
|
||||
list = user_validation_required_list.split(',')
|
||||
errors = []
|
||||
items.each do |item|
|
||||
if list.include?(item.type) && !@customer.validated_at?
|
||||
errors.push("User validation is required for reserve #{item.type}")
|
||||
end
|
||||
end
|
||||
unless errors.empty?
|
||||
return { success: nil, payment: nil, errors: errors }
|
||||
errors.push("User validation is required to reserve #{item.type}") if list.include?(item.type) && !@customer.validated_at?
|
||||
end
|
||||
return { success: nil, payment: nil, errors: errors } unless errors.empty?
|
||||
end
|
||||
end
|
||||
|
||||
@ -91,11 +88,18 @@ class ShoppingCart
|
||||
items.each do |item|
|
||||
next if item.valid?(@items)
|
||||
|
||||
@errors = item.errors
|
||||
return false
|
||||
end
|
||||
unless @coupon.valid?(items)
|
||||
@errors = @coupon.errors
|
||||
return false
|
||||
end
|
||||
return false unless @coupon.valid?([])
|
||||
|
||||
return false unless @payment_schedule.valid?([])
|
||||
unless @payment_schedule.valid?(items)
|
||||
@errors = @payment_schedule.errors
|
||||
return false
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
|
@ -1,9 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Time range, slicing an Availability.
|
||||
# Its duration is defined by globally by Setting.get('slot_duration') but can be overridden per availability
|
||||
# During a slot a Reservation is possible
|
||||
# Only reserved slots are persisted in DB, others are instantiated on the fly
|
||||
# A Time range, slicing an Availability.
|
||||
# Slots duration are defined globally by Setting.get('slot_duration') but can be
|
||||
# overridden per availability.
|
||||
class Slot < ApplicationRecord
|
||||
include NotifyWith::NotificationAttachedObject
|
||||
|
||||
@ -11,64 +10,20 @@ class Slot < ApplicationRecord
|
||||
has_many :reservations, through: :slots_reservations
|
||||
belongs_to :availability
|
||||
|
||||
attr_accessor :is_reserved, :machine, :space, :title, :can_modify, :is_reserved_by_current_user
|
||||
attr_accessor :is_reserved, :machine, :space, :title, :can_modify, :current_user_slots_reservations_ids
|
||||
|
||||
after_update :set_ex_start_end_dates_attrs, if: :dates_were_modified?
|
||||
after_update :notify_member_and_admin_slot_is_modified, if: :dates_were_modified?
|
||||
def full?(reservable = nil)
|
||||
availability_places = availability.available_places_per_slot(reservable)
|
||||
return false if availability_places.nil?
|
||||
|
||||
after_update :notify_member_and_admin_slot_is_canceled, if: :canceled?
|
||||
after_update :update_event_nb_free_places, if: :canceled?
|
||||
|
||||
# for backward compatibility
|
||||
def reservation
|
||||
reservations.first
|
||||
end
|
||||
|
||||
def destroy
|
||||
update_column(:destroying, true)
|
||||
super
|
||||
end
|
||||
|
||||
def complete?
|
||||
reservations.length >= availability.nb_total_places
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def notify_member_and_admin_slot_is_modified
|
||||
NotificationCenter.call type: 'notify_member_slot_is_modified',
|
||||
receiver: reservation.user,
|
||||
attached_object: self
|
||||
NotificationCenter.call type: 'notify_admin_slot_is_modified',
|
||||
receiver: User.admins_and_managers,
|
||||
attached_object: self
|
||||
end
|
||||
|
||||
def notify_member_and_admin_slot_is_canceled
|
||||
NotificationCenter.call type: 'notify_member_slot_is_canceled',
|
||||
receiver: reservation.user,
|
||||
attached_object: self
|
||||
NotificationCenter.call type: 'notify_admin_slot_is_canceled',
|
||||
receiver: User.admins_and_managers,
|
||||
attached_object: self
|
||||
end
|
||||
|
||||
def dates_were_modified?
|
||||
saved_change_to_start_at? || saved_change_to_end_at?
|
||||
end
|
||||
|
||||
def canceled?
|
||||
saved_change_to_canceled_at?
|
||||
end
|
||||
|
||||
def set_ex_start_end_dates_attrs
|
||||
update_columns(ex_start_at: start_at_before_last_save, ex_end_at: end_at_before_last_save)
|
||||
end
|
||||
|
||||
def update_event_nb_free_places
|
||||
return unless reservation.reservable_type == 'Event'
|
||||
raise NotImplementedError if reservations.count > 1
|
||||
|
||||
reservation.update_event_nb_free_places
|
||||
if reservable.nil?
|
||||
slots_reservations.where(canceled_at: nil).count >= availability_places
|
||||
else
|
||||
slots_reservations.includes(:reservation).where(canceled_at: nil).where('reservations.reservable': reservable).count >= availability_places
|
||||
end
|
||||
end
|
||||
|
||||
def duration
|
||||
(end_at - start_at).seconds
|
||||
end
|
||||
end
|
||||
|
@ -1,16 +1,56 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# SlotsReservation is the relation table between a Slot and a Reservation.
|
||||
# It holds detailed data about a Reservation for the attached Slot.
|
||||
class SlotsReservation < ApplicationRecord
|
||||
belongs_to :slot
|
||||
belongs_to :reservation
|
||||
after_destroy :cleanup_slots
|
||||
|
||||
# when the SlotsReservation is deleted (from Reservation destroy cascade), we delete the
|
||||
# corresponding slot
|
||||
def cleanup_slots
|
||||
return unless slot.destroying
|
||||
after_update :set_ex_start_end_dates_attrs, if: :slot_changed?
|
||||
after_update :notify_member_and_admin_slot_is_modified, if: :slot_changed?
|
||||
|
||||
slot.destroy
|
||||
after_update :notify_member_and_admin_slot_is_canceled, if: :canceled?
|
||||
after_update :update_event_nb_free_places, if: :canceled?
|
||||
|
||||
def set_ex_start_end_dates_attrs
|
||||
update_columns(ex_start_at: previous_slot.start_at, ex_end_at: previous_slot.end_at)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def slot_changed?
|
||||
saved_change_to_slot_id?
|
||||
end
|
||||
|
||||
def previous_slot
|
||||
Slot.find(slot_id_before_last_save)
|
||||
end
|
||||
|
||||
def canceled?
|
||||
saved_change_to_canceled_at?
|
||||
end
|
||||
|
||||
def update_event_nb_free_places
|
||||
return unless reservation.reservable_type == 'Event'
|
||||
|
||||
reservation.update_event_nb_free_places
|
||||
end
|
||||
|
||||
def notify_member_and_admin_slot_is_modified
|
||||
NotificationCenter.call type: 'notify_member_slot_is_modified',
|
||||
receiver: reservation.user,
|
||||
attached_object: self
|
||||
NotificationCenter.call type: 'notify_admin_slot_is_modified',
|
||||
receiver: User.admins_and_managers,
|
||||
attached_object: self
|
||||
end
|
||||
|
||||
def notify_member_and_admin_slot_is_canceled
|
||||
NotificationCenter.call type: 'notify_member_slot_is_canceled',
|
||||
receiver: reservation.user,
|
||||
attached_object: self
|
||||
NotificationCenter.call type: 'notify_admin_slot_is_canceled',
|
||||
receiver: User.admins_and_managers,
|
||||
attached_object: self
|
||||
end
|
||||
end
|
||||
|
@ -3,5 +3,5 @@
|
||||
# UserAvatar is the profile picture for an User
|
||||
class UserAvatar < Asset
|
||||
include ImageValidatorConcern
|
||||
mount_uploader :attachment, ProfilImageUploader
|
||||
mount_uploader :attachment, UserAvatarUploader
|
||||
end
|
||||
|
@ -29,7 +29,7 @@ class PDF::Invoice < Prawn::Document
|
||||
begin
|
||||
image StringIO.new(Base64.decode64(img_b64)), fit: [415, 40]
|
||||
rescue StandardError => e
|
||||
puts "Unable to decode invoice logo from base64: #{e}"
|
||||
Rails.logger.error "Unable to decode invoice logo from base64: #{e}"
|
||||
end
|
||||
move_down 20
|
||||
# the following line is a special comment to workaround RubyMine inspection problem
|
||||
@ -120,7 +120,7 @@ class PDF::Invoice < Prawn::Document
|
||||
when 'StatisticProfilePrepaidPack'
|
||||
object = I18n.t('invoices.prepaid_pack')
|
||||
else
|
||||
puts "ERROR : specified main_item.object_type type (#{invoice.main_item.object_type}) is unknown"
|
||||
Rails.logger.error "specified main_item.object_type type (#{invoice.main_item.object_type}) is unknown"
|
||||
end
|
||||
end
|
||||
text I18n.t('invoices.object') + ' ' + object
|
||||
@ -233,12 +233,14 @@ class PDF::Invoice < Prawn::Document
|
||||
|
||||
# total verification
|
||||
total = invoice.total / 100.00
|
||||
puts "ERROR: totals are NOT equals => expected: #{total}, computed: #{total_calc}" if total_calc != total
|
||||
Rails.logger.error "totals are NOT equals => expected: #{total}, computed: #{total_calc}" if total_calc != total
|
||||
|
||||
# TVA
|
||||
vat_service = VatHistoryService.new
|
||||
vat_rate_group = vat_service.invoice_vat(invoice)
|
||||
if total_vat != 0
|
||||
if total_vat.zero?
|
||||
data += [[I18n.t('invoices.total_amount'), number_to_currency(total)]]
|
||||
else
|
||||
data += [[I18n.t('invoices.total_including_all_taxes'), number_to_currency(total)]]
|
||||
vat_rate_group.each do |_type, rate|
|
||||
data += [[I18n.t('invoices.including_VAT_RATE', RATE: rate[:vat_rate], AMOUNT: number_to_currency(rate[:amount] / 100.00)), number_to_currency(rate[:total_vat] / 100.00)]]
|
||||
@ -247,13 +249,11 @@ class PDF::Invoice < Prawn::Document
|
||||
data += [[I18n.t('invoices.including_amount_payed_on_ordering'), number_to_currency(total)]]
|
||||
|
||||
# checking the round number
|
||||
rounded = sprintf('%.2f', total_vat / 100.00).to_f + sprintf('%.2f', total_ht / 100.00).to_f
|
||||
if rounded != sprintf('%.2f', total_calc).to_f
|
||||
puts 'ERROR: rounding the numbers cause an invoice inconsistency. ' \
|
||||
rounded = (sprintf('%.2f', total_vat / 100.00).to_f + sprintf('%.2f', total_ht / 100.00).to_f).to_s
|
||||
if rounded != sprintf('%.2f', total_calc)
|
||||
Rails.logger.error 'rounding the numbers cause an invoice inconsistency. ' \
|
||||
"Total expected: #{sprintf('%.2f', total_calc)}, total computed: #{rounded}"
|
||||
end
|
||||
else
|
||||
data += [[I18n.t('invoices.total_amount'), number_to_currency(total)]]
|
||||
end
|
||||
|
||||
# display table
|
||||
@ -305,7 +305,7 @@ class PDF::Invoice < Prawn::Document
|
||||
when 'none'
|
||||
payment_verbose = I18n.t('invoices.no_refund')
|
||||
else
|
||||
puts "ERROR : specified refunding method (#{payment_verbose}) is unknown"
|
||||
Rails.logger.error "specified refunding method (#{payment_verbose}) is unknown"
|
||||
end
|
||||
payment_verbose += ' ' + I18n.t('invoices.for_an_amount_of_AMOUNT', AMOUNT: number_to_currency(total))
|
||||
else
|
||||
|
@ -32,7 +32,7 @@ class PDF::PaymentSchedule < Prawn::Document
|
||||
begin
|
||||
image StringIO.new(Base64.decode64(img_b64)), fit: [415, 40]
|
||||
rescue StandardError => e
|
||||
puts "Unable to decode invoice logo from base64: #{e}"
|
||||
Rails.logger.error "Unable to decode invoice logo from base64: #{e}"
|
||||
end
|
||||
move_down 20
|
||||
font('Open-Sans', size: 10) do
|
||||
|
@ -1,7 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Check the access policies for API::SlotsController
|
||||
class SlotPolicy < ApplicationPolicy
|
||||
# Check the access policies for API::SlotsReservationsController
|
||||
class SlotsReservationPolicy < ApplicationPolicy
|
||||
def update?
|
||||
# check that the update is allowed and the prevention delay has not expired
|
||||
delay = Setting.get('booking_move_delay').to_i
|
||||
@ -9,7 +9,7 @@ class SlotPolicy < ApplicationPolicy
|
||||
|
||||
# these condition does not apply to admins
|
||||
user.admin? || user.manager? ||
|
||||
(record.reservation.user == user && enabled && ((record.start_at - DateTime.current).to_i / 3600 >= delay))
|
||||
(record.reservation.user == user && enabled && ((record.slot.start_at - DateTime.current).to_i / 3600 >= delay))
|
||||
end
|
||||
|
||||
def cancel?
|
@ -32,7 +32,7 @@ class AccountingExportService
|
||||
invoices = Invoice.where('created_at >= ? AND created_at <= ?', start_date, end_date).order('created_at ASC')
|
||||
invoices = invoices.where('total > 0') unless export_zeros
|
||||
invoices.each do |i|
|
||||
puts "processing invoice #{i.id}..." unless Rails.env.test?
|
||||
Rails.logger.debug { "processing invoice #{i.id}..." } unless Rails.env.test?
|
||||
content << generate_rows(i)
|
||||
end
|
||||
|
||||
@ -62,20 +62,25 @@ class AccountingExportService
|
||||
# Generate the "subscription" and "reservation" rows associated with the provided invoice
|
||||
def items_rows(invoice)
|
||||
rows = invoice.subscription_invoice? ? "#{subscription_row(invoice)}\n" : ''
|
||||
if invoice.main_item.object_type == 'Reservation'
|
||||
case invoice.main_item.object_type
|
||||
when 'Reservation'
|
||||
items = invoice.invoice_items.reject { |ii| ii.object_type == 'Subscription' }
|
||||
items.each do |item|
|
||||
rows << "#{reservation_row(invoice, item)}\n"
|
||||
end
|
||||
elsif invoice.main_item.object_type == 'WalletTransaction'
|
||||
when 'WalletTransaction'
|
||||
rows << "#{wallet_row(invoice)}\n"
|
||||
elsif invoice.main_item.object_type == 'StatisticProfilePrepaidPack'
|
||||
when 'StatisticProfilePrepaidPack'
|
||||
rows << "#{pack_row(invoice)}\n"
|
||||
elsif invoice.main_item.object_type == 'Error'
|
||||
when 'Error'
|
||||
items = invoice.invoice_items.reject { |ii| ii.object_type == 'Subscription' }
|
||||
items.each do |item|
|
||||
rows << "#{error_row(invoice, item)}\n"
|
||||
end
|
||||
when 'Subscription'
|
||||
# do nothing, subscription was already handled by subscription_row
|
||||
else
|
||||
Rails.logger.warn { "Unknown main object type #{invoice.main_item.object_type}" }
|
||||
end
|
||||
rows
|
||||
end
|
||||
@ -185,18 +190,14 @@ class AccountingExportService
|
||||
row << invoice.reference
|
||||
when 'line_label'
|
||||
row << line_label
|
||||
when 'debit_origin'
|
||||
when 'debit_origin', 'debit_euro'
|
||||
row << method(debit_method).call(invoice, amount)
|
||||
when 'credit_origin'
|
||||
row << method(credit_method).call(invoice, amount)
|
||||
when 'debit_euro'
|
||||
row << method(debit_method).call(invoice, amount)
|
||||
when 'credit_euro'
|
||||
when 'credit_origin', 'credit_euro'
|
||||
row << method(credit_method).call(invoice, amount)
|
||||
when 'lettering'
|
||||
row << ''
|
||||
else
|
||||
puts "Unsupported column: #{column}"
|
||||
Rails.logger.debug { "Unsupported column: #{column}" }
|
||||
end
|
||||
row << separator
|
||||
end
|
||||
@ -214,30 +215,30 @@ class AccountingExportService
|
||||
if invoice.subscription_invoice?
|
||||
Setting.get("accounting_subscription_#{type}")
|
||||
else
|
||||
puts "WARN: Invoice #{invoice.id} has no subscription"
|
||||
Rails.logger.debug { "WARN: Invoice #{invoice.id} has no subscription" }
|
||||
end
|
||||
when :reservation
|
||||
if invoice.main_item.object_type == 'Reservation'
|
||||
Setting.get("accounting_#{invoice.main_item.object.reservable_type}_#{type}")
|
||||
else
|
||||
puts "WARN: Invoice #{invoice.id} has no reservation"
|
||||
Rails.logger.debug { "WARN: Invoice #{invoice.id} has no reservation" }
|
||||
end
|
||||
when :wallet
|
||||
if invoice.main_item.object_type == 'WalletTransaction'
|
||||
Setting.get("accounting_wallet_#{type}")
|
||||
else
|
||||
puts "WARN: Invoice #{invoice.id} is not a wallet credit"
|
||||
Rails.logger.debug { "WARN: Invoice #{invoice.id} is not a wallet credit" }
|
||||
end
|
||||
when :pack
|
||||
if invoice.main_item.object_type == 'StatisticProfilePrepaidPack'
|
||||
Setting.get("accounting_Pack_#{type}")
|
||||
else
|
||||
puts "WARN: Invoice #{invoice.id} has no prepaid-pack"
|
||||
Rails.logger.debug { "WARN: Invoice #{invoice.id} has no prepaid-pack" }
|
||||
end
|
||||
when :error
|
||||
Setting.get("accounting_Error_#{type}")
|
||||
else
|
||||
puts "Unsupported account #{account}"
|
||||
Rails.logger.debug { "Unsupported account #{account}" }
|
||||
end || ''
|
||||
end
|
||||
|
||||
|
@ -1,142 +1,104 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Provides helper methods for Availability resources and properties
|
||||
# List all Availability's slots for the given resources
|
||||
class Availabilities::AvailabilitiesService
|
||||
|
||||
def initialize(current_user, maximum_visibility = {})
|
||||
def initialize(current_user, level = 'slot')
|
||||
@current_user = current_user
|
||||
@maximum_visibility = maximum_visibility
|
||||
@service = Availabilities::StatusService.new(current_user.role)
|
||||
@maximum_visibility = {
|
||||
year: Setting.get('visibility_yearly').to_i.months.since,
|
||||
other: Setting.get('visibility_others').to_i.months.since
|
||||
}
|
||||
@service = Availabilities::StatusService.new(current_user&.role)
|
||||
@level = level
|
||||
end
|
||||
|
||||
# list all slots for the given machine, with reservations info, relatives to the given user
|
||||
def machines(machine_id, user)
|
||||
machine = Machine.friendly.find(machine_id)
|
||||
reservations = reservations(machine, user)
|
||||
availabilities = availabilities(machine, 'machines', user)
|
||||
# list all slots for the given machines, with visibility relative to the given user
|
||||
def machines(machines, user, window)
|
||||
ma_availabilities = Availability.includes('machines_availabilities')
|
||||
.where('machines_availabilities.machine_id': machines.map(&:id))
|
||||
availabilities = availabilities(ma_availabilities, 'machines', user, window[:start], window[:end])
|
||||
|
||||
slots = []
|
||||
availabilities.each do |a|
|
||||
slot_duration = a.slot_duration || Setting.get('slot_duration').to_i
|
||||
((a.end_at - a.start_at) / slot_duration.minutes).to_i.times do |i|
|
||||
next unless (a.start_at + (i * slot_duration).minutes) > DateTime.current || user.admin?
|
||||
|
||||
slot = Slot.new(
|
||||
start_at: a.start_at + (i * slot_duration).minutes,
|
||||
end_at: a.start_at + (i * slot_duration).minutes + slot_duration.minutes,
|
||||
availability_id: a.id,
|
||||
availability: a,
|
||||
machine: machine,
|
||||
title: ''
|
||||
)
|
||||
slot = @service.machine_reserved_status(slot, reservations, @current_user)
|
||||
slots << slot
|
||||
if @level == 'slot'
|
||||
availabilities.map(&:slots).flatten.map { |s| @service.slot_reserved_status(s, user, (machines & s.availability.machines)) }
|
||||
else
|
||||
availabilities.map { |a| @service.availability_reserved_status(a, user, (machines & a.machines)) }
|
||||
end
|
||||
end
|
||||
slots
|
||||
end
|
||||
|
||||
# list all slots for the given space, with reservations info, relatives to the given user
|
||||
def spaces(space_id, user)
|
||||
space = Space.friendly.find(space_id)
|
||||
reservations = reservations(space, user)
|
||||
availabilities = availabilities(space, 'space', user)
|
||||
# list all slots for the given space, with visibility relative to the given user
|
||||
def spaces(spaces, user, window)
|
||||
sp_availabilities = Availability.includes('spaces_availabilities')
|
||||
.where('spaces_availabilities.space_id': spaces.map(&:id))
|
||||
availabilities = availabilities(sp_availabilities, 'space', user, window[:start], window[:end])
|
||||
|
||||
slots = []
|
||||
availabilities.each do |a|
|
||||
slot_duration = a.slot_duration || Setting.get('slot_duration').to_i
|
||||
((a.end_at - a.start_at) / slot_duration.minutes).to_i.times do |i|
|
||||
next unless (a.start_at + (i * slot_duration).minutes) > DateTime.current || user.admin?
|
||||
|
||||
slot = Slot.new(
|
||||
start_at: a.start_at + (i * slot_duration).minutes,
|
||||
end_at: a.start_at + (i * slot_duration).minutes + slot_duration.minutes,
|
||||
availability_id: a.id,
|
||||
availability: a,
|
||||
space: space,
|
||||
title: ''
|
||||
)
|
||||
slot = @service.space_reserved_status(slot, reservations, user)
|
||||
slots << slot
|
||||
if @level == 'slot'
|
||||
availabilities.map(&:slots).flatten.map { |s| @service.slot_reserved_status(s, user, (spaces & s.availability.spaces)) }
|
||||
else
|
||||
availabilities.map { |a| @service.availability_reserved_status(a, user, (spaces & a.spaces)) }
|
||||
end
|
||||
end
|
||||
slots.each do |s|
|
||||
s.title = I18n.t('availabilities.not_available') if s.complete? && !s.is_reserved
|
||||
|
||||
# list all slots for the given training(s), with visibility relative to the given user
|
||||
def trainings(trainings, user, window)
|
||||
tr_availabilities = Availability.includes('trainings_availabilities')
|
||||
.where('trainings_availabilities.training_id': trainings.map(&:id))
|
||||
availabilities = availabilities(tr_availabilities, 'training', user, window[:start], window[:end])
|
||||
|
||||
if @level == 'slot'
|
||||
availabilities.map(&:slots).flatten.map { |s| @service.slot_reserved_status(s, user, (trainings & s.availability.trainings)) }
|
||||
else
|
||||
availabilities.map { |a| @service.availability_reserved_status(a, user, (trainings & a.trainings)) }
|
||||
end
|
||||
slots
|
||||
end
|
||||
|
||||
# list all slots for the given training, with reservations info, relatives to the given user
|
||||
def trainings(training_id, user)
|
||||
# first, we get the already-made reservations
|
||||
reservations = user.reservations.where("reservable_type = 'Training'")
|
||||
reservations = reservations.where('reservable_id = :id', id: training_id.to_i) if training_id.is_number?
|
||||
reservations = reservations.joins(:slots).where('slots.start_at > ?', @current_user.admin? ? 1.month.ago : DateTime.current)
|
||||
# list all slots for the given event(s), with visibility relative to the given user
|
||||
def events(events, user, window)
|
||||
ev_availabilities = Availability.includes('event').where('events.id': events.map(&:id))
|
||||
availabilities = availabilities(ev_availabilities, 'event', user, window[:start], window[:end])
|
||||
|
||||
# visible availabilities depends on multiple parameters
|
||||
availabilities = training_availabilities(training_id, user)
|
||||
|
||||
# finally, we merge the availabilities with the reservations
|
||||
availabilities.each do |a|
|
||||
a = @service.training_event_reserved_status(a, reservations, user)
|
||||
if @level == 'slot'
|
||||
availabilities.map(&:slots).flatten.map { |s| @service.slot_reserved_status(s, user, [s.availability.event]) }
|
||||
else
|
||||
availabilities.map { |a| @service.availability_reserved_status(a, user, [a.event]) }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def subscription_year?(user)
|
||||
user.subscription && user.subscription.plan.interval == 'year' && user.subscription.expired_at >= DateTime.current
|
||||
user&.subscription && user.subscription.plan.interval == 'year' && user.subscription.expired_at >= DateTime.current
|
||||
end
|
||||
|
||||
# member must have validated at least 1 training and must have a valid yearly subscription.
|
||||
def show_extended_slots?(user)
|
||||
user.trainings.size.positive? && subscription_year?(user)
|
||||
end
|
||||
|
||||
def reservations(reservable, user)
|
||||
Reservation.where('reservable_type = ? and reservable_id = ?', reservable.class.name, reservable.id)
|
||||
.includes(:slots, statistic_profile: [user: [:profile]])
|
||||
.references(:slots, :user)
|
||||
.where('slots.start_at > ?', user.admin? ? 1.month.ago : DateTime.current)
|
||||
end
|
||||
|
||||
def availabilities(reservable, type, user)
|
||||
if user.admin? || user.manager?
|
||||
reservable.availabilities
|
||||
.includes(:tags, :plans)
|
||||
.where('end_at > ? AND available_type = ?', 1.month.ago, type)
|
||||
.where(lock: false)
|
||||
else
|
||||
end_at = @maximum_visibility[:other]
|
||||
end_at = @maximum_visibility[:year] if subscription_year?(user)
|
||||
reservable.availabilities
|
||||
.includes(:tags, :plans)
|
||||
.where('end_at > ? AND end_at < ? AND available_type = ?', DateTime.current, end_at, type)
|
||||
.where('availability_tags.tag_id' => user.tag_ids.concat([nil]))
|
||||
.where(lock: false)
|
||||
end
|
||||
end
|
||||
|
||||
def training_availabilities(training_id, user)
|
||||
availabilities = if training_id.is_number? || (training_id.length.positive? && training_id != 'all')
|
||||
Training.friendly.find(training_id).availabilities
|
||||
else
|
||||
Availability.trainings
|
||||
# members must have validated at least 1 training and must have a valid yearly subscription to view
|
||||
# the trainings further in the futur. This is used to prevent users with a rolling subscription to take
|
||||
# their first training in a very long delay.
|
||||
def show_more_trainings?(user)
|
||||
user&.trainings&.size&.positive? && subscription_year?(user)
|
||||
end
|
||||
|
||||
def availabilities(availabilities, type, user, range_start, range_end)
|
||||
# who made the request?
|
||||
# 1) an admin (he can see all availabilities of 1 month ago and future)
|
||||
if @current_user.admin?
|
||||
availabilities.includes(:tags, :slots, :plans, trainings: [:machines])
|
||||
.where('availabilities.start_at > ?', 1.month.ago)
|
||||
# 1) an admin (he can see all availabilities from 1 month ago to anytime in the future)
|
||||
if @current_user&.admin? || @current_user&.manager?
|
||||
window_start = [range_start, 1.month.ago].max
|
||||
availabilities.includes(:tags, :plans, :slots)
|
||||
.joins(:slots)
|
||||
.where('availabilities.start_at <= ? AND availabilities.end_at >= ? AND available_type = ?', range_end, window_start, type)
|
||||
.where('slots.start_at > ? AND slots.end_at < ?', window_start, range_end)
|
||||
.where(lock: false)
|
||||
# 2) an user (he cannot see availabilities further than 1 (or 3) months)
|
||||
# 2) an user (he cannot see past availabilities neither those further than 1 (or 3) months in the future)
|
||||
else
|
||||
end_at = @maximum_visibility[:other]
|
||||
end_at = @maximum_visibility[:year] if show_extended_slots?(user)
|
||||
availabilities.includes(:tags, :slots, :availability_tags, :plans, trainings: [:machines])
|
||||
.where('availabilities.start_at > ? AND availabilities.start_at < ?', DateTime.current, end_at)
|
||||
.where('availability_tags.tag_id' => user.tag_ids.concat([nil]))
|
||||
end_at = @maximum_visibility[:year] if subscription_year?(user) && type != 'training'
|
||||
end_at = @maximum_visibility[:year] if show_more_trainings?(user) && type == 'training'
|
||||
window_end = [end_at, range_end].min
|
||||
window_start = [range_start, DateTime.current].max
|
||||
availabilities.includes(:tags, :plans, :slots)
|
||||
.joins(:slots)
|
||||
.where('availabilities.start_at <= ? AND availabilities.end_at >= ? AND available_type = ?', window_end, window_start, type)
|
||||
.where('slots.start_at > ? AND slots.end_at < ?', window_start, window_end)
|
||||
.where('availability_tags.tag_id' => user&.tag_ids&.concat([nil]))
|
||||
.where(lock: false)
|
||||
end
|
||||
end
|
||||
|
@ -1,16 +1,22 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Provides helper methods to create an Availability with multiple occurrences
|
||||
# Provides an helper method to create the slots for an Availability and optionnaly, for its multiple occurrences
|
||||
class Availabilities::CreateAvailabilitiesService
|
||||
def create(availability, occurrences = [])
|
||||
def create(availability, occurrences)
|
||||
occurrences = [] if occurrences.nil?
|
||||
|
||||
availability.update_attributes(occurrence_id: availability.id)
|
||||
create_slots(availability)
|
||||
|
||||
occurrences.each do |o|
|
||||
next if availability.start_at == o[:start_at] && availability.end_at == o[:end_at]
|
||||
start_at = Time.zone.parse(o[:start_at])
|
||||
end_at = Time.zone.parse(o[:end_at])
|
||||
|
||||
Availability.new(
|
||||
start_at: o[:start_at],
|
||||
end_at: o[:end_at],
|
||||
next if availability.start_at == start_at && availability.end_at == end_at
|
||||
|
||||
occ = Availability.create!(
|
||||
start_at: start_at,
|
||||
end_at: end_at,
|
||||
available_type: availability.available_type,
|
||||
is_recurrent: availability.is_recurrent,
|
||||
period: availability.period,
|
||||
@ -24,6 +30,27 @@ class Availabilities::CreateAvailabilitiesService
|
||||
nb_total_places: availability.nb_total_places,
|
||||
slot_duration: availability.slot_duration,
|
||||
plan_ids: availability.plan_ids
|
||||
)
|
||||
create_slots(occ)
|
||||
end
|
||||
end
|
||||
|
||||
def create_slots(availability)
|
||||
slot_duration = availability.slot_duration || Setting.get('slot_duration').to_i
|
||||
|
||||
if %w[machines space].include?(availability.available_type)
|
||||
((availability.end_at - availability.start_at) / slot_duration.minutes).to_i.times do |i|
|
||||
Slot.new(
|
||||
start_at: availability.start_at + (i * slot_duration).minutes,
|
||||
end_at: availability.start_at + (i * slot_duration).minutes + slot_duration.minutes,
|
||||
availability_id: availability.id
|
||||
).save!
|
||||
end
|
||||
else
|
||||
Slot.new(
|
||||
start_at: availability.start_at,
|
||||
end_at: availability.end_at,
|
||||
availability_id: availability.id
|
||||
).save!
|
||||
end
|
||||
end
|
||||
|
@ -7,96 +7,16 @@ class Availabilities::PublicAvailabilitiesService
|
||||
@service = Availabilities::StatusService.new('public')
|
||||
end
|
||||
|
||||
# provides a list of slots and availabilities for the machines, between the given dates
|
||||
def machines(start_date, end_date, reservations, machine_ids)
|
||||
availabilities = Availability.includes(:tags, :machines)
|
||||
.where(available_type: 'machines')
|
||||
.where('start_at >= ? AND end_at <= ?', start_date, end_date)
|
||||
.where(lock: false)
|
||||
slots = []
|
||||
availabilities.each do |a|
|
||||
slot_duration = a.slot_duration || Setting.get('slot_duration').to_i
|
||||
a.machines.each do |machine|
|
||||
next unless machine_ids&.include?(machine.id.to_s)
|
||||
def public_availabilities(window, ids, events = false)
|
||||
level = in_same_day(window[:start], window[:end]) ? 'slot' : 'availability'
|
||||
service = Availabilities::AvailabilitiesService.new(@current_user, level)
|
||||
|
||||
((a.end_at - a.start_at) / slot_duration.minutes).to_i.times do |i|
|
||||
slot = Slot.new(
|
||||
start_at: a.start_at + (i * slot_duration).minutes,
|
||||
end_at: a.start_at + (i * slot_duration).minutes + slot_duration.minutes,
|
||||
availability_id: a.id,
|
||||
availability: a,
|
||||
machine: machine,
|
||||
title: machine.name
|
||||
)
|
||||
slot = @service.machine_reserved_status(slot, reservations, @current_user)
|
||||
slots << slot
|
||||
end
|
||||
end
|
||||
end
|
||||
{ availabilities: availabilities, slots: slots }
|
||||
end
|
||||
machines_slots = service.machines(Machine.where(id: ids[:machines]), @current_user, window)
|
||||
spaces_slots = service.spaces(Space.where(id:ids[:spaces]), @current_user, window)
|
||||
trainings_slots = service.trainings(Training.where(id: ids[:trainings]), @current_user, window)
|
||||
events_slots = events ? service.events(Event.all, @current_user, window) : []
|
||||
|
||||
# provides a list of slots and availabilities for the spaces, between the given dates
|
||||
def spaces(start_date, end_date, reservations, available_id)
|
||||
availabilities = Availability.includes(:tags, :spaces).where(available_type: 'space')
|
||||
.where('start_at >= ? AND end_at <= ?', start_date, end_date)
|
||||
.where(lock: false)
|
||||
|
||||
availabilities.where(available_id: available_id) if available_id
|
||||
|
||||
slots = []
|
||||
availabilities.each do |a|
|
||||
slot_duration = a.slot_duration || Setting.get('slot_duration').to_i
|
||||
space = a.spaces.first
|
||||
((a.end_at - a.start_at) / slot_duration.minutes).to_i.times do |i|
|
||||
next unless (a.start_at + (i * slot_duration).minutes) > DateTime.current
|
||||
|
||||
slot = Slot.new(
|
||||
start_at: a.start_at + (i * slot_duration).minutes,
|
||||
end_at: a.start_at + (i * slot_duration).minutes + slot_duration.minutes,
|
||||
availability_id: a.id,
|
||||
availability: a,
|
||||
space: space,
|
||||
title: space.name
|
||||
)
|
||||
slot = @service.space_reserved_status(slot, reservations, @current_user)
|
||||
slots << slot
|
||||
end
|
||||
end
|
||||
{ availabilities: availabilities, slots: slots }
|
||||
end
|
||||
|
||||
def public_availabilities(start_date, end_date, reservations, ids)
|
||||
if in_same_day(start_date, end_date)
|
||||
# request for 1 single day
|
||||
|
||||
# trainings, events
|
||||
training_event_availabilities = Availability.includes(:tags, :trainings, :slots)
|
||||
.where(available_type: %w[training event])
|
||||
.where('start_at >= ? AND end_at <= ?', start_date, end_date)
|
||||
.where(lock: false)
|
||||
# machines
|
||||
machines_avail = machines(start_date, end_date, reservations, ids[:machines])
|
||||
machine_slots = machines_avail[:slots]
|
||||
# spaces
|
||||
spaces_avail = spaces(start_date, end_date, reservations, ids[:spaces])
|
||||
space_slots = spaces_avail[:slots]
|
||||
|
||||
[].concat(training_event_availabilities).concat(machine_slots).concat(space_slots)
|
||||
else
|
||||
# request for many days (week or month)
|
||||
avails = Availability.includes(:tags, :machines, :trainings, :spaces, :event, :slots)
|
||||
.where('start_at >= ? AND end_at <= ?', start_date, end_date)
|
||||
.where(lock: false)
|
||||
avails.each do |a|
|
||||
if a.available_type == 'training' || a.available_type == 'event'
|
||||
a = @service.training_event_reserved_status(a, reservations, @current_user)
|
||||
elsif a.available_type == 'space'
|
||||
a.is_reserved = @service.reserved_availability?(a, @current_user)
|
||||
end
|
||||
end
|
||||
avails
|
||||
end
|
||||
[].concat(trainings_slots).concat(events_slots).concat(machines_slots).concat(spaces_slots)
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -4,88 +4,67 @@
|
||||
class Availabilities::StatusService
|
||||
def initialize(current_user_role)
|
||||
@current_user_role = current_user_role
|
||||
@show_name = (%w[admin manager].include?(@current_user_role) || Setting.get('display_name_enable'))
|
||||
@show_name = (%w[admin manager].include?(@current_user_role) || (current_user_role && Setting.get('display_name_enable')))
|
||||
end
|
||||
|
||||
# check that the provided slot is reserved for the given reservable (machine, training or space).
|
||||
# Mark it accordingly for display in the calendar
|
||||
def slot_reserved_status(slot, user, reservables)
|
||||
if reservables.map(&:class).map(&:name).uniq.size > 1
|
||||
raise TypeError('[Availabilities::StatusService#slot_reserved_status] reservables have differents types')
|
||||
end
|
||||
|
||||
# check that the provided machine slot is reserved or not and modify it accordingly
|
||||
def machine_reserved_status(slot, reservations, user)
|
||||
statistic_profile_id = user&.statistic_profile&.id
|
||||
reservations.each do |r|
|
||||
r.slots.each do |s|
|
||||
next unless slot.machine.id == r.reservable_id
|
||||
|
||||
next unless s.start_at == slot.start_at && s.canceled_at.nil?
|
||||
slots_reservations = slot.slots_reservations
|
||||
.includes(:reservation)
|
||||
.where('reservations.reservable_type': reservables.map(&:class).map(&:name))
|
||||
.where('reservations.reservable_id': reservables.map(&:id))
|
||||
.where('slots_reservations.canceled_at': nil)
|
||||
|
||||
slot.id = s.id
|
||||
slot.is_reserved = true
|
||||
user_name = r.user ? r.user&.profile&.full_name : I18n.t('availabilities.deleted_user');
|
||||
slot.title = "#{slot.machine.name} - #{@show_name ? user_name : I18n.t('availabilities.deleted_user')}"
|
||||
slot.can_modify = true if %w[admin manager].include?(@current_user_role)
|
||||
slot.reservations.push r
|
||||
user_slots_reservations = slots_reservations.where('reservations.statistic_profile_id': statistic_profile_id)
|
||||
|
||||
next unless r.statistic_profile_id == statistic_profile_id
|
||||
slot.is_reserved = !slots_reservations.empty?
|
||||
slot.title = slot_title(slots_reservations, user_slots_reservations, reservables)
|
||||
slot.can_modify = true if %w[admin manager].include?(@current_user_role) || !user_slots_reservations.empty?
|
||||
slot.current_user_slots_reservations_ids = user_slots_reservations.map(&:id)
|
||||
|
||||
slot.title = "#{slot.machine.name} - #{I18n.t('availabilities.i_ve_reserved')}"
|
||||
slot.can_modify = true
|
||||
slot.is_reserved_by_current_user = true
|
||||
end
|
||||
end
|
||||
slot
|
||||
end
|
||||
|
||||
# check that the provided space slot is reserved or not and modify it accordingly
|
||||
def space_reserved_status(slot, reservations, user)
|
||||
statistic_profile_id = user&.statistic_profile&.id
|
||||
reservations.each do |r|
|
||||
r.slots.each do |s|
|
||||
next unless slot.space.id == r.reservable_id
|
||||
|
||||
next unless s.start_at == slot.start_at && s.canceled_at.nil?
|
||||
|
||||
slot.can_modify = true if %w[admin manager].include?(@current_user_role)
|
||||
slot.reservations.push r
|
||||
|
||||
next unless r.statistic_profile_id == statistic_profile_id
|
||||
|
||||
slot.id = s.id
|
||||
slot.title = I18n.t('availabilities.i_ve_reserved')
|
||||
slot.can_modify = true
|
||||
slot.is_reserved = true
|
||||
end
|
||||
end
|
||||
slot
|
||||
end
|
||||
|
||||
# check that the provided availability (training or event) is reserved or not and modify it accordingly
|
||||
def training_event_reserved_status(availability, reservations, user)
|
||||
statistic_profile_id = user&.statistic_profile&.id
|
||||
reservations.each do |r|
|
||||
r.slots.each do |s|
|
||||
next unless (
|
||||
(availability.available_type == 'training' && availability.trainings.first.id == r.reservable_id) ||
|
||||
(availability.available_type == 'event' && availability.event.id == r.reservable_id)
|
||||
) && s.start_at == availability.start_at && s.canceled_at.nil?
|
||||
|
||||
availability.slot_id = s.id
|
||||
if r.statistic_profile_id == statistic_profile_id
|
||||
availability.is_reserved = true
|
||||
availability.can_modify = true
|
||||
end
|
||||
end
|
||||
end
|
||||
availability
|
||||
end
|
||||
|
||||
# check that the provided ability is reserved by the given user
|
||||
def reserved_availability?(availability, user)
|
||||
if user
|
||||
reserved_slots = []
|
||||
availability.slots.each do |s|
|
||||
reserved_slots << s if s.canceled_at.nil?
|
||||
def availability_reserved_status(availability, user, reservables)
|
||||
if reservables.map(&:class).map(&:name).uniq.size > 1
|
||||
raise TypeError('[Availabilities::StatusService#availability_reserved_status] reservables have differents types')
|
||||
end
|
||||
reserved_slots.map(&:reservations).flatten.map(&:statistic_profile_id).include? user.statistic_profile&.id
|
||||
|
||||
slots_reservations = availability.slots_reservations
|
||||
.includes(:reservation)
|
||||
.where('reservations.reservable_type': reservables.map(&:class).map(&:name))
|
||||
.where('reservations.reservable_id': reservables.map(&:id))
|
||||
.where('slots_reservations.canceled_at': nil)
|
||||
|
||||
user_slots_reservations = slots_reservations.where('reservations.statistic_profile_id': user&.statistic_profile&.id)
|
||||
|
||||
availability.is_reserved = !slots_reservations.empty?
|
||||
availability.current_user_slots_reservations_ids = user_slots_reservations.map(&:id)
|
||||
availability
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def slot_title(slots_reservations, user_slots_reservations, reservables)
|
||||
name = reservables.map(&:name).join(', ')
|
||||
if user_slots_reservations.empty? && slots_reservations.empty?
|
||||
name
|
||||
elsif user_slots_reservations.empty? && !slots_reservations.empty?
|
||||
user_names = slots_reservations.map(&:reservation)
|
||||
.map(&:user)
|
||||
.map { |u| u&.profile&.full_name || I18n.t('availabilities.deleted_user') }
|
||||
.join(', ')
|
||||
"#{name} #{@show_name ? "- #{user_names}" : ''}"
|
||||
else
|
||||
false
|
||||
"#{name} - #{I18n.t('availabilities.i_ve_reserved')}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -28,7 +28,9 @@ class CartService
|
||||
end
|
||||
|
||||
coupon = CartItem::Coupon.new(@customer, @operator, cart_items[:coupon_code])
|
||||
schedule = CartItem::PaymentSchedule.new(plan_info[:plan], coupon, cart_items[:payment_schedule], @customer, plan_info[:subscription]&.start_at)
|
||||
schedule = CartItem::PaymentSchedule.new(
|
||||
plan_info[:plan], coupon, cart_items[:payment_schedule], @customer, plan_info[:subscription]&.start_at
|
||||
)
|
||||
|
||||
ShoppingCart.new(
|
||||
@customer,
|
||||
@ -108,32 +110,32 @@ class CartService
|
||||
CartItem::MachineReservation.new(@customer,
|
||||
@operator,
|
||||
reservable,
|
||||
cart_item[:slots_attributes],
|
||||
cart_item[:slots_reservations_attributes],
|
||||
plan: plan_info[:plan],
|
||||
new_subscription: plan_info[:new_subscription])
|
||||
when Training
|
||||
CartItem::TrainingReservation.new(@customer,
|
||||
@operator,
|
||||
reservable,
|
||||
cart_item[:slots_attributes],
|
||||
cart_item[:slots_reservations_attributes],
|
||||
plan: plan_info[:plan],
|
||||
new_subscription: plan_info[:new_subscription])
|
||||
when Event
|
||||
CartItem::EventReservation.new(@customer,
|
||||
@operator,
|
||||
reservable,
|
||||
cart_item[:slots_attributes],
|
||||
cart_item[:slots_reservations_attributes],
|
||||
normal_tickets: cart_item[:nb_reserve_places],
|
||||
other_tickets: cart_item[:tickets_attributes])
|
||||
when Space
|
||||
CartItem::SpaceReservation.new(@customer,
|
||||
@operator,
|
||||
reservable,
|
||||
cart_item[:slots_attributes],
|
||||
cart_item[:slots_reservations_attributes],
|
||||
plan: plan_info[:plan],
|
||||
new_subscription: plan_info[:new_subscription])
|
||||
else
|
||||
STDERR.puts "WARNING: the reservable #{reservable} is not implemented"
|
||||
Rails.logger.warn "the reservable #{reservable} is not implemented"
|
||||
raise NotImplementedError
|
||||
end
|
||||
end
|
||||
@ -145,32 +147,32 @@ class CartService
|
||||
CartItem::MachineReservation.new(@customer,
|
||||
@operator,
|
||||
reservable,
|
||||
object.reservation.slots,
|
||||
object.reservation.slots_reservations,
|
||||
plan: plan,
|
||||
new_subscription: true)
|
||||
when Training
|
||||
CartItem::TrainingReservation.new(@customer,
|
||||
@operator,
|
||||
reservable,
|
||||
object.reservation.slots,
|
||||
object.reservation.slots_reservations,
|
||||
plan: plan,
|
||||
new_subscription: true)
|
||||
when Event
|
||||
CartItem::EventReservation.new(@customer,
|
||||
@operator,
|
||||
reservable,
|
||||
object.reservation.slots,
|
||||
object.reservation.slots_reservations,
|
||||
normal_tickets: object.reservation.nb_reserve_places,
|
||||
other_tickets: object.reservation.tickets)
|
||||
when Space
|
||||
CartItem::SpaceReservation.new(@customer,
|
||||
@operator,
|
||||
reservable,
|
||||
object.reservation.slots,
|
||||
object.reservation.slots_reservations,
|
||||
plan: plan,
|
||||
new_subscription: true)
|
||||
else
|
||||
STDERR.puts "WARNING: the reservable #{reservable} is not implemented"
|
||||
Rails.logger.warn "WARNING: the reservable #{reservable} is not implemented"
|
||||
raise NotImplementedError
|
||||
end
|
||||
end
|
||||
|
@ -166,27 +166,33 @@ class EventService
|
||||
e_params = e_params.merge(
|
||||
event_files_attributes: ef_attributes
|
||||
)
|
||||
original_slots_ids = event.availability.slots.map(&:id)
|
||||
begin
|
||||
results[:events].push status: !!e.update(e_params.permit!), event: e # rubocop:disable Style/DoubleNegation
|
||||
rescue StandardError => err
|
||||
results[:events].push status: false, event: e, error: err.try(:record).try(:class).try(:name), message: err.message
|
||||
end
|
||||
results[:slots].concat(update_slots(e.availability_id))
|
||||
results[:slots].concat(update_slots(e.availability_id, original_slots_ids))
|
||||
end
|
||||
original_slots_ids = event.availability.slots.map(&:id)
|
||||
begin
|
||||
event_params[:availability_attributes][:id] = event.availability_id
|
||||
results[:events].push status: !!event.update(event_params), event: event # rubocop:disable Style/DoubleNegation
|
||||
rescue StandardError => err
|
||||
results[:events].push status: false, event: event, error: err.try(:record).try(:class).try(:name), message: err.message
|
||||
end
|
||||
results[:slots].concat(update_slots(event.availability_id))
|
||||
results[:slots].concat(update_slots(event.availability_id, original_slots_ids))
|
||||
results
|
||||
end
|
||||
|
||||
def update_slots(availability_id)
|
||||
def update_slots(availability_id, original_slots_ids)
|
||||
results = []
|
||||
avail = Availability.find(availability_id)
|
||||
avail.slots.each do |s|
|
||||
results.push(status: !!s.update_attributes(start_at: avail.start_at, end_at: avail.end_at), slot: s) # rubocop:disable Style/DoubleNegation
|
||||
Slot.where(id: original_slots_ids).each do |slot|
|
||||
results.push(
|
||||
status: !!slot.update_attributes(availability_id: availability_id, start_at: avail.start_at, end_at: avail.end_at), # rubocop:disable Style/DoubleNegation
|
||||
slot: slot
|
||||
)
|
||||
rescue StandardError => err
|
||||
results.push status: false, slot: s, error: err.try(:record).try(:class).try(:name), message: err.message
|
||||
end
|
||||
|
@ -51,19 +51,18 @@ class FootprintService
|
||||
return saved if Rails.env.test?
|
||||
|
||||
if saved.nil?
|
||||
puts "Debug data not found for #{klass} [ id: #{item.id} ]"
|
||||
Rails.logger.debug { "Debug data not found for #{klass} [ id: #{item.id} ]" }
|
||||
else
|
||||
puts "Debug footprint for #{klass} [ id: #{item.id} ]"
|
||||
puts '-----------------------------------------'
|
||||
puts "columns: [ #{columns.join(', ')} ]"
|
||||
puts "current: #{current}"
|
||||
puts " saved: #{saved.format_data(item.id)}"
|
||||
puts '-----------------------------------------'
|
||||
Rails.logger.debug do
|
||||
"Debug footprint for #{klass} [ id: #{item.id} ]\n" \
|
||||
"-----------------------------------------\ncolumns: [ #{columns.join(', ')} ]\n" \
|
||||
"current: #{current}\n saved: #{saved.format_data(item.id)}\n" \
|
||||
'-----------------------------------------'
|
||||
end
|
||||
item.footprint_children.map(&:debug_footprint)
|
||||
end
|
||||
others = FootprintDebug.where('klass = ? AND data LIKE ? AND id != ?', klass, "#{item.id}%", saved&.id)
|
||||
puts "other possible matches IDs: #{others.map(&:id)}"
|
||||
puts '-----------------------------------------'
|
||||
Rails.logger.debug { "other possible matches IDs: #{others.map(&:id)}\n-----------------------------------------" }
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -121,7 +121,7 @@ class InvoicesService
|
||||
def self.generate_event_item(invoice, reservation, payment_details, main = false)
|
||||
raise TypeError unless reservation.reservable.is_a? Event
|
||||
|
||||
reservation.slots.each do |slot|
|
||||
reservation.slots_reservations.map(&:slot).each do |slot|
|
||||
description = "#{reservation.reservable.name}\n"
|
||||
description += if slot.start_at.to_date != slot.end_at.to_date
|
||||
I18n.t('events.from_STARTDATE_to_ENDDATE',
|
||||
@ -152,7 +152,7 @@ class InvoicesService
|
||||
def self.generate_reservation_item(invoice, reservation, payment_details, main = false)
|
||||
raise TypeError unless [Space, Machine, Training].include? reservation.reservable.class
|
||||
|
||||
reservation.slots.each do |slot|
|
||||
reservation.slots_reservations.map(&:slot).each do |slot|
|
||||
description = reservation.reservable.name +
|
||||
" #{I18n.l slot.start_at, format: :long} - #{I18n.l slot.end_at, format: :hour_minute}"
|
||||
|
||||
|
@ -26,7 +26,8 @@ class Members::ListService
|
||||
|
||||
# ILIKE => PostgreSQL case-insensitive LIKE
|
||||
if params[:search].size.positive?
|
||||
@query = @query.where('profiles.first_name ILIKE :search OR ' \
|
||||
@query = @query.where('users.username ILIKE :search OR ' \
|
||||
'profiles.first_name ILIKE :search OR ' \
|
||||
'profiles.last_name ILIKE :search OR ' \
|
||||
'profiles.phone ILIKE :search OR ' \
|
||||
'email ILIKE :search OR ' \
|
||||
@ -41,7 +42,7 @@ class Members::ListService
|
||||
end
|
||||
|
||||
def search(current_user, query, subscription, include_admins = 'false')
|
||||
members = User.includes(:profile)
|
||||
members = User.includes(:profile, :statistic_profile)
|
||||
.joins(:profile,
|
||||
:statistic_profile,
|
||||
:roles,
|
||||
@ -83,6 +84,8 @@ class Members::ListService
|
||||
offset = ((params[:page]&.to_i || 1) - 1) * (params[:size]&.to_i || 1)
|
||||
|
||||
order_key = case order_key
|
||||
when 'username'
|
||||
'users.username'
|
||||
when 'last_name'
|
||||
'profiles.last_name'
|
||||
when 'first_name'
|
||||
|
@ -36,17 +36,13 @@ class Members::MembersService
|
||||
end
|
||||
end
|
||||
|
||||
if params[:invoicing_profile_attributes][:organization] == 'false'
|
||||
params[:invoicing_profile_attributes].reject! { |p| %w[organization_attributes organization].include?(p) }
|
||||
else
|
||||
params[:invoicing_profile_attributes].reject! { |p| p == 'organization' }
|
||||
end
|
||||
Members::MembersService.handle_organization(params)
|
||||
|
||||
not_complete = member.need_completion?
|
||||
up_result = member.update(params)
|
||||
|
||||
notify_user_profile_complete(not_complete) if up_result
|
||||
member.notify_group_changed(ex_group, validated_at_changed) if group_changed
|
||||
member.notify_group_changed(ex_group, validated_at_changed) if group_changed && !ex_group.nil?
|
||||
up_result
|
||||
end
|
||||
|
||||
@ -99,6 +95,18 @@ class Members::MembersService
|
||||
is_updated
|
||||
end
|
||||
|
||||
def self.handle_organization(params)
|
||||
return params unless params[:invoicing_profile_attributes] && params[:invoicing_profile_attributes][:organization]
|
||||
|
||||
if params[:invoicing_profile_attributes][:organization] == 'false'
|
||||
params[:invoicing_profile_attributes].reject! { |p| %w[organization_attributes organization].include?(p) }
|
||||
else
|
||||
params[:invoicing_profile_attributes].reject! { |p| p == 'organization' }
|
||||
end
|
||||
|
||||
params
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def notify_user_profile_complete(previous_state)
|
||||
|
@ -1,21 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# helpers for managing slots (reservations sub-units)
|
||||
class SlotService
|
||||
def cancel(slot)
|
||||
# first we mark ths slot as cancelled in DB, to free a ticket
|
||||
slot.update_attributes(canceled_at: DateTime.current)
|
||||
|
||||
# then we try to remove this reservation from ElasticSearch, to keep the statistics up-to-date
|
||||
model_name = slot.reservation.reservable.class.name
|
||||
client = Elasticsearch::Model.client
|
||||
|
||||
model = "Stats::#{model_name}".constantize
|
||||
client.delete_by_query(
|
||||
index: model.index_name,
|
||||
type: model.document_type,
|
||||
conflicts: 'proceed',
|
||||
body: { query: { match: { reservationId: slot.reservation.id } } }
|
||||
)
|
||||
end
|
||||
end
|
25
app/services/slots_reservations_service.rb
Normal file
25
app/services/slots_reservations_service.rb
Normal file
@ -0,0 +1,25 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# helpers for managing slots reservations (reservations for a time unit)
|
||||
class SlotsReservationsService
|
||||
class << self
|
||||
def cancel(slot_reservation)
|
||||
# first we mark ths slot reservation as cancelled in DB, to free a ticket
|
||||
slot_reservation.update_attributes(canceled_at: DateTime.current)
|
||||
|
||||
# then we try to remove this reservation from ElasticSearch, to keep the statistics up-to-date
|
||||
model_name = slot_reservation.reservation.reservable.class.name
|
||||
client = Elasticsearch::Model.client
|
||||
|
||||
model = "Stats::#{model_name}".constantize
|
||||
client.delete_by_query(
|
||||
index: model.index_name,
|
||||
type: model.document_type,
|
||||
conflicts: 'proceed',
|
||||
body: { query: { match: { reservationId: slot_reservation.reservation_id } } }
|
||||
)
|
||||
rescue Faraday::ConnectionFailed
|
||||
warn 'Unable to update data in elasticsearch'
|
||||
end
|
||||
end
|
||||
end
|
@ -1,5 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Extend the user's current subscription after his first training reservation if
|
||||
# he subscribed to a rolling plan
|
||||
class SubscriptionExtensionAfterReservation
|
||||
attr_accessor :user, :reservation
|
||||
|
||||
@ -24,7 +26,7 @@ class SubscriptionExtensionAfterReservation
|
||||
|
||||
def extend_subscription
|
||||
user.subscription.update_columns(
|
||||
expiration_date: reservation.slots.first.start_at + user.subscribed_plan.duration
|
||||
expiration_date: reservation.slots_reservations.first.slot.start_at + user.subscribed_plan.duration
|
||||
)
|
||||
end
|
||||
end
|
||||
|
@ -94,7 +94,8 @@ module UsersCredits
|
||||
super
|
||||
|
||||
will_use_credits, free_hours_count, machine_credit = _will_use_credits?
|
||||
if will_use_credits
|
||||
return unless will_use_credits
|
||||
|
||||
users_credit = user.users_credits.find_or_initialize_by(credit_id: machine_credit.id)
|
||||
|
||||
if users_credit.new_record?
|
||||
@ -104,26 +105,22 @@ module UsersCredits
|
||||
end
|
||||
users_credit.save!
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def _will_use_credits?
|
||||
return false, 0 unless plan
|
||||
|
||||
if machine_credit = plan.machine_credits.find_by(creditable_id: reservation.reservable_id)
|
||||
machine_credit = plan.machine_credits.find_by(creditable_id: reservation.reservable_id)
|
||||
if machine_credit
|
||||
users_credit = user.users_credits.find_by(credit_id: machine_credit.id)
|
||||
already_used_hours = users_credit ? users_credit.hours_used : 0
|
||||
|
||||
remaining_hours = machine_credit.hours - already_used_hours
|
||||
|
||||
free_hours_count = [remaining_hours, reservation.slots.size].min
|
||||
free_hours_count = [remaining_hours, reservation.slots_reservations.size].min
|
||||
|
||||
if free_hours_count.positive?
|
||||
return true, free_hours_count, machine_credit
|
||||
else
|
||||
return false, free_hours_count, machine_credit
|
||||
end
|
||||
return free_hours_count&.positive?, free_hours_count, machine_credit
|
||||
end
|
||||
[false, 0]
|
||||
end
|
||||
@ -138,9 +135,8 @@ module UsersCredits
|
||||
def update_credits
|
||||
super
|
||||
will_use_credits, training_credit = _will_use_credits?
|
||||
if will_use_credits
|
||||
user.credits << training_credit # we create a new UsersCredit object
|
||||
end
|
||||
|
||||
user.credits << training_credit if will_use_credits # we create a new UsersCredit object
|
||||
end
|
||||
|
||||
private
|
||||
@ -149,11 +145,10 @@ module UsersCredits
|
||||
return false, nil unless plan
|
||||
|
||||
# if there is a training_credit defined for this plan and this training
|
||||
if training_credit = plan.training_credits.find_by(creditable_id: reservation.reservable_id)
|
||||
training_credit = plan.training_credits.find_by(creditable_id: reservation.reservable_id)
|
||||
if training_credit
|
||||
# if user has not used all the plan credits
|
||||
if user.training_credits.where(plan: plan).count < plan.training_credit_nb
|
||||
return true, training_credit
|
||||
end
|
||||
return true, training_credit if user.training_credits.where(plan: plan).count < plan.training_credit_nb
|
||||
end
|
||||
[false, nil]
|
||||
end
|
||||
@ -200,19 +195,16 @@ module UsersCredits
|
||||
def _will_use_credits?
|
||||
return false, 0 unless plan
|
||||
|
||||
if space_credit = plan.space_credits.find_by(creditable_id: reservation.reservable_id)
|
||||
space_credit = plan.space_credits.find_by(creditable_id: reservation.reservable_id)
|
||||
if space_credit
|
||||
users_credit = user.users_credits.find_by(credit_id: space_credit.id)
|
||||
already_used_hours = users_credit ? users_credit.hours_used : 0
|
||||
|
||||
remaining_hours = space_credit.hours - already_used_hours
|
||||
|
||||
free_hours_count = [remaining_hours, reservation.slots.size].min
|
||||
free_hours_count = [remaining_hours, reservation.slots_reservations.size].min
|
||||
|
||||
if free_hours_count.positive?
|
||||
return true, free_hours_count, space_credit
|
||||
else
|
||||
return false, free_hours_count, space_credit
|
||||
end
|
||||
return free_hours_count&.positive?, free_hours_count, space_credit
|
||||
end
|
||||
[false, 0]
|
||||
end
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user