mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-01-29 18:52:22 +01:00
Merge branch 'dev' for release 5.3.1
This commit is contained in:
commit
41f206cc43
72
CHANGELOG.md
72
CHANGELOG.md
@ -1,15 +1,47 @@
|
||||
# Changelog Fab-manager
|
||||
|
||||
# v5.3.1 2022 January 17
|
||||
|
||||
- Definition of extended prices for spaces is now made in hours (previously in minutes)
|
||||
- Support for JSONPath syntax in OAuth2 SSO fields mapping
|
||||
- Basic support for OAuth2 scopes through an environment variable
|
||||
- Ability to enable debug logs for the SSO authentication process using `SSO_DEBUG=true`
|
||||
- Remove case sensitivity for the SSO account mapping process
|
||||
- Ability to cancel a payement schedule from the interface
|
||||
- Ability to create slots in the past
|
||||
- Ability to disable public account creation
|
||||
- Ability to select "bank transfer" as the payment mean for a payment schedule
|
||||
- When a payment schedule was canceled by the payment gateway, alert the users
|
||||
- When a payment schedule is in error, alert the users
|
||||
- When a payment schedule is in error or canceled, ability to re-enable it with another payment method
|
||||
- Fix card image ratio
|
||||
- Update events heading style
|
||||
- Update some icons
|
||||
- Optimized the load time of the payment schedules list
|
||||
- Optimized multiple DB queries
|
||||
- Updated caniuse db
|
||||
- Fix a bug: do not load Stripe if no keys were defined
|
||||
- Fix a bug: some links redirect to the home page instead of triggering the requested action
|
||||
- Fix a bug: exports to Excel are corrupted (#49)
|
||||
- Fix a bug: if a specialized VAT rate was defined when the VAT was disabled, the resulting VAT rate is wrong
|
||||
- Fix a bug: unable to rebuild the PDF for invoices without subscriptions
|
||||
- Fix a bug: the switch to enable/disable the VAT does not reflect the current state of the VAT
|
||||
- Fix a bug: SSO configuration interface has a misnamed field (Common URL)
|
||||
- Fix a bug: unable to bind Profile.birthday and Profile.gender from an SSO
|
||||
- Fix a security issue: updated follow-redirects to 1.14.7 to fix [CVE-2022-0155](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-0155)
|
||||
- [TODO DEPLOY] `rails db:seed`
|
||||
|
||||
# v5.3.0 2021 December 29
|
||||
|
||||
- Ability to configure multiple VAT rates, per kind of invoiced item
|
||||
- Refactored the extended prices frontend code to allow future customization
|
||||
- Ability to export the collected VAT, by rates, to a CSV file
|
||||
- Refactored the extended prices' frontend code to allow future customization
|
||||
- Fix a bug: the amount label in not correctly shown in the extended prices modal
|
||||
- Fix a bug: `extended_prices_in_same_day` apply the extended prices to each days
|
||||
- Fix a bug: `extended_prices_in_same_day` apply the extended prices to each day
|
||||
|
||||
## v5.2.0 2021 December 23
|
||||
|
||||
- Ability to configure prices for spaces by time slots different than the default hourly rate
|
||||
- Ability to configure prices for spaces, by time slots different from the default hourly rate
|
||||
- Updated portuguese translation
|
||||
- Refactored the ReserveButton component to use the same user's data across all the component
|
||||
- First optimization the load time of the payment schedules list
|
||||
@ -26,21 +58,21 @@
|
||||
## v5.1.11 2021 October 22
|
||||
|
||||
- Refactored subscription new/renew/free extend interfaces and API
|
||||
- Ability to configure data sources for preventing booking on overlapping slots
|
||||
- Updated production documentation
|
||||
- Updated SSO documentation
|
||||
- Improved stripe subscription process with better error handling
|
||||
- Ability to configure the data sources of the booking prevention on overlapping slots
|
||||
- Updated the production documentation
|
||||
- Updated the SSO documentation
|
||||
- Improved the stripe subscription process with better error handling
|
||||
- The upgrade script will check and report the ability to access the hub API
|
||||
- Fix a bug: canceled training reservation is not marked as this in admin/edit members/trainings
|
||||
- Fix a bug: canceled training reservation is not marked as this in admin > edit members > trainings
|
||||
- Fix a bug: users can set their birthdate in the future
|
||||
- Fix a bug: the upgrade script won't add environment variables that are already present anymore
|
||||
- Fix a bug: the upgrade script won't add anymore the environment variables that are already present
|
||||
- Fix a bug: admin cannot take or renew a subscription for a member from member/edit interface
|
||||
- Fix a bug: missing translations
|
||||
- Fix a bug: the upgrade script report an invalid version to upgrade to
|
||||
- Fix a bug: invalid amount provided to the PayZen payment gateway when using a currency with anything else than 2 decimals
|
||||
- Fix a bug: invalid amount provided to the PayZen payment gateway, when using a 0-decimal or a 3-decimal currency
|
||||
- Fix a bug: incorrect behavior for the setting "email confirmation required"
|
||||
- Fix a bug: invalid text shown when a member confirms a free cart
|
||||
- Fix a bug: 3DS confirmation is not asked when an admin is subscribing a user through a payment schedule using PayZen
|
||||
- Fix a bug: 3DS confirmation is not asked when an admin is subscribing a user through a payment schedule, using PayZen
|
||||
- Updated @rails/webpacker to 5.4.3
|
||||
- Updated react-refresh-webpack-plugin to 0.5.1
|
||||
- Updated react-refresh to 0.10.0
|
||||
@ -55,17 +87,17 @@
|
||||
|
||||
## v5.1.10 2021 October 04
|
||||
|
||||
- Fix a bug: the image of the about page is not using the image set in backoffice
|
||||
- Fix a bug: the image of the about page is not using the image set in the backoffice
|
||||
- Fix a bug: updated sassc to 2.4.0 to fix ruby runtime error on some CPU architectures (#270)
|
||||
- Fix a security issue: prevent HTML code edition in projects, to prevent XSS vulnerability (#293)
|
||||
- Fix a bug : cover image doesn't display in profile
|
||||
- Fix a bug : it redirects to home when we delete a machine record photo
|
||||
- Fix a bug: cover image doesn't display in profile
|
||||
- Fix a bug: fab-manager redirects to the home page when we delete a machine photo
|
||||
|
||||
## v5.1.9 2021 September 21
|
||||
|
||||
- Add a setting for the purchase and use of a prepaid pack is only possible for the user with a valid subscription
|
||||
- Fix a bug: unable to show plan name in calendar reservations
|
||||
- Fix a bug: book overlapping slot setting label name
|
||||
- Add a setting to restrict the purchase and use of a prepaid pack to users with a valid subscription
|
||||
- Fix a bug: unable to view the plans names in the reservation calendar
|
||||
- Fix a bug: label name of the book overlapping slot setting
|
||||
|
||||
## v5.1.8 2021 September 13
|
||||
|
||||
@ -280,7 +312,11 @@
|
||||
- [TODO DEPLOY] `rails fablab:maintenance:rebuild_stylesheet`
|
||||
- [TODO DEPLOY] `\curl -sSL https://raw.githubusercontent.com/sleede/fab-manager/master/scripts/rename-adminsys.sh | bash`
|
||||
|
||||
## v4.7.13 2020 June 11
|
||||
## v4.7.14 2021 September 30
|
||||
|
||||
- Fix a bug: updated sassc to 2.4.0 to fix ruby runtime error on some CPU architectures
|
||||
|
||||
## v4.7.13 2021 June 11
|
||||
|
||||
- Fix a bug: unable to process stripe payments with 3DS authentication
|
||||
|
||||
|
3
Gemfile
3
Gemfile
@ -14,6 +14,7 @@ gem 'webpacker', '~> 5.x'
|
||||
gem 'jbuilder', '~> 2.5'
|
||||
gem 'jbuilder_cache_multi'
|
||||
gem 'json', '>= 2.3.0'
|
||||
gem 'jsonpath'
|
||||
|
||||
gem 'forgery'
|
||||
gem 'responders', '~> 2.0'
|
||||
@ -29,6 +30,7 @@ group :development do
|
||||
# Access an IRB console on exception pages or by using <%= console %> in views
|
||||
gem 'active_record_query_trace'
|
||||
gem 'awesome_print'
|
||||
gem 'bullet'
|
||||
gem 'coveralls_reborn', '~> 0.18.0', require: false
|
||||
gem 'foreman'
|
||||
gem 'web-console', '>= 3.3.0'
|
||||
@ -64,6 +66,7 @@ gem 'pg_search'
|
||||
# authentication
|
||||
gem 'devise', '>= 4.6.0'
|
||||
|
||||
|
||||
gem 'omniauth', '~> 1.9.0'
|
||||
gem 'omniauth-oauth2'
|
||||
gem 'omniauth-rails_csrf_protection', '~> 0.1'
|
||||
|
10
Gemfile.lock
10
Gemfile.lock
@ -67,6 +67,9 @@ GEM
|
||||
bootsnap (1.4.6)
|
||||
msgpack (~> 1.0)
|
||||
builder (3.2.4)
|
||||
bullet (7.0.0)
|
||||
activesupport (>= 3.0.0)
|
||||
uniform_notifier (~> 1.11)
|
||||
camertron-eprun (1.1.1)
|
||||
carrierwave (2.1.1)
|
||||
activemodel (>= 5.0.0)
|
||||
@ -172,6 +175,8 @@ GEM
|
||||
jbuilder_cache_multi (0.1.0)
|
||||
jbuilder (>= 1.5.0, < 3)
|
||||
json (2.3.1)
|
||||
jsonpath (1.1.0)
|
||||
multi_json
|
||||
jwt (2.2.1)
|
||||
kaminari (1.2.1)
|
||||
activesupport (>= 4.1.0)
|
||||
@ -394,6 +399,7 @@ GEM
|
||||
tzinfo-data (1.2020.4)
|
||||
tzinfo (>= 1.0.0)
|
||||
unicode-display_width (1.4.1)
|
||||
uniform_notifier (1.14.2)
|
||||
vcr (6.0.0)
|
||||
virtus (1.0.5)
|
||||
axiom-types (~> 0.1)
|
||||
@ -431,6 +437,7 @@ DEPENDENCIES
|
||||
apipie-rails
|
||||
awesome_print
|
||||
bootsnap
|
||||
bullet
|
||||
carrierwave
|
||||
caxlsx
|
||||
caxlsx_rails
|
||||
@ -451,6 +458,7 @@ DEPENDENCIES
|
||||
jbuilder (~> 2.5)
|
||||
jbuilder_cache_multi
|
||||
json (>= 2.3.0)
|
||||
jsonpath
|
||||
kaminari
|
||||
listen (~> 3.0.5)
|
||||
message_format
|
||||
@ -498,4 +506,4 @@ DEPENDENCIES
|
||||
webpacker (~> 5.x)
|
||||
|
||||
BUNDLED WITH
|
||||
2.1.4
|
||||
2.2.19
|
||||
|
@ -19,7 +19,7 @@ class API::MembersController < API::ApiController
|
||||
|
||||
def last_subscribed
|
||||
@query = User.active.with_role(:member)
|
||||
.includes(profile: [:user_avatar])
|
||||
.includes(:statistic_profile, profile: [:user_avatar])
|
||||
.where('is_allow_contact = true AND confirmed_at IS NOT NULL')
|
||||
.order('created_at desc')
|
||||
.limit(params[:last])
|
||||
|
@ -6,12 +6,15 @@ class API::NotificationsController < API::ApiController
|
||||
include NotifyWith::NotificationsApi
|
||||
before_action :authenticate_user!
|
||||
|
||||
# notifications can have anything attached, so we won't eager load the whole database
|
||||
around_action :skip_bullet, if: -> { defined?(Bullet) }
|
||||
|
||||
# Number of notifications added to the page when the user clicks on 'load next notifications'
|
||||
NOTIFICATIONS_PER_PAGE = 15
|
||||
|
||||
def index
|
||||
loop do
|
||||
@notifications = current_user.notifications.page(params[:page]).per(NOTIFICATIONS_PER_PAGE).order('created_at DESC')
|
||||
@notifications = current_user.notifications.includes(:attached_object).page(params[:page]).per(NOTIFICATIONS_PER_PAGE).order('created_at DESC')
|
||||
# we delete obsolete notifications on first access
|
||||
break unless delete_obsoletes(@notifications)
|
||||
end
|
||||
@ -24,7 +27,7 @@ class API::NotificationsController < API::ApiController
|
||||
|
||||
def last_unread
|
||||
loop do
|
||||
@notifications = current_user.notifications.where(is_read: false).limit(3).order('created_at DESC')
|
||||
@notifications = current_user.notifications.includes(:attached_object).where(is_read: false).limit(3).order('created_at DESC')
|
||||
# we delete obsolete notifications on first access
|
||||
break unless delete_obsoletes(@notifications)
|
||||
end
|
||||
|
@ -3,9 +3,10 @@
|
||||
# API Controller for resources of PaymentSchedule
|
||||
class API::PaymentSchedulesController < API::ApiController
|
||||
before_action :authenticate_user!
|
||||
before_action :set_payment_schedule, only: %i[download cancel]
|
||||
before_action :set_payment_schedule_item, only: %i[cash_check refresh_item pay_item]
|
||||
before_action :set_payment_schedule, only: %i[download cancel update]
|
||||
before_action :set_payment_schedule_item, only: %i[show_item cash_check confirm_transfer refresh_item pay_item]
|
||||
|
||||
# retrieve all payment schedules for the current user, paginated
|
||||
def index
|
||||
@payment_schedules = PaymentSchedule.where('invoicing_profile_id = ?', current_user.invoicing_profile.id)
|
||||
.includes(:invoicing_profile, :payment_schedule_items, :payment_schedule_objects)
|
||||
@ -15,6 +16,7 @@ class API::PaymentSchedulesController < API::ApiController
|
||||
.per(params[:size])
|
||||
end
|
||||
|
||||
# retrieve all payment schedules for all users. Filtering is supported
|
||||
def list
|
||||
authorize PaymentSchedule
|
||||
|
||||
@ -44,6 +46,15 @@ class API::PaymentSchedulesController < API::ApiController
|
||||
render json: attrs, status: :ok
|
||||
end
|
||||
|
||||
def confirm_transfer
|
||||
authorize @payment_schedule_item.payment_schedule
|
||||
PaymentScheduleService.new.generate_invoice(@payment_schedule_item, payment_method: 'transfer')
|
||||
attrs = { state: 'paid', payment_method: 'transfer' }
|
||||
@payment_schedule_item.update_attributes(attrs)
|
||||
|
||||
render json: attrs, status: :ok
|
||||
end
|
||||
|
||||
def refresh_item
|
||||
authorize @payment_schedule_item.payment_schedule
|
||||
PaymentScheduleItemWorker.new.perform(@payment_schedule_item.id)
|
||||
@ -62,6 +73,11 @@ class API::PaymentSchedulesController < API::ApiController
|
||||
end
|
||||
end
|
||||
|
||||
def show_item
|
||||
authorize @payment_schedule_item.payment_schedule
|
||||
render json: @payment_schedule_item, status: :ok
|
||||
end
|
||||
|
||||
def cancel
|
||||
authorize @payment_schedule
|
||||
|
||||
@ -69,6 +85,17 @@ class API::PaymentSchedulesController < API::ApiController
|
||||
render json: { canceled_at: canceled_at }, status: :ok
|
||||
end
|
||||
|
||||
## Only the update of the payment method is allowed
|
||||
def update
|
||||
authorize PaymentSchedule
|
||||
|
||||
if PaymentScheduleService.new.update_payment_mean(@payment_schedule, update_params)
|
||||
render :show, status: :ok, location: @payment_schedule
|
||||
else
|
||||
render json: @payment_schedule.errors, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_payment_schedule
|
||||
@ -78,4 +105,8 @@ class API::PaymentSchedulesController < API::ApiController
|
||||
def set_payment_schedule_item
|
||||
@payment_schedule_item = PaymentScheduleItem.find(params[:id])
|
||||
end
|
||||
|
||||
def update_params
|
||||
params.require(:payment_schedule).permit(:payment_method)
|
||||
end
|
||||
end
|
||||
|
@ -80,4 +80,13 @@ class ApplicationController < ActionController::Base
|
||||
def authenticate_user!
|
||||
super
|
||||
end
|
||||
|
||||
# N+1 query detection (https://github.com/flyerhzm/bullet)
|
||||
def skip_bullet
|
||||
previous_value = Bullet.enable?
|
||||
Bullet.enable = false
|
||||
yield
|
||||
ensure
|
||||
Bullet.enable = previous_value
|
||||
end
|
||||
end
|
||||
|
@ -4,6 +4,11 @@
|
||||
class RegistrationsController < Devise::RegistrationsController
|
||||
# POST /users.json
|
||||
def create
|
||||
# Is public registration allowed?
|
||||
unless Setting.get('public_registrations')
|
||||
render json: { errors: { signup: [t('errors.messages.registration_disabled')] } }, status: :forbidden and return
|
||||
end
|
||||
|
||||
# first check the recaptcha
|
||||
check = RecaptchaService.verify(params[:user][:recaptcha])
|
||||
render json: check['error-codes'], status: :unprocessable_entity and return unless check['success']
|
||||
|
@ -1,16 +1,25 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Handle authentication actions via OmniAuth (used by SSO providers)
|
||||
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
||||
require 'sso_logger'
|
||||
logger = SsoLogger.new
|
||||
|
||||
active_provider = AuthProvider.active
|
||||
define_method active_provider.strategy_name do
|
||||
logger.info "[Users::OmniauthCallbacksController##{active_provider.strategy_name}] initiated"
|
||||
if request.env['omniauth.params'].blank?
|
||||
logger.debug 'the user has not provided any authentication token'
|
||||
@user = User.from_omniauth(request.env['omniauth.auth'])
|
||||
|
||||
# Here we create the new user or update the existing one with values retrieved from the SSO.
|
||||
|
||||
if @user.id.nil? # => new user (ie. not updating existing)
|
||||
logger.debug 'trying to create a new user'
|
||||
# If the username is mapped, we just check its uniqueness as it would break the postgresql
|
||||
# unique constraint otherwise. If the name is not unique, another unique is generated
|
||||
if active_provider.sso_fields.include?('user.username')
|
||||
logger.debug 'the username was already in use, generating a new one'
|
||||
@user.username = generate_unique_username(@user.username)
|
||||
end
|
||||
# If the email is mapped, we check its uniqueness. If the email is already in use, we mark it as duplicate with an
|
||||
@ -18,17 +27,21 @@ class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
||||
# - if it is the same user, his email will be filled from the SSO when he merge his accounts
|
||||
# - if it is not the same user, this will prevent the raise of PG::UniqueViolation
|
||||
if active_provider.sso_fields.include?('user.email') && email_exists?(@user.email)
|
||||
logger.debug 'the email was already in use, marking it as duplicate'
|
||||
old_mail = @user.email
|
||||
@user.email = "<#{old_mail}>#{Devise.friendly_token}-duplicate"
|
||||
flash[:alert] = t('omniauth.email_already_linked_to_another_account_please_input_your_authentication_code', OLD_MAIL: old_mail)
|
||||
end
|
||||
else # => update of an existing user
|
||||
logger.debug "an existing user was found (id=#{@user.id})"
|
||||
if username_exists?(@user.username, @user.id)
|
||||
logger.debug 'the username was already in use, alerting user'
|
||||
flash[:alert] = t('omniauth.your_username_is_already_linked_to_another_account_unable_to_update_it', USERNAME: @user.username)
|
||||
@user.username = User.find(@user.id).username
|
||||
end
|
||||
|
||||
if email_exists?(@user.email, @user.id)
|
||||
logger.debug 'the email was already in use, alerting user'
|
||||
flash[:alert] = t('omniauth.your_email_address_is_already_linked_to_another_account_unable_to_update_it', EMAIL: @user.email)
|
||||
@user.email = User.find(@user.id).email
|
||||
end
|
||||
@ -36,19 +49,32 @@ class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
||||
|
||||
# We BYPASS THE VALIDATION because, in case of a new user, we want to save him anyway, we'll ask him later to complete his profile (on first login).
|
||||
# In case of an existing user, we trust the SSO validation as we want the SSO to have authority on users management and policy.
|
||||
@user.save(validate: false)
|
||||
logger.debug 'saving the user'
|
||||
unless @user.save(validate: false)
|
||||
logger.error "unable to save the user, an error occurred : #{@user.errors.full_messages.join(', ')}"
|
||||
end
|
||||
|
||||
logger.debug 'signing-in the user and redirecting'
|
||||
sign_in_and_redirect @user, event: :authentication # this will throw if @user is not activated
|
||||
else
|
||||
logger.debug 'the user has provided an authentication token'
|
||||
@user = User.find_by(auth_token: request.env['omniauth.params']['auth_token'])
|
||||
|
||||
# Here the user already exists in the database and request to be linked with the SSO
|
||||
# so let's update its sso attributes and log him on
|
||||
logger.debug "found user id=#{@user.id}"
|
||||
|
||||
begin
|
||||
logger.debug 'linking with the omniauth provider'
|
||||
@user.link_with_omniauth_provider(request.env['omniauth.auth'])
|
||||
logger.debug 'signing-in the user and redirecting'
|
||||
sign_in_and_redirect @user, event: :authentication
|
||||
rescue DuplicateIndexError
|
||||
logger.error 'user already linked'
|
||||
redirect_to root_url, alert: t('omniauth.this_account_is_already_linked_to_an_user_of_the_platform', NAME: active_provider.name)
|
||||
rescue StandardError => e
|
||||
logger.unknown "an expected error occurred: #{e}"
|
||||
raise e
|
||||
end
|
||||
end
|
||||
|
||||
@ -58,17 +84,17 @@ class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
||||
|
||||
def username_exists?(username, exclude_id = nil)
|
||||
if exclude_id.nil?
|
||||
User.where(username: username).size.positive?
|
||||
User.where('lower(username) = ?', username&.downcase).size.positive?
|
||||
else
|
||||
User.where(username: username).where.not(id: exclude_id).size.positive?
|
||||
User.where('lower(username) = ?', username&.downcase).where.not(id: exclude_id).size.positive?
|
||||
end
|
||||
end
|
||||
|
||||
def email_exists?(email, exclude_id = nil)
|
||||
if exclude_id.nil?
|
||||
User.where(email: email).size.positive?
|
||||
User.where('lower(email) = ?', email&.downcase).size.positive?
|
||||
else
|
||||
User.where(email: email).where.not(id: exclude_id).size.positive?
|
||||
User.where('lower(email) = ?', email&.downcase).where.not(id: exclude_id).size.positive?
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -2,5 +2,8 @@
|
||||
|
||||
# Raised when an an error occurred with the PayZen payment gateway
|
||||
class PayzenError < PaymentGatewayError
|
||||
def details
|
||||
JSON.parse(message.gsub('=>', ':').gsub('nil', 'null'))
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -4,7 +4,7 @@ import {
|
||||
CancelScheduleResponse,
|
||||
CashCheckResponse, PayItemResponse,
|
||||
PaymentSchedule,
|
||||
PaymentScheduleIndexRequest, RefreshItemResponse
|
||||
PaymentScheduleIndexRequest, PaymentScheduleItem, RefreshItemResponse
|
||||
} from '../models/payment-schedule';
|
||||
|
||||
export default class PaymentScheduleAPI {
|
||||
@ -23,6 +23,16 @@ export default class PaymentScheduleAPI {
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async confirmTransfer (paymentScheduleItemId: number): Promise<CashCheckResponse> {
|
||||
const res: AxiosResponse = await apiClient.post(`/api/payment_schedules/items/${paymentScheduleItemId}/confirm_transfer`);
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async getItem (paymentScheduleItemId: number): Promise<PaymentScheduleItem> {
|
||||
const res: AxiosResponse = await apiClient.get(`/api/payment_schedules/items/${paymentScheduleItemId}`);
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async refreshItem (paymentScheduleItemId: number): Promise<RefreshItemResponse> {
|
||||
const res: AxiosResponse = await apiClient.post(`/api/payment_schedules/items/${paymentScheduleItemId}/refresh_item`);
|
||||
return res?.data;
|
||||
@ -37,4 +47,9 @@ export default class PaymentScheduleAPI {
|
||||
const res: AxiosResponse = await apiClient.put(`/api/payment_schedules/${paymentScheduleId}/cancel`);
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async update (paymentSchedule: PaymentSchedule): Promise<PaymentSchedule> {
|
||||
const res:AxiosResponse<PaymentSchedule> = await apiClient.patch(`/api/payment_schedules/${paymentSchedule.id}`, paymentSchedule);
|
||||
return res?.data;
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface HtmlTranslateProps {
|
||||
trKey: string,
|
||||
options?: Record<string, string>
|
||||
options?: Record<string, string|number>
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { Suspense } from 'react';
|
||||
|
||||
/**
|
||||
* This component is a wrapper that display a loader while the children components have their rendering suspended
|
||||
* This component is a wrapper that display a loader while the children components have their rendering suspended.
|
||||
*/
|
||||
export const Loader: React.FC = ({ children }) => {
|
||||
const loading = (
|
||||
|
@ -0,0 +1,413 @@
|
||||
import {
|
||||
PaymentMethod,
|
||||
PaymentSchedule,
|
||||
PaymentScheduleItem,
|
||||
PaymentScheduleItemState
|
||||
} from '../../models/payment-schedule';
|
||||
import React, { ReactElement, useState } from 'react';
|
||||
import { FabButton } from '../base/fab-button';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { User, UserRole } from '../../models/user';
|
||||
import PaymentScheduleAPI from '../../api/payment-schedule';
|
||||
import { FabModal } from '../base/fab-modal';
|
||||
import FormatLib from '../../lib/format';
|
||||
import { StripeConfirmModal } from '../payment/stripe/stripe-confirm-modal';
|
||||
import { UpdateCardModal } from '../payment/update-card-modal';
|
||||
import { UpdatePaymentMeanModal } from './update-payment-mean-modal';
|
||||
|
||||
// we want to display some buttons only once. This is the types of buttons it applies to.
|
||||
export enum TypeOnce {
|
||||
CardUpdate = 'card-update',
|
||||
SubscriptionCancel = 'subscription-cancel',
|
||||
UpdatePaymentMean = 'update-payment-mean'
|
||||
}
|
||||
|
||||
interface PaymentScheduleItemActionsProps {
|
||||
paymentScheduleItem: PaymentScheduleItem,
|
||||
paymentSchedule: PaymentSchedule,
|
||||
onError: (message: string) => void,
|
||||
onSuccess: () => void,
|
||||
onCardUpdateSuccess: () => void
|
||||
operator: User,
|
||||
displayOnceMap: Map<TypeOnce, Map<number, number>>,
|
||||
show: boolean,
|
||||
}
|
||||
|
||||
/**
|
||||
* This component is responsible for rendering the actions buttons for a payment schedule item.
|
||||
*/
|
||||
export const PaymentScheduleItemActions: React.FC<PaymentScheduleItemActionsProps> = ({ paymentScheduleItem, paymentSchedule, onError, onSuccess, onCardUpdateSuccess, displayOnceMap, operator, show }) => {
|
||||
const { t } = useTranslation('shared');
|
||||
|
||||
// is open, the modal dialog to cancel the associated subscription?
|
||||
const [showCancelSubscription, setShowCancelSubscription] = useState<boolean>(false);
|
||||
// is open, the modal dialog to confirm the cashing of a check?
|
||||
const [showConfirmCashing, setShowConfirmCashing] = useState<boolean>(false);
|
||||
// is open, the modal dialog to confirm a back transfer?
|
||||
const [showConfirmTransfer, setShowConfirmTransfer] = useState<boolean>(false);
|
||||
// is open, the modal dialog the resolve a pending card payment?
|
||||
const [showResolveAction, setShowResolveAction] = useState<boolean>(false);
|
||||
// is open, the modal dialog to update the card details
|
||||
const [showUpdateCard, setShowUpdateCard] = useState<boolean>(false);
|
||||
// is open, the modal dialog to update the payment mean
|
||||
const [showUpdatePaymentMean, setShowUpdatePaymentMean] = useState<boolean>(false);
|
||||
// the user cannot confirm the action modal (3D secure), unless he has resolved the pending action
|
||||
const [isConfirmActionDisabled, setConfirmActionDisabled] = useState<boolean>(true);
|
||||
|
||||
/**
|
||||
* Check if the current operator has administrative rights or is a normal member
|
||||
*/
|
||||
const isPrivileged = (): boolean => {
|
||||
return (operator.role === UserRole.Admin || operator.role === UserRole.Manager);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a button to download a PDF invoice file
|
||||
*/
|
||||
const downloadInvoiceButton = (id: number): JSX.Element => {
|
||||
const link = `api/invoices/${id}/download`;
|
||||
return (
|
||||
<a href={link} target="_blank" className="download-button" rel="noreferrer">
|
||||
<i className="fas fa-download" />
|
||||
{t('app.shared.payment_schedule_item_actions.download')}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a button to cancel the given subscription, if the user is privileged enough
|
||||
*/
|
||||
const cancelSubscriptionButton = (): ReactElement => {
|
||||
const displayOnceStatus = displayOnceMap.get(TypeOnce.SubscriptionCancel).get(paymentSchedule.id);
|
||||
if (isPrivileged() && (!displayOnceStatus || displayOnceStatus === paymentScheduleItem.id)) {
|
||||
displayOnceMap.get(TypeOnce.SubscriptionCancel).set(paymentSchedule.id, paymentScheduleItem.id);
|
||||
return (
|
||||
<FabButton key={`cancel-subscription-${paymentSchedule.id}`}
|
||||
onClick={toggleCancelSubscriptionModal}
|
||||
icon={<i className="fas fa-times" />}>
|
||||
{t('app.shared.payment_schedule_item_actions.cancel_subscription')}
|
||||
</FabButton>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a button to confirm the receipt of the bank transfer, if the user is privileged enough
|
||||
*/
|
||||
const confirmTransferButton = (): ReactElement => {
|
||||
if (isPrivileged()) {
|
||||
return (
|
||||
<FabButton key={`confirm-transfer-${paymentScheduleItem.id}`}
|
||||
onClick={toggleConfirmTransferModal}
|
||||
icon={<i className="fas fa-university"/>}>
|
||||
{t('app.shared.payment_schedule_item_actions.confirm_payment')}
|
||||
</FabButton>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a button to confirm the cashing of the check, if the user is privileged enough
|
||||
*/
|
||||
const confirmCheckButton = (): ReactElement => {
|
||||
if (isPrivileged()) {
|
||||
return (
|
||||
<FabButton key={`confirm-check-${paymentScheduleItem.id}`}
|
||||
onClick={toggleConfirmCashingModal}
|
||||
icon={<i className="fas fa-check"/>}>
|
||||
{t('app.shared.payment_schedule_item_actions.confirm_check')}
|
||||
</FabButton>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a button to resolve the 3DS security check
|
||||
*/
|
||||
const solveActionButton = (): ReactElement => {
|
||||
return (
|
||||
<FabButton key={`solve-action-${paymentScheduleItem.id}`}
|
||||
onClick={toggleResolveActionModal}
|
||||
icon={<i className="fas fa-wrench"/>}>
|
||||
{t('app.shared.payment_schedule_item_actions.resolve_action')}
|
||||
</FabButton>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a button to update the default payment mean for the current payment schedule
|
||||
*/
|
||||
const updatePaymentMeanButton = (): ReactElement => {
|
||||
const displayOnceStatus = displayOnceMap.get(TypeOnce.UpdatePaymentMean).get(paymentSchedule.id);
|
||||
if (isPrivileged() && (!displayOnceStatus || displayOnceStatus === paymentScheduleItem.id)) {
|
||||
displayOnceMap.get(TypeOnce.UpdatePaymentMean).set(paymentSchedule.id, paymentScheduleItem.id);
|
||||
return (
|
||||
<FabButton key={`update-payment-mean-${paymentScheduleItem.id}`}
|
||||
onClick={toggleUpdatePaymentMeanModal}
|
||||
icon={<i className="fas fa-money-bill-alt" />}>
|
||||
{t('app.shared.payment_schedule_item_actions.update_payment_mean')}
|
||||
</FabButton>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a button to update the credit card associated with the payment schedule
|
||||
*/
|
||||
const updateCardButton = (): ReactElement => {
|
||||
const displayOnceStatus = displayOnceMap.get(TypeOnce.SubscriptionCancel).get(paymentSchedule.id);
|
||||
if (isPrivileged() && (!displayOnceStatus || displayOnceStatus === paymentScheduleItem.id)) {
|
||||
displayOnceMap.get(TypeOnce.CardUpdate).set(paymentSchedule.id, paymentScheduleItem.id);
|
||||
return (
|
||||
<FabButton key={`update-card-${paymentSchedule.id}`}
|
||||
onClick={toggleUpdateCardModal}
|
||||
icon={<i className="fas fa-credit-card"/>}>
|
||||
{t('app.shared.payment_schedule_item_actions.update_card')}
|
||||
</FabButton>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the actions button(s) for current paymentScheduleItem with state Pending
|
||||
*/
|
||||
const pendingActions = (): ReactElement => {
|
||||
if (isPrivileged()) {
|
||||
if (paymentSchedule.payment_method === PaymentMethod.Transfer) {
|
||||
return confirmTransferButton();
|
||||
}
|
||||
if (paymentSchedule.payment_method === PaymentMethod.Check) {
|
||||
return confirmCheckButton();
|
||||
}
|
||||
} else {
|
||||
return <span>{t('app.shared.payment_schedule_item_actions.please_ask_reception')}</span>;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the actions button(s) for current paymentScheduleItem with state Error or GatewayCanceled
|
||||
*/
|
||||
const errorActions = (): ReactElement[] => {
|
||||
// if the payment schedule is canceled/in error, the schedule is over, and we can't update the card
|
||||
displayOnceMap.get(TypeOnce.CardUpdate).set(paymentSchedule.id, paymentScheduleItem.id);
|
||||
|
||||
const buttons = [];
|
||||
if (isPrivileged()) {
|
||||
buttons.push(cancelSubscriptionButton());
|
||||
buttons.push(updatePaymentMeanButton());
|
||||
} else {
|
||||
buttons.push(<span>{t('app.shared.payment_schedule_item_actions.please_ask_reception')}</span>);
|
||||
}
|
||||
return buttons;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the actions button(s) for current paymentScheduleItem with state New
|
||||
*/
|
||||
const newActions = (): Array<ReactElement> => {
|
||||
const buttons = [];
|
||||
if (paymentSchedule.payment_method === PaymentMethod.Card) {
|
||||
buttons.push(updateCardButton());
|
||||
}
|
||||
if (isPrivileged()) {
|
||||
buttons.push(cancelSubscriptionButton());
|
||||
}
|
||||
return buttons;
|
||||
};
|
||||
|
||||
/**
|
||||
* Show/hide the modal dialog to cancel the current subscription
|
||||
*/
|
||||
const toggleCancelSubscriptionModal = (): void => {
|
||||
setShowCancelSubscription(!showCancelSubscription);
|
||||
};
|
||||
|
||||
/**
|
||||
* Show/hide the modal dialog that enable to confirm the cashing of the check for a given deadline.
|
||||
*/
|
||||
const toggleConfirmCashingModal = (): void => {
|
||||
setShowConfirmCashing(!showConfirmCashing);
|
||||
};
|
||||
|
||||
/**
|
||||
* Show/hide the modal dialog that enable to confirm the bank transfer for a given deadline.
|
||||
*/
|
||||
const toggleConfirmTransferModal = (): void => {
|
||||
setShowConfirmTransfer(!showConfirmTransfer);
|
||||
};
|
||||
|
||||
/**
|
||||
* Show/hide the modal dialog that trigger the card "action".
|
||||
*/
|
||||
const toggleResolveActionModal = (): void => {
|
||||
setShowResolveAction(!showResolveAction);
|
||||
};
|
||||
|
||||
/**
|
||||
* Enable/disable the confirm button of the "action" modal
|
||||
*/
|
||||
const toggleConfirmActionButton = (): void => {
|
||||
setConfirmActionDisabled(!isConfirmActionDisabled);
|
||||
};
|
||||
|
||||
/**
|
||||
* Show/hide the modal dialog to update the bank card details
|
||||
*/
|
||||
const toggleUpdateCardModal = (): void => {
|
||||
setShowUpdateCard(!showUpdateCard);
|
||||
};
|
||||
|
||||
/**
|
||||
* Show/hide the modal dialog to update the payment mean
|
||||
*/
|
||||
const toggleUpdatePaymentMeanModal = (): void => {
|
||||
setShowUpdatePaymentMean(!showUpdatePaymentMean);
|
||||
};
|
||||
|
||||
/**
|
||||
* After the user has confirmed that he wants to cash the check, update the API, refresh the list and close the modal.
|
||||
*/
|
||||
const onCheckCashingConfirmed = (): void => {
|
||||
PaymentScheduleAPI.cashCheck(paymentScheduleItem.id).then((res) => {
|
||||
if (res.state === PaymentScheduleItemState.Paid) {
|
||||
onSuccess();
|
||||
toggleConfirmCashingModal();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* After the user has confirmed that he validates the transfer, update the API, refresh the list and close the modal.
|
||||
*/
|
||||
const onTransferConfirmed = (): void => {
|
||||
PaymentScheduleAPI.confirmTransfer(paymentScheduleItem.id).then((res) => {
|
||||
if (res.state === PaymentScheduleItemState.Paid) {
|
||||
onSuccess();
|
||||
toggleConfirmTransferModal();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* When the card was successfully updated, pay the invoice (using the new payment method) and close the modal
|
||||
*/
|
||||
const handleCardUpdateSuccess = (): void => {
|
||||
if (paymentScheduleItem.state === PaymentScheduleItemState.RequirePaymentMethod) {
|
||||
PaymentScheduleAPI.payItem(paymentScheduleItem.id).then(() => {
|
||||
onSuccess();
|
||||
onCardUpdateSuccess();
|
||||
toggleUpdateCardModal();
|
||||
}).catch((err) => {
|
||||
onError(err);
|
||||
});
|
||||
} else {
|
||||
// the user is updating his card number in a pro-active way, we don't need to trigger the payment
|
||||
onCardUpdateSuccess();
|
||||
toggleUpdateCardModal();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* When the user has confirmed the cancellation, we transfer the request to the API
|
||||
*/
|
||||
const onCancelSubscriptionConfirmed = (): void => {
|
||||
PaymentScheduleAPI.cancel(paymentSchedule.id).then(() => {
|
||||
onSuccess();
|
||||
toggleCancelSubscriptionModal();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* After the 3DS confirmation was done (successfully or not), ask the API to refresh the item status,
|
||||
* then refresh the list and close the modal
|
||||
*/
|
||||
const afterConfirmAction = (): void => {
|
||||
toggleConfirmActionButton();
|
||||
PaymentScheduleAPI.refreshItem(paymentScheduleItem.id).then(() => {
|
||||
onSuccess();
|
||||
toggleResolveActionModal();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* When the update of the payment mean was successful, refresh the list and close the modal
|
||||
*/
|
||||
const onPaymentMeanUpdateSuccess = (): void => {
|
||||
onSuccess();
|
||||
toggleUpdatePaymentMeanModal();
|
||||
};
|
||||
|
||||
if (!show) return null;
|
||||
|
||||
return (
|
||||
<span className="payment-schedule-item-actions">
|
||||
{paymentScheduleItem.state === PaymentScheduleItemState.Paid && downloadInvoiceButton(paymentScheduleItem.invoice_id)}
|
||||
{paymentScheduleItem.state === PaymentScheduleItemState.Pending && pendingActions()}
|
||||
{paymentScheduleItem.state === PaymentScheduleItemState.RequireAction && solveActionButton()}
|
||||
{paymentScheduleItem.state === PaymentScheduleItemState.RequirePaymentMethod && updateCardButton()}
|
||||
{paymentScheduleItem.state === PaymentScheduleItemState.Error && errorActions()}
|
||||
{paymentScheduleItem.state === PaymentScheduleItemState.GatewayCanceled && errorActions()}
|
||||
{paymentScheduleItem.state === PaymentScheduleItemState.New && newActions()}
|
||||
<div className="modals">
|
||||
{/* Confirm the cashing of the current deadline by check */}
|
||||
<FabModal title={t('app.shared.payment_schedule_item_actions.confirm_check_cashing')}
|
||||
isOpen={showConfirmCashing}
|
||||
toggleModal={toggleConfirmCashingModal}
|
||||
onConfirm={onCheckCashingConfirmed}
|
||||
closeButton={true}
|
||||
confirmButton={t('app.shared.payment_schedule_item_actions.confirm_button')}>
|
||||
<span>
|
||||
{t('app.shared.payment_schedule_item_actions.confirm_check_cashing_body', {
|
||||
AMOUNT: FormatLib.price(paymentScheduleItem.amount),
|
||||
DATE: FormatLib.date(paymentScheduleItem.due_date)
|
||||
})}
|
||||
</span>
|
||||
</FabModal>
|
||||
{/* Confirm the bank transfer for the current deadline */}
|
||||
<FabModal title={t('app.shared.payment_schedule_item_actions.confirm_bank_transfer')}
|
||||
isOpen={showConfirmTransfer}
|
||||
toggleModal={toggleConfirmTransferModal}
|
||||
onConfirm={onTransferConfirmed}
|
||||
closeButton={true}
|
||||
confirmButton={t('app.shared.payment_schedule_item_actions.confirm_button')}>
|
||||
<span>
|
||||
{t('app.shared.payment_schedule_item_actions.confirm_bank_transfer_body', {
|
||||
AMOUNT: FormatLib.price(paymentScheduleItem.amount),
|
||||
DATE: FormatLib.date(paymentScheduleItem.due_date)
|
||||
})}
|
||||
</span>
|
||||
</FabModal>
|
||||
{/* Cancel the subscription */}
|
||||
<FabModal title={t('app.shared.payment_schedule_item_actions.cancel_subscription')}
|
||||
isOpen={showCancelSubscription}
|
||||
toggleModal={toggleCancelSubscriptionModal}
|
||||
onConfirm={onCancelSubscriptionConfirmed}
|
||||
closeButton={true}
|
||||
confirmButton={t('app.shared.payment_schedule_item_actions.confirm_button')}>
|
||||
{t('app.shared.payment_schedule_item_actions.confirm_cancel_subscription')}
|
||||
</FabModal>
|
||||
{/* 3D secure confirmation */}
|
||||
<StripeConfirmModal isOpen={showResolveAction}
|
||||
toggleModal={toggleResolveActionModal}
|
||||
onSuccess={afterConfirmAction}
|
||||
paymentScheduleItemId={paymentScheduleItem.id} />
|
||||
{/* Update credit card */}
|
||||
<UpdateCardModal isOpen={showUpdateCard}
|
||||
toggleModal={toggleUpdateCardModal}
|
||||
operator={operator}
|
||||
afterSuccess={handleCardUpdateSuccess}
|
||||
onError={onError}
|
||||
schedule={paymentSchedule}>
|
||||
</UpdateCardModal>
|
||||
{/* Update the payment mean */}
|
||||
<UpdatePaymentMeanModal isOpen={showUpdatePaymentMean}
|
||||
toggleModal={toggleUpdatePaymentMeanModal}
|
||||
onError={onError}
|
||||
afterSuccess={onPaymentMeanUpdateSuccess}
|
||||
paymentSchedule={paymentSchedule} />
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
PaymentScheduleItemActions.defaultProps = { show: false };
|
@ -2,15 +2,15 @@ import React, { ReactEventHandler, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Loader } from '../base/loader';
|
||||
import _ from 'lodash';
|
||||
import { FabButton } from '../base/fab-button';
|
||||
import { FabModal } from '../base/fab-modal';
|
||||
import { UpdateCardModal } from '../payment/update-card-modal';
|
||||
import { StripeElements } from '../payment/stripe/stripe-elements';
|
||||
import { StripeConfirm } from '../payment/stripe/stripe-confirm';
|
||||
import { User, UserRole } from '../../models/user';
|
||||
import { PaymentSchedule, PaymentScheduleItem, PaymentScheduleItemState } from '../../models/payment-schedule';
|
||||
import PaymentScheduleAPI from '../../api/payment-schedule';
|
||||
import { User } from '../../models/user';
|
||||
import {
|
||||
PaymentSchedule,
|
||||
PaymentScheduleItem,
|
||||
PaymentScheduleItemState
|
||||
} from '../../models/payment-schedule';
|
||||
import FormatLib from '../../lib/format';
|
||||
import { PaymentScheduleItemActions, TypeOnce } from './payment-schedule-item-actions';
|
||||
import { StripeElements } from '../payment/stripe/stripe-elements';
|
||||
|
||||
interface PaymentSchedulesTableProps {
|
||||
paymentSchedules: Array<PaymentSchedule>,
|
||||
@ -29,23 +29,12 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
|
||||
// for each payment schedule: are the details (all deadlines) shown or hidden?
|
||||
const [showExpanded, setShowExpanded] = useState<Map<number, boolean>>(new Map());
|
||||
// is open, the modal dialog to confirm the cashing of a check?
|
||||
const [showConfirmCashing, setShowConfirmCashing] = useState<boolean>(false);
|
||||
// is open, the modal dialog the resolve a pending card payment?
|
||||
const [showResolveAction, setShowResolveAction] = useState<boolean>(false);
|
||||
// the user cannot confirm the action modal (3D secure), unless he has resolved the pending action
|
||||
const [isConfirmActionDisabled, setConfirmActionDisabled] = useState<boolean>(true);
|
||||
// is open, the modal dialog to update the card details
|
||||
const [showUpdateCard, setShowUpdateCard] = useState<boolean>(false);
|
||||
// when an action is triggered on a deadline, the deadline is saved here until the action is done or cancelled.
|
||||
const [tempDeadline, setTempDeadline] = useState<PaymentScheduleItem>(null);
|
||||
// when an action is triggered on a deadline, the parent schedule is saved here until the action is done or cancelled.
|
||||
const [tempSchedule, setTempSchedule] = useState<PaymentSchedule>(null);
|
||||
// is open, the modal dialog to cancel the associated subscription?
|
||||
const [showCancelSubscription, setShowCancelSubscription] = useState<boolean>(false);
|
||||
|
||||
// we want to display the card update button, only once. This is an association table keeping when we already shown one
|
||||
const cardUpdateButton = new Map<number, boolean>();
|
||||
// we want to display some buttons only once. This map keep track of the buttons that have been displayed.
|
||||
const [displayOnceMap] = useState<Map<TypeOnce, Map<number, number>>>(new Map([
|
||||
[TypeOnce.SubscriptionCancel, new Map()],
|
||||
[TypeOnce.CardUpdate, new Map()],
|
||||
[TypeOnce.UpdatePaymentMean, new Map()]
|
||||
]));
|
||||
|
||||
/**
|
||||
* Check if the requested payment schedule is displayed with its deadlines (PaymentScheduleItem) or without them
|
||||
@ -89,19 +78,11 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* For use with downloadButton()
|
||||
*/
|
||||
enum TargetType {
|
||||
Invoice = 'invoices',
|
||||
PaymentSchedule = 'payment_schedules'
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a button to download a PDF file, may be an invoice, or a payment schedule, depending or the provided parameters
|
||||
*/
|
||||
const downloadButton = (target: TargetType, id: number): JSX.Element => {
|
||||
const link = `api/${target}/${id}/download`;
|
||||
const downloadScheduleButton = (id: number): JSX.Element => {
|
||||
const link = `api/payment_schedules/${id}/download`;
|
||||
return (
|
||||
<a href={link} target="_blank" className="download-button" rel="noreferrer">
|
||||
<i className="fas fa-download" />
|
||||
@ -113,8 +94,8 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
/**
|
||||
* Return the human-readable string for the status of the provided deadline.
|
||||
*/
|
||||
const formatState = (item: PaymentScheduleItem): JSX.Element => {
|
||||
let res = t(`app.shared.schedules_table.state_${item.state}`);
|
||||
const formatState = (item: PaymentScheduleItem, schedule: PaymentSchedule): JSX.Element => {
|
||||
let res = t(`app.shared.schedules_table.state_${item.state}${item.state === 'pending' ? '_' + schedule.payment_method : ''}`);
|
||||
if (item.state === PaymentScheduleItemState.Paid) {
|
||||
const key = `app.shared.schedules_table.method_${item.payment_method}`;
|
||||
res += ` (${t(key)})`;
|
||||
@ -122,96 +103,6 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
return <span className={`state-${item.state}`}>{res}</span>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the current operator has administrative rights or is a normal member
|
||||
*/
|
||||
const isPrivileged = (): boolean => {
|
||||
return (operator.role === UserRole.Admin || operator.role === UserRole.Manager);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the action button(s) for the given deadline
|
||||
*/
|
||||
const itemButtons = (item: PaymentScheduleItem, schedule: PaymentSchedule): JSX.Element => {
|
||||
switch (item.state) {
|
||||
case PaymentScheduleItemState.Paid:
|
||||
return downloadButton(TargetType.Invoice, item.invoice_id);
|
||||
case PaymentScheduleItemState.Pending:
|
||||
if (isPrivileged()) {
|
||||
return (
|
||||
<FabButton onClick={handleConfirmCheckPayment(item)}
|
||||
icon={<i className="fas fa-money-check" />}>
|
||||
{t('app.shared.schedules_table.confirm_payment')}
|
||||
</FabButton>
|
||||
);
|
||||
} else {
|
||||
return <span>{t('app.shared.schedules_table.please_ask_reception')}</span>;
|
||||
}
|
||||
case PaymentScheduleItemState.RequireAction:
|
||||
return (
|
||||
<FabButton onClick={handleSolveAction(item)}
|
||||
icon={<i className="fas fa-wrench" />}>
|
||||
{t('app.shared.schedules_table.solve')}
|
||||
</FabButton>
|
||||
);
|
||||
case PaymentScheduleItemState.RequirePaymentMethod:
|
||||
return (
|
||||
<FabButton onClick={handleUpdateCard(schedule, item)}
|
||||
icon={<i className="fas fa-credit-card" />}>
|
||||
{t('app.shared.schedules_table.update_card')}
|
||||
</FabButton>
|
||||
);
|
||||
case PaymentScheduleItemState.Error:
|
||||
// if the payment is in error, the schedule is over, and we can't update the card
|
||||
cardUpdateButton.set(schedule.id, true);
|
||||
if (isPrivileged()) {
|
||||
return (
|
||||
<FabButton onClick={handleCancelSubscription(schedule)}
|
||||
icon={<i className="fas fa-times" />}>
|
||||
{t('app.shared.schedules_table.cancel_subscription')}
|
||||
</FabButton>
|
||||
);
|
||||
} else {
|
||||
return <span>{t('app.shared.schedules_table.please_ask_reception')}</span>;
|
||||
}
|
||||
case PaymentScheduleItemState.New:
|
||||
if (!cardUpdateButton.get(schedule.id)) {
|
||||
cardUpdateButton.set(schedule.id, true);
|
||||
return (
|
||||
<FabButton onClick={handleUpdateCard(schedule)}
|
||||
icon={<i className="fas fa-credit-card" />}>
|
||||
{t('app.shared.schedules_table.update_card')}
|
||||
</FabButton>
|
||||
);
|
||||
}
|
||||
return <span />;
|
||||
default:
|
||||
return <span />;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when the user's clicks on the "cash check" button: show a confirmation modal
|
||||
*/
|
||||
const handleConfirmCheckPayment = (item: PaymentScheduleItem): ReactEventHandler => {
|
||||
return (): void => {
|
||||
setTempDeadline(item);
|
||||
toggleConfirmCashingModal();
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* After the user has confirmed that he wants to cash the check, update the API, refresh the list and close the modal.
|
||||
*/
|
||||
const onCheckCashingConfirmed = (): void => {
|
||||
PaymentScheduleAPI.cashCheck(tempDeadline.id).then((res) => {
|
||||
if (res.state === PaymentScheduleItemState.Paid) {
|
||||
refreshSchedulesTable();
|
||||
toggleConfirmCashingModal();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Refresh all payment schedules in the table
|
||||
*/
|
||||
@ -219,216 +110,74 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
refreshList();
|
||||
};
|
||||
|
||||
/**
|
||||
* Show/hide the modal dialog that enable to confirm the cashing of the check for a given deadline.
|
||||
*/
|
||||
const toggleConfirmCashingModal = (): void => {
|
||||
setShowConfirmCashing(!showConfirmCashing);
|
||||
};
|
||||
|
||||
/**
|
||||
* Show/hide the modal dialog that trigger the card "action".
|
||||
*/
|
||||
const toggleResolveActionModal = (): void => {
|
||||
setShowResolveAction(!showResolveAction);
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when the user's clicks on the "resolve" button: show a modal that will trigger the action
|
||||
*/
|
||||
const handleSolveAction = (item: PaymentScheduleItem): ReactEventHandler => {
|
||||
return (): void => {
|
||||
setTempDeadline(item);
|
||||
toggleResolveActionModal();
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* After the action was done (successfully or not), ask the API to refresh the item status, then refresh the list and close the modal
|
||||
*/
|
||||
const afterAction = (): void => {
|
||||
toggleConfirmActionButton();
|
||||
PaymentScheduleAPI.refreshItem(tempDeadline.id).then(() => {
|
||||
refreshSchedulesTable();
|
||||
toggleResolveActionModal();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Enable/disable the confirm button of the "action" modal
|
||||
*/
|
||||
const toggleConfirmActionButton = (): void => {
|
||||
setConfirmActionDisabled(!isConfirmActionDisabled);
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when the user's clicks on the "update card" button: show a modal to input a new card
|
||||
*/
|
||||
const handleUpdateCard = (paymentSchedule: PaymentSchedule, item?: PaymentScheduleItem): ReactEventHandler => {
|
||||
return (): void => {
|
||||
setTempDeadline(item);
|
||||
setTempSchedule(paymentSchedule);
|
||||
toggleUpdateCardModal();
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Show/hide the modal dialog to update the bank card details
|
||||
*/
|
||||
const toggleUpdateCardModal = (): void => {
|
||||
setShowUpdateCard(!showUpdateCard);
|
||||
};
|
||||
|
||||
/**
|
||||
* When the card was successfully updated, pay the invoice (using the new payment method) and close the modal
|
||||
*/
|
||||
const handleCardUpdateSuccess = (): void => {
|
||||
if (tempDeadline) {
|
||||
PaymentScheduleAPI.payItem(tempDeadline.id).then(() => {
|
||||
refreshSchedulesTable();
|
||||
onCardUpdateSuccess();
|
||||
toggleUpdateCardModal();
|
||||
}).catch((err) => {
|
||||
handleCardUpdateError(err);
|
||||
});
|
||||
} else {
|
||||
// if no tempDeadline (i.e. PaymentScheduleItem), then the user is updating his card number in a pro-active way, we don't need to trigger the payment
|
||||
onCardUpdateSuccess();
|
||||
toggleUpdateCardModal();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* When the card was not updated, raise the error
|
||||
*/
|
||||
const handleCardUpdateError = (error): void => {
|
||||
onError(error);
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when the user clicks on the "cancel subscription" button
|
||||
*/
|
||||
const handleCancelSubscription = (schedule: PaymentSchedule): ReactEventHandler => {
|
||||
return (): void => {
|
||||
setTempSchedule(schedule);
|
||||
toggleCancelSubscriptionModal();
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Show/hide the modal dialog to cancel the current subscription
|
||||
*/
|
||||
const toggleCancelSubscriptionModal = (): void => {
|
||||
setShowCancelSubscription(!showCancelSubscription);
|
||||
};
|
||||
|
||||
/**
|
||||
* When the user has confirmed the cancellation, we transfer the request to the API
|
||||
*/
|
||||
const onCancelSubscriptionConfirmed = (): void => {
|
||||
PaymentScheduleAPI.cancel(tempSchedule.id).then(() => {
|
||||
refreshSchedulesTable();
|
||||
toggleCancelSubscriptionModal();
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<table className="schedules-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="w-35" />
|
||||
<th className="w-200">{t('app.shared.schedules_table.schedule_num')}</th>
|
||||
<th className="w-200">{t('app.shared.schedules_table.date')}</th>
|
||||
<th className="w-120">{t('app.shared.schedules_table.price')}</th>
|
||||
{showCustomer && <th className="w-200">{t('app.shared.schedules_table.customer')}</th>}
|
||||
<th className="w-200"/>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{paymentSchedules.map(p => <tr key={p.id}>
|
||||
<td colSpan={showCustomer ? 6 : 5}>
|
||||
<table className="schedules-table-body">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="w-35 row-header" onClick={togglePaymentScheduleDetails(p.id)}>{expandCollapseIcon(p.id)}</td>
|
||||
<td className="w-200">{p.reference}</td>
|
||||
<td className="w-200">{FormatLib.date(_.minBy(p.items, 'due_date').due_date)}</td>
|
||||
<td className="w-120">{FormatLib.price(p.total)}</td>
|
||||
{showCustomer && <td className="w-200">{p.user.name}</td>}
|
||||
<td className="w-200">{downloadButton(TargetType.PaymentSchedule, p.id)}</td>
|
||||
</tr>
|
||||
<tr style={{ display: statusDisplay(p.id) }}>
|
||||
<td className="w-35" />
|
||||
<td colSpan={showCustomer ? 5 : 4}>
|
||||
<div>
|
||||
<table className="schedule-items-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="w-120">{t('app.shared.schedules_table.deadline')}</th>
|
||||
<th className="w-120">{t('app.shared.schedules_table.amount')}</th>
|
||||
<th className="w-200">{t('app.shared.schedules_table.state')}</th>
|
||||
<th className="w-200" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{_.orderBy(p.items, 'due_date').map(item => <tr key={item.id}>
|
||||
<td>{FormatLib.date(item.due_date)}</td>
|
||||
<td>{FormatLib.price(item.amount)}</td>
|
||||
<td>{formatState(item)}</td>
|
||||
<td>{itemButtons(item, p)}</td>
|
||||
</tr>)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>)}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="modals">
|
||||
<FabModal title={t('app.shared.schedules_table.confirm_check_cashing')}
|
||||
isOpen={showConfirmCashing}
|
||||
toggleModal={toggleConfirmCashingModal}
|
||||
onConfirm={onCheckCashingConfirmed}
|
||||
closeButton={true}
|
||||
confirmButton={t('app.shared.schedules_table.confirm_button')}>
|
||||
{tempDeadline && <span>
|
||||
{t('app.shared.schedules_table.confirm_check_cashing_body', {
|
||||
AMOUNT: FormatLib.price(tempDeadline.amount),
|
||||
DATE: FormatLib.date(tempDeadline.due_date)
|
||||
})}
|
||||
</span>}
|
||||
</FabModal>
|
||||
<FabModal title={t('app.shared.schedules_table.cancel_subscription')}
|
||||
isOpen={showCancelSubscription}
|
||||
toggleModal={toggleCancelSubscriptionModal}
|
||||
onConfirm={onCancelSubscriptionConfirmed}
|
||||
closeButton={true}
|
||||
confirmButton={t('app.shared.schedules_table.confirm_button')}>
|
||||
{t('app.shared.schedules_table.confirm_cancel_subscription')}
|
||||
</FabModal>
|
||||
<StripeElements>
|
||||
<FabModal title={t('app.shared.schedules_table.resolve_action')}
|
||||
isOpen={showResolveAction}
|
||||
toggleModal={toggleResolveActionModal}
|
||||
onConfirm={afterAction}
|
||||
confirmButton={t('app.shared.schedules_table.ok_button')}
|
||||
preventConfirm={isConfirmActionDisabled}>
|
||||
{tempDeadline && <StripeConfirm clientSecret={tempDeadline.client_secret} onResponse={toggleConfirmActionButton} />}
|
||||
</FabModal>
|
||||
{tempSchedule && <UpdateCardModal isOpen={showUpdateCard}
|
||||
toggleModal={toggleUpdateCardModal}
|
||||
operator={operator}
|
||||
afterSuccess={handleCardUpdateSuccess}
|
||||
onError={handleCardUpdateError}
|
||||
schedule={tempSchedule}>
|
||||
</UpdateCardModal>}
|
||||
</StripeElements>
|
||||
</div>
|
||||
<StripeElements>
|
||||
<table className="schedules-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="w-35" />
|
||||
<th className="w-200">{t('app.shared.schedules_table.schedule_num')}</th>
|
||||
<th className="w-200">{t('app.shared.schedules_table.date')}</th>
|
||||
<th className="w-120">{t('app.shared.schedules_table.price')}</th>
|
||||
{showCustomer && <th className="w-200">{t('app.shared.schedules_table.customer')}</th>}
|
||||
<th className="w-200"/>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{paymentSchedules.map(p => <tr key={p.id}>
|
||||
<td colSpan={showCustomer ? 6 : 5}>
|
||||
<table className="schedules-table-body">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="w-35 row-header" onClick={togglePaymentScheduleDetails(p.id)}>{expandCollapseIcon(p.id)}</td>
|
||||
<td className="w-200">{p.reference}</td>
|
||||
<td className="w-200">{FormatLib.date(_.minBy(p.items, 'due_date').due_date)}</td>
|
||||
<td className="w-120">{FormatLib.price(p.total)}</td>
|
||||
{showCustomer && <td className="w-200">{p.user.name}</td>}
|
||||
<td className="w-200">{downloadScheduleButton(p.id)}</td>
|
||||
</tr>
|
||||
<tr style={{ display: statusDisplay(p.id) }}>
|
||||
<td className="w-35" />
|
||||
<td colSpan={showCustomer ? 5 : 4}>
|
||||
<div>
|
||||
<table className="schedule-items-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="w-120">{t('app.shared.schedules_table.deadline')}</th>
|
||||
<th className="w-120">{t('app.shared.schedules_table.amount')}</th>
|
||||
<th className="w-200">{t('app.shared.schedules_table.state')}</th>
|
||||
<th className="w-200" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{_.orderBy(p.items, 'due_date').map(item => <tr key={item.id}>
|
||||
<td>{FormatLib.date(item.due_date)}</td>
|
||||
<td>{FormatLib.price(item.amount)}</td>
|
||||
<td>{formatState(item, p)}</td>
|
||||
<td>
|
||||
<PaymentScheduleItemActions paymentScheduleItem={item}
|
||||
paymentSchedule={p}
|
||||
onError={onError}
|
||||
onSuccess={refreshSchedulesTable}
|
||||
onCardUpdateSuccess={onCardUpdateSuccess}
|
||||
operator={operator}
|
||||
displayOnceMap={displayOnceMap}
|
||||
show={isExpanded(p.id)}/>
|
||||
</td>
|
||||
</tr>)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>)}
|
||||
</tbody>
|
||||
</table>
|
||||
</StripeElements>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,73 @@
|
||||
import React from 'react';
|
||||
import Select from 'react-select';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FabModal } from '../base/fab-modal';
|
||||
import { PaymentMethod, PaymentSchedule } from '../../models/payment-schedule';
|
||||
import PaymentScheduleAPI from '../../api/payment-schedule';
|
||||
|
||||
interface UpdatePaymentMeanModalProps {
|
||||
isOpen: boolean,
|
||||
toggleModal: () => void,
|
||||
onError: (message: string) => void,
|
||||
afterSuccess: () => void,
|
||||
paymentSchedule: PaymentSchedule
|
||||
}
|
||||
|
||||
/**
|
||||
* Option format, expected by react-select
|
||||
* @see https://github.com/JedWatson/react-select
|
||||
*/
|
||||
type selectOption = { value: PaymentMethod, label: string };
|
||||
|
||||
export const UpdatePaymentMeanModal: React.FC<UpdatePaymentMeanModalProps> = ({ isOpen, toggleModal, onError, afterSuccess, paymentSchedule }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
const [paymentMean, setPaymentMean] = React.useState<PaymentMethod>();
|
||||
|
||||
/**
|
||||
* Convert all payment means to the react-select format
|
||||
*/
|
||||
const buildOptions = (): Array<selectOption> => {
|
||||
return Object.keys(PaymentMethod).filter(pm => PaymentMethod[pm] !== PaymentMethod.Card).map(pm => {
|
||||
return { value: PaymentMethod[pm], label: t(`app.admin.update_payment_mean_modal.method_${pm}`) };
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* When the payment mean is changed in the select, update the state
|
||||
*/
|
||||
const handleMeanSelected = (option: selectOption): void => {
|
||||
setPaymentMean(option.value);
|
||||
};
|
||||
|
||||
/**
|
||||
* When the user clicks on the update button, update the default payment mean for the given payment schedule
|
||||
*/
|
||||
const handlePaymentMeanUpdate = (): void => {
|
||||
PaymentScheduleAPI.update({
|
||||
id: paymentSchedule.id,
|
||||
payment_method: paymentMean
|
||||
}).then(() => {
|
||||
afterSuccess();
|
||||
}).catch(error => {
|
||||
onError(error.message);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<FabModal isOpen={isOpen}
|
||||
className="update-payment-mean-modal"
|
||||
title={t('app.admin.update_payment_mean_modal.title')}
|
||||
confirmButton={t('app.admin.update_payment_mean_modal.confirm_button')}
|
||||
onConfirm={handlePaymentMeanUpdate}
|
||||
toggleModal={toggleModal}
|
||||
closeButton={true}>
|
||||
<span>{t('app.admin.update_payment_mean_modal.update_info')}</span>
|
||||
<Select className="payment-mean-select"
|
||||
placeholder={t('app.admin.update_payment_mean_modal.select_payment_mean')}
|
||||
id="payment-mean"
|
||||
onChange={handleMeanSelected}
|
||||
options={buildOptions()}></Select>
|
||||
</FabModal>
|
||||
);
|
||||
};
|
@ -55,7 +55,7 @@ interface AbstractPaymentModalProps {
|
||||
/**
|
||||
* This component is an abstract modal that must be extended by each payment gateway to include its payment form.
|
||||
*
|
||||
* This component must not be called directly but must be extended for each implemented payment gateway
|
||||
* This component must not be called directly but must be extended for each implemented payment gateway.
|
||||
* @see https://reactjs.org/docs/composition-vs-inheritance.html
|
||||
*/
|
||||
export const AbstractPaymentModal: React.FC<AbstractPaymentModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, cart, updateCart, currentUser, schedule, customer, logoFooter, GatewayForm, formId, className, formClassName, title, preventCgv, preventScheduleInfo, modalSize }) => {
|
||||
|
@ -8,9 +8,9 @@ import SettingAPI from '../../../api/setting';
|
||||
import { SettingName } from '../../../models/setting';
|
||||
import { PaymentModal } from '../payment-modal';
|
||||
import { PaymentSchedule } from '../../../models/payment-schedule';
|
||||
import { PaymentMethod } from '../../../models/payment';
|
||||
import { HtmlTranslate } from '../../base/html-translate';
|
||||
|
||||
const ALL_SCHEDULE_METHODS = ['card', 'check'] as const;
|
||||
const ALL_SCHEDULE_METHODS = ['card', 'check', 'transfer'] as const;
|
||||
type scheduleMethod = typeof ALL_SCHEDULE_METHODS[number];
|
||||
|
||||
/**
|
||||
@ -31,11 +31,7 @@ export const LocalPaymentForm: React.FC<GatewayFormProps> = ({ onSubmit, onSucce
|
||||
const [onlinePaymentModal, setOnlinePaymentModal] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (cart.payment_method === PaymentMethod.Card) {
|
||||
setMethod('card');
|
||||
} else {
|
||||
setMethod('check');
|
||||
}
|
||||
setMethod(cart.payment_method || 'check');
|
||||
}, [cart]);
|
||||
|
||||
/**
|
||||
@ -65,11 +61,7 @@ export const LocalPaymentForm: React.FC<GatewayFormProps> = ({ onSubmit, onSucce
|
||||
* Callback triggered when the user selects a payment method for the current payment schedule.
|
||||
*/
|
||||
const handleUpdateMethod = (option: selectOption) => {
|
||||
if (option.value === 'card') {
|
||||
updateCart(Object.assign({}, cart, { payment_method: PaymentMethod.Card }));
|
||||
} else {
|
||||
updateCart(Object.assign({}, cart, { payment_method: PaymentMethod.Other }));
|
||||
}
|
||||
updateCart(Object.assign({}, cart, { payment_method: option.value }));
|
||||
setMethod(option.value);
|
||||
};
|
||||
|
||||
@ -140,6 +132,7 @@ export const LocalPaymentForm: React.FC<GatewayFormProps> = ({ onSubmit, onSucce
|
||||
value={methodToOption(method)} />
|
||||
{method === 'card' && <p>{t('app.admin.local_payment.card_collection_info')}</p>}
|
||||
{method === 'check' && <p>{t('app.admin.local_payment.check_collection_info', { DEADLINES: paymentSchedule.items.length })}</p>}
|
||||
{method === 'transfer' && <HtmlTranslate trKey="app.admin.local_payment.transfer_collection_info" options={{ DEADLINES: paymentSchedule.items.length }} />}
|
||||
</div>
|
||||
<div className="full-schedule">
|
||||
<ul>
|
||||
|
@ -0,0 +1,55 @@
|
||||
import { StripeConfirm } from './stripe-confirm';
|
||||
import { FabModal } from '../../base/fab-modal';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PaymentScheduleAPI from '../../../api/payment-schedule';
|
||||
import { PaymentScheduleItem } from '../../../models/payment-schedule';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface StripeConfirmModalProps {
|
||||
isOpen: boolean,
|
||||
toggleModal: () => void,
|
||||
onSuccess: () => void,
|
||||
paymentScheduleItemId: number,
|
||||
}
|
||||
|
||||
/**
|
||||
* Modal dialog that trigger a 3D secure confirmation for the given payment schedule item (deadline for a payment schedule).
|
||||
*/
|
||||
export const StripeConfirmModal: React.FC<StripeConfirmModalProps> = ({ isOpen, toggleModal, onSuccess, paymentScheduleItemId }) => {
|
||||
const { t } = useTranslation('shared');
|
||||
|
||||
const [item, setItem] = useState<PaymentScheduleItem>(null);
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
PaymentScheduleAPI.getItem(paymentScheduleItemId).then(data => {
|
||||
setItem(data);
|
||||
});
|
||||
}, [paymentScheduleItemId]);
|
||||
|
||||
/**
|
||||
* Callback triggered when the confirm button was clicked in the modal.
|
||||
*/
|
||||
const onConfirmed = (): void => {
|
||||
togglePending();
|
||||
onSuccess();
|
||||
};
|
||||
|
||||
/**
|
||||
* Enable/disable the confirm button of the "action" modal
|
||||
*/
|
||||
const togglePending = (): void => {
|
||||
setIsPending(!isPending);
|
||||
};
|
||||
|
||||
return (
|
||||
<FabModal title={t('app.shared.schedules_table.resolve_action')}
|
||||
isOpen={isOpen}
|
||||
toggleModal={toggleModal}
|
||||
onConfirm={onConfirmed}
|
||||
confirmButton={t('app.shared.schedules_table.ok_button')}
|
||||
preventConfirm={isPending}>
|
||||
{item && <StripeConfirm clientSecret={item.client_secret} onResponse={togglePending} />}
|
||||
</FabModal>
|
||||
);
|
||||
};
|
@ -1,6 +1,6 @@
|
||||
import React, { memo, useEffect, useState } from 'react';
|
||||
import { Elements } from '@stripe/react-stripe-js';
|
||||
import { loadStripe } from '@stripe/stripe-js';
|
||||
import { loadStripe, Stripe } from '@stripe/stripe-js';
|
||||
import { SettingName } from '../../../models/setting';
|
||||
import SettingAPI from '../../../api/setting';
|
||||
|
||||
@ -8,15 +8,17 @@ import SettingAPI from '../../../api/setting';
|
||||
* This component initializes the stripe's Elements tag with the API key
|
||||
*/
|
||||
export const StripeElements: React.FC = memo(({ children }) => {
|
||||
const [stripe, setStripe] = useState(undefined);
|
||||
const [stripe, setStripe] = useState<Promise<Stripe | null>>(undefined);
|
||||
|
||||
/**
|
||||
* When this component is mounted, we initialize the <Elements> tag with the Stripe's public key
|
||||
*/
|
||||
useEffect(() => {
|
||||
SettingAPI.get(SettingName.StripePublicKey).then(key => {
|
||||
const promise = loadStripe(key.value);
|
||||
setStripe(promise);
|
||||
if (key?.value) {
|
||||
const promise = loadStripe(key.value);
|
||||
setStripe(promise);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
@ -24,11 +24,7 @@ const UpdateCardModalComponent: React.FC<UpdateCardModalProps> = ({ isOpen, togg
|
||||
const [gateway, setGateway] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
if (schedule.gateway_subscription.classname.match(/^PayZen::/)) {
|
||||
setGateway('payzen');
|
||||
} else if (schedule.gateway_subscription.classname.match(/^Stripe::/)) {
|
||||
setGateway('stripe');
|
||||
}
|
||||
setGateway(schedule.gateway);
|
||||
}, [schedule]);
|
||||
|
||||
/**
|
||||
@ -44,7 +40,7 @@ const UpdateCardModalComponent: React.FC<UpdateCardModalProps> = ({ isOpen, togg
|
||||
|
||||
/**
|
||||
* Render the PayZen update-card modal
|
||||
*/ // 1
|
||||
*/
|
||||
const renderPayZenModal = (): ReactElement => {
|
||||
return <PayzenCardUpdateModal isOpen={isOpen}
|
||||
toggleModal={toggleModal}
|
||||
@ -58,15 +54,16 @@ const UpdateCardModalComponent: React.FC<UpdateCardModalProps> = ({ isOpen, togg
|
||||
*/
|
||||
|
||||
switch (gateway) {
|
||||
case 'stripe':
|
||||
case 'Stripe':
|
||||
return renderStripeModal();
|
||||
case 'payzen':
|
||||
case 'PayZen':
|
||||
return renderPayZenModal();
|
||||
case '':
|
||||
case undefined:
|
||||
return <div/>;
|
||||
default:
|
||||
onError(t('app.shared.update_card_modal.unexpected_error'));
|
||||
console.error(`[UpdateCardModal] unexpected gateway: ${schedule.gateway_subscription?.classname}`);
|
||||
console.error(`[UpdateCardModal] unexpected gateway: ${schedule.gateway} for schedule ${schedule.id}`);
|
||||
return <div />;
|
||||
}
|
||||
};
|
||||
|
@ -35,7 +35,7 @@ const EditPlanCategoryComponent: React.FC<EditPlanCategoryProps> = ({ onSuccess,
|
||||
|
||||
/**
|
||||
* The edit has been confirmed by the user.
|
||||
* Call the API to trigger the update of the temporary set plan-category
|
||||
* Call the API to trigger the update of the temporary set plan-category.
|
||||
*/
|
||||
const onEditConfirmed = (): void => {
|
||||
PlanCategoryAPI.update(tempCategory).then((updatedCategory) => {
|
||||
|
@ -28,10 +28,10 @@ export const ConfigureExtendedPriceButton: React.FC<ConfigureExtendedPriceButton
|
||||
const [showList, setShowList] = useState<boolean>(false);
|
||||
|
||||
/**
|
||||
* Return the number of minutes, user-friendly formatted
|
||||
* Return the number of hours, user-friendly formatted
|
||||
*/
|
||||
const formatDuration = (minutes: number): string => {
|
||||
return t('app.admin.configure_extended_prices_button.extended_price_DURATION', { DURATION: minutes });
|
||||
return t('app.admin.configure_extended_prices_button.extended_price_DURATION', { DURATION: minutes / 60 });
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -40,11 +40,11 @@ export const ExtendedPriceForm: React.FC<ExtendedPriceFormProps> = ({ formId, on
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when the user inputs a number of minutes for the current extended price.
|
||||
* Callback triggered when the user inputs a number of hours for the current extended price.
|
||||
*/
|
||||
const handleUpdateHours = (minutes: string) => {
|
||||
updateExtendedPriceData(draft => {
|
||||
draft.duration = parseInt(minutes, 10);
|
||||
draft.duration = parseFloat(minutes) * 60;
|
||||
});
|
||||
};
|
||||
|
||||
@ -53,10 +53,10 @@ export const ExtendedPriceForm: React.FC<ExtendedPriceFormProps> = ({ formId, on
|
||||
<label htmlFor="duration">{t('app.admin.extended_price_form.duration')} *</label>
|
||||
<FabInput id="duration"
|
||||
type="number"
|
||||
defaultValue={extendedPriceData?.duration || ''}
|
||||
defaultValue={extendedPriceData?.duration / 60 || ''}
|
||||
onChange={handleUpdateHours}
|
||||
step={1}
|
||||
min={1}
|
||||
step={0.25}
|
||||
min={0.5}
|
||||
icon={<i className="fas fa-clock" />}
|
||||
required />
|
||||
<label htmlFor="amount">{t('app.admin.extended_price_form.amount')} *</label>
|
||||
|
@ -9,7 +9,7 @@ interface AvatarProps {
|
||||
}
|
||||
|
||||
/**
|
||||
* This component renders the user-profile's picture or a placeholder
|
||||
* This component renders the user-profile's picture or a placeholder.
|
||||
*/
|
||||
export const Avatar: React.FC<AvatarProps> = ({ user, className }) => {
|
||||
/**
|
||||
|
@ -407,10 +407,30 @@ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state
|
||||
// check if slot is not in the past
|
||||
const today = new Date();
|
||||
if (Math.trunc((start.valueOf() - today) / (60 * 1000)) < 0) {
|
||||
growl.warning(_t('app.admin.calendar.event_in_the_past'));
|
||||
return uiCalendarConfig.calendars.calendar.fullCalendar('unselect');
|
||||
return dialogs.confirm({
|
||||
resolve: {
|
||||
object () {
|
||||
return {
|
||||
title: _t('app.admin.calendar.event_in_the_past'),
|
||||
msg: _t('app.admin.calendar.confirm_create_event_in_the_past')
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
function () { // confirmed
|
||||
startAvailabilityCreation(start, end);
|
||||
}, function () { // canceled
|
||||
uiCalendarConfig.calendars.calendar.fullCalendar('unselect');
|
||||
});
|
||||
}
|
||||
|
||||
startAvailabilityCreation(start, end);
|
||||
};
|
||||
|
||||
/**
|
||||
* Start the process to create a new availability between the given start and end datetimes
|
||||
*/
|
||||
const startAvailabilityCreation = function (start, end) {
|
||||
// check that the selected slot is an multiple of SLOT_MULTIPLE (ie. not decimal)
|
||||
const slots = Math.trunc((end.valueOf() - start.valueOf()) / (60 * 1000)) / SLOT_MULTIPLE;
|
||||
if (!Number.isInteger(slots)) {
|
||||
|
@ -478,6 +478,13 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
|
||||
$scope.isSelected = active;
|
||||
$scope.history = [];
|
||||
|
||||
// callback on "enable VAT" switch toggle
|
||||
$scope.enableVATChanged = function (checked) {
|
||||
setTimeout(() => {
|
||||
$scope.isSelected = checked;
|
||||
$scope.$apply();
|
||||
}, 1);
|
||||
};
|
||||
$scope.ok = function () { $uibModalInstance.close({ rate: $scope.rate, active: $scope.isSelected }); };
|
||||
$scope.cancel = function () { $uibModalInstance.dismiss('cancel'); };
|
||||
$scope.editMultiVAT = function () {
|
||||
|
@ -1,11 +1,18 @@
|
||||
'use strict';
|
||||
|
||||
Application.Controllers.controller('HeaderController', ['$scope', '$rootScope', '$state',
|
||||
function ($scope, $rootScope, $state) {
|
||||
Application.Controllers.controller('HeaderController', ['$scope', '$rootScope', '$state', 'settingsPromise',
|
||||
function ($scope, $rootScope, $state, settingsPromise) {
|
||||
$scope.aboutPage = ($state.current.name === 'app.public.about');
|
||||
|
||||
$rootScope.$on('$stateChangeStart', function (event, toState) {
|
||||
$scope.aboutPage = (toState.name === 'app.public.about');
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns the current state of the public registration setting (allowed/blocked).
|
||||
*/
|
||||
$scope.registrationEnabled = function () {
|
||||
return settingsPromise.public_registrations === 'true';
|
||||
};
|
||||
}
|
||||
]);
|
||||
|
@ -13,7 +13,7 @@
|
||||
/**
|
||||
* Navigation controller. List the links availables in the left navigation pane and their icon.
|
||||
*/
|
||||
Application.Controllers.controller('MainNavController', ['$scope', function ($scope) {
|
||||
Application.Controllers.controller('MainNavController', ['$scope', 'settingsPromise', function ($scope, settingsPromise) {
|
||||
// Common links (public application)
|
||||
$scope.navLinks = [
|
||||
{
|
||||
@ -172,5 +172,12 @@ Application.Controllers.controller('MainNavController', ['$scope', function ($sc
|
||||
authorizedRoles: ['admin']
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current state of the public registration setting (allowed/blocked).
|
||||
*/
|
||||
$scope.registrationEnabled = function () {
|
||||
return settingsPromise.public_registrations === 'true';
|
||||
};
|
||||
}
|
||||
]);
|
||||
|
@ -34,8 +34,8 @@ Application.Directives.directive('fileread', [() =>
|
||||
Application.Directives.directive('bsHolder', [() =>
|
||||
({
|
||||
link (scope, element, attrs) {
|
||||
Holder.addTheme('icon', { background: 'white', foreground: '#e9e9e9', size: 80, font: 'FontAwesome' })
|
||||
.addTheme('icon-xs', { background: 'white', foreground: '#e0e0e0', size: 20, font: 'FontAwesome' })
|
||||
Holder.addTheme('icon', { background: 'white', foreground: '#ebebeb', size: 60, font: 'FontAwesome' })
|
||||
.addTheme('icon-xs', { background: 'white', foreground: '#ebebeb', size: 20, font: 'FontAwesome' })
|
||||
.addTheme('icon-black-xs', { background: 'black', foreground: 'white', size: 20, font: 'FontAwesome' })
|
||||
.addTheme('avatar', { background: '#eeeeee', foreground: '#555555', size: 16, font: 'FontAwesome' })
|
||||
.run(element[0]);
|
||||
|
@ -4,13 +4,16 @@ export enum PaymentScheduleItemState {
|
||||
RequirePaymentMethod = 'requires_payment_method',
|
||||
RequireAction = 'requires_action',
|
||||
Paid = 'paid',
|
||||
Error = 'error'
|
||||
Error = 'error',
|
||||
GatewayCanceled = 'gateway_canceled'
|
||||
}
|
||||
|
||||
export enum PaymentMethod {
|
||||
Stripe = 'stripe',
|
||||
Card = 'card',
|
||||
Transfer = 'transfer',
|
||||
Check = 'check'
|
||||
}
|
||||
|
||||
export interface PaymentScheduleItem {
|
||||
id: number,
|
||||
amount: number,
|
||||
@ -22,30 +25,28 @@ export interface PaymentScheduleItem {
|
||||
}
|
||||
|
||||
export interface PaymentSchedule {
|
||||
max_length: number;
|
||||
max_length?: number;
|
||||
id: number,
|
||||
total: number,
|
||||
reference: string,
|
||||
payment_method: string,
|
||||
items: Array<PaymentScheduleItem>,
|
||||
created_at: Date,
|
||||
chained_footprint: boolean,
|
||||
main_object: {
|
||||
total?: number,
|
||||
reference?: string,
|
||||
payment_method: PaymentMethod,
|
||||
items?: Array<PaymentScheduleItem>,
|
||||
created_at?: Date,
|
||||
chained_footprint?: boolean,
|
||||
main_object?: {
|
||||
type: string,
|
||||
id: number
|
||||
},
|
||||
user: {
|
||||
user?: {
|
||||
id: number,
|
||||
name: string
|
||||
},
|
||||
operator: {
|
||||
operator?: {
|
||||
id: number,
|
||||
first_name: string,
|
||||
last_name: string,
|
||||
},
|
||||
gateway_subscription: {
|
||||
classname: string
|
||||
}
|
||||
gateway?: 'PayZen' | 'Stripe',
|
||||
}
|
||||
|
||||
export interface PaymentScheduleIndexRequest {
|
||||
|
@ -18,7 +18,8 @@ export interface IntentConfirmation {
|
||||
|
||||
export enum PaymentMethod {
|
||||
Card = 'card',
|
||||
Other = ''
|
||||
Check = 'check',
|
||||
Transfer = 'transfer'
|
||||
}
|
||||
|
||||
export type CartItem = { reservation: Reservation }|
|
||||
|
@ -117,7 +117,8 @@ export enum SettingName {
|
||||
RenewPackThreshold = 'renew_pack_threshold',
|
||||
PackOnlyForSubscription = 'pack_only_for_subscription',
|
||||
OverlappingCategories = 'overlapping_categories',
|
||||
ExtendedPricesInSameDay = 'extended_prices_in_same_day'
|
||||
ExtendedPricesInSameDay = 'extended_prices_in_same_day',
|
||||
PublicRegistrations = 'public_registrations'
|
||||
}
|
||||
|
||||
export type SettingValue = string|boolean|number;
|
||||
|
@ -38,7 +38,8 @@ angular.module('application.router', ['ui.router'])
|
||||
logoFile: ['CustomAsset', function (CustomAsset) { return CustomAsset.get({ name: 'logo-file' }).$promise; }],
|
||||
logoBlackFile: ['CustomAsset', function (CustomAsset) { return CustomAsset.get({ name: 'logo-black-file' }).$promise; }],
|
||||
sharedTranslations: ['Translations', function (Translations) { return Translations.query(['app.shared', 'app.public.common']).$promise; }],
|
||||
modulesPromise: ['Setting', function (Setting) { return Setting.query({ names: "['spaces_module', 'plans_module', 'invoicing_module', 'wallet_module', 'statistics_module', 'trainings_module', 'public_agenda_module']" }).$promise; }]
|
||||
modulesPromise: ['Setting', function (Setting) { return Setting.query({ names: "['spaces_module', 'plans_module', 'invoicing_module', 'wallet_module', 'statistics_module', 'trainings_module', 'public_agenda_module']" }).$promise; }],
|
||||
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['public_registrations']" }).$promise; }]
|
||||
},
|
||||
onEnter: ['$rootScope', 'logoFile', 'logoBlackFile', 'modulesPromise', 'CSRF', function ($rootScope, logoFile, logoBlackFile, modulesPromise, CSRF) {
|
||||
// Retrieve Anti-CSRF tokens from cookies
|
||||
@ -1081,7 +1082,8 @@ angular.module('application.router', ['ui.router'])
|
||||
"'reminder_delay', 'visibility_yearly', 'visibility_others', 'wallet_module', 'trainings_module', " +
|
||||
"'display_name_enable', 'machines_sort_by', 'fab_analytics', 'statistics_module', 'address_required', " +
|
||||
"'link_name', 'home_content', 'home_css', 'phone_required', 'upcoming_events_shown', 'public_agenda_module'," +
|
||||
"'renew_pack_threshold', 'pack_only_for_subscription', 'overlapping_categories', 'extended_prices_in_same_day']"
|
||||
"'renew_pack_threshold', 'pack_only_for_subscription', 'overlapping_categories', 'public_registrations'," +
|
||||
"'extended_prices_in_same_day']"
|
||||
}).$promise;
|
||||
}],
|
||||
privacyDraftsPromise: ['Setting', function (Setting) { return Setting.get({ name: 'privacy_draft', history: true }).$promise; }],
|
||||
|
@ -191,7 +191,9 @@
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
max-height: 400px;
|
||||
aspect-ratio: 16/9;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
}
|
||||
|
||||
@ -245,6 +247,8 @@
|
||||
height: 250px;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-color: transparent;
|
||||
|
||||
@include transition(opacity 0.5s ease);
|
||||
|
||||
@ -302,6 +306,7 @@
|
||||
|
||||
&.avatar-block {
|
||||
white-space: inherit;
|
||||
width: 100%;
|
||||
height: 76px;
|
||||
|
||||
.user-name {
|
||||
|
@ -661,8 +661,121 @@ body.container {
|
||||
}
|
||||
|
||||
.home-events {
|
||||
.event-description {
|
||||
h4 {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
column-gap: 1rem;
|
||||
i { margin-right: 1rem;}
|
||||
}
|
||||
}
|
||||
|
||||
.home-events-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
|
||||
gap: 30px;
|
||||
margin-bottom: 5rem;
|
||||
@media (max-width: 480px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.Event {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
& .Event-picture {opacity: 0.7;}
|
||||
}
|
||||
|
||||
&-picture {
|
||||
height: 250px;
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #ebebeb;
|
||||
transition: opacity 0.4s ease-out;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
&-desc {
|
||||
position: relative;
|
||||
padding: 15px;
|
||||
h3 {
|
||||
max-width: 75%;
|
||||
margin-top: 0;
|
||||
font-size: 2rem;
|
||||
font-weight: 900;
|
||||
line-height: 1.3;
|
||||
text-transform: uppercase;
|
||||
color: #000 !important;
|
||||
}
|
||||
span {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
top: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
&-info {
|
||||
display: grid;
|
||||
grid-template-rows: repeat(2, max-content);
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 15px;
|
||||
margin-top: auto;
|
||||
padding: 15px 30px 30px;
|
||||
border-top: 1px solid #eee;
|
||||
&-item {
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
i {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
color: #cb1117;
|
||||
}
|
||||
h6 { margin: 0 0 0 15px;}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.month-events-list {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 15px;
|
||||
margin-bottom: 2rem;
|
||||
@media (min-width: 768px) {
|
||||
grid-template-columns: repeat(auto-fill, minmax(440px, 1fr));
|
||||
}
|
||||
|
||||
.Event {
|
||||
display: flex;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
|
||||
&-desc {
|
||||
flex: 1;
|
||||
padding: 10px 15px 15px;
|
||||
}
|
||||
&-picture {
|
||||
width: 33%;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
@media (max-width: 500px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -76,3 +76,5 @@
|
||||
@import "modules/subscriptions/renew-modal";
|
||||
|
||||
@import "app.responsive";
|
||||
|
||||
@import "overrides"
|
||||
|
@ -53,26 +53,28 @@
|
||||
height: 250px;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
transition: opacity 0.5s ease;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
color: #333333;
|
||||
background-color: #f5f5f5;
|
||||
border-color: #ddd;
|
||||
border-bottom: 1px solid transparent;
|
||||
border-bottom: 1px solid #ebebeb;
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
position: relative;
|
||||
|
||||
&.no-picture::before {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
content: '\f03e';
|
||||
font-family: 'Font Awesome 5 Free' !important;
|
||||
font-weight: 900;
|
||||
font-size: 4em;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding-top: 84px;
|
||||
font-size: 80px;
|
||||
color: #ebebeb;
|
||||
}
|
||||
}
|
||||
|
||||
|
26
app/frontend/src/stylesheets/overrides.scss
Normal file
26
app/frontend/src/stylesheets/overrides.scss
Normal file
@ -0,0 +1,26 @@
|
||||
.carousel {
|
||||
position: relative;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
&::before {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
content: '\f03e';
|
||||
font-family: 'Font Awesome 5 Free' !important;
|
||||
font-weight: 900;
|
||||
font-size: 80px;
|
||||
color: #ebebeb;
|
||||
}
|
||||
|
||||
&-inner {
|
||||
aspect-ratio: 16/9;
|
||||
}
|
||||
|
||||
&-inner > .item {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@
|
||||
<div class="row no-gutter">
|
||||
<div class="col-xs-2 col-sm-2 col-md-1">
|
||||
<section class="heading-btn">
|
||||
<a href="#" ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-xs-10 col-sm-10 col-md-8 b-l">
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="row no-gutter">
|
||||
<div class="col-xs-2 col-sm-2 col-md-1">
|
||||
<section class="heading-btn">
|
||||
<a href="#" ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-xs-10 col-sm-10 col-md-8 b-l">
|
||||
|
@ -64,10 +64,11 @@
|
||||
</td>
|
||||
<td ng-class="{'has-error': mappingForm['auth_mapping[api_field]'].$dirty && mappingForm['auth_mapping[api_field]'].$invalid}">
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
class="form-control help-cursor"
|
||||
placeholder="field_name"
|
||||
ng-model="newMapping.api_field"
|
||||
name="auth_mapping[api_field]"
|
||||
title="{{ 'app.shared.oauth2.api_field_help' | translate }}"
|
||||
required/>
|
||||
</td>
|
||||
<td>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="row no-gutter">
|
||||
<div class="col-xs-2 col-sm-2 col-md-1">
|
||||
<section class="heading-btn">
|
||||
<a href="#" ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-md-8 b-l b-r-md">
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="row no-gutter b-b">
|
||||
<div class="col-xs-2 col-sm-2 col-md-1">
|
||||
<section class="heading-btn">
|
||||
<a href="#" ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-xs-10 col-sm-10 col-md-8 b-l b-r-md">
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="row no-gutter b-b">
|
||||
<div class="col-xs-2 col-sm-2 col-md-1">
|
||||
<section class="heading-btn">
|
||||
<a href="#" ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-xs-10 col-sm-10 col-md-8 b-l">
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="row no-gutter">
|
||||
<div class="col-xs-2 col-sm-2 col-md-1">
|
||||
<section class="heading-btn">
|
||||
<a href="#" ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-xs-10 col-sm-10 col-md-8 b-l b-r">
|
||||
|
@ -37,13 +37,13 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:8%"></th>
|
||||
<th style="width:14%"><a href="" ng-click="setOrderInvoice('reference')">{{ 'app.admin.invoices.invoice_num' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': orderInvoice=='reference', 'fa fa-sort-numeric-desc': orderInvoice=='-reference', 'fa fa-arrows-v': orderInvoice }"></i></a></th>
|
||||
<th style="width:14%"><a ng-click="setOrderInvoice('reference')">{{ 'app.admin.invoices.invoice_num' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': orderInvoice=='reference', 'fa fa-sort-numeric-desc': orderInvoice=='-reference', 'fa fa-arrows-v': orderInvoice }"></i></a></th>
|
||||
|
||||
<th style="width:19%"><a href="" ng-click="setOrderInvoice('date')">{{ 'app.admin.invoices.date' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': orderInvoice=='date', 'fa fa-sort-numeric-desc': orderInvoice=='-date', 'fa fa-arrows-v': orderInvoice }"></i></a></th>
|
||||
<th style="width:19%"><a ng-click="setOrderInvoice('date')">{{ 'app.admin.invoices.date' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': orderInvoice=='date', 'fa fa-sort-numeric-desc': orderInvoice=='-date', 'fa fa-arrows-v': orderInvoice }"></i></a></th>
|
||||
|
||||
<th style="width:9%"><a href="" ng-click="setOrderInvoice('total')"> {{ 'app.admin.invoices.price' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': orderInvoice=='total', 'fa fa-sort-numeric-desc': orderInvoice=='-total', 'fa fa-arrows-v': orderInvoice }"></i></a></th>
|
||||
<th style="width:9%"><a ng-click="setOrderInvoice('total')"> {{ 'app.admin.invoices.price' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': orderInvoice=='total', 'fa fa-sort-numeric-desc': orderInvoice=='-total', 'fa fa-arrows-v': orderInvoice }"></i></a></th>
|
||||
|
||||
<th style="width:20%"><a href="" ng-click="setOrderInvoice('name')">{{ 'app.admin.invoices.customer' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderInvoice=='name', 'fa fa-sort-alpha-desc': orderInvoice=='-name', 'fa fa-arrows-v': orderInvoice }"></i></a></th>
|
||||
<th style="width:20%"><a ng-click="setOrderInvoice('name')">{{ 'app.admin.invoices.customer' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderInvoice=='name', 'fa fa-sort-alpha-desc': orderInvoice=='-name', 'fa fa-arrows-v': orderInvoice }"></i></a></th>
|
||||
|
||||
<th style="width:30%"></th>
|
||||
</tr>
|
||||
@ -64,7 +64,7 @@
|
||||
<td ng-if="invoice.is_avoir">{{ invoice.date | amDateFormat:'L' }}</td>
|
||||
<td>{{ invoice.total | currency}}</td>
|
||||
<td>
|
||||
<a href="" ui-sref="app.admin.members_edit({id: invoice.user_id})" ng-show="invoice.user_id">{{ invoice.name }}</a>
|
||||
<a ui-sref="app.admin.members_edit({id: invoice.user_id})" ng-show="invoice.user_id">{{ invoice.name }}</a>
|
||||
<span ng-hide="invoice.user_id">{{ invoice.name }}</span>
|
||||
<td>
|
||||
<div class="buttons">
|
||||
@ -88,4 +88,4 @@
|
||||
<p ng-if="invoices.length == 0" translate>{{ 'app.admin.invoices.no_invoices_for_now' }}</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -5,14 +5,11 @@
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="enableVAT" class="control-label" translate>{{ 'app.admin.invoices.enable_VAT' }}</label>
|
||||
<input bs-switch
|
||||
ng-model="isSelected"
|
||||
id="enableVAT"
|
||||
type="checkbox"
|
||||
class="form-control m-l-sm"
|
||||
switch-on-text="{{ 'app.admin.invoices.enabled' | translate }}"
|
||||
switch-off-text="{{ 'app.admin.invoices.disabled' | translate }}"
|
||||
switch-animate="true"/>
|
||||
<switch id="enableVAT"
|
||||
checked="isSelected"
|
||||
on-change="enableVATChanged"
|
||||
classname="form-control m-l-sm">
|
||||
</switch>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="isSelected">
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="row no-gutter">
|
||||
<div class="col-xs-2 col-sm-2 col-md-1">
|
||||
<section class="heading-btn">
|
||||
<a href="#" ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-xs-10 col-sm-10 col-md-8 b-l">
|
||||
|
@ -14,13 +14,13 @@
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:15%"><a href="" ng-click="setOrderAdmin('profile_attributes.last_name')">{{ 'app.admin.members.surname' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderAdmin =='profile_attributes.last_name', 'fa fa-sort-alpha-desc': orderAdmin =='-profile_attributes.last_name', 'fa fa-arrows-v': orderAdmin }"></i></a></th>
|
||||
<th style="width:15%"><a ng-click="setOrderAdmin('profile_attributes.last_name')">{{ 'app.admin.members.surname' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderAdmin =='profile_attributes.last_name', 'fa fa-sort-alpha-desc': orderAdmin =='-profile_attributes.last_name', 'fa fa-arrows-v': orderAdmin }"></i></a></th>
|
||||
|
||||
<th style="width:15%"><a href="" ng-click="setOrderAdmin('profile_attributes.first_name')">{{ 'app.admin.members.first_name' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderAdmin =='profile_attributes.first_name', 'fa fa-sort-alpha-desc': orderAdmin =='-profile_attributes.first_name', 'fa fa-arrows-v': orderAdmin }"></i></a></th>
|
||||
<th style="width:15%"><a ng-click="setOrderAdmin('profile_attributes.first_name')">{{ 'app.admin.members.first_name' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderAdmin =='profile_attributes.first_name', 'fa fa-sort-alpha-desc': orderAdmin =='-profile_attributes.first_name', 'fa fa-arrows-v': orderAdmin }"></i></a></th>
|
||||
|
||||
<th style="width:15%"><a href="" ng-click="setOrderAdmin('email')">{{ 'app.admin.members.email' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderAdmin =='email', 'fa fa-sort-alpha-desc': orderAdmin =='-email', 'fa fa-arrows-v': orderAdmin }"></i></a></th>
|
||||
<th style="width:15%"><a ng-click="setOrderAdmin('email')">{{ 'app.admin.members.email' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderAdmin =='email', 'fa fa-sort-alpha-desc': orderAdmin =='-email', 'fa fa-arrows-v': orderAdmin }"></i></a></th>
|
||||
|
||||
<th style="width:10%"><a href="" ng-click="setOrderAdmin('profile_attributes.phone')">{{ 'app.admin.members.phone' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': orderAdmin =='profile_attributes.phone', 'fa fa-sort-numeric-desc': orderAdmin =='-profile_attributes.phone', 'fa fa-arrows-v': orderAdmin }"></i></a></th>
|
||||
<th style="width:10%"><a ng-click="setOrderAdmin('profile_attributes.phone')">{{ 'app.admin.members.phone' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': orderAdmin =='profile_attributes.phone', 'fa fa-sort-numeric-desc': orderAdmin =='-profile_attributes.phone', 'fa fa-arrows-v': orderAdmin }"></i></a></th>
|
||||
<th style="width:10%"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="row no-gutter">
|
||||
<div class="col-xs-2 col-sm-2 col-md-1">
|
||||
<section class="heading-btn">
|
||||
<a href="#" ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-xs-8 col-sm-8 col-md-8 b-l">
|
||||
|
@ -18,13 +18,13 @@
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:15%"><a href="" ng-click="setOrderManager('last_name')">{{ 'app.admin.members.surname' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderAdmin =='last_name', 'fa fa-sort-alpha-desc': orderAdmin =='-last_name', 'fa fa-arrows-v': orderAdmin }"></i></a></th>
|
||||
<th style="width:15%"><a ng-click="setOrderManager('last_name')">{{ 'app.admin.members.surname' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderAdmin =='last_name', 'fa fa-sort-alpha-desc': orderAdmin =='-last_name', 'fa fa-arrows-v': orderAdmin }"></i></a></th>
|
||||
|
||||
<th style="width:15%"><a href="" ng-click="setOrderManager('first_name')">{{ 'app.admin.members.first_name' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderAdmin =='first_name', 'fa fa-sort-alpha-desc': orderAdmin =='-first_name', 'fa fa-arrows-v': orderAdmin }"></i></a></th>
|
||||
<th style="width:15%"><a ng-click="setOrderManager('first_name')">{{ 'app.admin.members.first_name' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderAdmin =='first_name', 'fa fa-sort-alpha-desc': orderAdmin =='-first_name', 'fa fa-arrows-v': orderAdmin }"></i></a></th>
|
||||
|
||||
<th style="width:15%"><a href="" ng-click="setOrderManager('email')">{{ 'app.admin.members.email' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderAdmin =='email', 'fa fa-sort-alpha-desc': orderAdmin =='-email', 'fa fa-arrows-v': orderAdmin }"></i></a></th>
|
||||
<th style="width:15%"><a ng-click="setOrderManager('email')">{{ 'app.admin.members.email' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderAdmin =='email', 'fa fa-sort-alpha-desc': orderAdmin =='-email', 'fa fa-arrows-v': orderAdmin }"></i></a></th>
|
||||
|
||||
<th style="width:10%"><a href="" ng-click="setOrderManager('phone')">{{ 'app.admin.members.phone' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': orderAdmin =='profile_attributes.phone', 'fa fa-sort-numeric-desc': orderAdmin =='-profile_attributes.phone', 'fa fa-arrows-v': orderAdmin }"></i></a></th>
|
||||
<th style="width:10%"><a ng-click="setOrderManager('phone')">{{ 'app.admin.members.phone' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': orderAdmin =='profile_attributes.phone', 'fa fa-sort-numeric-desc': orderAdmin =='-profile_attributes.phone', 'fa fa-arrows-v': orderAdmin }"></i></a></th>
|
||||
<th style="width:10%"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -37,12 +37,12 @@
|
||||
<table class="table members-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:15%"><a href="" 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 href="" 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 href="" 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:10%" class="hidden-xs hidden-sm hidden-md"><a href="" 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:15%" class="hidden-xs hidden-sm"><a href="" 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:15%" class="hidden-xs hidden-sm hidden-md"><a href="" 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: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:10%" 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:15%" 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:15%" 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:15%" class="buttons-col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -16,13 +16,13 @@
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:15%"><a href="" ng-click="setOrderPartner('last_name')">{{ 'app.admin.members.surname' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderPartner =='last_name', 'fa fa-sort-alpha-desc': orderPartner =='-last_name', 'fa fa-arrows-v': orderPartner }"></i></a></th>
|
||||
<th style="width:15%"><a ng-click="setOrderPartner('last_name')">{{ 'app.admin.members.surname' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderPartner =='last_name', 'fa fa-sort-alpha-desc': orderPartner =='-last_name', 'fa fa-arrows-v': orderPartner }"></i></a></th>
|
||||
|
||||
<th style="width:15%"><a href="" ng-click="setOrderPartner('first_name')">{{ 'app.admin.members.first_name' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderPartner =='first_name', 'fa fa-sort-alpha-desc': orderPartner =='-first_name', 'fa fa-arrows-v': orderPartner }"></i></a></th>
|
||||
<th style="width:15%"><a ng-click="setOrderPartner('first_name')">{{ 'app.admin.members.first_name' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderPartner =='first_name', 'fa fa-sort-alpha-desc': orderPartner =='-first_name', 'fa fa-arrows-v': orderPartner }"></i></a></th>
|
||||
|
||||
<th style="width:15%"><a href="" ng-click="setOrderPartner('email')">{{ 'app.admin.members.email' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderPartner =='email', 'fa fa-sort-alpha-desc': orderPartner =='-email', 'fa fa-arrows-v': orderPartner }"></i></a></th>
|
||||
<th style="width:15%"><a ng-click="setOrderPartner('email')">{{ 'app.admin.members.email' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderPartner =='email', 'fa fa-sort-alpha-desc': orderPartner =='-email', 'fa fa-arrows-v': orderPartner }"></i></a></th>
|
||||
|
||||
<th style="width:10%"><a href="" ng-click="setOrderPartner('resource')">{{ 'app.admin.members.associated_plan' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': orderPartner =='resource', 'fa fa-sort-numeric-desc': orderPartner =='-resource', 'fa fa-arrows-v': orderPartner }"></i></a></th>
|
||||
<th style="width:10%"><a ng-click="setOrderPartner('resource')">{{ 'app.admin.members.associated_plan' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': orderPartner =='resource', 'fa fa-sort-numeric-desc': orderPartner =='-resource', 'fa fa-arrows-v': orderPartner }"></i></a></th>
|
||||
<th style="width:10%"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -4,7 +4,7 @@
|
||||
<div class="row no-gutter">
|
||||
<div class="col-xs-2 col-sm-2 col-md-1">
|
||||
<section class="heading-btn">
|
||||
<a href="#" ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-xs-10 col-sm-10 col-md-7 b-l b-r-md">
|
||||
@ -50,13 +50,13 @@
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:20%"><a href="" ng-click="setOrder('name')">{{ 'app.admin.open_api_clients.name' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': order == 'name', 'fa fa-sort-alpha-desc': order == '-name', 'fa fa-arrows-v': order }"></i></a></th>
|
||||
<th style="width:20%"><a ng-click="setOrder('name')">{{ 'app.admin.open_api_clients.name' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': order == 'name', 'fa fa-sort-alpha-desc': order == '-name', 'fa fa-arrows-v': order }"></i></a></th>
|
||||
|
||||
<th style="width:15%"><a href="" ng-click="setOrder('calls_count')">{{ 'app.admin.open_api_clients.calls_count' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': order == 'calls_count', 'fa fa-sort-numeric-desc': order == '-calls_count', 'fa fa-arrows-v': order }"></i></a></th>
|
||||
<th style="width:15%"><a ng-click="setOrder('calls_count')">{{ 'app.admin.open_api_clients.calls_count' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': order == 'calls_count', 'fa fa-sort-numeric-desc': order == '-calls_count', 'fa fa-arrows-v': order }"></i></a></th>
|
||||
|
||||
<th style="width:20%"><a href="">{{ 'app.admin.open_api_clients.token' | translate }}</a></th>
|
||||
<th style="width:20%"><a>{{ 'app.admin.open_api_clients.token' | translate }}</a></th>
|
||||
|
||||
<th style="width:20%"><a href="" ng-click="setOrder('created_at')">{{ 'app.admin.open_api_clients.created_at' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': order == 'created_at', 'fa fa-sort-numeric-desc': order == '-created_at', 'fa fa-arrows-v': order }"></i></a></th>
|
||||
<th style="width:20%"><a ng-click="setOrder('created_at')">{{ 'app.admin.open_api_clients.created_at' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': order == 'created_at', 'fa fa-sort-numeric-desc': order == '-created_at', 'fa fa-arrows-v': order }"></i></a></th>
|
||||
|
||||
<th style="width:25%"></th>
|
||||
</tr>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="row no-gutter">
|
||||
<div class="col-xs-2 col-sm-2 col-md-1">
|
||||
<section class="heading-btn">
|
||||
<a href="#" ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-xs-10 col-sm-10 col-md-8 b-l">
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="row no-gutter">
|
||||
<div class="col-xs-2 col-sm-2 col-md-1">
|
||||
<section class="heading-btn">
|
||||
<a href="#" ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-xs-10 col-sm-10 col-md-8 b-l">
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="row no-gutter">
|
||||
<div class="col-xs-2 col-sm-2 col-md-1">
|
||||
<section class="heading-btn">
|
||||
<a href="#" ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-xs-10 col-sm-10 col-md-8 b-l">
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="row no-gutter">
|
||||
<div class="col-xs-2 col-sm-2 col-md-1">
|
||||
<section class="heading-btn">
|
||||
<a href="#" ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-xs-10 col-sm-10 col-md-8 b-l b-r">
|
||||
|
@ -23,12 +23,12 @@
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><a href="" ng-click="setOrderPlans('name')">{{ 'app.admin.pricing.name' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderPlans=='name', 'fa fa-sort-alpha-desc': orderPlans=='-name', 'fa fa-arrows-v': orderPlans }"></i></a></th>
|
||||
<th><a href="" ng-click="setOrderPlans('interval')">{{ 'app.admin.pricing.duration' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-amount-asc': orderPlans=='interval', 'fa fa-sort-amount-desc': orderPlans=='-interval', 'fa fa-arrows-v': orderPlans }"></i></a></th>
|
||||
<th><a href="" ng-click="setOrderPlans('group_id')">{{ 'app.admin.pricing.group' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderPlans=='group_id', 'fa fa-sort-alpha-desc': orderPlans=='-group_id', 'fa fa-arrows-v': orderPlans }"></i></a></th>
|
||||
<th><a href="" ng-click="setOrderPlans('plan_category_id')">{{ 'app.admin.pricing.category' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderPlans=='plan_category_id', 'fa fa-sort-alpha-desc': orderPlans=='-plan_category_id', 'fa fa-arrows-v': orderPlans }"></i></a></th>
|
||||
<th class="hidden-xs"><a href="" ng-click="setOrderPlans('app.admin.pricing.ui_weight')">{{ 'app.admin.pricing.prominence' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': orderPlans=='ui_weight', 'fa fa-sort-numeric-desc': orderPlans=='-ui_weight', 'fa fa-arrows-v': orderPlans }"></i></a></th>
|
||||
<th><a href="" ng-click="setOrderPlans('amount')">{{ 'app.admin.pricing.price' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': orderPlans=='amount', 'fa fa-sort-numeric-desc': orderPlans=='-amount', 'fa fa-arrows-v': orderPlans }"></i></a></th>
|
||||
<th><a ng-click="setOrderPlans('name')">{{ 'app.admin.pricing.name' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderPlans=='name', 'fa fa-sort-alpha-desc': orderPlans=='-name', 'fa fa-arrows-v': orderPlans }"></i></a></th>
|
||||
<th><a ng-click="setOrderPlans('interval')">{{ 'app.admin.pricing.duration' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-amount-asc': orderPlans=='interval', 'fa fa-sort-amount-desc': orderPlans=='-interval', 'fa fa-arrows-v': orderPlans }"></i></a></th>
|
||||
<th><a ng-click="setOrderPlans('group_id')">{{ 'app.admin.pricing.group' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderPlans=='group_id', 'fa fa-sort-alpha-desc': orderPlans=='-group_id', 'fa fa-arrows-v': orderPlans }"></i></a></th>
|
||||
<th><a ng-click="setOrderPlans('plan_category_id')">{{ 'app.admin.pricing.category' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderPlans=='plan_category_id', 'fa fa-sort-alpha-desc': orderPlans=='-plan_category_id', 'fa fa-arrows-v': orderPlans }"></i></a></th>
|
||||
<th class="hidden-xs"><a ng-click="setOrderPlans('app.admin.pricing.ui_weight')">{{ 'app.admin.pricing.prominence' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': orderPlans=='ui_weight', 'fa fa-sort-numeric-desc': orderPlans=='-ui_weight', 'fa fa-arrows-v': orderPlans }"></i></a></th>
|
||||
<th><a ng-click="setOrderPlans('amount')">{{ 'app.admin.pricing.price' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': orderPlans=='amount', 'fa fa-sort-numeric-desc': orderPlans=='-amount', 'fa fa-arrows-v': orderPlans }"></i></a></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="row no-gutter">
|
||||
<div class="col-xs-2 col-sm-2 col-md-1">
|
||||
<section class="heading-btn">
|
||||
<a href="#" ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-xs-10 col-sm-10 col-md-8 b-l">
|
||||
|
@ -202,7 +202,7 @@
|
||||
<input name="_method" type="hidden" ng-value="methods.logo">
|
||||
<h3 class="m-l" translate>{{ 'app.admin.settings.logo_white_background' }}</h3>
|
||||
<div class="custom-logo" style="background-image: url({{customLogo}});">
|
||||
<img src="data:image/png;base64," data-src="holder.js/100%x100%/text:/font:'Font Awesome 5 Free'/icon-xs" bs-holder ng-show="!customLogo" class="img-responsive">
|
||||
<img src="data:image/png;base64," data-src="holder.js/100%x100%/text:/font:'Font Awesome 5 Free'/icon" bs-holder ng-show="!customLogo" class="img-responsive">
|
||||
<img base-sixty-four-image="customLogo" ng-show="customLogo && customLogo.base64">
|
||||
<img ng-src="{{customLogo.custom_asset_file_attributes.attachment_url}}" alt="{{customLogo.custom_asset_file_attributes.attachment}}" ng-show="customLogo && customLogo.custom_asset_file_attributes" />
|
||||
<div class="tools-box">
|
||||
@ -254,7 +254,7 @@
|
||||
<input name="_method" type="hidden" ng-value="methods.favicon">
|
||||
<h3 class="m-l" translate>{{ 'app.admin.settings.favicon' }}</h3>
|
||||
<div class="custom-favicon" style="background-image: url({{customFavicon}});">
|
||||
<img src="data:image/png;base64," data-src="holder.js/32x32/text:/font:'Font Awesome 5 Free'/icon-xs" bs-holder ng-show="!customFavicon" class="img-responsive">
|
||||
<img src="data:image/png;base64," data-src="holder.js/32x32/text:/font:'Font Awesome 5 Free'/icon" bs-holder ng-show="!customFavicon" class="img-responsive">
|
||||
<img base-sixty-four-image="customFavicon" ng-show="customFavicon && customFavicon.base64">
|
||||
<img ng-src="{{customFavicon.custom_asset_file_attributes.attachment_url}}" alt="{{customFavicon.custom_asset_file_attributes.attachment}}" ng-show="customFavicon && customFavicon.custom_asset_file_attributes" />
|
||||
<div class="tools-box">
|
||||
@ -316,7 +316,7 @@
|
||||
<input name="_method" type="hidden" ng-value="methods.profileImage">
|
||||
<h3 class="m-l" translate>{{ 'app.admin.settings.background_picture_of_the_profile_banner' }}</h3>
|
||||
<div class="custom-profile-image" style="background-image: url({{profileImage}});">
|
||||
<img src="data:image/png;base64," data-src="holder.js/100%x100%/text:/font:'Font Awesome 5 Free'/icon-xs" bs-holder ng-show="!profileImage" class="img-responsive">
|
||||
<img src="data:image/png;base64," data-src="holder.js/100%x100%/text:/font:'Font Awesome 5 Free'/icon" bs-holder ng-show="!profileImage" class="img-responsive">
|
||||
<img base-sixty-four-image="profileImage" ng-show="profileImage && profileImage.base64">
|
||||
<img ng-src="{{profileImage.custom_asset_file_attributes.attachment_url}}" alt="{{profileImage.custom_asset_file_attributes.attachment}}" ng-show="profileImage && profileImage.custom_asset_file_attributes" />
|
||||
<div class="tools-box">
|
||||
@ -412,6 +412,18 @@
|
||||
<span class="font-sbold" translate>{{ 'app.admin.settings.account_creation' }}</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="row">
|
||||
<h3 class="m-l" translate>{{ 'app.admin.settings.general.public_registrations' }}</h3>
|
||||
<p class="alert alert-warning m-h-md" translate>
|
||||
{{ 'app.admin.settings.general.public_registrations_info' }}
|
||||
</p>
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<boolean-setting name="public_registrations"
|
||||
settings="allSettings"
|
||||
label="app.admin.settings.general.public_registrations_allowed">
|
||||
</boolean-setting>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<h3 class="m-l" translate>{{ 'app.admin.settings.phone' }}</h3>
|
||||
<p class="alert alert-warning m-h-md" translate>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="row no-gutter">
|
||||
<div class="col-xs-2 col-sm-2 col-md-1">
|
||||
<section class="heading-btn">
|
||||
<a href="#" ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-xs-10 col-sm-10 col-md-8 b-l">
|
||||
|
@ -56,7 +56,7 @@
|
||||
settings="allSettings"
|
||||
label="app.admin.settings.prior_period_hours"
|
||||
classes="col-md-8"
|
||||
fa-icon="fa-clock-o"
|
||||
fa-icon="fas fa-clock"
|
||||
min="0"
|
||||
required="allSettings.booking_move_enable === 'true'">
|
||||
</number-setting>
|
||||
@ -76,7 +76,7 @@
|
||||
settings="allSettings"
|
||||
label="app.admin.settings.prior_period_hours"
|
||||
classes="col-md-8"
|
||||
fa-icon="fa-clock-o"
|
||||
fa-icon="fas fa-clock"
|
||||
min="0"
|
||||
required="allSettings.booking_cancel_enable === 'true'">
|
||||
</number-setting>
|
||||
@ -112,7 +112,7 @@
|
||||
settings="allSettings"
|
||||
label="app.admin.settings.duration_minutes"
|
||||
classes="col-md-4"
|
||||
fa-icon="fa-clock-o"
|
||||
fa-icon="fas fa-clock"
|
||||
min="1"
|
||||
required="true">
|
||||
</number-setting>
|
||||
@ -161,7 +161,7 @@
|
||||
settings="allSettings"
|
||||
label="app.admin.settings.prior_period_hours"
|
||||
classes="col-md-4"
|
||||
fa-icon="fa-clock-o"
|
||||
fa-icon="fas fa-clock"
|
||||
helper-text="app.admin.settings.default_value_is_24_hours"
|
||||
min="0">
|
||||
</number-setting>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="row no-gutter">
|
||||
<div class="col-xs-2 col-sm-2 col-md-1">
|
||||
<section class="heading-btn">
|
||||
<a href="#" ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-xs-8 col-sm-10 col-md-8 b-l">
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="row no-gutter">
|
||||
<div class="col-xs-2 col-sm-2 col-md-1">
|
||||
<section class="heading-btn">
|
||||
<a href="#" ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-xs-8 col-sm-11 col-md-8 b-l">
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="row no-gutter">
|
||||
<div class="col-xs-2 col-sm-2 col-md-1">
|
||||
<section class="heading-btn">
|
||||
<a href="#" ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-xs-10 col-sm-10 col-md-8 b-l b-r">
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="row no-gutter">
|
||||
<div class="col-md-1 hidden-xs">
|
||||
<section class="heading-btn">
|
||||
<a href="#" ng-click="cancel()"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
<a ng-click="cancel()"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-md-8 b-l b-r">
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="row no-gutter">
|
||||
<div class="col-xs-2 col-sm-2 col-md-1">
|
||||
<section class="heading-btn">
|
||||
<a href="#" ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-xs-10 col-sm-10 col-md-8 b-l b-r-md hide-b-r-lg">
|
||||
|
@ -3,21 +3,21 @@
|
||||
|
||||
<div class="col-xs-2 col-sm-2 col-md-1">
|
||||
<section class="heading-btn">
|
||||
<a href="#" ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-xs-10 col-sm-10 col-md-10 b-l">
|
||||
<section class="heading-title m-l">
|
||||
<h4 class="m-l text-sm" translate>{{ 'app.public.common.dashboard' }}</h4>
|
||||
<ul class="nav-page nav nav-pills text-u-c text-sm">
|
||||
<li ui-sref-active="active"><a class="text-black" href="#" ui-sref="app.logged.dashboard.profile" translate>{{ 'app.public.common.my_profile' }}</a></li>
|
||||
<li ui-sref-active="active"><a class="text-black" href="#" ui-sref="app.logged.dashboard.settings" translate>{{ 'app.public.common.my_settings' }}</a></li>
|
||||
<li ui-sref-active="active"><a class="text-black" href="#" ui-sref="app.logged.dashboard.projects" translate>{{ 'app.public.common.my_projects' }}</a></li>
|
||||
<li ui-sref-active="active"><a class="text-black" href="#" ui-sref="app.logged.dashboard.trainings" translate>{{ 'app.public.common.my_trainings' }}</a></li>
|
||||
<li ui-sref-active="active"><a class="text-black" href="#" ui-sref="app.logged.dashboard.events" translate>{{ 'app.public.common.my_events' }}</a></li>
|
||||
<li ui-sref-active="active" ng-show="$root.modules.invoicing"><a class="text-black" href="#" ui-sref="app.logged.dashboard.invoices" translate>{{ 'app.public.common.my_invoices' }}</a></li>
|
||||
<li ui-sref-active="active" ng-show="$root.modules.invoicing"><a class="text-black" href="#" ui-sref="app.logged.dashboard.payment_schedules" translate>{{ 'app.public.common.my_payment_schedules' }}</a></li>
|
||||
<li ng-show="$root.modules.wallet" ui-sref-active="active"><a class="text-black" href="#" ui-sref="app.logged.dashboard.wallet" translate>{{ 'app.public.common.my_wallet' }}</a></li>
|
||||
<li ui-sref-active="active"><a class="text-black" ui-sref="app.logged.dashboard.profile" translate>{{ 'app.public.common.my_profile' }}</a></li>
|
||||
<li ui-sref-active="active"><a class="text-black" ui-sref="app.logged.dashboard.settings" translate>{{ 'app.public.common.my_settings' }}</a></li>
|
||||
<li ui-sref-active="active"><a class="text-black" ui-sref="app.logged.dashboard.projects" translate>{{ 'app.public.common.my_projects' }}</a></li>
|
||||
<li ui-sref-active="active"><a class="text-black" ui-sref="app.logged.dashboard.trainings" translate>{{ 'app.public.common.my_trainings' }}</a></li>
|
||||
<li ui-sref-active="active"><a class="text-black" ui-sref="app.logged.dashboard.events" translate>{{ 'app.public.common.my_events' }}</a></li>
|
||||
<li ui-sref-active="active" ng-show="$root.modules.invoicing"><a class="text-black" ui-sref="app.logged.dashboard.invoices" translate>{{ 'app.public.common.my_invoices' }}</a></li>
|
||||
<li ui-sref-active="active" ng-show="$root.modules.invoicing"><a class="text-black" ui-sref="app.logged.dashboard.payment_schedules" translate>{{ 'app.public.common.my_payment_schedules' }}</a></li>
|
||||
<li ng-show="$root.modules.wallet" ui-sref-active="active"><a class="text-black" ui-sref="app.logged.dashboard.wallet" translate>{{ 'app.public.common.my_wallet' }}</a></li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
|
@ -257,7 +257,7 @@
|
||||
</div>
|
||||
<div class="col-sm-1">
|
||||
<input type="hidden" name="event[event_price_categories_attributes][][_destroy]" ng-value="price._destroy">
|
||||
<a class="btn p-h-0" ng-click="removePrice(price, $event)" href="#"><i class="fa fa-times text-danger"></i></a>
|
||||
<a class="btn p-h-0" ng-click="removePrice(price, $event)"><i class="fa fa-times text-danger"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="link-icon m-b" ng-hide="event.prices.length == priceCategories.length">
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="row no-gutter b-b">
|
||||
<div class="col-xs-2 col-sm-2 col-md-1">
|
||||
<section class="heading-btn">
|
||||
<a href="#" ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-xs-10 col-sm-10 col-md-8 b-l b-r-md">
|
||||
@ -43,45 +43,37 @@
|
||||
<div ng-repeat="month in monthOrder">
|
||||
<h1>{{monthNames[month.split(',')[0] - 1]}}, {{month.split(',')[1]}}</h1>
|
||||
|
||||
<div class="row" ng-repeat="event in (eventsGroupByMonth[month].length/3 | array)">
|
||||
<div class="month-events-list" ng-repeat="event in (eventsGroupByMonth[month].length/3 | array)">
|
||||
<a class="Event" ng-repeat="event in eventsGroupByMonth[month].slice(3*$index, 3*$index + 3)" ui-sref="app.public.events_show({id: event.id})">
|
||||
<div class="Event-desc">
|
||||
<h5 class="text-xs m-t-n">{{event.category.name}}</h5>
|
||||
<h4 class="m-n text-sm clear l-n">{{event.title}}</h4>
|
||||
<h3 class="m-n" ng-show="onSingleDay(event)">{{event.start_date | amDateFormat:'L'}}</h3>
|
||||
<h3 class="m-n" ng-hide="onSingleDay(event)">{{event.start_date | amDateFormat:'L'}} <span class="text-sm font-thin" translate> {{ 'app.public.events_list.to_date' }} </span> {{event.end_date | amDateFormat:'L'}}</h3>
|
||||
|
||||
<div class="col-xs-12 col-sm-6 col-md-4" ng-repeat="event in eventsGroupByMonth[month].slice(3*$index, 3*$index + 3)" ng-click="showEvent(event)">
|
||||
<h6 class="m-n" ng-if="!event.amount" translate>{{ 'app.public.events_list.free_admission' }}</h6>
|
||||
<h6 class="m-n" ng-if="event.amount">{{ 'app.public.events_list.full_price_' | translate }} {{event.amount | currency}} <span ng-repeat="price in event.prices">/ {{ price.category.name }} {{price.amount | currency}}</span></h6>
|
||||
|
||||
|
||||
<a class="block bg-white img-full p-sm p-l-m box-h-m event b b-light-dark m-t-sm" ui-sref="app.public.events_show({id: event.id})">
|
||||
<div class="pull-left half-w m-t-n-sm">
|
||||
<h5 class="text-xs">{{event.category.name}}</h5>
|
||||
<h4 class="m-n text-sm clear l-n">{{event.title}}</h4>
|
||||
<h3 class="m-n" ng-show="onSingleDay(event)">{{event.start_date | amDateFormat:'L'}}</h3>
|
||||
<h3 class="m-n" ng-hide="onSingleDay(event)">{{event.start_date | amDateFormat:'L'}} <span class="text-sm font-thin" translate> {{ 'app.public.events_list.to_date' }} </span> {{event.end_date | amDateFormat:'L'}}</h3>
|
||||
|
||||
<h6 class="m-n" ng-if="!event.amount" translate>{{ 'app.public.events_list.free_admission' }}</h6>
|
||||
<h6 class="m-n" ng-if="event.amount">{{ 'app.public.events_list.full_price_' | translate }} {{event.amount | currency}} <span ng-repeat="price in event.prices">/ {{ price.category.name }} {{price.amount | currency}}</span></h6>
|
||||
|
||||
<div>
|
||||
<span class="text-black-light text-xs m-r-xs" ng-repeat="theme in event.event_themes">
|
||||
<i class="fa fa-tags" aria-hidden="true"></i> {{theme.name}}
|
||||
</span>
|
||||
<span class="text-black-light text-xs" ng-if="event.age_range"><i class="fa fa-users" aria-hidden="true"></i> {{event.age_range.name}}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="text-black-light text-xs" ng-if="event.nb_free_places > 0">{{event.nb_free_places}} {{ 'app.public.events_list.still_available' | translate }}</span>
|
||||
<span class="text-black-light text-xs" ng-if="event.nb_total_places > 0 && event.nb_free_places <= 0" translate>{{ 'app.public.events_list.sold_out' }}</span>
|
||||
<span class="text-black-light text-xs" ng-if="event.nb_total_places == -1" translate>{{ 'app.public.events_list.cancelled' }}</span>
|
||||
<span class="text-black-light text-xs" ng-if="!event.nb_total_places" translate>{{ 'app.public.events_list.without_reservation' }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-black-light text-xs m-r-xs" ng-repeat="theme in event.event_themes">
|
||||
<i class="fa fa-tags" aria-hidden="true"></i> {{theme.name}}
|
||||
</span>
|
||||
<span class="text-black-light text-xs" ng-if="event.age_range"><i class="fa fa-users" aria-hidden="true"></i> {{event.age_range.name}}</span>
|
||||
</div>
|
||||
|
||||
<!-- Event Image -->
|
||||
<div class="pull-right crop-155">
|
||||
<img class="pull-right" ng-src="{{event.event_image_small}}" title="{{event.title}}" ng-if="event.event_image">
|
||||
<img class="pull-right img-responsive" src="data:image/png;base64," data-src="holder.js/100%x100%/text:/font:'Font Awesome 5 Free'/icon" bs-holder ng-if="!event.event_image">
|
||||
<div>
|
||||
<span class="text-black-light text-xs" ng-if="event.nb_free_places > 0">{{event.nb_free_places}} {{ 'app.public.events_list.still_available' | translate }}</span>
|
||||
<span class="text-black-light text-xs" ng-if="event.nb_total_places > 0 && event.nb_free_places <= 0" translate>{{ 'app.public.events_list.sold_out' }}</span>
|
||||
<span class="text-black-light text-xs" ng-if="event.nb_total_places == -1" translate>{{ 'app.public.events_list.cancelled' }}</span>
|
||||
<span class="text-black-light text-xs" ng-if="!event.nb_total_places" translate>{{ 'app.public.events_list.without_reservation' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</a>
|
||||
|
||||
</div>
|
||||
<!-- Event Image -->
|
||||
<div class="Event-picture" ng-if="event.event_image">
|
||||
<img ng-src="{{event.event_image_small}}" title="{{event.title}}">
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
<div class="row no-gutter">
|
||||
<div class="col-xs-2 col-sm-2 col-md-1">
|
||||
<section class="heading-btn">
|
||||
<a href="#" ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-xs-10 col-sm-10 col-md-8 b-l b-r-md">
|
||||
@ -83,7 +83,7 @@
|
||||
<dl class="text-sm">
|
||||
<dt><i class="fa fa-calendar" aria-hidden="true"></i> {{ 'app.public.events_show.dates' | translate }}</dt>
|
||||
<dd>{{ 'app.public.events_show.beginning' | translate }} <span class="text-u-l">{{event.start_date | amDateFormat:'L'}}</span><br>{{ 'app.public.events_show.ending' | translate }} <span class="text-u-l">{{event.end_date | amDateFormat:'L'}}</span></dd>
|
||||
<dt><i class="fa fa-clock-o"></i> {{ 'app.public.events_show.opening_hours' | translate }}</dt>
|
||||
<dt><i class="fas fa-clock"></i> {{ 'app.public.events_show.opening_hours' | translate }}</dt>
|
||||
<dd ng-if="event.all_day == 'true'"><span translate>{{ 'app.public.events_show.all_day' }}</span></dd>
|
||||
<dd ng-if="event.all_day == 'false'">{{ 'app.public.events_show.from_time' | translate }} <span class="text-u-l">{{event.start_date | amDateFormat:'LT'}}</span> {{ 'app.public.events_show.to_time' | translate }} <span class="text-u-l">{{event.end_date | amDateFormat:'LT'}}</span></dd>
|
||||
</dl>
|
||||
@ -147,7 +147,7 @@
|
||||
</div>
|
||||
|
||||
<div class="clear">
|
||||
<a class="pull-right m-t-xs text-u-l ng-scope" href="#" ng-click="cancelReserve($event)" ng-show="reserve.toReserve" translate>{{ 'app.shared.buttons.cancel' }}</a>
|
||||
<a class="pull-right m-t-xs text-u-l ng-scope" ng-click="cancelReserve($event)" ng-show="reserve.toReserve" translate>{{ 'app.shared.buttons.cancel' }}</a>
|
||||
</div>
|
||||
|
||||
<div ng-if="reserveSuccess" class="alert alert-success">{{ 'app.public.events_show.thank_you_your_payment_has_been_successfully_registered' | translate }}<br>
|
||||
|
@ -1,63 +1,51 @@
|
||||
<section class="home-events">
|
||||
<h4 class="text-sm m-t-sm">{{ 'app.public.home.fablab_s_next_events' | translate }} <a ui-sref="app.public.events_list" class="pull-right"><i class="fa fa-tags"></i> {{ 'app.public.home.every_events' | translate }}</a></h4>
|
||||
<h4 class="text-sm m-t-sm">
|
||||
{{ 'app.public.home.fablab_s_next_events' | translate }}
|
||||
<a ui-sref="app.public.events_list">
|
||||
<i class="fa fa-tags"></i>{{ 'app.public.home.every_events' | translate }}
|
||||
</a>
|
||||
</h4>
|
||||
|
||||
<div class="row" ng-repeat="event in (upcomingEvents.length/3 | array)">
|
||||
<div class="home-events-list" ng-repeat="event in (upcomingEvents.length/3 | array)">
|
||||
<div class="Event" ng-repeat="event in upcomingEvents.slice(3*$index, 3*$index + 3)" ui-sref="app.public.events_show({id: event.id})">
|
||||
<div class="Event-picture">
|
||||
<img src="data:image/png;base64," data-src="holder.js/100%x100%/text:/font:'Font Awesome 5 Free'/icon" bs-holder ng-if="!event.event_image" class="img-responsive">
|
||||
<img ng-if="event.event_image" src="{{event.event_image_medium}}">
|
||||
</div>
|
||||
|
||||
<div class="col-xs-12 col-sm-6 col-md-6 col-lg-4" ng-repeat="event in upcomingEvents.slice(3*$index, 3*$index + 3)">
|
||||
<div class="Event-desc">
|
||||
<h3>{{event.title}}</h3>
|
||||
<span class="v-middle badge text-xs" ng-class="'bg-{{event.category.slug}}'">{{event.category.name}}</span>
|
||||
<p ng-bind-html="event.description | simpleText | humanize : 500 | breakFilter"></p>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="widget panel panel-default" ui-sref="app.public.events_show({id: event.id})">
|
||||
<div class="panel-heading picture" style="background-image: url({{event.event_image_medium}});" >
|
||||
<img src="data:image/png;base64," data-src="holder.js/100%x100%/text:/font:'Font Awesome 5 Free'/icon" bs-holder ng-if="!event.event_image" class="img-responsive">
|
||||
<div class="Event-info">
|
||||
<div class="Event-info-item">
|
||||
<i class="fa fa-calendar"></i>
|
||||
<h6 class="" ng-hide="isOneDayEvent(event)">{{ 'app.public.home.from_date_to_date' | translate:{START:(event.start_date | amDateFormat:'L'), END:(event.end_date | amDateFormat:'L')} }}</h6>
|
||||
<h6 class="" ng-show="isOneDayEvent(event)">{{ 'app.public.home.on_the_date' | translate:{DATE:(event.start_date | amDateFormat:'L')} }}</h6>
|
||||
</div>
|
||||
<div class="panel-body" style="heigth:170px;">
|
||||
<div class="row">
|
||||
<div class="col-xs-9">
|
||||
<h1 class="m-b">{{event.title}}</h1>
|
||||
</div>
|
||||
<div class="col-xs-3">
|
||||
<span class="v-middle badge text-xs" ng-class="'bg-{{event.category.slug}}'">{{event.category.name}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="event-description" ng-bind-html="event.description | simpleText | humanize : 500 | breakFilter"></p>
|
||||
|
||||
<hr/>
|
||||
<div class="row">
|
||||
<div class="col-sm-6 row m-b-sm">
|
||||
<i class="fa fa-calendar red col-xs-3 padder-icon"></i>
|
||||
<h6 class="m-n col-xs-9 " ng-hide="isOneDayEvent(event)">{{ 'app.public.home.from_date_to_date' | translate:{START:(event.start_date | amDateFormat:'L'), END:(event.end_date | amDateFormat:'L')} }}</h6>
|
||||
<h6 class="m-n col-xs-9 " ng-show="isOneDayEvent(event)">{{ 'app.public.home.on_the_date' | translate:{DATE:(event.start_date | amDateFormat:'L')} }}</h6>
|
||||
</div>
|
||||
<div class="col-sm-6 row m-b-sm">
|
||||
<i class="fa fa-clock-o red col-xs-3 padder-icon"></i>
|
||||
<h6 class="m-n col-xs-9">
|
||||
<span ng-if="event.all_day == 'true'" translate>{{ 'app.public.home.all_day' }}</span>
|
||||
<span ng-if="event.all_day == 'false'">{{ 'app.public.home.from_time_to_time' | translate:{START:(event.start_date | amDateFormat:'LT'), END:(event.end_date | amDateFormat:'LT')} }}</span>
|
||||
</h6>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-6 row m-b-sm">
|
||||
<i class="fa fa-user red col-xs-3 padder-icon"></i>
|
||||
<h6 class="m-n col-xs-9 ">
|
||||
<span ng-if="event.nb_free_places > 0">{{ 'app.public.home.still_available' | translate }} {{event.nb_free_places}}</span>
|
||||
<span ng-if="!event.nb_total_places" translate>{{ 'app.public.home.without_reservation' }}</span>
|
||||
<span ng-if="event.nb_total_places > 0 && event.nb_free_places <= 0" translate>{{ 'app.public.home.event_full' }}</span>
|
||||
</h6>
|
||||
</div>
|
||||
<div class="col-sm-6 row m-b-sm">
|
||||
<i class="fa fa-bookmark red col-xs-3 padder-icon"></i>
|
||||
<h6 class="m-n col-xs-9">
|
||||
<span ng-if="event.amount == 0" translate>{{ 'app.public.home.free_admission' }}</span>
|
||||
<span ng-if="event.amount > 0">{{ 'app.public.home.full_price' | translate }} {{event.amount | currency}}</span>
|
||||
</h6>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center clearfix ">
|
||||
<div class="btn btn-lg btn-warning bg-white b-2x rounded m-t-sm m-b-sm upper text-sm width-70" ui-sref="app.public.events_show({id: event.id})" ><span translate>{{ 'app.shared.buttons.consult' }}</span></div>
|
||||
</div>
|
||||
<div class="Event-info-item">
|
||||
<i class="fas fa-clock"></i>
|
||||
<h6 class="">
|
||||
<span ng-if="event.all_day == 'true'" translate>{{ 'app.public.home.all_day' }}</span>
|
||||
<span ng-if="event.all_day == 'false'">{{ 'app.public.home.from_time_to_time' | translate:{START:(event.start_date | amDateFormat:'LT'), END:(event.end_date | amDateFormat:'LT')} }}</span>
|
||||
</h6>
|
||||
</div>
|
||||
<div class="Event-info-item">
|
||||
<i class="fa fa-user"></i>
|
||||
<h6 class="">
|
||||
<span ng-if="event.nb_free_places > 0">{{ 'app.public.home.still_available' | translate }} {{event.nb_free_places}}</span>
|
||||
<span ng-if="!event.nb_total_places" translate>{{ 'app.public.home.without_reservation' }}</span>
|
||||
<span ng-if="event.nb_total_places > 0 && event.nb_free_places <= 0" translate>{{ 'app.public.home.event_full' }}</span>
|
||||
</h6>
|
||||
</div>
|
||||
<div class="Event-info-item">
|
||||
<i class="fa fa-bookmark"></i>
|
||||
<h6 class="">
|
||||
<span ng-if="event.amount == 0" translate>{{ 'app.public.home.free_admission' }}</span>
|
||||
<span ng-if="event.amount > 0">{{ 'app.public.home.full_price' | translate }} {{event.amount | currency}}</span>
|
||||
</h6>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -18,10 +18,10 @@
|
||||
|
||||
</div>
|
||||
<div class="m-t-sm m-b-sm text-center" ng-if="!isAuthenticated()">
|
||||
<button href="#" ng-click="signup($event)" class="btn btn-warning-full width-70 font-sbold rounded text-sm" translate>{{ 'app.public.home.create_an_account' }}</button>
|
||||
<button ng-click="signup($event)" class="btn btn-warning-full width-70 font-sbold rounded text-sm" translate>{{ 'app.public.home.create_an_account' }}</button>
|
||||
</div>
|
||||
|
||||
<div class="m-t-sm m-b-sm text-center" ng-if="isAuthenticated()">
|
||||
<button href="#" ui-sref="app.logged.members" class="btn btn-warning-full width-70 font-sbold rounded text-sm" translate>{{ 'app.public.home.discover_members' }}</button>
|
||||
<button ui-sref="app.logged.members" class="btn btn-warning-full width-70 font-sbold rounded text-sm" translate>{{ 'app.public.home.discover_members' }}</button>
|
||||
</div>
|
||||
</section>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="row no-gutter">
|
||||
<div class="col-xs-2 col-sm-2 col-md-1">
|
||||
<section class="heading-btn">
|
||||
<a href="#" ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-xs-10 col-sm-10 col-md-8 b-l b-r-md">
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="row no-gutter">
|
||||
<div class="col-md-1 hidden-xs">
|
||||
<section class="heading-btn">
|
||||
<a href="#" ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-md-8 b-l b-r">
|
||||
|
@ -4,7 +4,7 @@
|
||||
<div class="row no-gutter">
|
||||
<div class="col-xs-2 col-sm-2 col-md-1">
|
||||
<section class="heading-btn">
|
||||
<a href="#" ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-xs-10 col-sm-10 col-md-8 b-l">
|
||||
|
@ -4,7 +4,7 @@
|
||||
<div class="row no-gutter">
|
||||
<div class="col-xs-2 col-sm-2 col-md-1">
|
||||
<section class="heading-btn">
|
||||
<a href="#" ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-xs-10 col-sm-10 col-md-7 b-l b-r-md">
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="row no-gutter">
|
||||
<div class="col-xs-2 col-sm-2 col-md-1">
|
||||
<section class="heading-btn">
|
||||
<a href="#" ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-xs-10 col-sm-10 col-md-8 b-l">
|
||||
|
@ -4,7 +4,7 @@
|
||||
<div class="row no-gutter">
|
||||
<div class="col-xs-2 col-sm-2 col-md-1">
|
||||
<section class="heading-btn">
|
||||
<a href="#" ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-xs-10 col-sm-10 col-md-8 b-l b-r-md">
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="row no-gutter">
|
||||
<div class="col-xs-2 col-sm-2 col-md-1">
|
||||
<section class="heading-btn">
|
||||
<a href="#" ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-xs-10 col-sm-10 col-md-8 b-l">
|
||||
|
@ -7,7 +7,7 @@
|
||||
operator="currentUser">
|
||||
</plans-list>
|
||||
|
||||
<a class="m-t-lg btn btn-small btn-default pull-right" href="#" ng-click="doNotSubscribePlan($event)">{{ 'app.shared.plan_subscribe.do_not_subscribe' | translate }} <i class="fa fa-long-arrow-right"></i></a>
|
||||
<a class="m-t-lg btn btn-small btn-default pull-right" ng-click="doNotSubscribePlan($event)">{{ 'app.shared.plan_subscribe.do_not_subscribe' | translate }} <i class="fa fa-long-arrow-right"></i></a>
|
||||
|
||||
<div class="row row-centered m-t-lg">
|
||||
<div class="col-xs-12 col-md-12 col-lg-10 col-centered no-gutter">
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="row no-gutter">
|
||||
<div class="col-xs-2 col-sm-2 col-md-1">
|
||||
<section class="heading-btn">
|
||||
<a href="#" ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-xs-10 col-sm-10 col-md-8 b-l ">
|
||||
|
@ -3,7 +3,7 @@
|
||||
<h3 translate>{{ 'app.logged.profile_completion.do_you_already_have_an_account' }}</h3>
|
||||
<p ng-hide="hasDuplicate()" translate>{{ 'app.logged.profile_completion.do_not_fill_the_form_beside_but_specify_here_the_code_you_ve_received_by_email_to_recover_your_access' }}</p>
|
||||
<p ng-show="hasDuplicate()" translate>{{ 'app.logged.profile_completion.just_specify_code_here_to_recover_access' }}</p>
|
||||
<p class="pull-right"><a href="#" ng-click="resendCode($event)" translate>{{ 'app.logged.profile_completion.i_did_not_receive_the_code' }}</a></p>
|
||||
<p class="pull-right"><a ng-click="resendCode($event)" translate>{{ 'app.logged.profile_completion.i_did_not_receive_the_code' }}</a></p>
|
||||
<div class="row">
|
||||
<div class="col-lg-3 col-lg-offset-1 hidden-md col-sm-3 col-sm-offset-1"></div>
|
||||
<div class="col-lg-offset-1 col-lg-6 col-md-12 col-sm-offset-1 col-sm-6">
|
||||
|
@ -87,7 +87,7 @@
|
||||
{{ 'app.shared.project.step_N' | translate:{ INDEX:step.step_nb } }}/{{totalSteps}} <i class="fa fa-caret-down" aria-hidden="true"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu" uib-dropdown-menu role="menu" aria-labelledby="single-button">
|
||||
<li role="menuitem" ng-repeat="step_idx in intArray(1, totalSteps +1)"><a href="#" ng-click="changeStepIndex($event, step, step_idx)">{{ 'app.shared.project.step_N' | translate:{ INDEX:step_idx } }}</a></li>
|
||||
<li role="menuitem" ng-repeat="step_idx in intArray(1, totalSteps +1)"><a ng-click="changeStepIndex($event, step, step_idx)">{{ 'app.shared.project.step_N' | translate:{ INDEX:step_idx } }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<span class="label label-warning m-t m-b"></span>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="row no-gutter">
|
||||
<div class="col-xs-2 col-sm-2 col-md-1">
|
||||
<section class="heading-btn">
|
||||
<a href="#" ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-xs-10 col-sm-10 col-md-8 b-l b-r-md">
|
||||
|
@ -4,7 +4,7 @@
|
||||
<div class="row no-gutter">
|
||||
<div class="col-xs-2 col-sm-2 col-md-1">
|
||||
<section class="heading-btn">
|
||||
<a href="#" ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-xs-10 col-sm-10 col-md-8 b-l b-r-md">
|
||||
@ -153,7 +153,7 @@
|
||||
<section class="widget b-t">
|
||||
|
||||
<div class="widget-content text-center m-t">
|
||||
<a href="#" ng-click="signalAbuse($event)"><i class="fa fa-warning"></i> {{ 'app.public.projects_show.report_an_abuse' | translate }}</a>
|
||||
<a ng-click="signalAbuse($event)"><i class="fa fa-warning"></i> {{ 'app.public.projects_show.report_an_abuse' | translate }}</a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
@ -133,7 +133,7 @@
|
||||
<div class="panel-body">
|
||||
<div class="font-sbold text-u-c">{{ 'app.shared.cart.datetime_to_time' | translate:{START_DATETIME:(events.modifiable.start | amDateFormat:'LLLL'), END_TIME:(events.modifiable.end | amDateFormat:'LT') } }}</div>
|
||||
</div>
|
||||
<div class="clear"><a class="pull-right m-b-sm text-u-l ng-scope m-r-sm" href="#" ng-click="cancelModifySlot($event)" translate>{{ 'app.shared.cart.cancel_my_modification' }}</a></div>
|
||||
<div class="clear"><a class="pull-right m-b-sm text-u-l ng-scope m-r-sm" ng-click="cancelModifySlot($event)" translate>{{ 'app.shared.cart.cancel_my_modification' }}</a></div>
|
||||
</div>
|
||||
|
||||
<div class="widget-content no-bg">
|
||||
@ -145,7 +145,7 @@
|
||||
<div class="panel-body">
|
||||
<div class="font-sbold text-u-c">{{ 'app.shared.cart.datetime_to_time' | translate:{START_DATETIME:(events.placable.start | amDateFormat:'LLLL'), END_TIME:(events.placable.end | amDateFormat:'LT') } }}</div>
|
||||
</div>
|
||||
<div class="clear"><a class="pull-right m-b-sm text-u-l ng-scope m-r-sm" href="#" ng-click="removeSlotToPlace($event)" translate>{{ 'app.shared.cart.cancel_my_selection' }}</a></div>
|
||||
<div class="clear"><a class="pull-right m-b-sm text-u-l ng-scope m-r-sm" ng-click="removeSlotToPlace($event)" translate>{{ 'app.shared.cart.cancel_my_selection' }}</a></div>
|
||||
</div>
|
||||
|
||||
<div ng-if="events.placable && (events.modifiable.tags.length > 0 || events.placable.tags.length > 0)" ng-class="{'panel panel-danger bg-red': tagMissmatch()}">
|
||||
|
@ -42,9 +42,9 @@
|
||||
translate-attr="{ placeholder: 'app.public.common.your_password' }"
|
||||
ng-minlength="8"/>
|
||||
</div>
|
||||
<a href="#" ng-click="openResetPassword($event)" class="text-xs" translate translate-default="Forgotten password">{{ 'app.public.common.password_forgotten' }}</a>
|
||||
<a ng-click="openResetPassword($event)" class="text-xs" translate translate-default="Forgotten password">{{ 'app.public.common.password_forgotten' }}</a>
|
||||
<span ng-if="confirmationRequired">
|
||||
<br><a href="#" ng-click="openConfirmationNewModal($event)" class="text-xs" translate translate-default="Confirm account">{{ 'app.public.common.confirm_my_account' }}</a>
|
||||
<br><a ng-click="openConfirmationNewModal($event)" class="text-xs" translate translate-default="Confirm account">{{ 'app.public.common.confirm_my_account' }}</a>
|
||||
</span>
|
||||
<div class="alert alert-warning m-t-sm m-b-none text-xs p-sm" ng-show='isCapsLockOn' role="alert">
|
||||
<i class="fa fa-warning"></i>
|
||||
@ -63,7 +63,7 @@
|
||||
<p class="text-center font-sbold">
|
||||
<span translate translate-default="Not registered?">{{ 'app.public.common.not_registered_to_the_fablab' }}</span>
|
||||
<br/>
|
||||
<a href="#" ng-click="openSignup($event)" class="text-u-l" translate translate-default="Create an account">{{ 'app.public.common.create_an_account' }}</a></br>
|
||||
<a ng-click="openSignup($event)" class="text-u-l" translate translate-default="Create an account">{{ 'app.public.common.create_an_account' }}</a></br>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
@ -53,7 +53,7 @@
|
||||
<li><a class="text-black pointer" ng-click="logout($event)"><i class="fa fa-power-off"></i> {{ 'app.public.common.sign_out' | translate }}</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li ng-if="!isAuthenticated()"><a class="font-sbold label text-md pointer" ng-click="signup($event)"><i class="fa fa-rocket"></i> {{ 'app.public.common.sign_up' | translate }}</a></li>
|
||||
<li ng-if="!isAuthenticated() && registrationEnabled()"><a class="font-sbold label text-md pointer" ng-click="signup($event)"><i class="fa fa-rocket"></i> {{ 'app.public.common.sign_up' | translate }}</a></li>
|
||||
<li ng-if="!isAuthenticated()">
|
||||
<a class="font-sbold label text-md pointer" ng-click="login($event)"><i class="fa fa-sign-in"></i> {{ 'app.public.common.sign_in' | translate }}</a>
|
||||
</li>
|
||||
|
@ -7,12 +7,12 @@
|
||||
<nav class="nav-primary hidden-xs">
|
||||
<ul class="nav nav-main m-t-xs" data-ride="collapse">
|
||||
<!-- Disconnected user menu for small devices -->
|
||||
<li class="hidden-sm hidden-md hidden-lg" ng-if-start="!isAuthenticated()">
|
||||
<li class="hidden-sm hidden-md hidden-lg" ng-if="!isAuthenticated() && registrationEnabled()">
|
||||
<a class="auto pointer" ng-click="signup($event)">
|
||||
<i class="fa fa-rocket"></i> <span translate>{{ 'app.public.common.sign_up' }}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="hidden-sm hidden-md hidden-lg" ng-if-end>
|
||||
<li class="hidden-sm hidden-md hidden-lg" ng-if="!isAuthenticated()">
|
||||
<a class="auto pointer" ng-click="login($event)">
|
||||
<i class="fa fa-sign-in"></i> <span translate>{{ 'app.public.common.sign_in' }}</span>
|
||||
</a>
|
||||
|
@ -16,7 +16,7 @@
|
||||
<ng-repeat ng-repeat="network in social.networks.slice(0,3)">
|
||||
<social-link network="{{network}}" user="user"></social-link>
|
||||
</ng-repeat>
|
||||
<a href="#" ng-click="social.showAllLinks = !social.showAllLinks">
|
||||
<a ng-click="social.showAllLinks = !social.showAllLinks">
|
||||
<i class="fa fa-plus" ng-show="!social.showAllLinks"></i>
|
||||
<i class="fa fa-minus" ng-show="social.showAllLinks"></i>
|
||||
</a>
|
||||
|
@ -36,7 +36,7 @@
|
||||
required
|
||||
bs-jasny-fileinput>
|
||||
</span>
|
||||
<a href="#" class="btn btn-danger fileinput-exists" data-dismiss="fileinput" translate>{{ 'app.shared.buttons.delete' }}</a>
|
||||
<a class="btn btn-danger fileinput-exists" data-dismiss="fileinput" translate>{{ 'app.shared.buttons.delete' }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="row no-gutter">
|
||||
<div class="col-md-1 hidden-xs">
|
||||
<section class="heading-btn">
|
||||
<a href="#" ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-md-8 b-l b-r">
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="row no-gutter">
|
||||
<div class="col-xs-2 col-sm-2 col-md-1">
|
||||
<section class="heading-btn">
|
||||
<a href="#" ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-xs-10 col-sm-10 col-md-8 b-l b-r-md">
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="row no-gutter">
|
||||
<div class="col-md-1 hidden-xs">
|
||||
<section class="heading-btn">
|
||||
<a href="#" ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-md-8 b-l b-r">
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="row no-gutter">
|
||||
<div class="col-xs-2 col-sm-2 col-md-1">
|
||||
<section class="heading-btn">
|
||||
<a href="#" ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-xs-10 col-sm-10 col-md-8 b-l b-r-md">
|
||||
|
@ -4,7 +4,7 @@
|
||||
<div class="row no-gutter">
|
||||
<div class="col-xs-2 col-sm-2 col-md-1">
|
||||
<section class="heading-btn">
|
||||
<a href="#" ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-xs-10 col-sm-10 col-md-7 b-l b-r-md">
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="row no-gutter">
|
||||
<div class="col-xs-2 col-sm-2 col-md-1">
|
||||
<section class="heading-btn">
|
||||
<a href="#" ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-xs-10 col-sm-10 col-md-8 b-l b-r-md">
|
||||
@ -41,7 +41,7 @@
|
||||
</div>
|
||||
<div class="col-sm-6 no-padder">
|
||||
<div class="btn btn-default btn-block padder-v no-b red" ng-click="showTraining(training)">
|
||||
<i class="fa fa-eye"></i> {{ 'app.shared.buttons.consult' | translate }}
|
||||
<i class="fas fa-eye"></i> {{ 'app.shared.buttons.consult' | translate }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
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