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

Merge branch 'dev' for release 5.3.1

This commit is contained in:
Sylvain 2022-01-17 16:10:03 +01:00
commit 41f206cc43
183 changed files with 2246 additions and 849 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
interface HtmlTranslateProps {
trKey: string,
options?: Record<string, string>
options?: Record<string, string|number>
}
/**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 }) => {
/**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,7 +18,8 @@ export interface IntentConfirmation {
export enum PaymentMethod {
Card = 'card',
Other = ''
Check = 'check',
Transfer = 'transfer'
}
export type CartItem = { reservation: Reservation }|

View File

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

View File

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

View File

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

View File

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

View File

@ -76,3 +76,5 @@
@import "modules/subscriptions/renew-modal";
@import "app.responsive";
@import "overrides"

View File

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

View 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%;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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:&#xf03e;/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:&#xf03e;/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:&#xf03e;/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:&#xf03e;/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:&#xf03e;/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:&#xf03e;/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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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:&#xf03e;/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>

View File

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

View File

@ -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:&#xf03e;/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:&#xf03e;/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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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